Changelog Public Page #7
@ -70,4 +70,7 @@ jobs:
|
|||||||
run: bun docker:api
|
run: bun docker:api
|
||||||
|
|
||||||
- name: Build and Push App
|
- name: Build and Push App
|
||||||
run: bun docker:app
|
run: bun docker:app
|
||||||
|
|
||||||
|
- name: Build and Push Page
|
||||||
|
run: bun docker:page
|
@ -32,6 +32,11 @@ export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
|||||||
const result = await db.query.changelog.findFirst({
|
const result = await db.query.changelog.findFirst({
|
||||||
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
||||||
with: {
|
with: {
|
||||||
|
pages: {
|
||||||
|
with: {
|
||||||
|
page: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
versions: {
|
versions: {
|
||||||
orderBy: (changelog_version, { desc }) => [
|
orderBy: (changelog_version, { desc }) => [
|
||||||
desc(changelog_version.createdAt),
|
desc(changelog_version.createdAt),
|
||||||
|
@ -10,6 +10,7 @@ import { HTTPException } from 'hono/http-exception'
|
|||||||
export const route = createRoute({
|
export const route = createRoute({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
|
tags: ['changelog'],
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
|
@ -8,7 +8,9 @@ import changelog from './changelog'
|
|||||||
import version from './changelog/version'
|
import version from './changelog/version'
|
||||||
import user from './user'
|
import user from './user'
|
||||||
|
|
||||||
|
import pageApi from './page'
|
||||||
import { authentication } from './utils/authentication'
|
import { authentication } from './utils/authentication'
|
||||||
|
import { handleError, handleZodError } from './utils/errors'
|
||||||
import { startup } from './utils/startup'
|
import { startup } from './utils/startup'
|
||||||
|
|
||||||
type User = z.infer<typeof UserOutput>
|
type User = z.infer<typeof UserOutput>
|
||||||
@ -17,20 +19,24 @@ export type Variables = {
|
|||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export const app = new OpenAPIHono<{ Variables: Variables }>()
|
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||||
|
defaultHook: handleZodError,
|
||||||
|
})
|
||||||
|
|
||||||
app.use(
|
// app.use(
|
||||||
'*',
|
// '*',
|
||||||
sentry({
|
// sentry({
|
||||||
dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
||||||
}),
|
// }),
|
||||||
)
|
// )
|
||||||
|
app.onError(handleError)
|
||||||
app.use('*', cors())
|
app.use('*', cors())
|
||||||
app.use('/v1/*', authentication)
|
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/changelog/version', version)
|
app.route('/v1/changelog/version', version)
|
||||||
|
app.route('/v1/page', pageApi)
|
||||||
|
|
||||||
app.doc('/openapi.json', {
|
app.doc('/openapi.json', {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.0',
|
||||||
|
65
apps/api/src/page/byId.ts
Normal file
65
apps/api/src/page/byId.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { db, page } from '@boring.tools/database'
|
||||||
|
import { PageByIdParams, PageOutput } from '@boring.tools/schema'
|
||||||
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'get',
|
||||||
|
tags: ['page'],
|
||||||
|
description: 'Get a page by id',
|
||||||
|
path: '/:id',
|
||||||
|
request: {
|
||||||
|
params: PageByIdParams,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: PageOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Return changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPageById = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
|
const result = await db.query.page.findFirst({
|
||||||
|
where: and(eq(page.id, id), eq(page.userId, userId)),
|
||||||
|
with: {
|
||||||
|
changelogs: {
|
||||||
|
with: {
|
||||||
|
changelog: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { changelogs, ...rest } = result
|
||||||
|
|
||||||
|
const mappedResult = {
|
||||||
|
...rest,
|
||||||
|
changelogs: changelogs.map((log) => log.changelog),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(mappedResult, 200)
|
||||||
|
})
|
||||||
|
}
|
69
apps/api/src/page/create.ts
Normal file
69
apps/api/src/page/create.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { changelogs_to_pages, db, page } from '@boring.tools/database'
|
||||||
|
import { PageCreateInput, PageOutput } from '@boring.tools/schema'
|
||||||
|
import { createRoute, type z } from '@hono/zod-openapi'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'post',
|
||||||
|
tags: ['page'],
|
||||||
|
description: 'Create a page',
|
||||||
|
path: '/',
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': { schema: PageCreateInput },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: PageOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Return changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPageCreate = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
|
||||||
|
const { changelogIds, ...rest }: z.infer<typeof PageCreateInput> =
|
||||||
|
await c.req.json()
|
||||||
|
|
||||||
|
const [result] = await db
|
||||||
|
.insert(page)
|
||||||
|
.values({
|
||||||
|
...rest,
|
||||||
|
userId: userId,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
// TODO: implement transaction
|
||||||
|
if (changelogIds) {
|
||||||
|
await db.insert(changelogs_to_pages).values(
|
||||||
|
changelogIds.map((changelogId) => ({
|
||||||
|
changelogId,
|
||||||
|
pageId: result.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result, 200)
|
||||||
|
})
|
||||||
|
}
|
48
apps/api/src/page/delete.ts
Normal file
48
apps/api/src/page/delete.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { db, page } from '@boring.tools/database'
|
||||||
|
import { GeneralOutput, PageByIdParams } from '@boring.tools/schema'
|
||||||
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import type { pageApi } from '.'
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'delete',
|
||||||
|
path: '/:id',
|
||||||
|
request: {
|
||||||
|
params: PageByIdParams,
|
||||||
|
},
|
||||||
|
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: GeneralOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Removes a changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPageDelete = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
const { id } = c.req.valid('param')
|
||||||
|
const result = await db
|
||||||
|
.delete(page)
|
||||||
|
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||||
|
.returning()
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result, 200)
|
||||||
|
})
|
||||||
|
}
|
24
apps/api/src/page/index.ts
Normal file
24
apps/api/src/page/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
import type { Variables } from '..'
|
||||||
|
import type { ContextModule } from '../utils/sentry'
|
||||||
|
import { registerPageById } from './byId'
|
||||||
|
import { registerPageCreate } from './create'
|
||||||
|
import { registerPageDelete } from './delete'
|
||||||
|
import { registerPageList } from './list'
|
||||||
|
import { registerPagePublic } from './public'
|
||||||
|
import { registerPageUpdate } from './update'
|
||||||
|
|
||||||
|
export const pageApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
|
|
||||||
|
const module: ContextModule = {
|
||||||
|
name: 'page',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPageById(pageApi)
|
||||||
|
registerPageCreate(pageApi)
|
||||||
|
registerPageList(pageApi)
|
||||||
|
registerPagePublic(pageApi)
|
||||||
|
registerPageDelete(pageApi)
|
||||||
|
registerPageUpdate(pageApi)
|
||||||
|
|
||||||
|
export default pageApi
|
47
apps/api/src/page/list.ts
Normal file
47
apps/api/src/page/list.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { db, page } from '@boring.tools/database'
|
||||||
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import { PageListOutput } from '@boring.tools/schema'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'get',
|
||||||
|
tags: ['page'],
|
||||||
|
description: 'Get a page list',
|
||||||
|
path: '/',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: PageListOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Return changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPageList = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
|
||||||
|
const result = await db.query.page.findMany({
|
||||||
|
where: and(eq(page.userId, userId)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result, 200)
|
||||||
|
})
|
||||||
|
}
|
86
apps/api/src/page/public.ts
Normal file
86
apps/api/src/page/public.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { changelog_version, db, page } from '@boring.tools/database'
|
||||||
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'get',
|
||||||
|
tags: ['page'],
|
||||||
|
description: 'Get a page',
|
||||||
|
path: '/:id/public',
|
||||||
|
request: {
|
||||||
|
params: PagePublicParams,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: PagePublicOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Return changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPagePublic = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
|
const result = await db.query.page.findFirst({
|
||||||
|
where: eq(page.id, id),
|
||||||
|
columns: {
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
changelogs: {
|
||||||
|
with: {
|
||||||
|
changelog: {
|
||||||
|
columns: {
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
versions: {
|
||||||
|
where: eq(changelog_version.status, 'published'),
|
||||||
|
orderBy: (changelog_version, { desc }) => [
|
||||||
|
desc(changelog_version.createdAt),
|
||||||
|
],
|
||||||
|
columns: {
|
||||||
|
markdown: true,
|
||||||
|
version: true,
|
||||||
|
releasedAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { changelogs, ...rest } = result
|
||||||
|
|
||||||
|
const mappedResult = {
|
||||||
|
...rest,
|
||||||
|
changelogs: changelogs.map((log) => log.changelog),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(mappedResult, 200)
|
||||||
|
})
|
||||||
|
}
|
88
apps/api/src/page/update.ts
Normal file
88
apps/api/src/page/update.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { changelogs_to_pages, db, page } from '@boring.tools/database'
|
||||||
|
import { createRoute, type z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import {
|
||||||
|
PageOutput,
|
||||||
|
PageUpdateInput,
|
||||||
|
PageUpdateParams,
|
||||||
|
} from '@boring.tools/schema'
|
||||||
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'put',
|
||||||
|
tags: ['page'],
|
||||||
|
description: 'Update a page',
|
||||||
|
path: '/:id',
|
||||||
|
request: {
|
||||||
|
params: PageUpdateParams,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': { schema: PageUpdateInput },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: PageOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Return changelog by id',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerPageUpdate = (api: typeof pageApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
|
const { changelogIds, ...rest }: z.infer<typeof PageUpdateInput> =
|
||||||
|
await c.req.json()
|
||||||
|
|
||||||
|
const [result] = await db
|
||||||
|
.update(page)
|
||||||
|
.set({
|
||||||
|
...rest,
|
||||||
|
userId,
|
||||||
|
})
|
||||||
|
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
// TODO: implement transaction
|
||||||
|
if (changelogIds) {
|
||||||
|
if (changelogIds.length === 0) {
|
||||||
|
await db
|
||||||
|
.delete(changelogs_to_pages)
|
||||||
|
.where(eq(changelogs_to_pages.pageId, result.id))
|
||||||
|
}
|
||||||
|
if (changelogIds?.length >= 1) {
|
||||||
|
await db
|
||||||
|
.delete(changelogs_to_pages)
|
||||||
|
.where(eq(changelogs_to_pages.pageId, result.id))
|
||||||
|
await db.insert(changelogs_to_pages).values(
|
||||||
|
changelogIds.map((changelogId) => ({
|
||||||
|
changelogId,
|
||||||
|
pageId: result.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result, 200)
|
||||||
|
})
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import { type UserSelect, db, user as userDb } from '@boring.tools/database'
|
import { db, user as userDb } from '@boring.tools/database'
|
||||||
import { UserOutput } from '@boring.tools/schema'
|
import { UserOutput } from '@boring.tools/schema'
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
|
import type { userApi } from '.'
|
||||||
|
|
||||||
export const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
tags: ['user'],
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@ -22,19 +24,17 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const func = async ({ user }: { user: UserSelect }) => {
|
export const registerUserGet = (api: typeof userApi) => {
|
||||||
const result = await db.query.user.findFirst({
|
return api.openapi(route, async (c) => {
|
||||||
where: eq(userDb.id, user.id),
|
const user = c.get('user')
|
||||||
|
const result = await db.query.user.findFirst({
|
||||||
|
where: eq(userDb.id, user.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new Error('User not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
route,
|
|
||||||
func,
|
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,16 @@
|
|||||||
import { logger } from '@boring.tools/logger'
|
|
||||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import { Webhook } from 'svix'
|
|
||||||
import type { Variables } from '..'
|
import type { Variables } from '..'
|
||||||
import { type ContextModule, captureSentry } from '../utils/sentry'
|
import type { ContextModule } from '../utils/sentry'
|
||||||
import get from './get'
|
import { registerUserGet } from './get'
|
||||||
import webhook from './webhook'
|
import { registerUserWebhook } from './webhook'
|
||||||
|
|
||||||
const app = new OpenAPIHono<{ Variables: Variables }>()
|
export const userApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
|
|
||||||
const module: ContextModule = {
|
const module: ContextModule = {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
}
|
}
|
||||||
|
|
||||||
app.openapi(get.route, async (c) => {
|
registerUserGet(userApi)
|
||||||
const user = c.get('user')
|
registerUserWebhook(userApi)
|
||||||
try {
|
|
||||||
const result = await get.func({ user })
|
|
||||||
return c.json(result, 201)
|
|
||||||
} catch (error) {
|
|
||||||
return captureSentry({
|
|
||||||
c,
|
|
||||||
error,
|
|
||||||
module,
|
|
||||||
user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.openapi(webhook.route, async (c) => {
|
export default userApi
|
||||||
try {
|
|
||||||
const wh = new Webhook(import.meta.env.CLERK_WEBHOOK_SECRET as string)
|
|
||||||
const payload = await c.req.json()
|
|
||||||
const headers = c.req.header()
|
|
||||||
const verifiedPayload = wh.verify(JSON.stringify(payload), headers)
|
|
||||||
const result = await webhook.func({ payload: verifiedPayload })
|
|
||||||
logger.info('Clerk Webhook', result)
|
|
||||||
return c.json(result, 200)
|
|
||||||
} catch (error) {
|
|
||||||
return captureSentry({
|
|
||||||
c,
|
|
||||||
error,
|
|
||||||
module,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default app
|
|
||||||
|
@ -3,10 +3,13 @@ import { logger } from '@boring.tools/logger'
|
|||||||
import { UserOutput, UserWebhookInput } from '@boring.tools/schema'
|
import { UserOutput, UserWebhookInput } from '@boring.tools/schema'
|
||||||
import { createRoute, type z } from '@hono/zod-openapi'
|
import { createRoute, type z } from '@hono/zod-openapi'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import { Webhook } from 'svix'
|
||||||
|
import type userApi from '.'
|
||||||
|
|
||||||
export const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/webhook',
|
path: '/webhook',
|
||||||
|
tags: ['user'],
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
@ -59,20 +62,20 @@ const userCreate = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const func = async ({
|
export const registerUserWebhook = (api: typeof userApi) => {
|
||||||
payload,
|
return api.openapi(route, async (c) => {
|
||||||
}: {
|
const wh = new Webhook(import.meta.env.CLERK_WEBHOOK_SECRET as string)
|
||||||
payload: z.infer<typeof UserWebhookInput>
|
const payload = await c.req.json()
|
||||||
}) => {
|
const headers = c.req.header()
|
||||||
switch (payload.type) {
|
const verifiedPayload = wh.verify(JSON.stringify(payload), headers)
|
||||||
case 'user.created':
|
switch (verifiedPayload.type) {
|
||||||
return userCreate({ payload })
|
case 'user.created': {
|
||||||
default:
|
const result = await userCreate({ payload: verifiedPayload })
|
||||||
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
logger.info('Clerk Webhook', result)
|
||||||
}
|
return c.json(result, 200)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
export default {
|
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
||||||
route,
|
}
|
||||||
func,
|
})
|
||||||
}
|
}
|
||||||
|
46
apps/api/src/utils/errors/base-error.ts
Normal file
46
apps/api/src/utils/errors/base-error.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { ErrorCode } from '.'
|
||||||
|
|
||||||
|
type ErrorContext = Record<string, unknown>
|
||||||
|
|
||||||
|
export abstract class BaseError<
|
||||||
|
TContext extends ErrorContext = ErrorContext,
|
||||||
|
> extends Error {
|
||||||
|
public abstract readonly name: string
|
||||||
|
/**
|
||||||
|
* A distinct code for the error type used to differentiate between different types of errors.
|
||||||
|
* Used to build the URL for the error documentation.
|
||||||
|
* @example 'UNAUTHENTICATED' | 'INTERNAL_SERVER_ERROR'
|
||||||
|
*/
|
||||||
|
public abstract readonly code?: ErrorCode
|
||||||
|
public readonly cause?: BaseError
|
||||||
|
/**
|
||||||
|
* Additional context to help understand the error.
|
||||||
|
* @example { url: 'https://example.com/api', method: 'GET', statusCode: 401 }
|
||||||
|
*/
|
||||||
|
public readonly context?: TContext
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
message: string
|
||||||
|
cause?: BaseError
|
||||||
|
context?: TContext
|
||||||
|
}) {
|
||||||
|
super(opts.message)
|
||||||
|
this.cause = opts.cause
|
||||||
|
this.context = opts.context
|
||||||
|
|
||||||
|
// TODO: add logger here!
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return `${this.name}(${this.code}): ${
|
||||||
|
this.message
|
||||||
|
} - caused by ${this.cause?.toString()} - with context ${JSON.stringify(
|
||||||
|
this.context,
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// get docs(): string {
|
||||||
|
// if (!this.code) return "https://example.com/docs/errors"
|
||||||
|
// return `https://example.com/docs/errors/${this.code}`;
|
||||||
|
// }
|
||||||
|
}
|
35
apps/api/src/utils/errors/http-error.ts
Normal file
35
apps/api/src/utils/errors/http-error.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { type ErrorCode, statusToCode } from '.'
|
||||||
|
import { BaseError } from './base-error'
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
url?: string
|
||||||
|
method?: string
|
||||||
|
statusCode?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpError extends BaseError<Context> {
|
||||||
|
public readonly name = HttpError.name
|
||||||
|
public readonly code: ErrorCode
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
code: ErrorCode
|
||||||
|
message: string
|
||||||
|
cause?: BaseError
|
||||||
|
context?: Context
|
||||||
|
}) {
|
||||||
|
super(opts)
|
||||||
|
this.code = opts.code
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromRequest(request: Request, response: Response) {
|
||||||
|
return new HttpError({
|
||||||
|
code: statusToCode(response.status),
|
||||||
|
message: response.statusText, // can be overriden with { ...res, statusText: 'Custom message' }
|
||||||
|
context: {
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
|
statusCode: response.status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
120
apps/api/src/utils/errors/index.ts
Normal file
120
apps/api/src/utils/errors/index.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import type { Context } from 'hono'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
import { ZodError, z } from 'zod'
|
||||||
|
import { SchemaError } from './schema-error'
|
||||||
|
|
||||||
|
export const ErrorCodeEnum = z.enum([
|
||||||
|
'BAD_REQUEST',
|
||||||
|
'FORBIDDEN',
|
||||||
|
'INTERNAL_SERVER_ERROR',
|
||||||
|
'USAGE_EXCEEDED',
|
||||||
|
'DISABLED',
|
||||||
|
'CONFLICT',
|
||||||
|
'NOT_FOUND',
|
||||||
|
'NOT_UNIQUE',
|
||||||
|
'UNAUTHORIZED',
|
||||||
|
'METHOD_NOT_ALLOWED',
|
||||||
|
'UNPROCESSABLE_ENTITY',
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ErrorCode = z.infer<typeof ErrorCodeEnum>
|
||||||
|
|
||||||
|
export function statusToCode(status: number): ErrorCode {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
return 'BAD_REQUEST'
|
||||||
|
case 401:
|
||||||
|
return 'UNAUTHORIZED'
|
||||||
|
case 403:
|
||||||
|
return 'FORBIDDEN'
|
||||||
|
case 404:
|
||||||
|
return 'NOT_FOUND'
|
||||||
|
case 405:
|
||||||
|
return 'METHOD_NOT_ALLOWED'
|
||||||
|
case 409:
|
||||||
|
return 'METHOD_NOT_ALLOWED'
|
||||||
|
case 422:
|
||||||
|
return 'UNPROCESSABLE_ENTITY'
|
||||||
|
case 500:
|
||||||
|
return 'INTERNAL_SERVER_ERROR'
|
||||||
|
default:
|
||||||
|
return 'INTERNAL_SERVER_ERROR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ErrorSchema = z.infer<ReturnType<typeof createErrorSchema>>
|
||||||
|
|
||||||
|
export function createErrorSchema(code: ErrorCode) {
|
||||||
|
return z.object({
|
||||||
|
code: ErrorCodeEnum.openapi({
|
||||||
|
example: code,
|
||||||
|
description: 'The error code related to the status code.',
|
||||||
|
}),
|
||||||
|
message: z.string().openapi({
|
||||||
|
description: 'A human readable message describing the issue.',
|
||||||
|
example: "Missing required field 'name'.",
|
||||||
|
}),
|
||||||
|
docs: z.string().openapi({
|
||||||
|
description: 'A link to the documentation for the error.',
|
||||||
|
example: `https://docs.openstatus.dev/api-references/errors/code/${code}`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleError(err: Error, c: Context): Response {
|
||||||
|
if (err instanceof ZodError) {
|
||||||
|
const error = SchemaError.fromZod(err, c)
|
||||||
|
return c.json<ErrorSchema>(
|
||||||
|
{
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: error.message,
|
||||||
|
docs: 'https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST',
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (err instanceof HTTPException) {
|
||||||
|
const code = statusToCode(err.status)
|
||||||
|
return c.json<ErrorSchema>(
|
||||||
|
{
|
||||||
|
code: code,
|
||||||
|
message: err.message,
|
||||||
|
docs: `https://docs.openstatus.dev/api-references/errors/code/${code}`,
|
||||||
|
},
|
||||||
|
{ status: err.status },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return c.json<ErrorSchema>(
|
||||||
|
{
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: err.message ?? 'Something went wrong',
|
||||||
|
docs: 'https://docs.openstatus.dev/api-references/errors/code/INTERNAL_SERVER_ERROR',
|
||||||
|
},
|
||||||
|
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export function handleZodError(
|
||||||
|
result:
|
||||||
|
| {
|
||||||
|
success: true
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false
|
||||||
|
error: ZodError
|
||||||
|
},
|
||||||
|
c: Context,
|
||||||
|
) {
|
||||||
|
if (!result.success) {
|
||||||
|
const error = SchemaError.fromZod(result.error, c)
|
||||||
|
return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
|
||||||
|
{
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
docs: 'https://docs.openstatus.dev/api-references/errors/code/BAD_REQUEST',
|
||||||
|
message: error.message,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
30
apps/api/src/utils/errors/schema-error.ts
Normal file
30
apps/api/src/utils/errors/schema-error.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { ZodError } from 'zod'
|
||||||
|
|
||||||
|
import type { ErrorCode } from '.'
|
||||||
|
import { BaseError } from './base-error'
|
||||||
|
import { parseZodErrorIssues } from './utils'
|
||||||
|
|
||||||
|
type Context = { raw: unknown }
|
||||||
|
|
||||||
|
export class SchemaError extends BaseError<Context> {
|
||||||
|
public readonly name = SchemaError.name
|
||||||
|
public readonly code: ErrorCode
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
code: ErrorCode
|
||||||
|
message: string
|
||||||
|
cause?: BaseError
|
||||||
|
context?: Context
|
||||||
|
}) {
|
||||||
|
super(opts)
|
||||||
|
this.code = opts.code
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromZod<T>(e: ZodError<T>, raw: unknown): SchemaError {
|
||||||
|
return new SchemaError({
|
||||||
|
code: 'UNPROCESSABLE_ENTITY',
|
||||||
|
message: parseZodErrorIssues(e.issues),
|
||||||
|
context: { raw: JSON.stringify(raw) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
66
apps/api/src/utils/errors/utils.ts
Normal file
66
apps/api/src/utils/errors/utils.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import type { ZodIssue } from 'zod'
|
||||||
|
import type { ErrorCode } from '.'
|
||||||
|
|
||||||
|
export function statusToCode(status: number): ErrorCode {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
return 'BAD_REQUEST'
|
||||||
|
case 401:
|
||||||
|
return 'UNAUTHORIZED'
|
||||||
|
case 403:
|
||||||
|
return 'FORBIDDEN'
|
||||||
|
case 404:
|
||||||
|
return 'NOT_FOUND'
|
||||||
|
case 405:
|
||||||
|
return 'METHOD_NOT_ALLOWED'
|
||||||
|
case 409:
|
||||||
|
return 'METHOD_NOT_ALLOWED'
|
||||||
|
case 422:
|
||||||
|
return 'UNPROCESSABLE_ENTITY'
|
||||||
|
case 500:
|
||||||
|
return 'INTERNAL_SERVER_ERROR'
|
||||||
|
default:
|
||||||
|
return 'INTERNAL_SERVER_ERROR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function codeToStatus(code: ErrorCode): number {
|
||||||
|
switch (code) {
|
||||||
|
case 'BAD_REQUEST':
|
||||||
|
return 400
|
||||||
|
case 'UNAUTHORIZED':
|
||||||
|
return 401
|
||||||
|
case 'FORBIDDEN':
|
||||||
|
return 403
|
||||||
|
case 'NOT_FOUND':
|
||||||
|
return 404
|
||||||
|
case 'METHOD_NOT_ALLOWED':
|
||||||
|
return 405
|
||||||
|
case 'CONFLICT':
|
||||||
|
return 409
|
||||||
|
case 'UNPROCESSABLE_ENTITY':
|
||||||
|
return 422
|
||||||
|
case 'INTERNAL_SERVER_ERROR':
|
||||||
|
return 500
|
||||||
|
default:
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props to cal.com: https://github.com/calcom/cal.com/blob/5d325495a9c30c5a9d89fc2adfa620b8fde9346e/packages/lib/server/getServerErrorFromUnknown.ts#L17
|
||||||
|
export function parseZodErrorIssues(issues: ZodIssue[]): string {
|
||||||
|
return issues
|
||||||
|
.map((i) =>
|
||||||
|
i.code === 'invalid_union'
|
||||||
|
? i.unionErrors.map((ue) => parseZodErrorIssues(ue.issues)).join('; ')
|
||||||
|
: i.code === 'unrecognized_keys'
|
||||||
|
? i.message
|
||||||
|
: `${i.path.length ? `${i.code} in '${i.path}': ` : ''}${i.message}`,
|
||||||
|
)
|
||||||
|
.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactError<TError extends Error | unknown>(err: TError) {
|
||||||
|
if (!(err instanceof Error)) return err
|
||||||
|
console.error(`Type of Error: ${err.constructor}`)
|
||||||
|
}
|
72
apps/app/src/components/Page/Delete.tsx
Normal file
72
apps/app/src/components/Page/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 { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Trash2Icon } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { usePageDelete } from '../../hooks/usePage'
|
||||||
|
|
||||||
|
export const PageDelete = ({ id }: { id: string }) => {
|
||||||
|
const remove = usePageDelete()
|
||||||
|
const navigate = useNavigate({ from: `/page/${id}` })
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const removeChangelog = () => {
|
||||||
|
remove.mutate(
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
navigate({ to: '/page' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<AlertDialog open={isOpen}>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant={'ghost'} 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
|
||||||
|
page and remove your data from our servers.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild>
|
||||||
|
<Button onClick={removeChangelog} variant={'destructive'}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Remove</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
@ -1,37 +1,19 @@
|
|||||||
import { ChevronRightIcon, FileStackIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
Sidebar as SidebarComp,
|
Sidebar as SidebarComp,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { useChangelogList } from '../hooks/useChangelog'
|
import { SidebarChangelog } from './SidebarChangelog'
|
||||||
|
import { SidebarPage } from './SidebarPage'
|
||||||
import { SidebarUser } from './SidebarUser'
|
import { SidebarUser } from './SidebarUser'
|
||||||
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
title: 'Changelog',
|
|
||||||
url: '/changelog',
|
|
||||||
icon: FileStackIcon,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { data, error } = useChangelogList()
|
|
||||||
return (
|
return (
|
||||||
<SidebarComp>
|
<SidebarComp>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@ -53,47 +35,8 @@ export function Sidebar() {
|
|||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
<SidebarChangelog />
|
||||||
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
|
<SidebarPage />
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild tooltip={item.title}>
|
|
||||||
<Link
|
|
||||||
to={item.url}
|
|
||||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
|
||||||
>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
|
||||||
<ChevronRightIcon />
|
|
||||||
<span className="sr-only">Toggle</span>
|
|
||||||
</SidebarMenuAction>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<SidebarMenuSub>
|
|
||||||
{!error &&
|
|
||||||
data?.map((changelog) => (
|
|
||||||
<SidebarMenuSubItem key={changelog.id}>
|
|
||||||
<SidebarMenuSubButton asChild>
|
|
||||||
<Link
|
|
||||||
to={`/changelog/${changelog.id}`}
|
|
||||||
activeProps={{
|
|
||||||
className: 'bg-sidebar-primary',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{changelog.title}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenuSub>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Collapsible>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
76
apps/app/src/components/SidebarChangelog.tsx
Normal file
76
apps/app/src/components/SidebarChangelog.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { ChevronRightIcon, FileStackIcon, PlusIcon } from 'lucide-react'
|
||||||
|
import { useChangelogList } from '../hooks/useChangelog'
|
||||||
|
|
||||||
|
export const SidebarChangelog = () => {
|
||||||
|
const { data, error } = useChangelogList()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible asChild>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild tooltip="Changelog">
|
||||||
|
<Link
|
||||||
|
to="/changelog"
|
||||||
|
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||||
|
>
|
||||||
|
<FileStackIcon />
|
||||||
|
<span>Changelog</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
<span className="sr-only">Toggle</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{!error &&
|
||||||
|
data?.map((changelog) => (
|
||||||
|
<SidebarMenuSubItem key={changelog.id}>
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<Link
|
||||||
|
to={`/changelog/${changelog.id}`}
|
||||||
|
activeProps={{
|
||||||
|
className: 'bg-sidebar-primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{changelog.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<SidebarMenuSubItem className="opacity-60">
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<Link
|
||||||
|
to="/changelog/create"
|
||||||
|
activeProps={{
|
||||||
|
className: 'bg-sidebar-primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<PlusIcon className="w-3 h-3" />
|
||||||
|
New changelog
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
)
|
||||||
|
}
|
73
apps/app/src/components/SidebarPage.tsx
Normal file
73
apps/app/src/components/SidebarPage.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { ChevronRightIcon, NotebookTextIcon, PlusIcon } from 'lucide-react'
|
||||||
|
import { usePageList } from '../hooks/usePage'
|
||||||
|
|
||||||
|
export const SidebarPage = () => {
|
||||||
|
const { data, error } = usePageList()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible asChild>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild tooltip="Page">
|
||||||
|
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||||
|
<NotebookTextIcon />
|
||||||
|
<span>Page</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
<span className="sr-only">Toggle</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{!error &&
|
||||||
|
data?.map((page) => (
|
||||||
|
<SidebarMenuSubItem key={page.id}>
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<Link
|
||||||
|
to={`/page/${page?.id}`}
|
||||||
|
activeProps={{
|
||||||
|
className: 'bg-sidebar-primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{page.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<SidebarMenuSubItem className="opacity-60">
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<Link
|
||||||
|
to="/page/create"
|
||||||
|
activeProps={{
|
||||||
|
className: 'bg-sidebar-primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<PlusIcon className="w-3 h-3" />
|
||||||
|
New page
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
)
|
||||||
|
}
|
196
apps/app/src/hooks/usePage.ts
Normal file
196
apps/app/src/hooks/usePage.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import type {
|
||||||
|
PageByIdOutput,
|
||||||
|
PageCreateInput,
|
||||||
|
PageListOutput,
|
||||||
|
PageOutput,
|
||||||
|
PageUpdateInput,
|
||||||
|
} 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 Page = z.infer<typeof PageOutput>
|
||||||
|
type PageList = z.infer<typeof PageListOutput>
|
||||||
|
type PageById = z.infer<typeof PageByIdOutput>
|
||||||
|
type PageCreate = z.infer<typeof PageCreateInput>
|
||||||
|
type PageUpdate = z.infer<typeof PageUpdateInput>
|
||||||
|
|
||||||
|
export const usePageList = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pageList'],
|
||||||
|
queryFn: async (): Promise<Readonly<PageList>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: 'page',
|
||||||
|
method: 'get',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePageById = ({ id }: { id: string }) => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pageById', id],
|
||||||
|
queryFn: async (): Promise<Readonly<PageById>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `page/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePageCreate = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: PageCreate): Promise<Readonly<Page>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: 'page',
|
||||||
|
data: payload,
|
||||||
|
method: 'post',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['pageList'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePageDelete = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id }: { id: string }): Promise<Readonly<Page>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `page/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['pageList'],
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['pageById', data.id],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePageUpdate = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
payload: PageUpdate
|
||||||
|
}): Promise<Readonly<Page>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `page/${id}`,
|
||||||
|
data: payload,
|
||||||
|
method: 'put',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['pageById', data.id],
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['pageList'],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
export const useChangelogVersionCreate = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: VersionCreate): Promise<Readonly<Version>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: 'changelog/version',
|
||||||
|
data: payload,
|
||||||
|
method: 'post',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['changelogList'] })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['changelogById', data.changelogId],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChangelogVersionById = ({ id }: { id: string }) => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['changelogVersionById', id],
|
||||||
|
queryFn: async (): Promise<Readonly<Version>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `changelog/version/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChangelogVersionUpdate = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
payload: VersionUpdate
|
||||||
|
}): Promise<Readonly<Version>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `changelog/version/${id}`,
|
||||||
|
data: payload,
|
||||||
|
method: 'put',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['changelogById', data.id],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChangelogVersionRemove = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id }: { id: string }): Promise<Readonly<Version>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: `changelog/version/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['changelogList', 'changelogById', data.id],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
@ -19,10 +19,15 @@ import { Route as ChangelogIdVersionVersionIdImport } from './routes/changelog.$
|
|||||||
|
|
||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||||
|
const PageIndexLazyImport = createFileRoute('/page/')()
|
||||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||||
|
const PageCreateLazyImport = createFileRoute('/page/create')()
|
||||||
|
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 PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
||||||
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
||||||
|
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
||||||
const ChangelogIdVersionCreateLazyImport = createFileRoute(
|
const ChangelogIdVersionCreateLazyImport = createFileRoute(
|
||||||
'/changelog/$id/versionCreate',
|
'/changelog/$id/versionCreate',
|
||||||
)()
|
)()
|
||||||
@ -40,6 +45,11 @@ const UserIndexLazyRoute = UserIndexLazyImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
||||||
|
path: '/page/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||||
path: '/changelog/',
|
path: '/changelog/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -47,6 +57,16 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
|||||||
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||||
|
path: '/page/create',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const PageIdLazyRoute = PageIdLazyImport.update({
|
||||||
|
path: '/page/$id',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||||
path: '/changelog/create',
|
path: '/changelog/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -59,6 +79,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 PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/page.$id.index.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
@ -66,6 +93,11 @@ const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
|||||||
import('./routes/changelog.$id.index.lazy').then((d) => d.Route),
|
import('./routes/changelog.$id.index.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
||||||
|
path: '/edit',
|
||||||
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
|
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIdVersionCreateLazyRoute =
|
const ChangelogIdVersionCreateLazyRoute =
|
||||||
ChangelogIdVersionCreateLazyImport.update({
|
ChangelogIdVersionCreateLazyImport.update({
|
||||||
path: '/versionCreate',
|
path: '/versionCreate',
|
||||||
@ -112,6 +144,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ChangelogCreateLazyImport
|
preLoaderRoute: typeof ChangelogCreateLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/page/$id': {
|
||||||
|
id: '/page/$id'
|
||||||
|
path: '/page/$id'
|
||||||
|
fullPath: '/page/$id'
|
||||||
|
preLoaderRoute: typeof PageIdLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/page/create': {
|
||||||
|
id: '/page/create'
|
||||||
|
path: '/page/create'
|
||||||
|
fullPath: '/page/create'
|
||||||
|
preLoaderRoute: typeof PageCreateLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/changelog/': {
|
'/changelog/': {
|
||||||
id: '/changelog/'
|
id: '/changelog/'
|
||||||
path: '/changelog'
|
path: '/changelog'
|
||||||
@ -119,6 +165,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ChangelogIndexLazyImport
|
preLoaderRoute: typeof ChangelogIndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/page/': {
|
||||||
|
id: '/page/'
|
||||||
|
path: '/page'
|
||||||
|
fullPath: '/page'
|
||||||
|
preLoaderRoute: typeof PageIndexLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/user/': {
|
'/user/': {
|
||||||
id: '/user/'
|
id: '/user/'
|
||||||
path: '/user'
|
path: '/user'
|
||||||
@ -140,6 +193,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ChangelogIdVersionCreateLazyImport
|
preLoaderRoute: typeof ChangelogIdVersionCreateLazyImport
|
||||||
parentRoute: typeof ChangelogIdLazyImport
|
parentRoute: typeof ChangelogIdLazyImport
|
||||||
}
|
}
|
||||||
|
'/page/$id/edit': {
|
||||||
|
id: '/page/$id/edit'
|
||||||
|
path: '/edit'
|
||||||
|
fullPath: '/page/$id/edit'
|
||||||
|
preLoaderRoute: typeof PageIdEditLazyImport
|
||||||
|
parentRoute: typeof PageIdLazyImport
|
||||||
|
}
|
||||||
'/changelog/$id/': {
|
'/changelog/$id/': {
|
||||||
id: '/changelog/$id/'
|
id: '/changelog/$id/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@ -147,6 +207,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ChangelogIdIndexLazyImport
|
preLoaderRoute: typeof ChangelogIdIndexLazyImport
|
||||||
parentRoute: typeof ChangelogIdLazyImport
|
parentRoute: typeof ChangelogIdLazyImport
|
||||||
}
|
}
|
||||||
|
'/page/$id/': {
|
||||||
|
id: '/page/$id/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/page/$id/'
|
||||||
|
preLoaderRoute: typeof PageIdIndexLazyImport
|
||||||
|
parentRoute: typeof PageIdLazyImport
|
||||||
|
}
|
||||||
'/changelog/$id/version/$versionId': {
|
'/changelog/$id/version/$versionId': {
|
||||||
id: '/changelog/$id/version/$versionId'
|
id: '/changelog/$id/version/$versionId'
|
||||||
path: '/version/$versionId'
|
path: '/version/$versionId'
|
||||||
@ -177,26 +244,49 @@ const ChangelogIdLazyRouteWithChildren = ChangelogIdLazyRoute._addFileChildren(
|
|||||||
ChangelogIdLazyRouteChildren,
|
ChangelogIdLazyRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface PageIdLazyRouteChildren {
|
||||||
|
PageIdEditLazyRoute: typeof PageIdEditLazyRoute
|
||||||
|
PageIdIndexLazyRoute: typeof PageIdIndexLazyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageIdLazyRouteChildren: PageIdLazyRouteChildren = {
|
||||||
|
PageIdEditLazyRoute: PageIdEditLazyRoute,
|
||||||
|
PageIdIndexLazyRoute: PageIdIndexLazyRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren(
|
||||||
|
PageIdLazyRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
||||||
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
||||||
|
'/page/$id/edit': typeof PageIdEditLazyRoute
|
||||||
'/changelog/$id/': typeof ChangelogIdIndexLazyRoute
|
'/changelog/$id/': typeof ChangelogIdIndexLazyRoute
|
||||||
|
'/page/$id/': typeof PageIdIndexLazyRoute
|
||||||
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
||||||
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
||||||
|
'/page/$id/edit': typeof PageIdEditLazyRoute
|
||||||
'/changelog/$id': typeof ChangelogIdIndexLazyRoute
|
'/changelog/$id': typeof ChangelogIdIndexLazyRoute
|
||||||
|
'/page/$id': typeof PageIdIndexLazyRoute
|
||||||
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,11 +295,16 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||||
|
'/page/': typeof PageIndexLazyRoute
|
||||||
'/user/': typeof UserIndexLazyRoute
|
'/user/': typeof UserIndexLazyRoute
|
||||||
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
'/changelog/$id/edit': typeof ChangelogIdEditLazyRoute
|
||||||
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
'/changelog/$id/versionCreate': typeof ChangelogIdVersionCreateLazyRoute
|
||||||
|
'/page/$id/edit': typeof PageIdEditLazyRoute
|
||||||
'/changelog/$id/': typeof ChangelogIdIndexLazyRoute
|
'/changelog/$id/': typeof ChangelogIdIndexLazyRoute
|
||||||
|
'/page/$id/': typeof PageIdIndexLazyRoute
|
||||||
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
'/changelog/$id/version/$versionId': typeof ChangelogIdVersionVersionIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,32 +314,46 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
|
| '/page/$id'
|
||||||
|
| '/page/create'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
| '/changelog/$id/edit'
|
| '/changelog/$id/edit'
|
||||||
| '/changelog/$id/versionCreate'
|
| '/changelog/$id/versionCreate'
|
||||||
|
| '/page/$id/edit'
|
||||||
| '/changelog/$id/'
|
| '/changelog/$id/'
|
||||||
|
| '/page/$id/'
|
||||||
| '/changelog/$id/version/$versionId'
|
| '/changelog/$id/version/$versionId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
|
| '/page/create'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
| '/changelog/$id/edit'
|
| '/changelog/$id/edit'
|
||||||
| '/changelog/$id/versionCreate'
|
| '/changelog/$id/versionCreate'
|
||||||
|
| '/page/$id/edit'
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
|
| '/page/$id'
|
||||||
| '/changelog/$id/version/$versionId'
|
| '/changelog/$id/version/$versionId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
|
| '/page/$id'
|
||||||
|
| '/page/create'
|
||||||
| '/changelog/'
|
| '/changelog/'
|
||||||
|
| '/page/'
|
||||||
| '/user/'
|
| '/user/'
|
||||||
| '/changelog/$id/edit'
|
| '/changelog/$id/edit'
|
||||||
| '/changelog/$id/versionCreate'
|
| '/changelog/$id/versionCreate'
|
||||||
|
| '/page/$id/edit'
|
||||||
| '/changelog/$id/'
|
| '/changelog/$id/'
|
||||||
|
| '/page/$id/'
|
||||||
| '/changelog/$id/version/$versionId'
|
| '/changelog/$id/version/$versionId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
@ -253,7 +362,10 @@ export interface RootRouteChildren {
|
|||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
||||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||||
|
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
||||||
|
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
||||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||||
|
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
||||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +373,10 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
||||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||||
|
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
||||||
|
PageCreateLazyRoute: PageCreateLazyRoute,
|
||||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||||
|
PageIndexLazyRoute: PageIndexLazyRoute,
|
||||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +395,10 @@ export const routeTree = rootRoute
|
|||||||
"/",
|
"/",
|
||||||
"/changelog/$id",
|
"/changelog/$id",
|
||||||
"/changelog/create",
|
"/changelog/create",
|
||||||
|
"/page/$id",
|
||||||
|
"/page/create",
|
||||||
"/changelog/",
|
"/changelog/",
|
||||||
|
"/page/",
|
||||||
"/user/"
|
"/user/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -299,9 +417,22 @@ export const routeTree = rootRoute
|
|||||||
"/changelog/create": {
|
"/changelog/create": {
|
||||||
"filePath": "changelog.create.lazy.tsx"
|
"filePath": "changelog.create.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/page/$id": {
|
||||||
|
"filePath": "page.$id.lazy.tsx",
|
||||||
|
"children": [
|
||||||
|
"/page/$id/edit",
|
||||||
|
"/page/$id/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/page/create": {
|
||||||
|
"filePath": "page.create.lazy.tsx"
|
||||||
|
},
|
||||||
"/changelog/": {
|
"/changelog/": {
|
||||||
"filePath": "changelog.index.lazy.tsx"
|
"filePath": "changelog.index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/page/": {
|
||||||
|
"filePath": "page.index.lazy.tsx"
|
||||||
|
},
|
||||||
"/user/": {
|
"/user/": {
|
||||||
"filePath": "user/index.lazy.tsx"
|
"filePath": "user/index.lazy.tsx"
|
||||||
},
|
},
|
||||||
@ -313,10 +444,18 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "changelog.$id.versionCreate.lazy.tsx",
|
"filePath": "changelog.$id.versionCreate.lazy.tsx",
|
||||||
"parent": "/changelog/$id"
|
"parent": "/changelog/$id"
|
||||||
},
|
},
|
||||||
|
"/page/$id/edit": {
|
||||||
|
"filePath": "page.$id.edit.lazy.tsx",
|
||||||
|
"parent": "/page/$id"
|
||||||
|
},
|
||||||
"/changelog/$id/": {
|
"/changelog/$id/": {
|
||||||
"filePath": "changelog.$id.index.lazy.tsx",
|
"filePath": "changelog.$id.index.lazy.tsx",
|
||||||
"parent": "/changelog/$id"
|
"parent": "/changelog/$id"
|
||||||
},
|
},
|
||||||
|
"/page/$id/": {
|
||||||
|
"filePath": "page.$id.index.lazy.tsx",
|
||||||
|
"parent": "/page/$id"
|
||||||
|
},
|
||||||
"/changelog/$id/version/$versionId": {
|
"/changelog/$id/version/$versionId": {
|
||||||
"filePath": "changelog.$id.version.$versionId.tsx",
|
"filePath": "changelog.$id.version.$versionId.tsx",
|
||||||
"parent": "/changelog/$id"
|
"parent": "/changelog/$id"
|
||||||
|
@ -13,7 +13,7 @@ import { useChangelogById } from '../hooks/useChangelog'
|
|||||||
const Component = () => {
|
const Component = () => {
|
||||||
const { id } = Route.useParams()
|
const { id } = Route.useParams()
|
||||||
const { data, error, isPending, refetch } = useChangelogById({ id })
|
const { data, error, isPending, refetch } = useChangelogById({ id })
|
||||||
|
console.log(data)
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center mt-32 flex-col">
|
<div className="flex items-center justify-center mt-32 flex-col">
|
||||||
|
192
apps/app/src/routes/page.$id.edit.lazy.tsx
Normal file
192
apps/app/src/routes/page.$id.edit.lazy.tsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { PageUpdateInput } from '@boring.tools/schema'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Textarea,
|
||||||
|
cn,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import type { z } from 'zod'
|
||||||
|
import { useChangelogList } from '../hooks/useChangelog'
|
||||||
|
import { usePageById, usePageUpdate } from '../hooks/usePage'
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const navigate = useNavigate({ from: `/page/${id}/edit` })
|
||||||
|
const page = usePageById({ id })
|
||||||
|
const changelogList = useChangelogList()
|
||||||
|
const pageUpdate = usePageUpdate()
|
||||||
|
const form = useForm<z.infer<typeof PageUpdateInput>>({
|
||||||
|
resolver: zodResolver(PageUpdateInput),
|
||||||
|
defaultValues: {
|
||||||
|
...page.data,
|
||||||
|
changelogIds: page.data?.changelogs.map((log) => log.id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const onSubmit = (values: z.infer<typeof PageUpdateInput>) => {
|
||||||
|
pageUpdate.mutate(
|
||||||
|
{ id, payload: values },
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
navigate({ to: '/page/$id', params: { id: data.id } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<h1 className="text-3xl">Edit page</h1>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8 max-w-screen-md"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Some details about the page..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="changelogIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Changelogs</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
'w-[200px] justify-between',
|
||||||
|
!field.value && 'text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value.length === 1 &&
|
||||||
|
changelogList.data?.find((changelog) =>
|
||||||
|
field.value?.includes(changelog.id),
|
||||||
|
)?.title}
|
||||||
|
{field.value.length <= 0 && 'No changelog selected'}
|
||||||
|
{field.value.length > 1 &&
|
||||||
|
`${field.value.length} selected`}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{changelogList.data?.map((changelog) => (
|
||||||
|
<CommandItem
|
||||||
|
value={changelog.title}
|
||||||
|
key={changelog.id}
|
||||||
|
onSelect={() => {
|
||||||
|
const getIds = () => {
|
||||||
|
if (field.value.includes(changelog.id)) {
|
||||||
|
const asd = field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
return asd
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...field.value, changelog.id]
|
||||||
|
}
|
||||||
|
form.setValue('changelogIds', getIds())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
field.value.includes(changelog.id)
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{changelog.title}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This changelogs are shown on this page.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => navigate({ to: '/page/$id', params: { id } })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Update</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/page/$id/edit')({
|
||||||
|
component: Component,
|
||||||
|
})
|
73
apps/app/src/routes/page.$id.index.lazy.tsx
Normal file
73
apps/app/src/routes/page.$id.index.lazy.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
import { CircleMinusIcon, PlusCircleIcon } from 'lucide-react'
|
||||||
|
import { usePageById, usePageUpdate } from '../hooks/usePage'
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const { data, isPending } = usePageById({ id })
|
||||||
|
const pageUpdate = usePageUpdate()
|
||||||
|
const removeChangelog = (idToRemove: string) => {
|
||||||
|
const payload = {
|
||||||
|
title: data?.title,
|
||||||
|
description: data?.description,
|
||||||
|
changelogIds: data?.changelogs
|
||||||
|
.filter((log) => log.id !== idToRemove)
|
||||||
|
.map((l) => l.id),
|
||||||
|
}
|
||||||
|
pageUpdate.mutate({ id, payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{!isPending && data && (
|
||||||
|
<div>
|
||||||
|
<Card className="w-full max-w-screen-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>Changelogs ({data.changelogs?.length})</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{data.changelogs.map((changelog) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-3" key={changelog.id}>
|
||||||
|
<Link
|
||||||
|
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center w-full"
|
||||||
|
to="/changelog/$id"
|
||||||
|
params={{
|
||||||
|
id: changelog.id,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{changelog.title}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size={'icon'}
|
||||||
|
variant={'ghost-destructive'}
|
||||||
|
onClick={() => removeChangelog(changelog.id)}
|
||||||
|
>
|
||||||
|
<CircleMinusIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/page/$id/')({
|
||||||
|
component: Component,
|
||||||
|
})
|
114
apps/app/src/routes/page.$id.lazy.tsx
Normal file
114
apps/app/src/routes/page.$id.lazy.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { Link, Outlet, createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
import { FileStackIcon, Globe2Icon, PencilIcon } from 'lucide-react'
|
||||||
|
import { PageDelete } from '../components/Page/Delete'
|
||||||
|
import { PageWrapper } from '../components/PageWrapper'
|
||||||
|
import { usePageById } from '../hooks/usePage'
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const { data, error, isPending, refetch } = usePageById({ id })
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center mt-32 flex-col">
|
||||||
|
<h1 className="text-3xl">Changelogs</h1>
|
||||||
|
<p>Please try again later</p>
|
||||||
|
|
||||||
|
<Button onClick={() => refetch()}>Retry</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
name: 'Page',
|
||||||
|
to: '/page',
|
||||||
|
},
|
||||||
|
{ name: data?.title ?? '', to: `/page/${data?.id}` },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{!isPending && data && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<FileStackIcon
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className="w-10 h-10 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl">{data.title}</h1>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
{data.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* <Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant={'ghost'}>
|
||||||
|
<TerminalSquareIcon strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>CLI</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant={'ghost'} asChild>
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.DEV ? 'http://localhost:4020' : 'https://page.boring.tools'}/${id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<Globe2Icon strokeWidth={1.5} />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Public Page</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link to={'/page/$id/edit'} params={{ id }}>
|
||||||
|
<Button variant={'ghost'}>
|
||||||
|
<PencilIcon strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Edit</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<PageDelete id={id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/page/$id')({
|
||||||
|
component: Component,
|
||||||
|
})
|
190
apps/app/src/routes/page.create.lazy.tsx
Normal file
190
apps/app/src/routes/page.create.lazy.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { PageCreateInput } from '@boring.tools/schema'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Textarea,
|
||||||
|
cn,
|
||||||
|
} from '@boring.tools/ui'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import type { z } from 'zod'
|
||||||
|
import { PageWrapper } from '../components/PageWrapper'
|
||||||
|
import { useChangelogList } from '../hooks/useChangelog'
|
||||||
|
import { usePageCreate } from '../hooks/usePage'
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const navigate = useNavigate({ from: '/page/create' })
|
||||||
|
const changelogList = useChangelogList()
|
||||||
|
const pageCreate = usePageCreate()
|
||||||
|
const form = useForm<z.infer<typeof PageCreateInput>>({
|
||||||
|
resolver: zodResolver(PageCreateInput),
|
||||||
|
defaultValues: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
|
changelogIds: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (values: z.infer<typeof PageCreateInput>) => {
|
||||||
|
pageCreate.mutate(values, {
|
||||||
|
onSuccess(data) {
|
||||||
|
navigate({ to: '/page/$id', params: { id: data.id } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
name: 'Page',
|
||||||
|
to: '/page',
|
||||||
|
},
|
||||||
|
{ name: 'New', to: '/page/create' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<h1 className="text-3xl">New page</h1>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8 max-w-screen-md"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Some details about the page..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="changelogIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Changelogs</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
'w-[200px] justify-between',
|
||||||
|
!field.value && 'text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value.length === 1 &&
|
||||||
|
changelogList.data?.find((changelog) =>
|
||||||
|
field.value?.includes(changelog.id),
|
||||||
|
)?.title}
|
||||||
|
{field.value.length <= 0 && 'No changelog selected'}
|
||||||
|
{field.value.length > 1 &&
|
||||||
|
`${field.value.length} selected`}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{changelogList.data?.map((changelog) => (
|
||||||
|
<CommandItem
|
||||||
|
value={changelog.title}
|
||||||
|
key={changelog.id}
|
||||||
|
onSelect={() => {
|
||||||
|
const getIds = () => {
|
||||||
|
if (field.value.includes(changelog.id)) {
|
||||||
|
const asd = field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
return asd
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...field.value, changelog.id]
|
||||||
|
}
|
||||||
|
form.setValue('changelogIds', getIds())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
field.value.includes(changelog.id)
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{changelog.title}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This changelogs are shown on this page.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/page/create')({
|
||||||
|
component: Component,
|
||||||
|
})
|
12
apps/app/src/routes/page.index.lazy.tsx
Normal file
12
apps/app/src/routes/page.index.lazy.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
//import { usePageById, usePageList } from '../hooks/usePage'
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
//const { data, error } = usePageList()
|
||||||
|
|
||||||
|
return <div>some</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute('/page/')({
|
||||||
|
component: Component,
|
||||||
|
})
|
@ -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/Sidebar.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionStatus.tsx","./src/hooks/useChangelog.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/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/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"}
|
@ -4,6 +4,9 @@ import { defineConfig } from 'vite'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 4000,
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: '../../build/app',
|
outDir: '../../build/app',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
24
apps/page/.gitignore
vendored
Normal file
24
apps/page/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
47
apps/page/README.md
Normal file
47
apps/page/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
21
apps/page/astro.config.mjs
Normal file
21
apps/page/astro.config.mjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
|
import react from '@astrojs/react'
|
||||||
|
|
||||||
|
import tailwind from '@astrojs/tailwind'
|
||||||
|
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server',
|
||||||
|
outDir: '../../build/page',
|
||||||
|
integrations: [react(), tailwind({ nesting: true })],
|
||||||
|
server: {
|
||||||
|
port: 4020,
|
||||||
|
},
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
})
|
30
apps/page/package.json
Normal file
30
apps/page/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@boring.tools/page",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/node": "^8.3.4",
|
||||||
|
"@astrojs/react": "^3.6.2",
|
||||||
|
"@astrojs/tailwind": "^5.1.2",
|
||||||
|
"@boring.tools/schema": "workspace:*",
|
||||||
|
"@boring.tools/ui": "workspace:*",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"astro": "^4.16.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
9
apps/page/public/favicon.svg
Normal file
9
apps/page/public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
1
apps/page/src/env.d.ts
vendored
Normal file
1
apps/page/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
108
apps/page/src/pages/[id].astro
Normal file
108
apps/page/src/pages/[id].astro
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
import type { PageByIdOutput } from '@boring.tools/schema'
|
||||||
|
import { Separator } from '@boring.tools/ui'
|
||||||
|
import type { z } from 'astro/zod'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
|
type PageById = z.infer<typeof PageByIdOutput>
|
||||||
|
const url = import.meta.env.DEV
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://api.boring.tools'
|
||||||
|
const { id } = Astro.params
|
||||||
|
const response = await fetch(`${url}/v1/page/${id}/public`)
|
||||||
|
const data: PageById = await response.json()
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>{data.title} | boring.tools</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-neutral-100">
|
||||||
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-screen-sm flex my-6 flex-col gap-8">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="prose prose-2xl">{data.title}</h1>
|
||||||
|
<p class="prose text-sm">{data.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.changelogs.length >= 2 && <div class="flex flex-col">
|
||||||
|
<h2 class="prose prose-xl">Changelogs</h2>
|
||||||
|
{data.changelogs.map((changelog) => {
|
||||||
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<h3 class="font-bold">{changelog.title}</h3>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex flex-col gap-5 my-6">
|
||||||
|
|
||||||
|
{changelog.versions?.map((version) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-10 bg-white rounded p-3 border border-neutral-300">
|
||||||
|
<div>
|
||||||
|
<h2 class="prose text-3xl font-bold">
|
||||||
|
{version.version}
|
||||||
|
</h2>
|
||||||
|
{version.releasedAt &&
|
||||||
|
|
||||||
|
<p class="prose text-xs text-center">
|
||||||
|
{format(new Date(String(version.releasedAt)), "dd-MM-yy")}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{version.markdown}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
|
{data.changelogs.length === 1 && <div>
|
||||||
|
<h2 class="uppercase text-sm prose tracking-widest">Changelog</h2>
|
||||||
|
{data.changelogs.map((changelog) => {
|
||||||
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<div class="flex flex-col gap-5 my-3">
|
||||||
|
|
||||||
|
{changelog.versions?.map((version) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-10 bg-white rounded p-3 border border-neutral-300">
|
||||||
|
<div>
|
||||||
|
<h2 class="prose text-3xl font-bold">
|
||||||
|
{version.version}
|
||||||
|
</h2>
|
||||||
|
{version.releasedAt &&
|
||||||
|
|
||||||
|
<p class="prose text-xs text-center">
|
||||||
|
{format(new Date(String(version.releasedAt)), "dd-MM-yy")}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{version.markdown}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
apps/page/src/pages/index.astro
Normal file
18
apps/page/src/pages/index.astro
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>Astro</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="flex items-center justify-center w-full h-screen flex-col">
|
||||||
|
<h1 class="prose prose-2xl">boring.tools</h1>
|
||||||
|
<p class="prose prose-sm">CHANGELOG</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
86
apps/page/tailwind.config.mjs
Normal file
86
apps/page/tailwind.config.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
|
||||||
|
'../../packages/ui/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||||
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
|
primary: 'hsl(var(--sidebar-primary))',
|
||||||
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
|
border: 'hsl(var(--sidebar-border))',
|
||||||
|
ring: 'hsl(var(--sidebar-ring))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
/* fontFamily: {
|
||||||
|
sans: ['var(--font-sans)', ...fontFamily.sans],
|
||||||
|
}, */
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: { height: '0' },
|
||||||
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
to: { height: '0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
||||||
|
}
|
7
apps/page/tsconfig.json
Normal file
7
apps/page/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,9 @@ import starlight from '@astrojs/starlight'
|
|||||||
import { defineConfig } from 'astro/config'
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 4010,
|
||||||
|
},
|
||||||
outDir: '../../build/website',
|
outDir: '../../build/website',
|
||||||
integrations: [
|
integrations: [
|
||||||
starlight({
|
starlight({
|
||||||
|
12
ci/docker/page/Dockerfile
Normal file
12
ci/docker/page/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM oven/bun:1
|
||||||
|
|
||||||
|
COPY ./build/page/dist .
|
||||||
|
COPY ./node_modules ./node_modules
|
||||||
|
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=4020
|
||||||
|
|
||||||
|
USER bun
|
||||||
|
EXPOSE 4020/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT [ "bun", "run", "server/entry.mjs" ]
|
@ -1,6 +1,6 @@
|
|||||||
pre-commit:
|
pre-commit:
|
||||||
commands:
|
commands:
|
||||||
check:
|
check:
|
||||||
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
|
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,astro}"
|
||||||
run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||||
stage_fixed: true
|
stage_fixed: true
|
@ -20,7 +20,10 @@
|
|||||||
"docker:api": "bun docker:api:build && bun docker:api:push",
|
"docker:api": "bun docker:api:build && bun docker:api:push",
|
||||||
"docker:app:build": "docker build -t git.hashdot.co/boring.tools/boring.tools/app -f ci/docker/app/Dockerfile .",
|
"docker:app:build": "docker build -t git.hashdot.co/boring.tools/boring.tools/app -f ci/docker/app/Dockerfile .",
|
||||||
"docker:app:push": "docker push git.hashdot.co/boring.tools/boring.tools/app",
|
"docker:app:push": "docker push git.hashdot.co/boring.tools/boring.tools/app",
|
||||||
"docker:app": "bun docker:app:build && bun docker:app:push"
|
"docker:app": "bun docker:app:build && bun docker:app:push",
|
||||||
|
"docker:page:build": "docker build -t git.hashdot.co/boring.tools/boring.tools/page -f ci/docker/page/Dockerfile .",
|
||||||
|
"docker:page:push": "docker push git.hashdot.co/boring.tools/boring.tools/page",
|
||||||
|
"docker:page": "bun docker:page:build && bun docker:page:push"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.29",
|
"packageManager": "bun@1.1.29",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@boring.tools/database",
|
"name": "@boring.tools/database",
|
||||||
"module": "src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
|
38
packages/database/src/migrations/0002_fantastic_sleeper.sql
Normal file
38
packages/database/src/migrations/0002_fantastic_sleeper.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "changelogs_to_pages" (
|
||||||
|
"changelogId" uuid NOT NULL,
|
||||||
|
"pageId" uuid NOT NULL,
|
||||||
|
CONSTRAINT "changelogs_to_pages_changelogId_pageId_pk" PRIMARY KEY("changelogId","pageId")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "page" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"userId" varchar(32),
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"icon" text DEFAULT ''
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "changelog" ADD COLUMN "pageId" uuid;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "changelogs_to_pages" ADD CONSTRAINT "changelogs_to_pages_changelogId_changelog_id_fk" FOREIGN KEY ("changelogId") REFERENCES "public"."changelog"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "changelogs_to_pages" ADD CONSTRAINT "changelogs_to_pages_pageId_page_id_fk" FOREIGN KEY ("pageId") REFERENCES "public"."page"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "page" ADD CONSTRAINT "page_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "changelog" ADD CONSTRAINT "changelog_pageId_page_id_fk" FOREIGN KEY ("pageId") REFERENCES "public"."page"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
509
packages/database/src/migrations/meta/0002_snapshot.json
Normal file
509
packages/database/src/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
{
|
||||||
|
"id": "3dd358bb-891a-46fa-b49d-6018c900ce5f",
|
||||||
|
"prevId": "6217594b-c287-4f69-b0f2-9c80ed3f7f83",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"public.access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"lastUsedOn": {
|
||||||
|
"name": "lastUsedOn",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"access_token_userId_user_id_fk": {
|
||||||
|
"name": "access_token_userId_user_id_fk",
|
||||||
|
"tableFrom": "access_token",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.changelog": {
|
||||||
|
"name": "changelog",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pageId": {
|
||||||
|
"name": "pageId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"isSemver": {
|
||||||
|
"name": "isSemver",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"changelog_userId_user_id_fk": {
|
||||||
|
"name": "changelog_userId_user_id_fk",
|
||||||
|
"tableFrom": "changelog",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"changelog_pageId_page_id_fk": {
|
||||||
|
"name": "changelog_pageId_page_id_fk",
|
||||||
|
"tableFrom": "changelog",
|
||||||
|
"tableTo": "page",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pageId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.changelog_commit": {
|
||||||
|
"name": "changelog_commit",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"changelogId": {
|
||||||
|
"name": "changelogId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"versionId": {
|
||||||
|
"name": "versionId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"shortHash": {
|
||||||
|
"name": "shortHash",
|
||||||
|
"type": "varchar(8)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"name": "body",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"unique": {
|
||||||
|
"name": "unique",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "changelogId",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "shortHash",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"changelog_commit_changelogId_changelog_id_fk": {
|
||||||
|
"name": "changelog_commit_changelogId_changelog_id_fk",
|
||||||
|
"tableFrom": "changelog_commit",
|
||||||
|
"tableTo": "changelog",
|
||||||
|
"columnsFrom": [
|
||||||
|
"changelogId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"changelog_commit_versionId_changelog_version_id_fk": {
|
||||||
|
"name": "changelog_commit_versionId_changelog_version_id_fk",
|
||||||
|
"tableFrom": "changelog_commit",
|
||||||
|
"tableTo": "changelog_version",
|
||||||
|
"columnsFrom": [
|
||||||
|
"versionId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.changelog_version": {
|
||||||
|
"name": "changelog_version",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"releasedAt": {
|
||||||
|
"name": "releasedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"changelogId": {
|
||||||
|
"name": "changelogId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"name": "markdown",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'draft'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"changelog_version_changelogId_changelog_id_fk": {
|
||||||
|
"name": "changelog_version_changelogId_changelog_id_fk",
|
||||||
|
"tableFrom": "changelog_version",
|
||||||
|
"tableTo": "changelog",
|
||||||
|
"columnsFrom": [
|
||||||
|
"changelogId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.changelogs_to_pages": {
|
||||||
|
"name": "changelogs_to_pages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"changelogId": {
|
||||||
|
"name": "changelogId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pageId": {
|
||||||
|
"name": "pageId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"changelogs_to_pages_changelogId_changelog_id_fk": {
|
||||||
|
"name": "changelogs_to_pages_changelogId_changelog_id_fk",
|
||||||
|
"tableFrom": "changelogs_to_pages",
|
||||||
|
"tableTo": "changelog",
|
||||||
|
"columnsFrom": [
|
||||||
|
"changelogId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"changelogs_to_pages_pageId_page_id_fk": {
|
||||||
|
"name": "changelogs_to_pages_pageId_page_id_fk",
|
||||||
|
"tableFrom": "changelogs_to_pages",
|
||||||
|
"tableTo": "page",
|
||||||
|
"columnsFrom": [
|
||||||
|
"pageId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"changelogs_to_pages_changelogId_pageId_pk": {
|
||||||
|
"name": "changelogs_to_pages_changelogId_pageId_pk",
|
||||||
|
"columns": [
|
||||||
|
"changelogId",
|
||||||
|
"pageId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.page": {
|
||||||
|
"name": "page",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "''"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"page_userId_user_id_fk": {
|
||||||
|
"name": "page_userId_user_id_fk",
|
||||||
|
"tableFrom": "page",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.status": {
|
||||||
|
"name": "status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"draft",
|
||||||
|
"review",
|
||||||
|
"published"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,13 @@
|
|||||||
"when": 1728640705376,
|
"when": 1728640705376,
|
||||||
"tag": "0001_daffy_rattler",
|
"tag": "0001_daffy_rattler",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1729804659796,
|
||||||
|
"tag": "0002_fantastic_sleeper",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ import {
|
|||||||
boolean,
|
boolean,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
pgTable,
|
pgTable,
|
||||||
|
primaryKey,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uuid,
|
uuid,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
} from 'drizzle-orm/pg-core'
|
} from 'drizzle-orm/pg-core'
|
||||||
import { json } from 'drizzle-orm/pg-core'
|
import { json } from 'drizzle-orm/pg-core'
|
||||||
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
||||||
|
import { page } from './page'
|
||||||
import { user } from './user'
|
import { user } from './user'
|
||||||
|
|
||||||
export const changelog = pgTable('changelog', {
|
export const changelog = pgTable('changelog', {
|
||||||
@ -21,11 +23,42 @@ export const changelog = pgTable('changelog', {
|
|||||||
onDelete: 'cascade',
|
onDelete: 'cascade',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
pageId: uuid('pageId').references(() => page.id),
|
||||||
|
|
||||||
title: varchar('title', { length: 256 }),
|
title: varchar('title', { length: 256 }),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
isSemver: boolean('isSemver').default(true),
|
isSemver: boolean('isSemver').default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const changelogs_to_pages = pgTable(
|
||||||
|
'changelogs_to_pages',
|
||||||
|
{
|
||||||
|
changelogId: uuid('changelogId')
|
||||||
|
.notNull()
|
||||||
|
.references(() => changelog.id, { onDelete: 'cascade' }),
|
||||||
|
pageId: uuid('pageId')
|
||||||
|
.notNull()
|
||||||
|
.references(() => page.id, { onDelete: 'cascade' }),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
pk: primaryKey({ columns: [t.changelogId, t.pageId] }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const changelogs_to_pages_relations = relations(
|
||||||
|
changelogs_to_pages,
|
||||||
|
({ one }) => ({
|
||||||
|
changelog: one(changelog, {
|
||||||
|
fields: [changelogs_to_pages.changelogId],
|
||||||
|
references: [changelog.id],
|
||||||
|
}),
|
||||||
|
page: one(page, {
|
||||||
|
fields: [changelogs_to_pages.pageId],
|
||||||
|
references: [page.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const changelog_relation = relations(changelog, ({ many, one }) => ({
|
export const changelog_relation = relations(changelog, ({ many, one }) => ({
|
||||||
versions: many(changelog_version),
|
versions: many(changelog_version),
|
||||||
commits: many(changelog_commit),
|
commits: many(changelog_commit),
|
||||||
@ -33,6 +66,7 @@ export const changelog_relation = relations(changelog, ({ many, one }) => ({
|
|||||||
fields: [changelog.userId],
|
fields: [changelog.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
|
pages: many(changelogs_to_pages),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const changelog_version_status = pgEnum('status', [
|
export const changelog_version_status = pgEnum('status', [
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './access_token'
|
export * from './access_token'
|
||||||
export * from './changelog'
|
export * from './changelog'
|
||||||
|
export * from './page'
|
||||||
|
20
packages/database/src/schema/page.ts
Normal file
20
packages/database/src/schema/page.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import { pgTable, text, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||||
|
import { changelog, changelogs_to_pages } from './changelog'
|
||||||
|
import { user } from './user'
|
||||||
|
|
||||||
|
export const page = pgTable('page', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
userId: varchar('userId', { length: 32 }).references(() => user.id, {
|
||||||
|
onDelete: 'cascade',
|
||||||
|
}),
|
||||||
|
|
||||||
|
title: text('title').notNull(),
|
||||||
|
description: text('description').notNull(),
|
||||||
|
icon: text('icon').default(''),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const pageRelation = relations(page, ({ many }) => ({
|
||||||
|
changelogs: many(changelogs_to_pages),
|
||||||
|
}))
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@boring.tools/logger",
|
"name": "@boring.tools/logger",
|
||||||
"module": "src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { z } from '@hono/zod-openapi'
|
import { z } from '@hono/zod-openapi'
|
||||||
|
import { PageOutput } from '../page'
|
||||||
import { ChangelogOutput } from './base'
|
import { ChangelogOutput } from './base'
|
||||||
|
|
||||||
export const ChangelogListOutput = z.array(ChangelogOutput)
|
export const ChangelogListOutput = z.array(
|
||||||
|
ChangelogOutput.extend({
|
||||||
|
pages: z.array(PageOutput).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ -2,3 +2,4 @@ export * from './general'
|
|||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './changelog'
|
export * from './changelog'
|
||||||
export * from './version'
|
export * from './version'
|
||||||
|
export * from './page'
|
||||||
|
12
packages/schema/src/page/base.ts
Normal file
12
packages/schema/src/page/base.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const PageOutput = z
|
||||||
|
.object({
|
||||||
|
id: z.string().uuid().openapi({
|
||||||
|
example: '',
|
||||||
|
}),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
icon: z.string(),
|
||||||
|
})
|
||||||
|
.openapi('Page')
|
20
packages/schema/src/page/byId.ts
Normal file
20
packages/schema/src/page/byId.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
import { ChangelogOutput } from '../changelog'
|
||||||
|
import { PageOutput } from './base'
|
||||||
|
|
||||||
|
export const PageByIdOutput = PageOutput.extend({
|
||||||
|
changelogs: z.array(ChangelogOutput),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PageByIdParams = z.object({
|
||||||
|
id: z
|
||||||
|
.string()
|
||||||
|
.uuid()
|
||||||
|
.openapi({
|
||||||
|
param: {
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
},
|
||||||
|
example: 'a5ed5965-0506-44e6-aaec-0465ff9fe092',
|
||||||
|
}),
|
||||||
|
})
|
16
packages/schema/src/page/create.ts
Normal file
16
packages/schema/src/page/create.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const PageCreateInput = z
|
||||||
|
.object({
|
||||||
|
title: z.string().min(3).openapi({
|
||||||
|
example: 'My page',
|
||||||
|
}),
|
||||||
|
description: z.string().optional().openapi({
|
||||||
|
example: '',
|
||||||
|
}),
|
||||||
|
icon: z.string().optional().openapi({
|
||||||
|
example: 'base64...',
|
||||||
|
}),
|
||||||
|
changelogIds: z.array(z.string().uuid()),
|
||||||
|
})
|
||||||
|
.openapi('Page')
|
6
packages/schema/src/page/index.ts
Normal file
6
packages/schema/src/page/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './base'
|
||||||
|
export * from './create'
|
||||||
|
export * from './byId'
|
||||||
|
export * from './list'
|
||||||
|
export * from './public'
|
||||||
|
export * from './update'
|
9
packages/schema/src/page/list.ts
Normal file
9
packages/schema/src/page/list.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
import { ChangelogOutput } from '../changelog'
|
||||||
|
import { PageOutput } from './base'
|
||||||
|
|
||||||
|
export const PageListOutput = z.array(
|
||||||
|
PageOutput.extend({
|
||||||
|
changelogs: z.array(ChangelogOutput),
|
||||||
|
}),
|
||||||
|
)
|
33
packages/schema/src/page/public.ts
Normal file
33
packages/schema/src/page/public.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const PagePublicOutput = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
changelogs: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
versions: z.array(
|
||||||
|
z.object({
|
||||||
|
markdown: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
releasedAt: z.date(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PagePublicParams = z.object({
|
||||||
|
id: z
|
||||||
|
.string()
|
||||||
|
.uuid()
|
||||||
|
.openapi({
|
||||||
|
param: {
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
},
|
||||||
|
example: 'a5ed5965-0506-44e6-aaec-0465ff9fe092',
|
||||||
|
}),
|
||||||
|
})
|
26
packages/schema/src/page/update.ts
Normal file
26
packages/schema/src/page/update.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
import { PageOutput } from './base'
|
||||||
|
import { PageCreateInput } from './create'
|
||||||
|
|
||||||
|
export const PageUpdateOutput = PageOutput
|
||||||
|
export const PageUpdateInput = z
|
||||||
|
.object({
|
||||||
|
title: z.string().min(3).optional().openapi({
|
||||||
|
example: 'My page',
|
||||||
|
}),
|
||||||
|
description: z.string().optional().openapi({
|
||||||
|
example: '',
|
||||||
|
}),
|
||||||
|
icon: z.string().optional().openapi({
|
||||||
|
example: 'base64...',
|
||||||
|
}),
|
||||||
|
changelogIds: z.array(z.string().uuid()).optional(),
|
||||||
|
})
|
||||||
|
.openapi('Page')
|
||||||
|
export const PageUpdateParams = z
|
||||||
|
.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
required: ['id'],
|
||||||
|
})
|
@ -11,7 +11,7 @@
|
|||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.1",
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
@ -18,6 +18,8 @@ const buttonVariants = cva(
|
|||||||
secondary:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
'ghost-destructive':
|
||||||
|
'hover:bg-destructive hover:text-destructive-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
153
packages/ui/src/command.tsx
Normal file
153
packages/ui/src/command.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
import { Command as CommandPrimitive } from 'cmdk'
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Dialog, DialogContent } from './dialog'
|
||||||
|
import { cn } from './lib/cn'
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
|
interface CommandDialogProps extends DialogProps {}
|
||||||
|
|
||||||
|
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
ref={ref}
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn('-mx-1 h-px bg-border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const CommandShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CommandShortcut.displayName = 'CommandShortcut'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
120
packages/ui/src/dialog.tsx
Normal file
120
packages/ui/src/dialog.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from './lib/cn'
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = 'DialogHeader'
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = 'DialogFooter'
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'text-lg font-semibold leading-none tracking-tight',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
@ -26,3 +26,5 @@ export * from './collapsible'
|
|||||||
export * from './avatar'
|
export * from './avatar'
|
||||||
export * from './global.css'
|
export * from './global.css'
|
||||||
export * from './breadcrumb'
|
export * from './breadcrumb'
|
||||||
|
export * from './command'
|
||||||
|
export * from './dialog'
|
||||||
|
Loading…
Reference in New Issue
Block a user