feat: access tokens (create, list, delete)
This commit is contained in:
parent
415bba96f0
commit
90232feb1e
58
apps/api/src/access-token/create.ts
Normal file
58
apps/api/src/access-token/create.ts
Normal file
@ -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<typeof AccessTokenCreateInput> = 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)
|
||||||
|
})
|
||||||
|
}
|
45
apps/api/src/access-token/delete.ts
Normal file
45
apps/api/src/access-token/delete.ts
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
16
apps/api/src/access-token/index.ts
Normal file
16
apps/api/src/access-token/index.ts
Normal file
@ -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)
|
49
apps/api/src/access-token/list.ts
Normal file
49
apps/api/src/access-token/list.ts
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
@ -5,10 +5,9 @@ import { apiReference } from '@scalar/hono-api-reference'
|
|||||||
import { cors } from 'hono/cors'
|
import { cors } from 'hono/cors'
|
||||||
|
|
||||||
import changelog from './changelog'
|
import changelog from './changelog'
|
||||||
import { changelogCommitApi } from './changelog/commit'
|
|
||||||
import version from './changelog/version'
|
|
||||||
import user from './user'
|
import user from './user'
|
||||||
|
|
||||||
|
import { accessTokenApi } from './access-token'
|
||||||
import pageApi from './page'
|
import pageApi from './page'
|
||||||
import { authentication } from './utils/authentication'
|
import { authentication } from './utils/authentication'
|
||||||
import { handleError, handleZodError } from './utils/errors'
|
import { handleError, handleZodError } from './utils/errors'
|
||||||
@ -21,7 +20,7 @@ export type Variables = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||||
// defaultHook: handleZodError,
|
defaultHook: handleZodError,
|
||||||
})
|
})
|
||||||
|
|
||||||
// app.use(
|
// app.use(
|
||||||
@ -37,6 +36,7 @@ app.use('/v1/*', authentication)
|
|||||||
app.route('/v1/user', user)
|
app.route('/v1/user', user)
|
||||||
app.route('/v1/changelog', changelog)
|
app.route('/v1/changelog', changelog)
|
||||||
app.route('/v1/page', pageApi)
|
app.route('/v1/page', pageApi)
|
||||||
|
app.route('/v1/access-token', accessTokenApi)
|
||||||
|
|
||||||
app.doc('/openapi.json', {
|
app.doc('/openapi.json', {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.0',
|
||||||
|
@ -20,12 +20,15 @@ const generatedToken = async (c: Context, next: Next) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(accessTokenResult)
|
|
||||||
|
|
||||||
if (!accessTokenResult) {
|
if (!accessTokenResult) {
|
||||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
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)
|
c.set('user', accessTokenResult.user)
|
||||||
|
|
||||||
await next()
|
await next()
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-router": "^1.58.15",
|
"@tanstack/react-router": "^1.58.15",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
72
apps/app/src/components/AccessToken/Delete.tsx
Normal file
72
apps/app/src/components/AccessToken/Delete.tsx
Normal file
@ -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 (
|
||||||
|
<Tooltip>
|
||||||
|
<AlertDialog open={isOpen}>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={'ghost-destructive'}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2Icon strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete your
|
||||||
|
changelog and remove your data from our servers.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild>
|
||||||
|
<Button onClick={removeChangelog} variant={'destructive'}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Delete access token</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
35
apps/app/src/components/AccessToken/Table/Columns.tsx
Normal file
35
apps/app/src/components/AccessToken/Table/Columns.tsx
Normal file
@ -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<typeof AccessTokenOutput>
|
||||||
|
>[] = [
|
||||||
|
{
|
||||||
|
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) => <AccessTokenDelete id={props.row.original.id} />,
|
||||||
|
},
|
||||||
|
]
|
83
apps/app/src/components/AccessToken/Table/DataTable.tsx
Normal file
83
apps/app/src/components/AccessToken/Table/DataTable.tsx
Normal file
@ -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<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead
|
||||||
|
key={header.id}
|
||||||
|
style={{ width: header.getSize() }}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -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 (
|
|
||||||
<div className="hidden border-r bg-muted/40 md:block">
|
|
||||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
|
||||||
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
|
||||||
<Link to="/" className="flex items-center gap-2 font-semibold">
|
|
||||||
<span className="">boring.tools</span>
|
|
||||||
</Link>
|
|
||||||
<Button variant="outline" size="icon" className="ml-auto h-8 w-8">
|
|
||||||
<BellIcon className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Toggle notifications</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<nav className="grid items-start px-2 text-sm font-medium lg:px-4 gap-2">
|
|
||||||
{NavigationRoutes.map((route) => {
|
|
||||||
if (!route.childrens) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={route.name}
|
|
||||||
to={route.to}
|
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
|
||||||
activeProps={{ className: 'bg-muted text-primary' }}
|
|
||||||
>
|
|
||||||
<route.icon className="h-4 w-4" />
|
|
||||||
{route.name}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
key={route.name}
|
|
||||||
defaultValue="changelog"
|
|
||||||
>
|
|
||||||
<AccordionItem value="changelog">
|
|
||||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
|
||||||
<AccordionContent className="gap-2 flex flex-col">
|
|
||||||
{route.childrens.map((childRoute) => (
|
|
||||||
<Link
|
|
||||||
key={childRoute.name}
|
|
||||||
to={childRoute.to}
|
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
|
||||||
activeProps={{ className: 'bg-muted text-primary' }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
<childRoute.icon className="h-4 w-4" />
|
|
||||||
{childRoute.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div className="mt-auto p-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="p-2 pt-0 md:p-4">
|
|
||||||
<CardTitle>More Infos</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
If you want more information about boring.tools, visit our
|
|
||||||
documentation!
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
|
||||||
<a href="https://boring.tools">
|
|
||||||
<Button size="sm" className="w-full">
|
|
||||||
Documentation
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<Sheet>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" className="shrink-0 md:hidden">
|
|
||||||
<MenuIcon className="h-5 w-5" />
|
|
||||||
<span className="sr-only">Toggle navigation menu</span>
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="left" className="flex flex-col">
|
|
||||||
<nav className="grid gap-2 text-lg font-medium">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="flex items-center gap-2 text-lg font-semibold"
|
|
||||||
>
|
|
||||||
<span className="sr-only">boring.tools</span>
|
|
||||||
</Link>
|
|
||||||
{NavigationRoutes.map((route) => {
|
|
||||||
if (!route.childrens) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={route.name}
|
|
||||||
to={route.to}
|
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
|
||||||
activeProps={{ className: 'bg-muted text-primary' }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
<route.icon className="h-4 w-4" />
|
|
||||||
{route.name}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
key={route.name}
|
|
||||||
defaultValue="changelog"
|
|
||||||
>
|
|
||||||
<AccordionItem value="changelog">
|
|
||||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
|
||||||
<AccordionContent className="gap-2 flex flex-col">
|
|
||||||
{route.childrens.map((childRoute) => (
|
|
||||||
<Link
|
|
||||||
key={childRoute.name}
|
|
||||||
to={childRoute.to}
|
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
|
||||||
activeProps={{ className: 'bg-muted text-primary' }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
<childRoute.icon className="h-4 w-4" />
|
|
||||||
{childRoute.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
<div className="mt-auto">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="p-2 pt-0 md:p-4">
|
|
||||||
<CardTitle>More Infos</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
If you want more information about boring.tools, visit our
|
|
||||||
documentation!
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
|
||||||
<a href="https://boring.tools">
|
|
||||||
<Button size="sm" className="w-full">
|
|
||||||
Documentation
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
@ -34,10 +34,8 @@ export function Sidebar() {
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarMenu>
|
<SidebarChangelog />
|
||||||
<SidebarChangelog />
|
<SidebarPage />
|
||||||
<SidebarPage />
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -28,60 +29,62 @@ export const SidebarChangelog = () => {
|
|||||||
}, [location, setValue])
|
}, [location, setValue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||||
<SidebarMenuButton asChild tooltip="Changelog">
|
<SidebarMenuItem>
|
||||||
<Link
|
<SidebarMenuButton asChild tooltip="Changelog">
|
||||||
to="/changelog"
|
<Link
|
||||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
to="/changelog"
|
||||||
>
|
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||||
<FileStackIcon />
|
>
|
||||||
<span>Changelog</span>
|
<FileStackIcon />
|
||||||
</Link>
|
<span>Changelog</span>
|
||||||
</SidebarMenuButton>
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
<span className="sr-only">Toggle</span>
|
<span className="sr-only">Toggle</span>
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{!error &&
|
{!error &&
|
||||||
data?.map((changelog) => (
|
data?.map((changelog) => (
|
||||||
<SidebarMenuSubItem key={changelog.id}>
|
<SidebarMenuSubItem key={changelog.id}>
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/changelog/${changelog.id}`}
|
to={`/changelog/${changelog.id}`}
|
||||||
activeProps={{
|
activeProps={{
|
||||||
className: 'bg-sidebar-primary',
|
className: 'bg-sidebar-primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{changelog.title}</span>
|
<span>{changelog.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<SidebarMenuSubItem className="opacity-60">
|
<SidebarMenuSubItem className="opacity-60">
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
<Link
|
<Link
|
||||||
to="/changelog/create"
|
to="/changelog/create"
|
||||||
activeProps={{
|
activeProps={{
|
||||||
className: 'bg-sidebar-primary',
|
className: 'bg-sidebar-primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<PlusIcon className="w-3 h-3" />
|
<PlusIcon className="w-3 h-3" />
|
||||||
New changelog
|
New changelog
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
</SidebarMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -28,57 +29,59 @@ export const SidebarPage = () => {
|
|||||||
}, [location, setValue])
|
}, [location, setValue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||||
<SidebarMenuButton asChild tooltip="Page">
|
<SidebarMenuItem>
|
||||||
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
<SidebarMenuButton asChild tooltip="Page">
|
||||||
<NotebookTextIcon />
|
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||||
<span>Page</span>
|
<NotebookTextIcon />
|
||||||
</Link>
|
<span>Page</span>
|
||||||
</SidebarMenuButton>
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
<span className="sr-only">Toggle</span>
|
<span className="sr-only">Toggle</span>
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{!error &&
|
{!error &&
|
||||||
data?.map((page) => (
|
data?.map((page) => (
|
||||||
<SidebarMenuSubItem key={page.id}>
|
<SidebarMenuSubItem key={page.id}>
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/page/${page?.id}`}
|
to={`/page/${page?.id}`}
|
||||||
activeProps={{
|
activeProps={{
|
||||||
className: 'bg-sidebar-primary',
|
className: 'bg-sidebar-primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{page.title}</span>
|
<span>{page.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<SidebarMenuSubItem className="opacity-60">
|
<SidebarMenuSubItem className="opacity-60">
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
<Link
|
<Link
|
||||||
to="/page/create"
|
to="/page/create"
|
||||||
activeProps={{
|
activeProps={{
|
||||||
className: 'bg-sidebar-primary',
|
className: 'bg-sidebar-primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<PlusIcon className="w-3 h-3" />
|
<PlusIcon className="w-3 h-3" />
|
||||||
New page
|
New page
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
</SidebarMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
96
apps/app/src/components/SidebarSettings.tsx
Normal file
96
apps/app/src/components/SidebarSettings.tsx
Normal file
@ -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 (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage
|
||||||
|
src={user?.imageUrl}
|
||||||
|
alt={user?.fullName ?? 'Avatar'}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">{user?.fullName}</span>
|
||||||
|
<span className="truncate text-xs">
|
||||||
|
{user?.primaryEmailAddress?.emailAddress}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
|
side={isMobile ? 'bottom' : 'right'}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage
|
||||||
|
src={user?.imageUrl}
|
||||||
|
alt={user?.fullName ?? 'Avatar'}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="rounded-lg">
|
||||||
|
{user?.firstName?.substring(0, 1)}
|
||||||
|
{user?.lastName?.substring(0, 1)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">
|
||||||
|
{user?.fullName}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-xs">
|
||||||
|
{user?.primaryEmailAddress?.emailAddress}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link to="/user">Profile</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<SignOutButton>
|
||||||
|
<button type="button">Sign out</button>
|
||||||
|
</SignOutButton>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ChevronsUpDown } from 'lucide-react'
|
import { ChevronsUpDown, KeyRoundIcon } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
SidebarSeparator,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
||||||
@ -25,6 +26,18 @@ export function SidebarUser() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild tooltip="Access Tokens">
|
||||||
|
<Link
|
||||||
|
to="/access-tokens"
|
||||||
|
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||||
|
>
|
||||||
|
<KeyRoundIcon />
|
||||||
|
<span>Access Tokens</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarSeparator />
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
67
apps/app/src/hooks/useAccessToken.ts
Normal file
67
apps/app/src/hooks/useAccessToken.ts
Normal file
@ -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<typeof AccessTokenOutput>
|
||||||
|
type AccessTokenList = z.infer<typeof AccessTokenListOutput>
|
||||||
|
type AccessTokenCreate = z.infer<typeof AccessTokenCreateInput>
|
||||||
|
|
||||||
|
export const useAccessTokenList = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['accessTokenList'],
|
||||||
|
queryFn: async (): Promise<AccessTokenList> =>
|
||||||
|
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<Readonly<AccessToken>> =>
|
||||||
|
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<Readonly<AccessToken>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `access-token/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['accessTokenList'],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -21,10 +21,12 @@ const IndexLazyImport = createFileRoute('/')()
|
|||||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||||
const PageIndexLazyImport = createFileRoute('/page/')()
|
const PageIndexLazyImport = createFileRoute('/page/')()
|
||||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||||
|
const AccessTokensIndexLazyImport = createFileRoute('/access-tokens/')()
|
||||||
const PageCreateLazyImport = createFileRoute('/page/create')()
|
const PageCreateLazyImport = createFileRoute('/page/create')()
|
||||||
const PageIdLazyImport = createFileRoute('/page/$id')()
|
const PageIdLazyImport = createFileRoute('/page/$id')()
|
||||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||||
|
const AccessTokensNewLazyImport = createFileRoute('/access-tokens/new')()
|
||||||
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
||||||
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
||||||
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
||||||
@ -57,6 +59,13 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
|||||||
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
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({
|
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||||
path: '/page/create',
|
path: '/page/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -79,6 +88,13 @@ const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
} 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({
|
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
@ -130,6 +146,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexLazyImport
|
preLoaderRoute: typeof IndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/changelog/$id': {
|
||||||
id: '/changelog/$id'
|
id: '/changelog/$id'
|
||||||
path: '/changelog/$id'
|
path: '/changelog/$id'
|
||||||
@ -158,6 +181,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PageCreateLazyImport
|
preLoaderRoute: typeof PageCreateLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/access-tokens/': {
|
||||||
|
id: '/access-tokens/'
|
||||||
|
path: '/access-tokens'
|
||||||
|
fullPath: '/access-tokens'
|
||||||
|
preLoaderRoute: typeof AccessTokensIndexLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/changelog/': {
|
'/changelog/': {
|
||||||
id: '/changelog/'
|
id: '/changelog/'
|
||||||
path: '/changelog'
|
path: '/changelog'
|
||||||
@ -260,10 +290,12 @@ const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren(
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
|
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/page': typeof PageIndexLazyRoute
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
@ -277,8 +309,10 @@ export interface FileRoutesByFullPath {
|
|||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
|
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/page': typeof PageIndexLazyRoute
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
@ -293,10 +327,12 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
|
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
|
'/access-tokens/': typeof AccessTokensIndexLazyRoute
|
||||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||||
'/page/': typeof PageIndexLazyRoute
|
'/page/': typeof PageIndexLazyRoute
|
||||||
'/user/': typeof UserIndexLazyRoute
|
'/user/': typeof UserIndexLazyRoute
|
||||||
@ -312,10 +348,12 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/access-tokens/new'
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/$id'
|
| '/page/$id'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
|
| '/access-tokens'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/page'
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
@ -328,8 +366,10 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/access-tokens/new'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
|
| '/access-tokens'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/page'
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
@ -342,10 +382,12 @@ export interface FileRouteTypes {
|
|||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/access-tokens/new'
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/$id'
|
| '/page/$id'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
|
| '/access-tokens/'
|
||||||
| '/changelog/'
|
| '/changelog/'
|
||||||
| '/page/'
|
| '/page/'
|
||||||
| '/user/'
|
| '/user/'
|
||||||
@ -360,10 +402,12 @@ export interface FileRouteTypes {
|
|||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
|
AccessTokensNewLazyRoute: typeof AccessTokensNewLazyRoute
|
||||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
||||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||||
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
||||||
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
||||||
|
AccessTokensIndexLazyRoute: typeof AccessTokensIndexLazyRoute
|
||||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||||
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
||||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||||
@ -371,10 +415,12 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
|
AccessTokensNewLazyRoute: AccessTokensNewLazyRoute,
|
||||||
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
||||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||||
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
||||||
PageCreateLazyRoute: PageCreateLazyRoute,
|
PageCreateLazyRoute: PageCreateLazyRoute,
|
||||||
|
AccessTokensIndexLazyRoute: AccessTokensIndexLazyRoute,
|
||||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||||
PageIndexLazyRoute: PageIndexLazyRoute,
|
PageIndexLazyRoute: PageIndexLazyRoute,
|
||||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||||
@ -393,10 +439,12 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/access-tokens/new",
|
||||||
"/changelog/$id",
|
"/changelog/$id",
|
||||||
"/changelog/create",
|
"/changelog/create",
|
||||||
"/page/$id",
|
"/page/$id",
|
||||||
"/page/create",
|
"/page/create",
|
||||||
|
"/access-tokens/",
|
||||||
"/changelog/",
|
"/changelog/",
|
||||||
"/page/",
|
"/page/",
|
||||||
"/user/"
|
"/user/"
|
||||||
@ -405,6 +453,9 @@ export const routeTree = rootRoute
|
|||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.lazy.tsx"
|
"filePath": "index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/access-tokens/new": {
|
||||||
|
"filePath": "access-tokens.new.lazy.tsx"
|
||||||
|
},
|
||||||
"/changelog/$id": {
|
"/changelog/$id": {
|
||||||
"filePath": "changelog.$id.lazy.tsx",
|
"filePath": "changelog.$id.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
@ -427,6 +478,9 @@ export const routeTree = rootRoute
|
|||||||
"/page/create": {
|
"/page/create": {
|
||||||
"filePath": "page.create.lazy.tsx"
|
"filePath": "page.create.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/access-tokens/": {
|
||||||
|
"filePath": "access-tokens.index.lazy.tsx"
|
||||||
|
},
|
||||||
"/changelog/": {
|
"/changelog/": {
|
||||||
"filePath": "changelog.index.lazy.tsx"
|
"filePath": "changelog.index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
36
apps/app/src/routes/access-tokens.index.lazy.tsx
Normal file
36
apps/app/src/routes/access-tokens.index.lazy.tsx
Normal file
@ -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 (
|
||||||
|
<PageWrapper
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
name: 'Access tokens',
|
||||||
|
to: '/access-tokens',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex w-full gap-5 justify-between items-center">
|
||||||
|
<h1 className="text-3xl">Access Tokens</h1>
|
||||||
|
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/access-tokens/new">Generate new token</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{data && !isPending && (
|
||||||
|
<DataTable data={data} columns={AccessTokenColumns} />
|
||||||
|
)}
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/access-tokens/')({
|
||||||
|
component: Component,
|
||||||
|
})
|
136
apps/app/src/routes/access-tokens.new.lazy.tsx
Normal file
136
apps/app/src/routes/access-tokens.new.lazy.tsx
Normal file
@ -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 | string>(null)
|
||||||
|
const accessTokenCreate = useAccessTokenCreate()
|
||||||
|
const form = useForm<z.infer<typeof AccessTokenCreateInput>>({
|
||||||
|
resolver: zodResolver(AccessTokenCreateInput),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (values: z.infer<typeof AccessTokenCreateInput>) => {
|
||||||
|
accessTokenCreate.mutate(values, {
|
||||||
|
onSuccess(data) {
|
||||||
|
if (data.token) {
|
||||||
|
setToken(data.token)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
name: 'Access tokens',
|
||||||
|
to: '/access-tokens',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New',
|
||||||
|
to: '/access-tokens/new',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex w-full gap-5 justify-between items-center">
|
||||||
|
<h1 className="text-3xl">New access token</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{token && (
|
||||||
|
<div className="flex flex-col gap-3 w-full max-w-screen-md">
|
||||||
|
<h2 className="text-xl">Your token</h2>
|
||||||
|
<pre className="bg-muted text-xl p-3 rounded text-center flex justify-between items-center">
|
||||||
|
{token}
|
||||||
|
<Button
|
||||||
|
onClick={() => copy(token)}
|
||||||
|
size={'icon'}
|
||||||
|
variant={'outline'}
|
||||||
|
>
|
||||||
|
<CopyIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Reminder</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your token is only visible this time. Please notify it securely.
|
||||||
|
If you forget it, you have to create a new token.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.history.back()}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!token && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8 max-w-screen-md"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="CLI Token" {...field} autoFocus />
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex items-center justify-end gap-5">
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.history.back()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/access-tokens/new')({
|
||||||
|
component: Component,
|
||||||
|
})
|
@ -26,47 +26,45 @@ const Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
||||||
<>
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex flex-col gap-5">
|
<h1 className="text-3xl">Changelog</h1>
|
||||||
<h1 className="text-3xl">Changelog</h1>
|
|
||||||
|
|
||||||
<div className="flex gap-10 w-full">
|
<div className="flex gap-10 w-full">
|
||||||
{!isPending &&
|
{!isPending &&
|
||||||
data &&
|
data &&
|
||||||
data.map((changelog) => {
|
data.map((changelog) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to="/changelog/$id"
|
to="/changelog/$id"
|
||||||
params={{ id: changelog.id }}
|
params={{ id: changelog.id }}
|
||||||
key={changelog.id}
|
key={changelog.id}
|
||||||
>
|
>
|
||||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||||
<CardHeader className="flex items-center justify-center">
|
<CardHeader className="flex items-center justify-center">
|
||||||
<CardTitle>{changelog.title}</CardTitle>
|
<CardTitle>{changelog.title}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-center flex-col">
|
<CardContent className="flex items-center justify-center flex-col">
|
||||||
<span>Versions: {changelog.computed.versionCount}</span>
|
<span>Versions: {changelog.computed.versionCount}</span>
|
||||||
|
|
||||||
<span>Commits: {changelog.computed.commitCount}</span>
|
<span>Commits: {changelog.computed.commitCount}</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Link to="/changelog/create">
|
<Link to="/changelog/create">
|
||||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||||
<CardHeader className="flex items-center justify-center">
|
<CardHeader className="flex items-center justify-center">
|
||||||
<CardTitle>New Changelog</CardTitle>
|
<CardTitle>New Changelog</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-center">
|
<CardContent className="flex items-center justify-center">
|
||||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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"}
|
{"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"}
|
12
packages/schema/src/access-token/base.ts
Normal file
12
packages/schema/src/access-token/base.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const AccessTokenOutput = z
|
||||||
|
.object({
|
||||||
|
id: z.string().openapi({
|
||||||
|
example: 'user_2metCkqOhUhHN1jEhLyh8wMODu7',
|
||||||
|
}),
|
||||||
|
token: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
lastUsedOn: z.string().or(z.date()),
|
||||||
|
})
|
||||||
|
.openapi('Access Token')
|
7
packages/schema/src/access-token/create.ts
Normal file
7
packages/schema/src/access-token/create.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const AccessTokenCreateInput = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.openapi('Access Token')
|
3
packages/schema/src/access-token/index.ts
Normal file
3
packages/schema/src/access-token/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './base'
|
||||||
|
export * from './create'
|
||||||
|
export * from './list'
|
4
packages/schema/src/access-token/list.ts
Normal file
4
packages/schema/src/access-token/list.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
import { AccessTokenOutput } from './base'
|
||||||
|
|
||||||
|
export const AccessTokenListOutput = z.array(AccessTokenOutput)
|
@ -4,3 +4,4 @@ export * from './changelog'
|
|||||||
export * from './version'
|
export * from './version'
|
||||||
export * from './page'
|
export * from './page'
|
||||||
export * from './commit'
|
export * from './commit'
|
||||||
|
export * from './access-token'
|
||||||
|
@ -29,3 +29,4 @@ export * from './breadcrumb'
|
|||||||
export * from './command'
|
export * from './command'
|
||||||
export * from './dialog'
|
export * from './dialog'
|
||||||
export * from './scroll-area'
|
export * from './scroll-area'
|
||||||
|
export * from './table'
|
||||||
|
117
packages/ui/src/table.tsx
Normal file
117
packages/ui/src/table.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from './lib/cn'
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn('w-full caption-bottom text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = 'Table'
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = 'TableHeader'
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn('[&_tr:last-child]:border-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = 'TableBody'
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = 'TableFooter'
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = 'TableRow'
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = 'TableHead'
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = 'TableCell'
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = 'TableCaption'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user