add notification
This commit is contained in:
@@ -17,7 +17,8 @@ export default [
|
||||
]),
|
||||
route('konviva', 'routes/konviva.ts'),
|
||||
route('player/:id', 'routes/player.tsx'),
|
||||
route('proxy/*', 'routes/proxy.tsx')
|
||||
route('proxy/*', 'routes/proxy.tsx'),
|
||||
route('api/*', 'routes/api.ts')
|
||||
]),
|
||||
route('logout', 'routes/auth/logout.ts'),
|
||||
route('login', 'routes/auth/login.ts')
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import Fuse from 'fuse.js'
|
||||
import {
|
||||
BanIcon,
|
||||
@@ -15,6 +8,8 @@ import {
|
||||
CircleOffIcon,
|
||||
CirclePlusIcon,
|
||||
CircleXIcon,
|
||||
EllipsisIcon,
|
||||
FileBadgeIcon,
|
||||
TimerIcon,
|
||||
type LucideIcon
|
||||
} from 'lucide-react'
|
||||
@@ -27,12 +22,28 @@ import { userContext } from '@repo/auth/context'
|
||||
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@repo/ui/components/ui/dropdown-menu'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
CardTitle,
|
||||
CardAction
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Progress } from '@repo/ui/components/ui/progress'
|
||||
@@ -40,6 +51,7 @@ import { createSearch } from '@repo/util/meili'
|
||||
|
||||
import placeholder from '@/assets/placeholder.webp'
|
||||
import { Container } from '@/components/container'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
|
||||
type Course = {
|
||||
name: string
|
||||
@@ -200,8 +212,10 @@ function Enrollment({ id, course, progress }: Enrollment) {
|
||||
<Card className="overflow-hidden relative h-96">
|
||||
<CardHeader className="z-1 relative">
|
||||
<CardTitle className="text-xl/6">{course.name}</CardTitle>
|
||||
<CardAction>
|
||||
<ActionMenu />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="z-1"></CardContent>
|
||||
<CardFooter className="absolute z-1 bottom-6 w-full flex gap-1.5">
|
||||
<Progress value={progress} />
|
||||
<span className="text-xs">{progress}%</span>
|
||||
@@ -216,6 +230,23 @@ function Enrollment({ id, course, progress }: Enrollment) {
|
||||
)
|
||||
}
|
||||
|
||||
function ActionMenu() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm" className="cursor-pointer">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem disabled={true}>
|
||||
<FileBadgeIcon /> Baixar certificado
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const statuses: Record<
|
||||
string,
|
||||
{ icon: LucideIcon; color?: string; label: string }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Route } from './+types'
|
||||
import type { Route } from './+types/layout'
|
||||
|
||||
import { useToggle } from 'ahooks'
|
||||
import { MenuIcon } from 'lucide-react'
|
||||
import { Link, NavLink, Outlet } from 'react-router'
|
||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||
|
||||
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||
import { NavUser } from '@repo/ui/components/nav-user'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
@@ -22,11 +22,12 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from '@repo/ui/components/ui/sheet'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
export async function loader({ context }: Route.ActionArgs) {
|
||||
const user = context.get(userContext)
|
||||
const user = context.get(userContext) as User
|
||||
return Response.json({ user })
|
||||
}
|
||||
|
||||
@@ -45,8 +46,9 @@ const navMain = [
|
||||
}
|
||||
]
|
||||
|
||||
export default function Component({ loaderData }: Route.ComponentProps) {
|
||||
const { user } = loaderData
|
||||
export default function Component({
|
||||
loaderData: { user }
|
||||
}: Route.ComponentProps) {
|
||||
const [isOpen, { toggle }] = useToggle()
|
||||
|
||||
return (
|
||||
@@ -119,6 +121,12 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
||||
</header>
|
||||
|
||||
<Outlet />
|
||||
<Toaster
|
||||
position="top-center"
|
||||
richColors={true}
|
||||
duration={Infinity}
|
||||
closeButton={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import type { Route } from './+types/emails'
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { Await } from 'react-router'
|
||||
import {
|
||||
Suspense,
|
||||
type ComponentProps,
|
||||
type MouseEvent,
|
||||
createContext,
|
||||
use
|
||||
} from 'react'
|
||||
|
||||
import { Await } from 'react-router'
|
||||
import { EllipsisIcon, CircleXIcon, SendIcon } from 'lucide-react'
|
||||
import { useRequest, useToggle } from 'ahooks'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import {
|
||||
Card,
|
||||
@@ -11,17 +22,53 @@ import {
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@repo/ui/components/ui/alert-dialog'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@repo/ui/components/ui/dropdown-menu'
|
||||
import { request as req } from '@repo/util/request'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import { useOutletContext } from 'react-router'
|
||||
import type { User as AuthUser } from '@repo/auth/auth'
|
||||
import type { User } from '@repo/ui/routes/users/data'
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle
|
||||
} from '@repo/ui/components/ui/item'
|
||||
|
||||
type Email = {
|
||||
sk: string
|
||||
email: string
|
||||
email_verified: boolean
|
||||
email_primary: boolean
|
||||
}
|
||||
|
||||
const ActionMenuContext = createContext<Email | null>(null)
|
||||
|
||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
const user = context.get(userContext) as User
|
||||
const user = context.get(userContext) as AuthUser
|
||||
const data = req({
|
||||
url: `/users/${user.sub}/emails`,
|
||||
request,
|
||||
context
|
||||
}).then((r) => r.json())
|
||||
}).then((r) => r.json() as Promise<{ items: Email[] }>)
|
||||
|
||||
return { data }
|
||||
}
|
||||
@@ -40,13 +87,24 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
principal receberá as mensagens.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul>
|
||||
{items.map(({ sk }: { sk: string }, idx: number) => {
|
||||
const [, email] = sk.split('#')
|
||||
return <li key={idx}>{email}</li>
|
||||
})}
|
||||
</ul>
|
||||
<CardContent className="flex flex-col gap-2.5">
|
||||
{items.map(({ sk, ...props }) => {
|
||||
const [, email] = sk.split('#') as [string, string]
|
||||
|
||||
return (
|
||||
<Item key={email} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{email}</ItemTitle>
|
||||
<ItemDescription>...</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ActionMenuContext value={{ ...props, sk, email }}>
|
||||
<ActionMenu />
|
||||
</ActionMenuContext>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -54,3 +112,94 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionMenu() {
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm" className="cursor-pointer">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="*:cursor-pointer">
|
||||
<DropdownMenuItem>
|
||||
<SendIcon /> Reenviar email de verificação
|
||||
</DropdownMenuItem>
|
||||
<RemoveItem />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
function RemoveItem({ onSuccess, ...props }: ItemProps) {
|
||||
const { user } = useOutletContext() as { user: User }
|
||||
const { email } = use(ActionMenuContext) as Email
|
||||
const [open, { set: setOpen }] = useToggle(false)
|
||||
const { runAsync, loading } = useRequest(
|
||||
async () => {
|
||||
const r = await fetch(`/api/users/${user.id}/emails/${email}`, {
|
||||
method: 'DELETE',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' })
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
throw await r.json()
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
const cancel = async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await runAsync()
|
||||
|
||||
toast.success('O email foi removido')
|
||||
onSuccess?.()
|
||||
setOpen(false)
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
if (err?.type === 'EmailConflictError') {
|
||||
toast.error('O email não pode ser removido')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
{...props}
|
||||
>
|
||||
<CircleXIcon /> Remover
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Tem certeza absoluta?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Esta ação não pode ser desfeita. Isso{' '}
|
||||
<span className="font-bold">remove permanentemente</span> o seu
|
||||
endereço de email.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="*:cursor-pointer">
|
||||
<AlertDialogAction asChild>
|
||||
<Button onClick={cancel} disabled={loading} variant="destructive">
|
||||
{loading ? <Spinner /> : null} Continuar
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user