From 90232feb1e0b86943bb2a6c34aacc2ee9c174469 Mon Sep 17 00:00:00 2001 From: Lars Hampe Date: Wed, 30 Oct 2024 20:20:35 +0100 Subject: [PATCH] feat: access tokens (create, list, delete) --- apps/api/src/access-token/create.ts | 58 ++++++++ apps/api/src/access-token/delete.ts | 45 ++++++ apps/api/src/access-token/index.ts | 16 +++ apps/api/src/access-token/list.ts | 49 +++++++ apps/api/src/index.ts | 6 +- apps/api/src/utils/authentication.ts | 7 +- apps/app/package.json | 1 + .../app/src/components/AccessToken/Delete.tsx | 72 ++++++++++ .../components/AccessToken/Table/Columns.tsx | 35 +++++ .../AccessToken/Table/DataTable.tsx | 83 +++++++++++ apps/app/src/components/Navigation.tsx | 97 ------------- apps/app/src/components/NavigationMobile.tsx | 102 ------------- apps/app/src/components/Sidebar.tsx | 6 +- apps/app/src/components/SidebarChangelog.tsx | 109 +++++++------- apps/app/src/components/SidebarPage.tsx | 103 ++++++------- apps/app/src/components/SidebarSettings.tsx | 96 +++++++++++++ apps/app/src/components/SidebarUser.tsx | 15 +- apps/app/src/hooks/useAccessToken.ts | 67 +++++++++ apps/app/src/routeTree.gen.ts | 54 +++++++ .../src/routes/access-tokens.index.lazy.tsx | 36 +++++ .../app/src/routes/access-tokens.new.lazy.tsx | 136 ++++++++++++++++++ apps/app/src/routes/changelog.index.lazy.tsx | 72 +++++----- apps/app/tsconfig.tsbuildinfo | 2 +- bun.lockb | Bin 542768 -> 543584 bytes packages/schema/src/access-token/base.ts | 12 ++ packages/schema/src/access-token/create.ts | 7 + packages/schema/src/access-token/index.ts | 3 + packages/schema/src/access-token/list.ts | 4 + packages/schema/src/index.ts | 1 + packages/ui/src/index.ts | 1 + packages/ui/src/table.tsx | 117 +++++++++++++++ 31 files changed, 1062 insertions(+), 350 deletions(-) create mode 100644 apps/api/src/access-token/create.ts create mode 100644 apps/api/src/access-token/delete.ts create mode 100644 apps/api/src/access-token/index.ts create mode 100644 apps/api/src/access-token/list.ts create mode 100644 apps/app/src/components/AccessToken/Delete.tsx create mode 100644 apps/app/src/components/AccessToken/Table/Columns.tsx create mode 100644 apps/app/src/components/AccessToken/Table/DataTable.tsx delete mode 100644 apps/app/src/components/Navigation.tsx delete mode 100644 apps/app/src/components/NavigationMobile.tsx create mode 100644 apps/app/src/components/SidebarSettings.tsx create mode 100644 apps/app/src/hooks/useAccessToken.ts create mode 100644 apps/app/src/routes/access-tokens.index.lazy.tsx create mode 100644 apps/app/src/routes/access-tokens.new.lazy.tsx create mode 100644 packages/schema/src/access-token/base.ts create mode 100644 packages/schema/src/access-token/create.ts create mode 100644 packages/schema/src/access-token/index.ts create mode 100644 packages/schema/src/access-token/list.ts create mode 100644 packages/ui/src/table.tsx diff --git a/apps/api/src/access-token/create.ts b/apps/api/src/access-token/create.ts new file mode 100644 index 0000000..7fa9b3a --- /dev/null +++ b/apps/api/src/access-token/create.ts @@ -0,0 +1,58 @@ +import crypto from 'node:crypto' +import { access_token, db } from '@boring.tools/database' +import { AccessTokenCreateInput, AccessTokenOutput } from '@boring.tools/schema' +import { createRoute, type z } from '@hono/zod-openapi' +import { HTTPException } from 'hono/http-exception' +import type { accessTokenApi } from '.' +import { verifyAuthentication } from '../utils/authentication' + +export const route = createRoute({ + method: 'post', + path: '/', + tags: ['access-token'], + request: { + body: { + content: { + 'application/json': { schema: AccessTokenCreateInput }, + }, + }, + }, + responses: { + 201: { + content: { + 'application/json': { schema: AccessTokenOutput }, + }, + description: 'Commits created', + }, + 400: { + description: 'Bad Request', + }, + 500: { + description: 'Internal Server Error', + }, + }, +}) + +export const registerAccessTokenCreate = (api: typeof accessTokenApi) => { + return api.openapi(route, async (c) => { + const userId = verifyAuthentication(c) + + const data: z.infer = await c.req.json() + const token = crypto.randomBytes(20).toString('hex') + + const [result] = await db + .insert(access_token) + .values({ + ...data, + userId, + token: `bt_${token}`, + }) + .returning() + + if (!result) { + throw new HTTPException(404, { message: 'Not Found' }) + } + + return c.json(result, 200) + }) +} diff --git a/apps/api/src/access-token/delete.ts b/apps/api/src/access-token/delete.ts new file mode 100644 index 0000000..0d84f01 --- /dev/null +++ b/apps/api/src/access-token/delete.ts @@ -0,0 +1,45 @@ +import { access_token, db } from '@boring.tools/database' +import { GeneralOutput } from '@boring.tools/schema' +import { createRoute, type z } from '@hono/zod-openapi' +import { and, eq } from 'drizzle-orm' +import { HTTPException } from 'hono/http-exception' +import type { accessTokenApi } from '.' +import { verifyAuthentication } from '../utils/authentication' + +export const route = createRoute({ + method: 'delete', + path: '/:id', + tags: ['access-token'], + responses: { + 201: { + content: { + 'application/json': { schema: GeneralOutput }, + }, + description: 'Removes a access token by id', + }, + 400: { + description: 'Bad Request', + }, + 500: { + description: 'Internal Server Error', + }, + }, +}) + +export const registerAccessTokenDelete = (api: typeof accessTokenApi) => { + return api.openapi(route, async (c) => { + const id = c.req.param('id') + const userId = verifyAuthentication(c) + + const result = await db + .delete(access_token) + .where(and(eq(access_token.userId, userId), eq(access_token.id, id))) + .returning() + + if (!result) { + throw new HTTPException(404, { message: 'Not Found' }) + } + + return c.json(result, 200) + }) +} diff --git a/apps/api/src/access-token/index.ts b/apps/api/src/access-token/index.ts new file mode 100644 index 0000000..2330372 --- /dev/null +++ b/apps/api/src/access-token/index.ts @@ -0,0 +1,16 @@ +import { OpenAPIHono } from '@hono/zod-openapi' +import type { Variables } from '..' +import type { ContextModule } from '../utils/sentry' +import { registerAccessTokenCreate } from './create' +import { registerAccessTokenDelete } from './delete' +import { registerAccessTokenList } from './list' + +export const accessTokenApi = new OpenAPIHono<{ Variables: Variables }>() + +const module: ContextModule = { + name: 'access-token', +} + +registerAccessTokenCreate(accessTokenApi) +registerAccessTokenList(accessTokenApi) +registerAccessTokenDelete(accessTokenApi) diff --git a/apps/api/src/access-token/list.ts b/apps/api/src/access-token/list.ts new file mode 100644 index 0000000..6a6ce18 --- /dev/null +++ b/apps/api/src/access-token/list.ts @@ -0,0 +1,49 @@ +import { changelog, db } from '@boring.tools/database' +import { AccessTokenListOutput } from '@boring.tools/schema' +import { createRoute } from '@hono/zod-openapi' +import { eq } from 'drizzle-orm' +import { HTTPException } from 'hono/http-exception' +import type { accessTokenApi } from '.' +import { verifyAuthentication } from '../utils/authentication' + +const route = createRoute({ + method: 'get', + path: '/', + tags: ['access-token'], + responses: { + 200: { + content: { + 'application/json': { + schema: AccessTokenListOutput, + }, + }, + description: 'Return version by id', + }, + 400: { + description: 'Bad Request', + }, + 500: { + description: 'Internal Server Error', + }, + }, +}) + +export const registerAccessTokenList = (api: typeof accessTokenApi) => { + return api.openapi(route, async (c) => { + const userId = verifyAuthentication(c) + const result = await db.query.access_token.findMany({ + where: eq(changelog.userId, userId), + }) + + if (!result) { + throw new HTTPException(404, { message: 'Access Tokens not found' }) + } + + const mappedData = result.map((at) => ({ + ...at, + token: at.token.substring(0, 10), + })) + + return c.json(mappedData, 200) + }) +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e2d2eb4..188dac3 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,10 +5,9 @@ import { apiReference } from '@scalar/hono-api-reference' import { cors } from 'hono/cors' import changelog from './changelog' -import { changelogCommitApi } from './changelog/commit' -import version from './changelog/version' import user from './user' +import { accessTokenApi } from './access-token' import pageApi from './page' import { authentication } from './utils/authentication' import { handleError, handleZodError } from './utils/errors' @@ -21,7 +20,7 @@ export type Variables = { } export const app = new OpenAPIHono<{ Variables: Variables }>({ - // defaultHook: handleZodError, + defaultHook: handleZodError, }) // app.use( @@ -37,6 +36,7 @@ app.use('/v1/*', authentication) app.route('/v1/user', user) app.route('/v1/changelog', changelog) app.route('/v1/page', pageApi) +app.route('/v1/access-token', accessTokenApi) app.doc('/openapi.json', { openapi: '3.0.0', diff --git a/apps/api/src/utils/authentication.ts b/apps/api/src/utils/authentication.ts index 0b8bba0..9b8c9c6 100644 --- a/apps/api/src/utils/authentication.ts +++ b/apps/api/src/utils/authentication.ts @@ -20,12 +20,15 @@ const generatedToken = async (c: Context, next: Next) => { }, }) - console.log(accessTokenResult) - if (!accessTokenResult) { throw new HTTPException(401, { message: 'Unauthorized' }) } + await db + .update(access_token) + .set({ lastUsedOn: new Date() }) + .where(eq(access_token.id, accessTokenResult.id)) + c.set('user', accessTokenResult.user) await next() diff --git a/apps/app/package.json b/apps/app/package.json index 3134f16..47bb5ca 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -20,6 +20,7 @@ "@tailwindcss/typography": "^0.5.15", "@tanstack/react-query": "^5.59.0", "@tanstack/react-router": "^1.58.15", + "@tanstack/react-table": "^8.20.5", "axios": "^1.7.7", "lucide-react": "^0.446.0", "react": "^18.3.1", diff --git a/apps/app/src/components/AccessToken/Delete.tsx b/apps/app/src/components/AccessToken/Delete.tsx new file mode 100644 index 0000000..cffa46a --- /dev/null +++ b/apps/app/src/components/AccessToken/Delete.tsx @@ -0,0 +1,72 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@boring.tools/ui' +import { Trash2Icon } from 'lucide-react' +import { useState } from 'react' +import { useAccessTokenDelete } from '../../hooks/useAccessToken' + +export const AccessTokenDelete = ({ id }: { id: string }) => { + const accessTokenDelete = useAccessTokenDelete() + const [isOpen, setIsOpen] = useState(false) + + const removeChangelog = () => { + accessTokenDelete.mutate( + { id }, + { + onSuccess: () => { + setIsOpen(false) + }, + }, + ) + } + return ( + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + changelog and remove your data from our servers. + + + + setIsOpen(false)}> + Cancel + + + + + + + + +

Delete access token

+
+
+ ) +} diff --git a/apps/app/src/components/AccessToken/Table/Columns.tsx b/apps/app/src/components/AccessToken/Table/Columns.tsx new file mode 100644 index 0000000..f61303c --- /dev/null +++ b/apps/app/src/components/AccessToken/Table/Columns.tsx @@ -0,0 +1,35 @@ +import type { AccessTokenOutput } from '@boring.tools/schema' +import type { ColumnDef } from '@tanstack/react-table' +import { format } from 'date-fns' +import type { z } from 'zod' +import { AccessTokenDelete } from '../Delete' + +export const AccessTokenColumns: ColumnDef< + z.infer +>[] = [ + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'lastUsedOn', + header: 'Last seen', + accessorFn: (row) => { + if (!row.lastUsedOn) { + return 'Never' + } + return format(new Date(row.lastUsedOn), 'HH:mm dd.MM.yyyy') + }, + }, + { + accessorKey: 'token', + header: 'Token', + accessorFn: (row) => `${row.token}...`, + }, + { + accessorKey: 'id', + header: '', + size: 10, + cell: (props) => , + }, +] diff --git a/apps/app/src/components/AccessToken/Table/DataTable.tsx b/apps/app/src/components/AccessToken/Table/DataTable.tsx new file mode 100644 index 0000000..be1a44a --- /dev/null +++ b/apps/app/src/components/AccessToken/Table/DataTable.tsx @@ -0,0 +1,83 @@ +'use client' + +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@boring.tools/ui' + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ) +} diff --git a/apps/app/src/components/Navigation.tsx b/apps/app/src/components/Navigation.tsx deleted file mode 100644 index 33619e7..0000000 --- a/apps/app/src/components/Navigation.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@boring.tools/ui' -import { Link } from '@tanstack/react-router' -import { BellIcon } from 'lucide-react' -import { NavigationRoutes } from '../utils/navigation-routes' - -export const Navigation = () => { - return ( -
-
-
- - boring.tools - - -
-
- -
-
- - - More Infos - - If you want more information about boring.tools, visit our - documentation! - - - - - - - - -
-
-
- ) -} diff --git a/apps/app/src/components/NavigationMobile.tsx b/apps/app/src/components/NavigationMobile.tsx deleted file mode 100644 index a4496dc..0000000 --- a/apps/app/src/components/NavigationMobile.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Sheet, - SheetContent, - SheetTrigger, -} from '@boring.tools/ui' -import { Link } from '@tanstack/react-router' -import { MenuIcon } from 'lucide-react' -import { NavigationRoutes } from '../utils/navigation-routes' - -export const NavigationMobile = () => { - return ( - - - - - - -
- - - More Infos - - If you want more information about boring.tools, visit our - documentation! - - - - - - - - -
-
-
- ) -} diff --git a/apps/app/src/components/Sidebar.tsx b/apps/app/src/components/Sidebar.tsx index fa580d0..920381c 100644 --- a/apps/app/src/components/Sidebar.tsx +++ b/apps/app/src/components/Sidebar.tsx @@ -34,10 +34,8 @@ export function Sidebar() { - - - - + + diff --git a/apps/app/src/components/SidebarChangelog.tsx b/apps/app/src/components/SidebarChangelog.tsx index f6824a9..b0b06d4 100644 --- a/apps/app/src/components/SidebarChangelog.tsx +++ b/apps/app/src/components/SidebarChangelog.tsx @@ -2,6 +2,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, + SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, @@ -28,60 +29,62 @@ export const SidebarChangelog = () => { }, [location, setValue]) return ( - setValue(!value)}> - - - - - Changelog - - + + setValue(!value)}> + + + + + Changelog + + - - - - Toggle - - - - - {!error && - data?.map((changelog) => ( - - - - {changelog.title} - - - - ))} + + + + Toggle + + + + + {!error && + data?.map((changelog) => ( + + + + {changelog.title} + + + + ))} - - - - - - New changelog - - - - - - - - + + + + + + New changelog + + + + + + + + + ) } diff --git a/apps/app/src/components/SidebarPage.tsx b/apps/app/src/components/SidebarPage.tsx index 3d0d714..92beb2f 100644 --- a/apps/app/src/components/SidebarPage.tsx +++ b/apps/app/src/components/SidebarPage.tsx @@ -2,6 +2,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, + SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, @@ -28,57 +29,59 @@ export const SidebarPage = () => { }, [location, setValue]) return ( - setValue(!value)}> - - - - - Page - - + + setValue(!value)}> + + + + + Page + + - - - - Toggle - - - - - {!error && - data?.map((page) => ( - - - - {page.title} - - - - ))} + + + + Toggle + + + + + {!error && + data?.map((page) => ( + + + + {page.title} + + + + ))} - - - - - - New page - - - - - - - - + + + + + + New page + + + + + + + + + ) } diff --git a/apps/app/src/components/SidebarSettings.tsx b/apps/app/src/components/SidebarSettings.tsx new file mode 100644 index 0000000..0e781dd --- /dev/null +++ b/apps/app/src/components/SidebarSettings.tsx @@ -0,0 +1,96 @@ +import { ChevronsUpDown } from 'lucide-react' + +import { + Avatar, + AvatarFallback, + AvatarImage, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@boring.tools/ui' +import { SignOutButton, useUser } from '@clerk/clerk-react' +import { Link } from '@tanstack/react-router' + +export function SidebarUser() { + const { isMobile } = useSidebar() + const { user } = useUser() + + return ( + + + + + + + + CN + +
+ {user?.fullName} + + {user?.primaryEmailAddress?.emailAddress} + +
+ +
+
+ + +
+ + + + {user?.firstName?.substring(0, 1)} + {user?.lastName?.substring(0, 1)} + + +
+ + {user?.fullName} + + + {user?.primaryEmailAddress?.emailAddress} + +
+
+
+ + + + Profile + + + + + + + + +
+
+
+
+ ) +} diff --git a/apps/app/src/components/SidebarUser.tsx b/apps/app/src/components/SidebarUser.tsx index 0e781dd..49086a2 100644 --- a/apps/app/src/components/SidebarUser.tsx +++ b/apps/app/src/components/SidebarUser.tsx @@ -1,4 +1,4 @@ -import { ChevronsUpDown } from 'lucide-react' +import { ChevronsUpDown, KeyRoundIcon } from 'lucide-react' import { Avatar, @@ -14,6 +14,7 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarSeparator, useSidebar, } from '@boring.tools/ui' import { SignOutButton, useUser } from '@clerk/clerk-react' @@ -25,6 +26,18 @@ export function SidebarUser() { return ( + + + + + Access Tokens + + + + diff --git a/apps/app/src/hooks/useAccessToken.ts b/apps/app/src/hooks/useAccessToken.ts new file mode 100644 index 0000000..d665c4d --- /dev/null +++ b/apps/app/src/hooks/useAccessToken.ts @@ -0,0 +1,67 @@ +import type { + AccessTokenCreateInput, + AccessTokenListOutput, + AccessTokenOutput, +} from '@boring.tools/schema' +import { useAuth } from '@clerk/clerk-react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import type { z } from 'zod' +import { queryFetch } from '../utils/queryFetch' + +type AccessToken = z.infer +type AccessTokenList = z.infer +type AccessTokenCreate = z.infer + +export const useAccessTokenList = () => { + const { getToken } = useAuth() + return useQuery({ + queryKey: ['accessTokenList'], + queryFn: async (): Promise => + await queryFetch({ + path: 'access-token', + method: 'get', + token: await getToken(), + }), + }) +} + +export const useAccessTokenCreate = () => { + const { getToken } = useAuth() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ( + payload: AccessTokenCreate, + ): Promise> => + await queryFetch({ + path: 'access-token', + data: payload, + method: 'post', + token: await getToken(), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['accessTokenList'] }) + }, + }) +} + +export const useAccessTokenDelete = () => { + const { getToken } = useAuth() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + id, + }: { id: string }): Promise> => + await queryFetch({ + path: `access-token/${id}`, + method: 'delete', + token: await getToken(), + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['accessTokenList'], + }) + }, + }) +} diff --git a/apps/app/src/routeTree.gen.ts b/apps/app/src/routeTree.gen.ts index c0e14ce..f3f2237 100644 --- a/apps/app/src/routeTree.gen.ts +++ b/apps/app/src/routeTree.gen.ts @@ -21,10 +21,12 @@ const IndexLazyImport = createFileRoute('/')() const UserIndexLazyImport = createFileRoute('/user/')() const PageIndexLazyImport = createFileRoute('/page/')() const ChangelogIndexLazyImport = createFileRoute('/changelog/')() +const AccessTokensIndexLazyImport = createFileRoute('/access-tokens/')() const PageCreateLazyImport = createFileRoute('/page/create')() const PageIdLazyImport = createFileRoute('/page/$id')() const ChangelogCreateLazyImport = createFileRoute('/changelog/create')() const ChangelogIdLazyImport = createFileRoute('/changelog/$id')() +const AccessTokensNewLazyImport = createFileRoute('/access-tokens/new')() const PageIdIndexLazyImport = createFileRoute('/page/$id/')() const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')() const PageIdEditLazyImport = createFileRoute('/page/$id/edit')() @@ -57,6 +59,13 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({ import('./routes/changelog.index.lazy').then((d) => d.Route), ) +const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({ + path: '/access-tokens/', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/access-tokens.index.lazy').then((d) => d.Route), +) + const PageCreateLazyRoute = PageCreateLazyImport.update({ path: '/page/create', getParentRoute: () => rootRoute, @@ -79,6 +88,13 @@ const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route)) +const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({ + path: '/access-tokens/new', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/access-tokens.new.lazy').then((d) => d.Route), +) + const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({ path: '/', getParentRoute: () => PageIdLazyRoute, @@ -130,6 +146,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexLazyImport parentRoute: typeof rootRoute } + '/access-tokens/new': { + id: '/access-tokens/new' + path: '/access-tokens/new' + fullPath: '/access-tokens/new' + preLoaderRoute: typeof AccessTokensNewLazyImport + parentRoute: typeof rootRoute + } '/changelog/$id': { id: '/changelog/$id' path: '/changelog/$id' @@ -158,6 +181,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PageCreateLazyImport parentRoute: typeof rootRoute } + '/access-tokens/': { + id: '/access-tokens/' + path: '/access-tokens' + fullPath: '/access-tokens' + preLoaderRoute: typeof AccessTokensIndexLazyImport + parentRoute: typeof rootRoute + } '/changelog/': { id: '/changelog/' path: '/changelog' @@ -260,10 +290,12 @@ const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren( export interface FileRoutesByFullPath { '/': typeof IndexLazyRoute + '/access-tokens/new': typeof AccessTokensNewLazyRoute '/changelog/$id': typeof ChangelogIdLazyRouteWithChildren '/changelog/create': typeof ChangelogCreateLazyRoute '/page/$id': typeof PageIdLazyRouteWithChildren '/page/create': typeof PageCreateLazyRoute + '/access-tokens': typeof AccessTokensIndexLazyRoute '/changelog': typeof ChangelogIndexLazyRoute '/page': typeof PageIndexLazyRoute '/user': typeof UserIndexLazyRoute @@ -277,8 +309,10 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexLazyRoute + '/access-tokens/new': typeof AccessTokensNewLazyRoute '/changelog/create': typeof ChangelogCreateLazyRoute '/page/create': typeof PageCreateLazyRoute + '/access-tokens': typeof AccessTokensIndexLazyRoute '/changelog': typeof ChangelogIndexLazyRoute '/page': typeof PageIndexLazyRoute '/user': typeof UserIndexLazyRoute @@ -293,10 +327,12 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexLazyRoute + '/access-tokens/new': typeof AccessTokensNewLazyRoute '/changelog/$id': typeof ChangelogIdLazyRouteWithChildren '/changelog/create': typeof ChangelogCreateLazyRoute '/page/$id': typeof PageIdLazyRouteWithChildren '/page/create': typeof PageCreateLazyRoute + '/access-tokens/': typeof AccessTokensIndexLazyRoute '/changelog/': typeof ChangelogIndexLazyRoute '/page/': typeof PageIndexLazyRoute '/user/': typeof UserIndexLazyRoute @@ -312,10 +348,12 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/access-tokens/new' | '/changelog/$id' | '/changelog/create' | '/page/$id' | '/page/create' + | '/access-tokens' | '/changelog' | '/page' | '/user' @@ -328,8 +366,10 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/access-tokens/new' | '/changelog/create' | '/page/create' + | '/access-tokens' | '/changelog' | '/page' | '/user' @@ -342,10 +382,12 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/access-tokens/new' | '/changelog/$id' | '/changelog/create' | '/page/$id' | '/page/create' + | '/access-tokens/' | '/changelog/' | '/page/' | '/user/' @@ -360,10 +402,12 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexLazyRoute: typeof IndexLazyRoute + AccessTokensNewLazyRoute: typeof AccessTokensNewLazyRoute ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute PageIdLazyRoute: typeof PageIdLazyRouteWithChildren PageCreateLazyRoute: typeof PageCreateLazyRoute + AccessTokensIndexLazyRoute: typeof AccessTokensIndexLazyRoute ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute PageIndexLazyRoute: typeof PageIndexLazyRoute UserIndexLazyRoute: typeof UserIndexLazyRoute @@ -371,10 +415,12 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexLazyRoute: IndexLazyRoute, + AccessTokensNewLazyRoute: AccessTokensNewLazyRoute, ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren, ChangelogCreateLazyRoute: ChangelogCreateLazyRoute, PageIdLazyRoute: PageIdLazyRouteWithChildren, PageCreateLazyRoute: PageCreateLazyRoute, + AccessTokensIndexLazyRoute: AccessTokensIndexLazyRoute, ChangelogIndexLazyRoute: ChangelogIndexLazyRoute, PageIndexLazyRoute: PageIndexLazyRoute, UserIndexLazyRoute: UserIndexLazyRoute, @@ -393,10 +439,12 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", + "/access-tokens/new", "/changelog/$id", "/changelog/create", "/page/$id", "/page/create", + "/access-tokens/", "/changelog/", "/page/", "/user/" @@ -405,6 +453,9 @@ export const routeTree = rootRoute "/": { "filePath": "index.lazy.tsx" }, + "/access-tokens/new": { + "filePath": "access-tokens.new.lazy.tsx" + }, "/changelog/$id": { "filePath": "changelog.$id.lazy.tsx", "children": [ @@ -427,6 +478,9 @@ export const routeTree = rootRoute "/page/create": { "filePath": "page.create.lazy.tsx" }, + "/access-tokens/": { + "filePath": "access-tokens.index.lazy.tsx" + }, "/changelog/": { "filePath": "changelog.index.lazy.tsx" }, diff --git a/apps/app/src/routes/access-tokens.index.lazy.tsx b/apps/app/src/routes/access-tokens.index.lazy.tsx new file mode 100644 index 0000000..574299d --- /dev/null +++ b/apps/app/src/routes/access-tokens.index.lazy.tsx @@ -0,0 +1,36 @@ +import { Button } from '@boring.tools/ui' +import { Link, createLazyFileRoute } from '@tanstack/react-router' +import { AccessTokenColumns } from '../components/AccessToken/Table/Columns' +import { DataTable } from '../components/AccessToken/Table/DataTable' +import { PageWrapper } from '../components/PageWrapper' +import { useAccessTokenList } from '../hooks/useAccessToken' + +const Component = () => { + const { data, isPending } = useAccessTokenList() + + return ( + +
+

Access Tokens

+ + +
+ {data && !isPending && ( + + )} +
+ ) +} + +export const Route = createLazyFileRoute('/access-tokens/')({ + component: Component, +}) diff --git a/apps/app/src/routes/access-tokens.new.lazy.tsx b/apps/app/src/routes/access-tokens.new.lazy.tsx new file mode 100644 index 0000000..7eaa0bc --- /dev/null +++ b/apps/app/src/routes/access-tokens.new.lazy.tsx @@ -0,0 +1,136 @@ +import { AccessTokenCreateInput } from '@boring.tools/schema' +import { + Alert, + AlertDescription, + AlertTitle, + Button, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from '@boring.tools/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { createLazyFileRoute, useRouter } from '@tanstack/react-router' +import { AlertCircle, CopyIcon } from 'lucide-react' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { useCopyToClipboard } from 'usehooks-ts' +import type { z } from 'zod' +import { PageWrapper } from '../components/PageWrapper' +import { useAccessTokenCreate } from '../hooks/useAccessToken' + +const Component = () => { + const router = useRouter() + const [, copy] = useCopyToClipboard() + const [token, setToken] = useState(null) + const accessTokenCreate = useAccessTokenCreate() + const form = useForm>({ + resolver: zodResolver(AccessTokenCreateInput), + defaultValues: { + name: '', + }, + }) + + const onSubmit = (values: z.infer) => { + accessTokenCreate.mutate(values, { + onSuccess(data) { + if (data.token) { + setToken(data.token) + } + }, + }) + } + return ( + +
+

New access token

+
+ + {token && ( +
+

Your token

+
+            {token}
+            
+          
+ + + + Reminder + + Your token is only visible this time. Please notify it securely. + If you forget it, you have to create a new token. + + +
+ +
+
+ )} + + {!token && ( +
+ + ( + + Name + + + {' '} + + + )} + /> + +
+ + +
+ + + )} +
+ ) +} + +export const Route = createLazyFileRoute('/access-tokens/new')({ + component: Component, +}) diff --git a/apps/app/src/routes/changelog.index.lazy.tsx b/apps/app/src/routes/changelog.index.lazy.tsx index c4713e8..46566e7 100644 --- a/apps/app/src/routes/changelog.index.lazy.tsx +++ b/apps/app/src/routes/changelog.index.lazy.tsx @@ -26,47 +26,45 @@ const Component = () => { return ( - <> -
-

Changelog

+
+

Changelog

-
- {!isPending && - data && - data.map((changelog) => { - return ( - - - - {changelog.title} - - - Versions: {changelog.computed.versionCount} +
+ {!isPending && + data && + data.map((changelog) => { + return ( + + + + {changelog.title} + + + Versions: {changelog.computed.versionCount} - Commits: {changelog.computed.commitCount} - - - - ) - })} + Commits: {changelog.computed.commitCount} + + + + ) + })} - - - - New Changelog - - - - - - -
+ + + + New Changelog + + + + + +
- +
) } diff --git a/apps/app/tsconfig.tsbuildinfo b/apps/app/tsconfig.tsbuildinfo index 7f486b9..09a0137 100644 --- a/apps/app/tsconfig.tsbuildinfo +++ b/apps/app/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 08eb98ca49a895e98bad924ebdbd32a1bc496340..46b62d520f50b9e6b97c0e3560cd7e8d7532dae5 100755 GIT binary patch delta 28958 zcmeHwd3a4%+y2=n&XE&C#Tio2r`HnyRIf-+iCGc9MGA_j|AJyMF)mTyD>L?)9u^&1I5#F8ZML(Z0vGH2t;fj*o`BoA_x| z9M#+$=)Qk;NVB{-ftsm?$&~*F93hQAzz2AX)=w@e-;5| zunCe0yn=LxX2?d+dnrc^2I`I;f?f}NZu*S0+~m~smV50LZ$~XJln0XC-E5cY)4ACl1#u-wA%(K=(pf zYC$d^p{LJEbj0U$O3j`J9c9VOKNqT*n!#W#Br`ay^mG_7p+m_?NVG=&Zp3>*y2CLu z%uP>CNKHyIm4|Dl=FpQIxoJ7*ourJc=?(%aAO{tT4U1heqlx)(?`7lEAYd17OE)j2)-*BOy6FIx4;i zBwH*#E74Uz59oFBKs<&F*{vH@#)}}?5=oFgkiSOjHHc7lL6CIZ4U*m23X;R4E~KkV z#_M_+3dVw)6u%QX;}1{J`J2%5nDE_+dIl3Anczj0@FnQ1(HGD;o_0ZJO+SQAnX2r= zlw1eNf)y$`6OsiS1j(+OGnLcEWJ*ho>q=>LXw6+2cABCaE`?)n7_N)cTf{ToIrqVb z@>(Y7Et;5}&Ym`Hg3fX_f~4P-FlSFJfMk9-N_V7ZCuSj^B=bEmc80vh)^PrqXArbg@eBoDF{oa1f%1mq(CrhmXJjS9PgD4*hX%>_M1bwG zaFKIo>(1Ku&J(Sh=iQsDH{w-DHu49MTryu(d|Gea+f&e4noRu1&N;9|cQ$dUF2f)> z8m>UH8Isd7;v6W|cBEsbD7=GTR|TH1FpH(Alcj3 z(4KTO8F~xImXJ*$2Y_e%H_n$IYGYfzT4&EVFF(}A;=LA&kMld9r<{k|)X6h0RT;41 z3=4;34l{e}J)fMB5yv@=TwEi=YolKE?3t;v+2Cc+S+#nb^r~7Q*>R=H?kngl;Xdf> zm=Yz|LSm%l<)>t%XVB5T4VtM53}$T69lZs~8Fvtp4(FyMIxsOzDH$2l9C2xh-@~3m zwaYerVt7Kb$<9GCqpuLpj2+wc0veY!<1(jT-o}{nKZYSY@sS;RM(335w#YR`^ShSl zHBQLL=@jqCPE5~m2~+c(IzQbpqfhEgs`1K^bnV^qr~psxu2d?GCiwxb=Dc}Zp#7_ zfOwO$WW3$>B(!1B+@0kw*=^Oa!5M03=287+(-5tn_Jf=r0>?cEXr=)!$G%hSwwciM zGL>iBZLdHJp;K{an%&krNHg`RRA)jR=St~Fuxsy2Oo-Os(tRNElGqTfk2X-wglPRN zpMwdIt$A7>cWjiHz~pqq*==i~u|{sr@_4)D6KGL#Hbe_FPaZ5yhHL$7JFwU33Im(7 z#9?>;3>w?CvdUh;dW)fwF?QP+Xl#IL&hl8hZ3VQR&@4{h1$J8*G^Vw>w0p`@kFyWa zE#24)w&~Dli5kz>0^FhbL#yNTy<*qy$mvkj@LxkU(+D{mss&n_hiImedN2(^#gD--((QeCt#-_G99Wi#xQ_wP8jbm$q z{aPR*Q0bef7&Nx1o6`|*H#Z8E@!@E=h)~VczryBMXk#lh3@lfx>*?1+tBkt>tP|;uJw%g7?8|<_##XgV`h&IuS~x7-oF(Y-yU;lIbuaxVX{LdO zW+|X1XD6VH2vz3%+aEOhWPRDvJUYf+Aw-11A?~IKJ z)V`37ahMPtv5{u^T|H+@g4P3?KIV5r)9Z}I&2kyq0M|0mJ<&+RHF7#Mebp~1vD;p# zh~rTG4q7+*6GweqWRj51c{VoC7K6|v@K{%)?Amt{lZc`WNY+Wr1Pr`w(A4xhVYjtT z(QArgPO)2@(E2#Hjt{gPM`*0`Y;>TlWvXTx1edtDd}TM!O_e6eXrXt(axi0!!tCla zRW^Y&XHJzfkhmUrrJ#|uX)-{Wnxp#F{Mwz z*P*cmFa=TY+UfdAfoh}&xI-DN8)Eu6q4_({rUu%MA*3ha%1|47d0JthIPA93O0$Zi zH5QTeP%BI=7opjmTfYmmHNys9=dqkvGN9SnvF0ac$oMo=?l;+(hF0yBX_SD=*ivY` zTA&^G2e?Dg>%{tc;R^v*WX;KbjD}W`t8E)JuBB)T)bLV89CPlKt#Y=_K&YZbwlea1 zR;bh+XdFvcDPN!kRL6G(E>o2k%9&vFhGK)f2~961N6}qqY6inoFup%97hD?e+HEgG zw<3~oHS-B znS!QY0#%=+Y`Zt1>4UjE2iIL_uFH$wOU1J_QzTNl>*}64YCEH=0dw{G*qn}^?6ybd zsr5-p=3!P)ld~h@v0UB2c3$N(5_n~nE=FW|aTQem8Y+_&A`231C$7os$joYMZPRDb0+k4RH4P}8h zTY+wAcKV_WO2PO`Z9|GU}`Rf#_q+M z7-M%YgT^J@wPST{LOmbWd$F37d!QGgaoANC--Q^K(6~RVu+|QV*L+MO_r;oNA|g;m zOydM-;jWH(5h1ojHC~D>m!JhWx0VE&TNKLp1!&)-Lj3}a-arL*L*p31l8k!)3XNIo z*VzF}s*aFcXx#AX8@@7VoTnVW0q*z)r$@1VDN3s-v270+PBdh4)n%-Ybv8|>B7H{I zW=~lXpkZ4V9cbQCB%AV4+MmI2NSj^PzroA&{WK&-upy;udzAJbc!RElW0HG>V=3}T~6?Qh?m48T<1huzoB=10oo%Db> z5;ePaO1u_p{o9UQrI|9pTJXh81B{Oyd_(Z&j|+ahTGpj2W1TAf01qqGAW2b%9n*N}409ujO&;4 zTvx$HRR*^rI9RtWuMDySd30 z*|ZjOV8IssdV(4Ih28cUG|mWU(ROp|tunqCHgQ|kg}_;U%5HlU8ix#4!-@Qg>GcQ} zjDg$qMbS0!Ez6-{ml73dyNr-tAbtgIv%O;NPQvP=w92{sxMHxfJz=+<`-9eahn}q~ z&Jqr7Aiv3&ckGZ&kD`#@g5mOlVvV<(dzVO)bzpN#^ftrz#)7mTni}oUE<@8VlZW&dC4%y^Z`Xti=k zX$AkN;@2sjk}bFek~QC<6tU5+lHL9+GxnL(8CCv(>H+R zhmv|@74M_-c_s#MpU%`q$!?G=P%lV2?hVNgC0nEqBr_PG;)4`FNXcLoKMazWqfwB| zI06!vB2zLX$5;lWnd2|h6)>5ymCS`?26G@;fVq$?P=V4HLDFF%Bnz}u$z>{jxr$$< z^wmnPf#iphe%C@a;P~66h$kTFaF5dWLh?gN{wYWn>{-QAQh!0kAAn@R4yyS7M&>cW zt11B{OZFNh`}Pe;X7HAZuPRxfV~VF_3mu1~zmFj~zCKg>IVH;>*(Kj7dDR2$&py#G z0GP2Ek{P>0(oqeidn&y?Bn#A7@y(QM1xd&4AepWsBn#MC>3%A{r_y^VyENu=|C6NMIb}!5p?gv3l+5_D(kW?wMd_7NT}eo=M&Bues*;YbDV~xe z>nBL^cNI^`41QMpuaK-Ef5YI&vGDqJpQODt4-pJhhim}pqYNqOpoP*YS-_T%jUfXS z|9_Qv|5pe9m-ML6I8#4W6H3;2AS9c1FeIC97$jS0I3#|JGetr2L&^AwkhGtqc$bv# z{-HI(?@K1r6h*tFY`uuh6Ri^zO-VgT=~X35GgZZ>DVYw*(qt$-6O#Rs1IZ61<8xK~ zEG6eava}0pVk^r81t9pLWC0#lvQWvTkaSc8$xnqW-G4|M^!LV*ZS(iW@$Ze}-y6sO zX2Zx8gm!;#99@g=-y6sOv|;36VLX@Ezc-GqW~kgSvT?YP{Cnf5HYWd<4IrEHf0O?2 zjpP4fn+oZxE*?qBn@> zKoAE=d?s2T2=_i95(7b;mHi|Rl4#He#24b|10tm_h@&LR#j`Jn#{EF#_62cX%19g| z(Yhaq3zFFn#O(ecPLjANzWqV89RQ-RKZwh6g2X8j{sTZ1(7%e#BZ{n#6c1bhJv^!j-en@LO>h^Vb-ci-4M5N(s&q{ z+z^!?GzvsW7>He?Ks1mWB<_%i z3b2jL|V;UJOXfPT?b8(CYkunCv zQ4+r5IR-@Iu^@8CfM_XYB#x12Jr+c3$s7w}b|i?CB-)5?B#5@-KomxTXeTE~oFd^r z4nzkj7zdd98xoy0E+L_jo%bx|O?NI8j1B!Z(s_(^dzhz&6yZjVgiWhi69P;2o!B12=`bJi4#HemHi|Rl4uYM zqQ5v|L8MFqag;=mcuoS*cru9GNgxJE8Hr;gT2BTMESZx*%$@?`B#EKoI|W2r2Z+Kc zAcn~a5~oP`J3x$(0tbkqI1m>|j1<2(5CQQZ*2RGclX4Q5NCd}&h>+rV5E~Le+$J$b zf)YT4B!bwL03uRukhnu4G7&_Slq7=KlLW$&1R_Qvl0ZZ!gE&B9f@sMg+*3d#CWDBT z{Ui>OXpjP8vN%#eq@;p4O2Q$YsUR9p1(BNyB3{Z!93#y6ix$?Dkn&sBH^C~Vwx1Bfhd{|;sS~3;x`>cKstzZ(?MiNIf+Xog401{N^v@f z4H+PAlgO5!3=knRK#TnE_%?CJ0L=h&d9G2_iZR!~qiXM9Tu< zo(&=~3&aB1PvRhn2H7C;#E}gmB?rV&5(VO!1EO&*h};|y4@((|VCToAKo zf;dTHsrb$W(RLPy!kHkJ$q5ptNchhJu|f)Ffhd{{;sS|P;x`*az#I_kW`kHGeV z2%ZC?Sc>O>*f1BwZ4yoinhPRi9*A9YL9CM-B<_%ioCiX1b_B$p`5-LwL2Q(W`5>Yf zfH**6vuF!IxGw~exB$df*-zpii3STnY!}Bu5Gi>ej*=)5&pZ%~^Fie1fp|>HNE{>4 zIv>O?$;<~ay8y&V61&B>07TnGAPNgW?3EKFPLc3m1mYp+mnC8;i0C2^2S~gsS`i5MWgrrZK)f#dNgO26U>S%*;#dYEWjTnWB+A5dIf%w9 zK;$k5aYV{U93#ABf*-5CLmItXmD@xRjH)L?U<%h!ax023OUO~3EB)IWDAI0n?c-`8zkk z1roKy?{N?TyFjda9K-`sPT~@Y;9Vd*rFa*J4Nri$O`@IzJpm$QH;7$NfM_5$Kxhq1 zhwXMdG|Ks9L|Ytls#fF6A^P70Tp6O3h8=P9HT+)=QBM@ zBaXOrcg4SEW?JcEuI6s(fFomrTj`FMs#&V>|K}edbKUa)_y4Fr$!q;Sw_=tcO5^|4YVrR))%pL5 z5X8kX)UdiozB}(|10{X;GYH_v(hfIpV-L!Gz1S@(YYsRp0H%7%P@ z#Rba=tEFDwAu5U)yrg^#RUCg1%Tip3;`poFRQ`w0F!8Mp7sC-n&ve{QFQ{pxGRK`T zQwPP30>@I}h6n%n##QQY$%Ko%2t<+Q&kl*o`$XlUHaOng=OTfn)aQ zaMzDJVSRA?osgeI#qpP;Es9H09DkvG zN^!}G!{4}>MyQIUh{GLqOjS|%J26wPGM}orCg9#s9WzaF-r$ZXE=_Sh;EpM7y5iWB zA1E$eam~Pernn59o5Vj?GCiWm8OpE)IOF(9CO9^vFOUp7*X#jD`5;h^p#ar16aTS( zErD0Sv3plZgbhQX7*QOIt%1#Isy z%G?i878SKuaX4jbvMTOL#o@TH=@rfwe4dhGy4Z&(jwpY?i8kU?IH=5dRAnIB8lRWJ zu_8E)Y}yJi&8s;6i6L`Xnd2OrX}gLlQydOXn%GACyrDQ8H8v%qWSmAv6o)ftrUhzb zzp1#s;C^PF_#9On&V5zuAcSwQC-HetPSij%y|1EzL1iMU1>^_dSiB)Xw&IQ}ZYa15YRH@r-})9c_kBFmK>4xyT!RCZs9XDzt+T1Da9scDdk zx}aQ)1Gij_u#3tr3fv0CT@nXfTvkyrh+3_pt|)FixLxQ?9F8+xRon!GcPs8&*-sbW zsi;^)J*}d?SKK6U&nfO2I2Lj;uwQXEM5_%KHhxjkxxOhzk zcr1>ex+pwXt!V_p9QF0&23^!wQPV-)!T=ut*+6mW2+xFzV917w%Ru-NhSpHXMiTS@ zTy#=VnV{AoDg?5#;<6Avj}r0IMRC~(mnyES6w`&jipoWl4K6|=$c0O!a} z0m^O`!e_y8DE5+ay6CN<=70)D6o*)#;^rd!7Qjy*#mz(bTh&W_#jg%r^g|T>nHB)X zkLrVz`9g5@V4eaw7#vgP0sP^ZO+8c!=psZ#6@U^{gl#fHxmbiS*Fx4mRB;a@{F-Wu zk#d4AMyaSmMAd`~wn-Q`=C}k9RE|5o(aLTq!c*Xa6&fSHo^UZ%MJ)rxC6k{>#Vtpe zCj|Hzr??deH-vvyG74vX;UZc^tpYV4;e5y##jQqo0mA%@SKJze7b?36QbrdORa7ye z3Y2-Q;vNCV#d;PB710pL*xeo~d)MueNdjuoG(xJ?MJMv+)tP7=7TZQ861S++FUk22MruI#UK z>fp1dy?j#FGO7Vjm5czwfN&rJ7%ltjTk1(@JxjheT;8c?`LrE(dOY&M;~~iaPm}Nf zN;D7yj0YwF!E(C3rAvdl2-X9*q2vCo5zts_HL!H?;6IFkXM&xw`kUY4-DJ_zz)Q&Yf>jtl^L0sM9CJn#*` zBVX@h(D01d8Q?R3=g2+*GLc;tkPVzhC-U6cKY@>ccL5$Oo(H^#^!x#n$LJ0Lhk>#h z7_LX8ZxhQr9~1%0fK{@!iDi5690cbA^MC~ae=Qye3<8D#LxFESa99lZ z9=HbF0B!=efFFR{QtWN<_WcdP-+_Aoze-$&yaHH&Vqk-O;B9FXvmL=5KnbuD7!J$> z76PMzaX>UM9+&`31SSFTKmyPf;HHMhf`0+HnYjtvl1@IB$+!!#%E!{dZv&W(z$Rcb zFdaw-GJvVTWMB%w^X3u2=+etRmQT&KaAwJM4dYpYQDB~Hx1DFau5r_pogdPpq7w`u<0d0XgKy`pe zTVDqb0f&JyfX5kL26ztlVPFNY5@-ZCF{l!OBp?}>=4O)PEi7JsJb2v}%S9e!0k9Y- z1eO3xfg)fTupF2Lj4k!@wM1#S>6!VUrCzOGXaSxj{0PA5A2ZWylR?DuImNx!e?UsV(dEuvlXMhn{ z&hS^>ru_(qLvDeKrWi@h0bd|lv{sgQ$6O@kL9{F&8?d0tH35EU;m@l)%~%fb+~(VG zF%sG2Bis&dn*uy;eiiryX?O;n8{RB{o7@y&TItbNmO#xfAHI6PhPxInw%mH~B;++K zBAx_y_M}n zv;((=A4tJSOTA&s5#;{UenFDENM0RomO`%!sR8NW7C^rPX@UG1+&Q2f!0qEn zIXx1q+P@Io4U@gVX@pM!p8}r%ydgrK@t;9I0~ofHXO;dH6L1;fUPyce@+!bh(@#Je@FVa(Pz<}< zkUs#ofSbS#;5u*(NMlnf>4y570S)*K9ES=k@T(j4Q&l7HA)*?LOu+95-yc_1r%hFz z85o&V<#}6RGQfK(4S{+Cn3+(cHsx?IJ8_1$d4iSp)MZsTbQLe?p8Jp$4X{JjM0hnVBrp(dl=V#2hs+e z`x0)h)8DJtNFeEu66K0@{|Im!I=Nw|w z&IjfK%xpHmW}gW#;~XFx$O2{n=|CDV6-Z(KCnJyqBm(h(1DFEP@%@Ehrg4B#*!v60 ziAgR&Nuxrvr$5G#W7st|*#9ILK_g~pglWXu8wnU@4H=)Rq>%w_r`^ZV-lzc6OusLl z1;{fpp+SZ+FlxudG%{>hyG&(c6w;^}3uaV=8Pk^O3?D|EVV{dQ_{ghhN*d47NtYv| zHH^%e;hg*888=UHv^QGH)wK8VOQ7ci3jt$REoT2e3}TUrWOt|^MD7(JTnKOrzy!w- zUIr8an}BzKqkt1&+#%p?;0W**upFR0ugGsg8vGk7?l9ybX81br8gKx35qJT39@r1y zfe@y)z;g&c1?&OVqA+y28{u8RZlC@t8`3O)9tO594eHB2SI^{Zm+qdPGCIaG-ojS;R$h*=w9LB>+8CE7VQK=* z0B;B8K!yU`;MRm*9dHAFLz-WKo-n@!c@y{r_!+nh`~>^}{0Q6zxRJaA`8&XLylZn0 z(rm#*)Sy^^Y5*fyH$z&%QJ;=_^9MxoH6T3zTNO~R1u&iyj`uh7Avx)4Bg`8)b$|x| zPk=is-ZCA_zmKY?2$F4i`wE{sJ43}eA7R~sGc&27*6!LHvNhD&!FD20e>B2mIT330(o*Cq#Z0k`w08G4o?II?tNnKW z6VLu`W=>r@2jC04jDbNn2>(&`}w!MNRYm~LUr7bM#ir)=O z0~IVzur`qAu3Fs94e%g1nAqA4)PFWFlUiZcWFOA(mxWtcTO_KTD|_N4FAE;U*xR({*}XuO5R`Dzi{VcJTEo!pYx9G zu4VriL!hTV1p0)@Bs7VX}{g#xrj3&I(HV ze%7p3FznXZk8@^cn0yHff8+7HCkFg7>1sruPhiotv!A`QpUHTl@03HSXOkXIYEl{V zp>&F}dU+d9>U}jUW?%T~T_YdJ#k3uPoX9eH;rG4Dxb(wEh8nz0T=V)O( zUAS}Lvp)selUu{Wud`og{2mk(E?-As#6*Nk<7jKBkMW%1)brQ!TYdkk$z|DgHoS)Ox%7Cm%(2b}y)-UlC9N zO@&M4brHL^6q0iDEn%w`d6onjmC^O%3C;5t|MhnpN(taH<1>EWUOxVT`E7PiW^7;1# z#VFZJ6Xh_x;~71tQ>@9VEsctsjT+xqQ@NR99d&9nx$%Rxjg0@bA@J zw{riA#?4)m#u5{R` zx#miKp0$AmK}@*s;iBiC&P=ZPCgbVk)t@{+W$uh$IA?Le=!Ja~mViF#81BY%&5z}6 z&A;b=(GM2gv8o_#Qk=|4#{|iVlgA-@F2J{h2hidFy4<`1*d*XSMxGSH3<%u3k-f~}asj_^=6WCW5*IPC!s_rCLuGq@r zrnAh;w2s6dmb{aREtv5{_LtgKoABV76<5s~u0%95)k%@iEaW{aMUt{`G5U8)R}Eom zO_2{_>0>;^eXrM}XXlO!2u7Z`SaCa7Wi9u?Rp38dFRCn)RabeRsj`ii53I>E=2;t5 zL#{8&iEMqfW>*=P3w^|R_59Nz;#w??w+UR!#trDVlSZSIJ@qQzm$$d^u=&@!&nUid zz8CAmK6T~IYmc|_RQf9?$DY3#yKsWbz;)44g)=T8_g_+!j%$gl4po}Rb?H+D(JwU( zn`1ElMNW3th_9N4yCF85aTW9V>kFg0g83Uyl&`fRGV7BVzd^2UbJfCl_I!=&kuO;X zyi?QFRIWyJ%}XzD<6-rtfXkt2r?Y}84P4XJ%N&v>9cQ7#|GGP3(_{)PdKwSN_n);c z@t19%j;(ZMJVO7if6Z83Z`AV-RmQXolV_0D-*^ard`5YAL5jU^rG@dh{+(fChljl8 z`$T2TJMzmcbfNLQ|D$z&pWc0&cVMN#E$K4b>ebVDLBQ1>QB#v{vUN-8+9_?La^wVHMHoTHcmoy#Wvnka6aqPm*$7nyyc2<%{yaX z@z-O*81|#EJMuAJaPY&UH%~0vv%e)AsqcwZ#@rTI-Z$2K{;O%gt()5LcpI-yn38&B z%GYm352(!PF9y3&097NS0LGj!Xnmyo`3P~X>H3_nsDR;CHIU)fppEu3Ci8vS8T&4* z6RYN9i{jcvHmrsz_PLblgsXjxWcRuCHr@l#L}o;Gjp)HnSMB{5t)R;B=hr&dP8}uw za)Hr74Vz(P884UEFin0q&~`GHb@dJ5&ojZHd|Vv8?%ygKJ!ia|Vz&D`xt}-R*5TjE ztSaeaykO<{#h1&@4V~+w^2LIO#tzLx87niwWwvrxZ|w3tvgAXWxr*t!Y^z`cw7wHs1NMen?>T zeKkI@{#!z$%0_{VX4MryW{O{A-U^OyJ$X;{ z`i%VYqR(>nk9yY~?Se0Ryf7K>#V9U4awPRZzoJUV1LjEAM^L26b0lD?wY^Us9wF?B zBHdqA%dn+3Yz_K3|69P3Osv%dGk4uJdH}a_r6r z%#$ydW5aE{ZKeK-Ghao%e{>i8U_ZkhTFgA@yaM;nR7)?MC+k;WKorfBcPTyQ%kPjr z#(Q24*S@%S=nDy~8}E?feqz)461ozWZRShPW^lfGfuVl{j!DPy0)F}=jq)@8V#er+}G7amw5hu2t>{Eaua zJmnj`^XI~QylJG?bK~7E-`u+L-O&Ip+ZqwcNuDbgNboT3J zyhx^~{<60}8u%t|yP{S6v3D`;UM+pr;hV|w)upG_S$CSv-fLz42K1Woewp#wi?0nk z@(4FlT|47e9&W!6D*bwcb*hHH<_p_oP4+h4!xA<)q}k>g&W#W9YnQ8GzmgN1(6Haj zRf>CJ*=$WVdu^2T&Dcz-7s=#*-fPV2mU}Ocah1uH%y`wzxcx8syT6y&1u;GJJ3RX~ z%4vrZj;C#gWo&CzT}v~Kq-@Jo2_0x#@lReTyK47XZM~TzyMzbIq2$bl2%(V z@{RY~e1B&3H_w&+e$eFts}6qO-6C1A@HSpxb13=D=Ho9f!<_^)S})Fxx8%?kj1o+-PupFk%Xe7c((-a~B&a4x@nd~(1yg=_0L5nl`O!NVH+U%wX+x&wM6=}u^dPIv@ZIq;&S-jpWN(3z5*z_la$q|-n4Y;x#~ULI=N!HbgF9CN&oqy zb58QyN}xSzX6>E`n4PsDrdPz635 zA|ry`Tgi-I_ZA-R!}MAl8z!5A-GBGEqx1p8Y1CTY9^yXCqe#8v@T1|SO@_KR)czl} C;vk;@ delta 28518 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 d33$~BGn{_u +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = 'TableBody' + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)) +TableFooter.displayName = 'TableFooter' + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = 'TableCell' + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = 'TableCaption' + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}