add notification
This commit is contained in:
@@ -3,10 +3,10 @@
|
||||
import {
|
||||
BookCopyIcon,
|
||||
CalendarClockIcon,
|
||||
DollarSign,
|
||||
DollarSignIcon,
|
||||
FileBadgeIcon,
|
||||
GraduationCap,
|
||||
LayoutDashboard,
|
||||
LayoutDashboardIcon,
|
||||
ShieldUserIcon,
|
||||
UploadIcon,
|
||||
UsersIcon
|
||||
@@ -26,12 +26,12 @@ const data = {
|
||||
{
|
||||
title: 'Visão geral',
|
||||
url: '/main',
|
||||
icon: LayoutDashboard
|
||||
icon: LayoutDashboardIcon
|
||||
},
|
||||
{
|
||||
title: 'Histórico de compras',
|
||||
url: '/orders',
|
||||
icon: DollarSign
|
||||
icon: DollarSignIcon
|
||||
}
|
||||
],
|
||||
navUser: [
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BellIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from '@repo/ui/components/ui/popover'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
|
||||
export function Notification() {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
>
|
||||
<BellIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-80">
|
||||
<>Notificações</>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@repo/ui/components/ui/alert-dialog'
|
||||
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -58,24 +57,24 @@ export const columns: ColumnDef<Enrollment>[] = [
|
||||
}
|
||||
]
|
||||
|
||||
async function getEnrollment(id: string) {
|
||||
const r = await fetch(`/~/api/enrollments/${id}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, 150))
|
||||
|
||||
return (await r.json()) as {
|
||||
cancel_policy?: any
|
||||
lock?: { hash: string }
|
||||
}
|
||||
}
|
||||
|
||||
function ActionMenu({ row }: { row: any }) {
|
||||
const [open, { set: setOpen }] = useToggle(false)
|
||||
const cert = row.original?.cert
|
||||
const { data, loading, run, refresh } = useRequest(getEnrollment, {
|
||||
manual: true
|
||||
})
|
||||
const { data, loading, runAsync, refresh } = useRequest(
|
||||
async () => {
|
||||
const r = await fetch(`/~/api/enrollments/${row.id}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
return (await r.json()) as {
|
||||
cancel_policy?: any
|
||||
lock?: { hash: string }
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
const onSuccess = () => {
|
||||
refresh()
|
||||
@@ -90,7 +89,7 @@ function ActionMenu({ row }: { row: any }) {
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
if (data) return
|
||||
run(row.id)
|
||||
runAsync()
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -140,22 +139,27 @@ type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
|
||||
}
|
||||
|
||||
function DownloadItem({ id, onSuccess, ...props }: ItemProps) {
|
||||
const [loading, { set }] = useToggle(false)
|
||||
const { runAsync, loading } = useRequest(
|
||||
async () => {
|
||||
return await fetch(`/~/api/enrollments/${id}/download`)
|
||||
},
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
const download = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
set(true)
|
||||
const r = await fetch(`/~/api/enrollments/${id}/download`, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
const { presigned_url } = (await r.json()) as { presigned_url: string }
|
||||
try {
|
||||
const r = await runAsync()
|
||||
const { presigned_url } = (await r.json()) as {
|
||||
presigned_url: string
|
||||
}
|
||||
|
||||
window.open(presigned_url, '_blank')
|
||||
|
||||
set(false)
|
||||
onSuccess?.()
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -299,7 +303,7 @@ function CancelItem({
|
||||
<AlertDialogDescription>
|
||||
Esta ação não pode ser desfeita. Isso{' '}
|
||||
<span className="font-bold">
|
||||
cancelar permanentemente a matrícula
|
||||
cancela permanentemente a matrícula
|
||||
</span>{' '}
|
||||
deste colaborador.
|
||||
</AlertDialogDescription>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||
import { request as req } from '@repo/util/request'
|
||||
|
||||
import { AppSidebar } from '@/components/app-sidebar'
|
||||
import { Notification } from '@/components/notification'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
@@ -78,6 +79,7 @@ export default function Route({ loaderData }: Route.ComponentProps) {
|
||||
<ThemedImage className="max-md:hidden" />
|
||||
|
||||
<div className="ml-auto flex gap-2.5 items-center">
|
||||
<Notification />
|
||||
<ModeToggle />
|
||||
<NavUser user={user} />
|
||||
</div>
|
||||
|
||||
@@ -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