Compare commits
No commits in common. "main" and "feature/changelog-public-page" have entirely different histories.
main
...
feature/ch
@ -52,16 +52,6 @@ jobs:
|
||||
# environment: production
|
||||
# sourcemaps: ./build/app/
|
||||
|
||||
- name: Upload CLI to R2
|
||||
uses: shallwefootball/s3-upload-action@master
|
||||
with:
|
||||
aws_key_id: ${{ secrets.R2_ACCESS_ID }}
|
||||
aws_secret_access_key: ${{ secrets.R2_ACCESS_SECRET }}
|
||||
aws_bucket: ${{ secrets.R2_BUCKET }}
|
||||
endpoint: ${{ secrets.R2_URL }}
|
||||
source_dir: "build/cli"
|
||||
destination_dir: "cli"
|
||||
|
||||
|
||||
- name: Set docker chmod (temp solution)
|
||||
run: sudo chmod 666 /var/run/docker.sock
|
||||
|
@ -6,12 +6,6 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis:7.4.1-alpine
|
||||
# command: redis-server --save 20 1 --loglevel warning --requirepass development
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
|
||||
@ -50,6 +44,4 @@ jobs:
|
||||
run: bun test:api
|
||||
env:
|
||||
NODE_ENV: test
|
||||
POSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres
|
||||
REDIS_URL: redis://redis:6379
|
||||
REDIS_PASSWORD: development
|
||||
POSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres
|
1
apps/api/.env.example
Normal file
1
apps/api/.env.example
Normal file
@ -0,0 +1 @@
|
||||
POSTGRES_URL=postgres://postgres:postgres@localhost:5432/postgres
|
@ -2,7 +2,7 @@
|
||||
"name": "@boring.tools/api",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"build": "bun build --entrypoints ./src/index.ts --outdir ../../build/api --target bun --splitting",
|
||||
"build": "bun build --entrypoints ./src/index.ts --outdir ../../build/api --target bun --splitting --sourcemap=linked",
|
||||
"test": "bun test --preload ./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -13,12 +13,9 @@
|
||||
"@hono/sentry": "^1.2.0",
|
||||
"@hono/zod-openapi": "^0.16.2",
|
||||
"@scalar/hono-api-reference": "^0.5.149",
|
||||
"convert-gitmoji": "^0.1.5",
|
||||
"hono": "^4.6.3",
|
||||
"redis": "^4.7.0",
|
||||
"semver": "^7.6.3",
|
||||
"svix": "^1.36.0",
|
||||
"ts-pattern": "^5.5.0"
|
||||
"svix": "^1.36.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
@ -1,151 +0,0 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
||||
import { access_token, db, user } from '@boring.tools/database'
|
||||
import type {
|
||||
AccessTokenCreateInput,
|
||||
AccessTokenListOutput,
|
||||
AccessTokenOutput,
|
||||
ChangelogCreateInput,
|
||||
ChangelogCreateOutput,
|
||||
ChangelogListOutput,
|
||||
ChangelogOutput,
|
||||
UserOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import type { z } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { fetch } from '../utils/testing/fetch'
|
||||
|
||||
describe('AccessToken', () => {
|
||||
let testUser: z.infer<typeof UserOutput>
|
||||
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
||||
let createdAccessToken: z.infer<typeof AccessTokenOutput>
|
||||
let testChangelog: z.infer<typeof ChangelogOutput>
|
||||
|
||||
beforeAll(async () => {
|
||||
const createdUser = await db
|
||||
.insert(user)
|
||||
.values({ email: 'access_token@test.local', providerId: 'test_000' })
|
||||
.returning()
|
||||
const tAccessToken = await db
|
||||
.insert(access_token)
|
||||
.values({
|
||||
token: '1234567890',
|
||||
userId: createdUser[0].id,
|
||||
name: 'testtoken',
|
||||
})
|
||||
.returning()
|
||||
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
||||
testUser = createdUser[0] as z.infer<typeof UserOutput>
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.delete(user).where(eq(user.email, 'access_token@test.local'))
|
||||
})
|
||||
|
||||
describe('Create', () => {
|
||||
test('Success', async () => {
|
||||
const payload: z.infer<typeof AccessTokenCreateInput> = {
|
||||
name: 'Test Token',
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/access-token',
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof AccessTokenOutput>
|
||||
|
||||
createdAccessToken = json
|
||||
expect(res.status).toBe(201)
|
||||
})
|
||||
})
|
||||
|
||||
// describe('By Id', () => {
|
||||
// test('Success', async () => {
|
||||
// const res = await fetch(
|
||||
// {
|
||||
// path: `/v1/changelog/${testChangelog.id}`,
|
||||
// method: 'GET',
|
||||
// },
|
||||
// testAccessToken.token as string,
|
||||
// )
|
||||
|
||||
// expect(res.status).toBe(200)
|
||||
// })
|
||||
|
||||
// test('Not Found', async () => {
|
||||
// const res = await fetch(
|
||||
// {
|
||||
// path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||
// method: 'GET',
|
||||
// },
|
||||
// testAccessToken.token as string,
|
||||
// )
|
||||
|
||||
// expect(res.status).toBe(404)
|
||||
// })
|
||||
|
||||
// test('Invalid Id', async () => {
|
||||
// const res = await fetch(
|
||||
// {
|
||||
// path: '/v1/changelog/some',
|
||||
// method: 'GET',
|
||||
// },
|
||||
// testAccessToken.token as string,
|
||||
// )
|
||||
|
||||
// expect(res.status).toBe(400)
|
||||
|
||||
// const json = (await res.json()) as { success: boolean }
|
||||
// expect(json.success).toBeFalse()
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('List', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/access-token',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof AccessTokenListOutput>
|
||||
// Check if token is redacted
|
||||
expect(json[0].token).toHaveLength(10)
|
||||
expect(json).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/access-token/${createdAccessToken.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
test('Not found', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/access-token/${createdAccessToken.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,56 +0,0 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { access_token, db } from '@boring.tools/database'
|
||||
import { AccessTokenCreateInput, AccessTokenOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
tags: ['access-token'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: AccessTokenCreateInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: AccessTokenOutput },
|
||||
},
|
||||
description: 'Commits created',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerAccessTokenCreate = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
|
||||
const data: z.infer<typeof AccessTokenCreateInput> = await c.req.json()
|
||||
const token = crypto.randomBytes(20).toString('hex')
|
||||
|
||||
const [result] = await db
|
||||
.insert(access_token)
|
||||
.values({
|
||||
...data,
|
||||
userId,
|
||||
token: `bt_${token}`,
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(AccessTokenOutput.parse(result), 201)
|
||||
})
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { access_token, db } from '@boring.tools/database'
|
||||
import { GeneralOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
tags: ['access-token'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': { schema: GeneralOutput },
|
||||
},
|
||||
description: 'Removes a access token by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const id = c.req.param('id')
|
||||
const userId = await verifyAuthentication(c)
|
||||
|
||||
const [result] = await db
|
||||
.delete(access_token)
|
||||
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(GeneralOutput.parse({ message: 'Access token deleted' }), 200)
|
||||
})
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import type { Variables } from '..'
|
||||
import type { ContextModule } from '../utils/sentry'
|
||||
import { registerAccessTokenCreate } from './create'
|
||||
import { registerAccessTokenDelete } from './delete'
|
||||
import { registerAccessTokenList } from './list'
|
||||
|
||||
export const accessTokenApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'access-token',
|
||||
}
|
||||
|
||||
registerAccessTokenCreate(accessTokenApi)
|
||||
registerAccessTokenList(accessTokenApi)
|
||||
registerAccessTokenDelete(accessTokenApi)
|
@ -1,48 +0,0 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import { AccessTokenListOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { accessTokenApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
tags: ['access-token'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AccessTokenListOutput,
|
||||
},
|
||||
},
|
||||
description: 'Return version by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerAccessTokenList = (api: typeof accessTokenApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const result = await db.query.access_token.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
orderBy: () => desc(changelog.createdAt),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Access Tokens not found' })
|
||||
}
|
||||
|
||||
const mappedData = result.map((at) => ({
|
||||
...at,
|
||||
token: at.token.substring(0, 10),
|
||||
}))
|
||||
|
||||
return c.json(AccessTokenListOutput.parse(mappedData), 200)
|
||||
})
|
||||
}
|
@ -4,14 +4,9 @@ import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { changelogApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/:id',
|
||||
tags: ['changelog'],
|
||||
request: {
|
||||
params: ChangelogByIdParams,
|
||||
},
|
||||
@ -24,35 +19,39 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerChangelogById = (api: typeof changelogApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const { id } = c.req.valid('param')
|
||||
|
||||
const result = await db.query.changelog.findFirst({
|
||||
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
||||
with: {
|
||||
pages: {
|
||||
with: {
|
||||
page: true,
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
orderBy: (changelog_version, { desc }) => [
|
||||
desc(changelog_version.createdAt),
|
||||
],
|
||||
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||
const result = await db.query.changelog.findFirst({
|
||||
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
||||
with: {
|
||||
pages: {
|
||||
with: {
|
||||
page: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
return c.json(ChangelogOutput.parse(result), 200)
|
||||
versions: {
|
||||
orderBy: (changelog_version, { desc }) => [
|
||||
desc(changelog_version.createdAt),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
||||
|
@ -12,23 +12,18 @@ import { eq } from 'drizzle-orm'
|
||||
import { fetch } from '../utils/testing/fetch'
|
||||
|
||||
describe('Changelog', () => {
|
||||
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
||||
let testAccessToken: AccessTokenOutput
|
||||
let testChangelog: z.infer<typeof ChangelogOutput>
|
||||
|
||||
beforeAll(async () => {
|
||||
const createdUser = await db
|
||||
await db
|
||||
.insert(user)
|
||||
.values({ email: 'changelog@test.local', providerId: 'test_000' })
|
||||
.returning()
|
||||
.values({ email: 'changelog@test.local', id: 'test_000' })
|
||||
const tAccessToken = await db
|
||||
.insert(access_token)
|
||||
.values({
|
||||
token: 'test123',
|
||||
userId: createdUser[0].id,
|
||||
name: 'testtoken',
|
||||
})
|
||||
.values({ token: 'test123', userId: 'test_000', name: 'testtoken' })
|
||||
.returning()
|
||||
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
||||
testAccessToken = tAccessToken[0]
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -40,8 +35,6 @@ describe('Changelog', () => {
|
||||
const payload: z.infer<typeof ChangelogCreateInput> = {
|
||||
title: 'changelog',
|
||||
description: 'description',
|
||||
isSemver: true,
|
||||
isConventional: true,
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
@ -50,10 +43,10 @@ describe('Changelog', () => {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof ChangelogCreateOutput>
|
||||
const json: z.infer<typeof ChangelogCreateOutput> = await res.json()
|
||||
testChangelog = json
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
@ -68,7 +61,7 @@ describe('Changelog', () => {
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
@ -80,7 +73,7 @@ describe('Changelog', () => {
|
||||
path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
@ -92,12 +85,12 @@ describe('Changelog', () => {
|
||||
path: '/v1/changelog/some',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
const json = (await res.json()) as { success: boolean }
|
||||
const json = await res.json()
|
||||
expect(json.success).toBeFalse()
|
||||
})
|
||||
})
|
||||
@ -109,12 +102,12 @@ describe('Changelog', () => {
|
||||
path: '/v1/changelog',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof ChangelogListOutput>
|
||||
const json: z.infer<typeof ChangelogListOutput> = await res.json()
|
||||
expect(json).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@ -126,7 +119,7 @@ describe('Changelog', () => {
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
@ -138,7 +131,7 @@ describe('Changelog', () => {
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
|
@ -1,67 +0,0 @@
|
||||
import { changelog, changelog_commit, db } from '@boring.tools/database'
|
||||
import { CommitCreateInput, CommitCreateOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { changelogCommitApi } from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
tags: ['commit'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: CommitCreateInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: CommitCreateOutput },
|
||||
},
|
||||
description: 'Commits created',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerCommitCreate = (api: typeof changelogCommitApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
|
||||
const data: z.infer<typeof CommitCreateInput> = await c.req.json()
|
||||
const changelogResult = await db.query.changelog.findFirst({
|
||||
where: and(
|
||||
eq(changelog.id, data[0].changelogId),
|
||||
eq(changelog.userId, userId),
|
||||
),
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
const mappedData = data.map((entry) => ({
|
||||
...entry,
|
||||
createdAt: new Date(entry.author.date),
|
||||
}))
|
||||
|
||||
const [result] = await db
|
||||
.insert(changelog_commit)
|
||||
.values(mappedData)
|
||||
.onConflictDoNothing()
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(CommitCreateOutput.parse(result), 201)
|
||||
})
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import type { Variables } from '../..'
|
||||
import type { ContextModule } from '../../utils/sentry'
|
||||
import { registerCommitCreate } from './create'
|
||||
import { registerCommitList } from './list'
|
||||
|
||||
export const changelogCommitApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'changelog',
|
||||
sub_module: 'version',
|
||||
}
|
||||
|
||||
registerCommitCreate(changelogCommitApi)
|
||||
registerCommitList(changelogCommitApi)
|
@ -1,83 +0,0 @@
|
||||
import { changelog, changelog_commit, db } from '@boring.tools/database'
|
||||
import { CommitListOutput, CommitListParams } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq, isNotNull, isNull } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { P, match } from 'ts-pattern'
|
||||
|
||||
import type { changelogCommitApi } from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
tags: ['commit'],
|
||||
request: {
|
||||
query: CommitListParams,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CommitListOutput,
|
||||
},
|
||||
},
|
||||
description: 'Return version by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerCommitList = (api: typeof changelogCommitApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const { changelogId, limit, offset, hasVersion } = c.req.valid('query')
|
||||
const result = await db.query.changelog.findFirst({
|
||||
where: and(eq(changelog.userId, userId), eq(changelog.id, changelogId)),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Changelog not found' })
|
||||
}
|
||||
|
||||
const where = match({ changelogId, hasVersion })
|
||||
.with(
|
||||
{
|
||||
changelogId: P.select('changelogId'),
|
||||
hasVersion: P.when((hasVersion) => hasVersion === true),
|
||||
},
|
||||
({ changelogId }) =>
|
||||
and(
|
||||
eq(changelog_commit.changelogId, changelogId),
|
||||
isNotNull(changelog_commit.versionId),
|
||||
),
|
||||
)
|
||||
.with(
|
||||
{
|
||||
changelogId: P.select('changelogId'),
|
||||
hasVersion: P.when((hasVersion) => hasVersion === false),
|
||||
},
|
||||
({ changelogId }) =>
|
||||
and(
|
||||
eq(changelog_commit.changelogId, changelogId),
|
||||
isNull(changelog_commit.versionId),
|
||||
),
|
||||
)
|
||||
.otherwise(() => eq(changelog_commit.changelogId, changelogId))
|
||||
|
||||
const commits = await db.query.changelog_commit.findMany({
|
||||
where,
|
||||
limit: Number(limit) ?? 10,
|
||||
offset: Number(offset) ?? 0,
|
||||
orderBy: (_, { desc }) => [desc(changelog_commit.createdAt)],
|
||||
})
|
||||
|
||||
if (!commits) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(CommitListOutput.parse(commits), 200)
|
||||
})
|
||||
}
|
@ -5,14 +5,9 @@ import {
|
||||
} from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
|
||||
import type { changelogApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
tags: ['changelog'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
@ -27,24 +22,32 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Return created changelog',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerChangelogCreate = (api: typeof changelogApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const payload: z.infer<typeof ChangelogCreateInput> = await c.req.json()
|
||||
|
||||
const [result] = await db
|
||||
.insert(changelog)
|
||||
.values({
|
||||
...payload,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
|
||||
return c.json(ChangelogCreateOutput.parse(result), 201)
|
||||
})
|
||||
export const func = async ({
|
||||
userId,
|
||||
payload,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof ChangelogCreateInput>
|
||||
}) => {
|
||||
return await db
|
||||
.insert(changelog)
|
||||
.values({
|
||||
...payload,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
||||
|
@ -4,14 +4,9 @@ import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { changelogApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
tags: ['changelog'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
@ -21,25 +16,29 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Removes a changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerChangelogDelete = async (api: typeof changelogApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const id = c.req.param('id')
|
||||
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||
const result = await db
|
||||
.delete(changelog)
|
||||
.where(and(eq(changelog.userId, userId), eq(changelog.id, id)))
|
||||
.returning()
|
||||
|
||||
const [result] = await db
|
||||
.delete(changelog)
|
||||
.where(and(eq(changelog.userId, userId), eq(changelog.id, id)))
|
||||
.returning()
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
return c.json(GeneralOutput.parse(result), 200)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
||||
|
@ -1,28 +1,130 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import { cors } from 'hono/cors'
|
||||
import type { Variables } from '..'
|
||||
import type { ContextModule } from '../utils/sentry'
|
||||
import { registerChangelogById } from './byId'
|
||||
import { changelogCommitApi } from './commit'
|
||||
import { registerChangelogCreate } from './create'
|
||||
import { registerChangelogDelete } from './delete'
|
||||
import { registerChangelogList } from './list'
|
||||
import { registerChangelogUpdate } from './update'
|
||||
import version from './version'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { type ContextModule, captureSentry } from '../utils/sentry'
|
||||
import ById from './byId'
|
||||
import Create from './create'
|
||||
import Delete from './delete'
|
||||
import List from './list'
|
||||
import Update from './update'
|
||||
|
||||
export const changelogApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'changelog',
|
||||
}
|
||||
changelogApi.use('*', cors())
|
||||
changelogApi.route('/commit', changelogCommitApi)
|
||||
changelogApi.route('/version', version)
|
||||
|
||||
registerChangelogById(changelogApi)
|
||||
registerChangelogCreate(changelogApi)
|
||||
registerChangelogDelete(changelogApi)
|
||||
registerChangelogUpdate(changelogApi)
|
||||
registerChangelogList(changelogApi)
|
||||
app.openapi(ById.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await ById.func({ userId, id })
|
||||
return c.json(result, 200)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default changelogApi
|
||||
app.openapi(List.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const result = await List.func({ userId })
|
||||
return c.json(result, 200)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Create.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const [result] = await Create.func({
|
||||
userId,
|
||||
payload: await c.req.json(),
|
||||
})
|
||||
return c.json(result, 201)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Delete.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await Delete.func({ userId, id })
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
return c.json({ message: 'Changelog removed' })
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Update.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
|
||||
if (!id) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
const result = await Update.func({
|
||||
userId,
|
||||
payload: await c.req.json(),
|
||||
id,
|
||||
})
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
return c.json(result)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
|
@ -3,14 +3,9 @@ import { ChangelogListOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import type { changelogApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
tags: ['changelog'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
@ -20,37 +15,40 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Return changelogs for current user',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerChangelogList = (api: typeof changelogApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
|
||||
const result = await db.query.changelog.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
with: {
|
||||
versions: true,
|
||||
commits: {
|
||||
columns: { id: true },
|
||||
},
|
||||
export const func = async ({ userId }: { userId: string }) => {
|
||||
const result = await db.query.changelog.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
with: {
|
||||
versions: true,
|
||||
commits: {
|
||||
columns: { id: true },
|
||||
},
|
||||
orderBy: (changelog, { asc }) => [asc(changelog.createdAt)],
|
||||
})
|
||||
},
|
||||
orderBy: (changelog, { asc }) => [asc(changelog.createdAt)],
|
||||
})
|
||||
|
||||
const mappedData = result.map((changelog) => {
|
||||
const { versions, commits, ...rest } = changelog
|
||||
return {
|
||||
...rest,
|
||||
computed: {
|
||||
versionCount: versions.length,
|
||||
commitCount: commits.length,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return c.json(ChangelogListOutput.parse(mappedData), 200)
|
||||
return result.map((changelog) => {
|
||||
const { versions, commits, ...rest } = changelog
|
||||
return {
|
||||
...rest,
|
||||
computed: {
|
||||
versionCount: versions.length,
|
||||
commitCount: commits.length,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
||||
|
@ -7,11 +7,6 @@ import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type { changelogApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
import { redis } from '../utils/redis'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'put',
|
||||
path: '/:id',
|
||||
@ -30,32 +25,40 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Return updated changelog',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerChangelogUpdate = (api: typeof changelogApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const id = c.req.param('id')
|
||||
const payload: z.infer<typeof ChangelogUpdateInput> = await c.req.json()
|
||||
export const func = async ({
|
||||
userId,
|
||||
payload,
|
||||
id,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof ChangelogUpdateInput>
|
||||
id: string
|
||||
}) => {
|
||||
const [result] = await db
|
||||
.update(changelog)
|
||||
.set({
|
||||
...payload,
|
||||
})
|
||||
.where(and(eq(changelog.id, id), eq(changelog.userId, userId)))
|
||||
.returning()
|
||||
|
||||
const [result] = await db
|
||||
.update(changelog)
|
||||
.set({
|
||||
...payload,
|
||||
})
|
||||
.where(and(eq(changelog.id, id), eq(changelog.userId, userId)))
|
||||
.returning()
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
if (result.pageId) {
|
||||
redis.del(result.pageId)
|
||||
}
|
||||
return c.json(ChangelogUpdateOutput.parse(result), 200)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
||||
|
@ -1,22 +1,11 @@
|
||||
import {
|
||||
changelog,
|
||||
changelog_commit,
|
||||
changelog_version,
|
||||
db,
|
||||
} from '@boring.tools/database'
|
||||
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||
import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type changelogVersionApi from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
|
||||
export const byId = createRoute({
|
||||
method: 'get',
|
||||
path: '/:id',
|
||||
tags: ['version'],
|
||||
request: {
|
||||
params: VersionByIdParams,
|
||||
},
|
||||
@ -29,46 +18,49 @@ export const byId = createRoute({
|
||||
},
|
||||
description: 'Return version by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerVersionById = (api: typeof changelogVersionApi) => {
|
||||
return api.openapi(byId, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const { id } = c.req.valid('param')
|
||||
|
||||
const versionResult = await db.query.changelog_version.findFirst({
|
||||
where: eq(changelog_version.id, id),
|
||||
with: {
|
||||
commits: {
|
||||
orderBy: () => desc(changelog_commit.createdAt),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!versionResult) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
if (!versionResult.changelogId) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
|
||||
const changelogIds = changelogResult.map((cl) => cl.id)
|
||||
|
||||
if (!changelogIds.includes(versionResult.changelogId)) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(VersionOutput.parse(versionResult), 200)
|
||||
export const byIdFunc = async ({
|
||||
userId,
|
||||
id,
|
||||
}: {
|
||||
userId: string
|
||||
id: string
|
||||
}) => {
|
||||
const versionResult = await db.query.changelog_version.findFirst({
|
||||
where: eq(changelog_version.id, id),
|
||||
with: {
|
||||
commits: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!versionResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!versionResult.changelogId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
|
||||
const changelogIds = changelogResult.map((cl) => cl.id)
|
||||
|
||||
if (!changelogIds.includes(versionResult.changelogId)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return versionResult
|
||||
}
|
||||
|
@ -1,24 +1,13 @@
|
||||
import {
|
||||
changelog,
|
||||
changelog_commit,
|
||||
changelog_version,
|
||||
db,
|
||||
} from '@boring.tools/database'
|
||||
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||
import { VersionCreateInput, VersionCreateOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import semver from 'semver'
|
||||
|
||||
import type changelogVersionApi from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
import { redis } from '../../utils/redis'
|
||||
|
||||
export const route = createRoute({
|
||||
export const create = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
tags: ['version'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
@ -33,71 +22,67 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Version created',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerVersionCreate = (api: typeof changelogVersionApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const payload: z.infer<typeof VersionCreateInput> = await c.req.json()
|
||||
export const createFunc = async ({
|
||||
userId,
|
||||
payload,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof VersionCreateInput>
|
||||
}) => {
|
||||
const formattedVersion = semver.coerce(payload.version)
|
||||
const validVersion = semver.valid(formattedVersion)
|
||||
|
||||
const changelogResult = await db.query.changelog.findFirst({
|
||||
where: and(
|
||||
eq(changelog.userId, userId),
|
||||
eq(changelog.id, payload.changelogId),
|
||||
),
|
||||
with: {
|
||||
versions: {
|
||||
where: and(
|
||||
eq(changelog_version.changelogId, payload.changelogId),
|
||||
eq(changelog_version.version, payload.version),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Changelog not found',
|
||||
})
|
||||
}
|
||||
|
||||
if (changelogResult.versions.length) {
|
||||
throw new HTTPException(409, {
|
||||
message: 'Version exists already',
|
||||
})
|
||||
}
|
||||
|
||||
const formattedVersion = semver.coerce(payload.version)
|
||||
const validVersion = semver.valid(formattedVersion)
|
||||
|
||||
if (validVersion === null) {
|
||||
throw new HTTPException(409, {
|
||||
message: 'Version is not semver compatible',
|
||||
})
|
||||
}
|
||||
|
||||
const [versionCreateResult] = await db
|
||||
.insert(changelog_version)
|
||||
.values({
|
||||
changelogId: payload.changelogId,
|
||||
version: validVersion,
|
||||
status: payload.status,
|
||||
markdown: payload.markdown,
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (changelogResult.pageId) {
|
||||
redis.del(changelogResult.pageId)
|
||||
}
|
||||
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: versionCreateResult.id })
|
||||
.where(inArray(changelog_commit.id, payload.commitIds))
|
||||
|
||||
return c.json(VersionCreateOutput.parse(versionCreateResult), 201)
|
||||
const changelogResult = await db.query.changelog.findFirst({
|
||||
where: and(
|
||||
eq(changelog.userId, userId),
|
||||
eq(changelog.id, payload.changelogId),
|
||||
),
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'changelog not found',
|
||||
})
|
||||
}
|
||||
|
||||
if (validVersion === null) {
|
||||
throw new HTTPException(409, {
|
||||
message: 'Version is not semver compatible',
|
||||
})
|
||||
}
|
||||
|
||||
// Check if a version with the same version already exists
|
||||
const versionResult = await db.query.changelog_version.findFirst({
|
||||
where: and(
|
||||
eq(changelog_version.changelogId, payload.changelogId),
|
||||
eq(changelog_version.version, validVersion),
|
||||
),
|
||||
})
|
||||
|
||||
if (versionResult) {
|
||||
throw new HTTPException(409, {
|
||||
message: 'Version exists already',
|
||||
})
|
||||
}
|
||||
|
||||
const [versionCreateResult] = await db
|
||||
.insert(changelog_version)
|
||||
.values({
|
||||
changelogId: payload.changelogId,
|
||||
version: validVersion,
|
||||
status: payload.status,
|
||||
markdown: payload.markdown,
|
||||
})
|
||||
.returning()
|
||||
|
||||
return versionCreateResult
|
||||
}
|
||||
|
@ -1,176 +0,0 @@
|
||||
import {
|
||||
changelog,
|
||||
changelog_commit,
|
||||
changelog_version,
|
||||
db,
|
||||
} from '@boring.tools/database'
|
||||
import {
|
||||
VersionCreateAutoInput,
|
||||
VersionCreateOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { format } from 'date-fns'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import semver from 'semver'
|
||||
|
||||
import type { changelogVersionApi } from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { commitsToMarkdown } from '../../utils/git/commitsToMarkdown'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
import { redis } from '../../utils/redis'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/auto',
|
||||
tags: ['commit'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: VersionCreateAutoInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: VersionCreateOutput },
|
||||
},
|
||||
description: 'Commits created',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
const getVersion = (version: string) => {
|
||||
const isValid = semver.valid(semver.coerce(version))
|
||||
|
||||
if (isValid) {
|
||||
return isValid
|
||||
}
|
||||
return format(new Date(), 'dd.MM.yy')
|
||||
}
|
||||
|
||||
const getNextVersion = ({
|
||||
version,
|
||||
isSemver,
|
||||
}: { version: string; isSemver: boolean }): string => {
|
||||
if (isSemver) {
|
||||
if (version === '') {
|
||||
return '1.0.0'
|
||||
}
|
||||
const isValid = semver.valid(semver.coerce(version))
|
||||
|
||||
if (isValid) {
|
||||
const nextVersion = semver.inc(isValid, 'patch')
|
||||
if (!nextVersion) {
|
||||
throw new Error('Incorrect semver')
|
||||
}
|
||||
return nextVersion
|
||||
}
|
||||
}
|
||||
return format(new Date(), 'dd.MM.yy')
|
||||
}
|
||||
|
||||
export const registerVersionCreateAuto = (api: typeof changelogVersionApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const data: z.infer<typeof VersionCreateAutoInput> = await c.req.json()
|
||||
const changelogResult = await db.query.changelog.findFirst({
|
||||
where: and(
|
||||
eq(changelog.id, data.changelogId),
|
||||
eq(changelog.userId, userId),
|
||||
),
|
||||
with: {
|
||||
versions: {
|
||||
columns: {
|
||||
version: true,
|
||||
},
|
||||
orderBy: (_, { asc }) => asc(changelog_version.createdAt),
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version could not be created. Changelog not found.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!changelogResult.isConventional) {
|
||||
throw new HTTPException(500, {
|
||||
message: 'Auto generating only works with conventional commits.',
|
||||
})
|
||||
}
|
||||
|
||||
const commits = await db.query.changelog_commit.findMany({
|
||||
where: and(
|
||||
isNull(changelog_commit.versionId),
|
||||
eq(changelog_commit.changelogId, data.changelogId),
|
||||
),
|
||||
})
|
||||
// @ts-ignore
|
||||
const markdown = await commitsToMarkdown(commits)
|
||||
|
||||
// If no version exists, create the first one
|
||||
if (changelogResult?.versions.length === 0) {
|
||||
// If the changelog follows semver starts with version 1.0.0
|
||||
const inputVersion = changelog.isSemver
|
||||
? semver.valid(semver.coerce(data.version))
|
||||
: data.version
|
||||
const [versionCreateResult] = await db
|
||||
.insert(changelog_version)
|
||||
.values({
|
||||
changelogId: changelogResult.id,
|
||||
version:
|
||||
inputVersion ??
|
||||
getNextVersion({
|
||||
version: '',
|
||||
isSemver: changelogResult.isSemver ?? true,
|
||||
}),
|
||||
status: 'draft',
|
||||
markdown,
|
||||
})
|
||||
.returning()
|
||||
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: versionCreateResult.id })
|
||||
.where(isNull(changelog_commit.versionId))
|
||||
|
||||
if (changelogResult.pageId) {
|
||||
redis.del(changelogResult.pageId)
|
||||
}
|
||||
|
||||
return c.json(versionCreateResult, 201)
|
||||
}
|
||||
|
||||
const [versionCreateResult] = await db
|
||||
.insert(changelog_version)
|
||||
.values({
|
||||
changelogId: changelogResult.id,
|
||||
version: data.version
|
||||
? getVersion(data.version)
|
||||
: getNextVersion({
|
||||
version: changelogResult.versions[0].version,
|
||||
isSemver: changelogResult.isSemver ?? true,
|
||||
}),
|
||||
status: 'draft',
|
||||
markdown,
|
||||
})
|
||||
.returning()
|
||||
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: versionCreateResult.id })
|
||||
.where(isNull(changelog_commit.versionId))
|
||||
|
||||
if (changelogResult.pageId) {
|
||||
redis.del(changelogResult.pageId)
|
||||
}
|
||||
|
||||
return c.json(VersionCreateOutput.parse(versionCreateResult), 201)
|
||||
})
|
||||
}
|
@ -1,22 +1,12 @@
|
||||
import {
|
||||
changelog,
|
||||
changelog_commit,
|
||||
changelog_version,
|
||||
db,
|
||||
} from '@boring.tools/database'
|
||||
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||
import { GeneralOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type changelogVersionApi from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
import { redis } from '../../utils/redis'
|
||||
|
||||
export const route = createRoute({
|
||||
export const remove = createRoute({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
tags: ['version'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
@ -26,55 +16,48 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Removes a version by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerVersionDelete = async (
|
||||
api: typeof changelogVersionApi,
|
||||
) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const id = c.req.param('id')
|
||||
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
with: {
|
||||
versions: {
|
||||
where: eq(changelog_version.id, id),
|
||||
404: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GeneralOutput,
|
||||
},
|
||||
},
|
||||
})
|
||||
description: 'Version not found',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
const findChangelog = changelogResult.find((change) =>
|
||||
change.versions.find((ver) => ver.id === id),
|
||||
)
|
||||
|
||||
if (!findChangelog?.versions.length) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version not found',
|
||||
})
|
||||
}
|
||||
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: null })
|
||||
.where(eq(changelog_commit.versionId, id))
|
||||
|
||||
if (findChangelog.pageId) {
|
||||
redis.del(findChangelog.pageId)
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(changelog_version)
|
||||
.where(and(eq(changelog_version.id, id)))
|
||||
.returning()
|
||||
|
||||
return c.json(GeneralOutput.parse({ message: 'Version deleted' }), 200)
|
||||
export const removeFunc = async ({
|
||||
userId,
|
||||
id,
|
||||
}: {
|
||||
userId: string
|
||||
id: string
|
||||
}) => {
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
with: {
|
||||
versions: {
|
||||
where: eq(changelog_version.id, id),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const findChangelog = changelogResult.find((change) =>
|
||||
change.versions.find((ver) => ver.id === id),
|
||||
)
|
||||
|
||||
if (!findChangelog?.versions.length) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version not found',
|
||||
})
|
||||
}
|
||||
|
||||
return db
|
||||
.delete(changelog_version)
|
||||
.where(and(eq(changelog_version.id, id)))
|
||||
.returning()
|
||||
}
|
||||
|
@ -1,24 +1,126 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
|
||||
import type { Variables } from '../..'
|
||||
import type { ContextModule } from '../../utils/sentry'
|
||||
import { registerVersionById } from './byId'
|
||||
import { registerVersionCreate } from './create'
|
||||
import { registerVersionCreateAuto } from './createAuto'
|
||||
import { registerVersionDelete } from './delete'
|
||||
import { registerVersionUpdate } from './update'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { type ContextModule, captureSentry } from '../../utils/sentry'
|
||||
import { byId, byIdFunc } from './byId'
|
||||
import { create, createFunc } from './create'
|
||||
import { remove, removeFunc } from './delete'
|
||||
import { update, updateFunc } from './update'
|
||||
|
||||
export const changelogVersionApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'changelog',
|
||||
sub_module: 'version',
|
||||
}
|
||||
|
||||
registerVersionCreateAuto(changelogVersionApi)
|
||||
registerVersionById(changelogVersionApi)
|
||||
registerVersionCreate(changelogVersionApi)
|
||||
registerVersionDelete(changelogVersionApi)
|
||||
registerVersionUpdate(changelogVersionApi)
|
||||
app.openapi(create, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const payload = await c.req.json()
|
||||
const result = await createFunc({ userId, payload })
|
||||
|
||||
export default changelogVersionApi
|
||||
if (!result) {
|
||||
return c.json({ message: 'Version not created' }, 400)
|
||||
}
|
||||
|
||||
return c.json(result, 201)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(byId, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await byIdFunc({ userId, id })
|
||||
|
||||
if (!result) {
|
||||
return c.json({ message: 'Version not found' }, 404)
|
||||
}
|
||||
|
||||
// Ensure all required properties are present and non-null
|
||||
return c.json(
|
||||
{
|
||||
...result,
|
||||
changelogId: result.changelogId || '',
|
||||
version: result.version || '',
|
||||
status: result.status || 'draft',
|
||||
releasedAt: result.releasedAt,
|
||||
commits: result.commits || [],
|
||||
markdown: result.markdown || '',
|
||||
},
|
||||
200,
|
||||
)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(update, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
|
||||
if (!id) {
|
||||
return c.json({ message: 'Version not found' }, 404)
|
||||
}
|
||||
|
||||
const result = await updateFunc({
|
||||
userId,
|
||||
payload: await c.req.json(),
|
||||
id,
|
||||
})
|
||||
|
||||
return c.json(result)
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(remove, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await removeFunc({ userId, id })
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ message: 'Version not found' }, 404)
|
||||
}
|
||||
|
||||
return c.json({ message: 'Version removed' })
|
||||
} catch (error) {
|
||||
return captureSentry({
|
||||
c,
|
||||
error,
|
||||
module,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
|
@ -1,23 +1,12 @@
|
||||
import {
|
||||
changelog,
|
||||
changelog_commit,
|
||||
changelog_version,
|
||||
db,
|
||||
} from '@boring.tools/database'
|
||||
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||
import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq, inArray, notInArray } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import type changelogVersionApi from '.'
|
||||
import { verifyAuthentication } from '../../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
||||
import { redis } from '../../utils/redis'
|
||||
|
||||
export const route = createRoute({
|
||||
export const update = createRoute({
|
||||
method: 'put',
|
||||
path: '/:id',
|
||||
tags: ['version'],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
@ -32,69 +21,52 @@ export const route = createRoute({
|
||||
},
|
||||
description: 'Return updated version',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const id = c.req.param('id')
|
||||
const payload: z.infer<typeof VersionUpdateInput> = await c.req.json()
|
||||
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
with: {
|
||||
versions: {
|
||||
where: eq(changelog_version.id, id),
|
||||
},
|
||||
export const updateFunc = async ({
|
||||
userId,
|
||||
payload,
|
||||
id,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof VersionUpdateInput>
|
||||
id: string
|
||||
}) => {
|
||||
const changelogResult = await db.query.changelog.findMany({
|
||||
where: and(eq(changelog.userId, userId)),
|
||||
with: {
|
||||
versions: {
|
||||
where: eq(changelog_version.id, id),
|
||||
},
|
||||
})
|
||||
|
||||
if (!changelogResult) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version not found',
|
||||
})
|
||||
}
|
||||
|
||||
const findChangelog = changelogResult.find((change) =>
|
||||
change.versions.find((ver) => ver.id === id),
|
||||
)
|
||||
|
||||
if (!findChangelog?.versions.length) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version not found',
|
||||
})
|
||||
}
|
||||
|
||||
const [versionUpdateResult] = await db
|
||||
.update(changelog_version)
|
||||
.set({
|
||||
version: payload.version,
|
||||
status: payload.status,
|
||||
markdown: payload.markdown,
|
||||
releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
|
||||
})
|
||||
.where(and(eq(changelog_version.id, id)))
|
||||
.returning()
|
||||
|
||||
if (payload.commitIds) {
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: null })
|
||||
.where(notInArray(changelog_commit.id, payload.commitIds))
|
||||
|
||||
await db
|
||||
.update(changelog_commit)
|
||||
.set({ versionId: versionUpdateResult.id })
|
||||
.where(inArray(changelog_commit.id, payload.commitIds))
|
||||
}
|
||||
|
||||
if (findChangelog.pageId) {
|
||||
redis.del(findChangelog.pageId)
|
||||
}
|
||||
|
||||
return c.json(VersionUpdateOutput.parse(versionUpdateResult), 200)
|
||||
},
|
||||
})
|
||||
|
||||
const findChangelog = changelogResult.find((change) =>
|
||||
change.versions.find((ver) => ver.id === id),
|
||||
)
|
||||
|
||||
if (!findChangelog?.versions.length) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Version not found',
|
||||
})
|
||||
}
|
||||
|
||||
const [versionUpdateResult] = await db
|
||||
.update(changelog_version)
|
||||
.set({
|
||||
status: payload.status,
|
||||
markdown: payload.markdown,
|
||||
releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
|
||||
})
|
||||
.where(and(eq(changelog_version.id, id)))
|
||||
.returning()
|
||||
|
||||
return versionUpdateResult
|
||||
}
|
||||
|
@ -1,19 +1,16 @@
|
||||
import type { UserOutput } from '@boring.tools/schema'
|
||||
// import { sentry } from '@hono/sentry'
|
||||
import { sentry } from '@hono/sentry'
|
||||
import { OpenAPIHono, type z } from '@hono/zod-openapi'
|
||||
import { apiReference } from '@scalar/hono-api-reference'
|
||||
import { cors } from 'hono/cors'
|
||||
import { requestId } from 'hono/request-id'
|
||||
|
||||
import changelog from './changelog'
|
||||
import version from './changelog/version'
|
||||
import user from './user'
|
||||
|
||||
import { accessTokenApi } from './access-token'
|
||||
import pageApi from './page'
|
||||
import statisticApi from './statistic'
|
||||
import userApi from './user'
|
||||
import { authentication } from './utils/authentication'
|
||||
import { handleError, handleZodError } from './utils/errors'
|
||||
import { logger } from './utils/logger'
|
||||
import { startup } from './utils/startup'
|
||||
|
||||
type User = z.infer<typeof UserOutput>
|
||||
@ -24,7 +21,6 @@ export type Variables = {
|
||||
|
||||
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||
defaultHook: handleZodError,
|
||||
strict: false,
|
||||
})
|
||||
|
||||
// app.use(
|
||||
@ -33,26 +29,14 @@ export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
||||
// }),
|
||||
// )
|
||||
|
||||
app.onError(handleError)
|
||||
app.use('*', cors())
|
||||
app.use('/v1/*', authentication)
|
||||
app.use('*', requestId())
|
||||
app.use(logger())
|
||||
app.openAPIRegistry.registerComponent('securitySchemes', 'AccessToken', {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
})
|
||||
app.openAPIRegistry.registerComponent('securitySchemes', 'Clerk', {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
})
|
||||
|
||||
app.route('/v1/user', userApi)
|
||||
app.route('/v1/user', user)
|
||||
app.route('/v1/changelog', changelog)
|
||||
app.route('/v1/changelog/version', version)
|
||||
app.route('/v1/page', pageApi)
|
||||
app.route('/v1/access-token', accessTokenApi)
|
||||
app.route('/v1/statistic', statisticApi)
|
||||
|
||||
app.doc('/openapi.json', {
|
||||
openapi: '3.0.0',
|
||||
|
@ -5,7 +5,6 @@ import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
import type { pageApi } from './index'
|
||||
|
||||
const route = createRoute({
|
||||
@ -25,14 +24,18 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerPageById = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const userId = verifyAuthentication(c)
|
||||
const { id } = c.req.valid('param')
|
||||
|
||||
const result = await db.query.page.findFirst({
|
||||
@ -57,6 +60,6 @@ export const registerPageById = (api: typeof pageApi) => {
|
||||
changelogs: changelogs.map((log) => log.changelog),
|
||||
}
|
||||
|
||||
return c.json(PageOutput.parse(mappedResult), 200)
|
||||
return c.json(mappedResult, 200)
|
||||
})
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
import type { pageApi } from './index'
|
||||
|
||||
const route = createRoute({
|
||||
@ -20,7 +19,7 @@ const route = createRoute({
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PageOutput,
|
||||
@ -28,14 +27,18 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerPageCreate = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
const { changelogIds, ...rest }: z.infer<typeof PageCreateInput> =
|
||||
await c.req.json()
|
||||
@ -48,12 +51,8 @@ export const registerPageCreate = (api: typeof pageApi) => {
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
// TODO: implement transaction
|
||||
if (changelogIds.length > 0) {
|
||||
if (changelogIds) {
|
||||
await db.insert(changelogs_to_pages).values(
|
||||
changelogIds.map((changelogId) => ({
|
||||
changelogId,
|
||||
@ -61,7 +60,10 @@ export const registerPageCreate = (api: typeof pageApi) => {
|
||||
})),
|
||||
)
|
||||
}
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(PageOutput.parse(result), 201)
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
||||
|
@ -3,14 +3,11 @@ 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'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'delete',
|
||||
tags: ['page'],
|
||||
path: '/:id',
|
||||
request: {
|
||||
params: PageByIdParams,
|
||||
@ -25,25 +22,27 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Removes a changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerPageDelete = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const userId = verifyAuthentication(c)
|
||||
const { id } = c.req.valid('param')
|
||||
|
||||
const [result] = await db
|
||||
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({}, 200)
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import { timing } from 'hono/timing'
|
||||
import type { Variables } from '..'
|
||||
import { handleZodError } from '../utils/errors'
|
||||
import type { ContextModule } from '../utils/sentry'
|
||||
import { registerPageById } from './byId'
|
||||
import { registerPageCreate } from './create'
|
||||
@ -10,10 +8,8 @@ import { registerPageList } from './list'
|
||||
import { registerPagePublic } from './public'
|
||||
import { registerPageUpdate } from './update'
|
||||
|
||||
export const pageApi = new OpenAPIHono<{ Variables: Variables }>({
|
||||
defaultHook: handleZodError,
|
||||
})
|
||||
pageApi.use('*', timing())
|
||||
export const pageApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'page',
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { db, page } from '@boring.tools/database'
|
||||
import { PageListOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
|
||||
import { PageListOutput } from '@boring.tools/schema'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
import type { pageApi } from './index'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
tags: ['page'],
|
||||
description: 'Get a list of pages',
|
||||
description: 'Get a page list',
|
||||
path: '/',
|
||||
responses: {
|
||||
200: {
|
||||
@ -22,24 +21,27 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerPageList = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
const result = await db.query.page.findMany({
|
||||
where: and(eq(page.userId, userId)),
|
||||
orderBy: () => desc(page.createdAt),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(PageListOutput.parse(result), 200)
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
||||
|
@ -1,171 +0,0 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
||||
import { access_token, db, user } from '@boring.tools/database'
|
||||
import type {
|
||||
AccessTokenCreateInput,
|
||||
AccessTokenListOutput,
|
||||
AccessTokenOutput,
|
||||
ChangelogCreateInput,
|
||||
ChangelogCreateOutput,
|
||||
ChangelogListOutput,
|
||||
ChangelogOutput,
|
||||
PageCreateInput,
|
||||
PageListOutput,
|
||||
PageOutput,
|
||||
PageUpdateInput,
|
||||
UserOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import type { z } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { fetch } from '../utils/testing/fetch'
|
||||
|
||||
describe('Page', () => {
|
||||
let testUser: z.infer<typeof UserOutput>
|
||||
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
||||
let createdAccessToken: z.infer<typeof AccessTokenOutput>
|
||||
let testPage: z.infer<typeof PageOutput>
|
||||
|
||||
beforeAll(async () => {
|
||||
const createdUser = await db
|
||||
.insert(user)
|
||||
.values({ email: 'page@test.local', providerId: 'test_000' })
|
||||
.returning()
|
||||
const tAccessToken = await db
|
||||
.insert(access_token)
|
||||
.values({
|
||||
token: '1234567890',
|
||||
userId: createdUser[0].id,
|
||||
name: 'testtoken',
|
||||
})
|
||||
.returning()
|
||||
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
||||
testUser = createdUser[0] as z.infer<typeof UserOutput>
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.delete(user).where(eq(user.email, 'page@test.local'))
|
||||
})
|
||||
|
||||
describe('Create', () => {
|
||||
test('Success', async () => {
|
||||
const payload: z.infer<typeof PageCreateInput> = {
|
||||
title: 'Test Page',
|
||||
changelogIds: [],
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/page',
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof PageOutput>
|
||||
testPage = json
|
||||
expect(res.status).toBe(201)
|
||||
})
|
||||
})
|
||||
|
||||
describe('By Id', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/page/${testPage.id}`,
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
test('Not Found', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/page/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update', () => {
|
||||
test('Success', async () => {
|
||||
const update: z.infer<typeof PageUpdateInput> = {
|
||||
title: 'Test Update',
|
||||
}
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/page/${testPage.id}`,
|
||||
method: 'PUT',
|
||||
body: update,
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Public', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/page/${testPage.id}/public`,
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/page',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = (await res.json()) as z.infer<typeof PageListOutput>
|
||||
// Check if token is redacted
|
||||
expect(json).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/page/${testPage.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
test('Not found', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/page/${testPage.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token as string,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,18 +1,15 @@
|
||||
import { changelog_version, db, page } from '@boring.tools/database'
|
||||
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { endTime, startTime } from 'hono/timing'
|
||||
|
||||
import { openApiErrorResponses } from '../utils/openapi'
|
||||
import { redis } from '../utils/redis'
|
||||
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 by id for public view',
|
||||
description: 'Get a page',
|
||||
path: '/:id/public',
|
||||
request: {
|
||||
params: PagePublicParams,
|
||||
@ -24,24 +21,20 @@ const route = createRoute({
|
||||
schema: PagePublicOutput,
|
||||
},
|
||||
},
|
||||
description: 'Get a page by id for public view',
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
})
|
||||
|
||||
export const registerPagePublic = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const { id } = c.req.valid('param')
|
||||
const cache = await redis.get(id)
|
||||
|
||||
if (cache) {
|
||||
c.header('Cache-Control', 'public, max-age=86400')
|
||||
c.header('X-Cache', 'HIT')
|
||||
return c.json(JSON.parse(cache), 200)
|
||||
}
|
||||
|
||||
startTime(c, 'database')
|
||||
|
||||
const result = await db.query.page.findFirst({
|
||||
where: eq(page.id, id),
|
||||
@ -77,8 +70,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
||||
},
|
||||
})
|
||||
|
||||
endTime(c, 'database')
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
@ -90,8 +81,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
||||
changelogs: changelogs.map((log) => log.changelog),
|
||||
}
|
||||
|
||||
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
||||
const asd = PagePublicOutput.parse(mappedResult)
|
||||
return c.json(asd, 200)
|
||||
return c.json(mappedResult, 200)
|
||||
})
|
||||
}
|
||||
|
@ -9,8 +9,6 @@ import {
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
import { redis } from '../utils/redis'
|
||||
import type { pageApi } from './index'
|
||||
|
||||
const route = createRoute({
|
||||
@ -35,14 +33,18 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerPageUpdate = (api: typeof pageApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const userId = verifyAuthentication(c)
|
||||
const { id } = c.req.valid('param')
|
||||
|
||||
const { changelogIds, ...rest }: z.infer<typeof PageUpdateInput> =
|
||||
@ -57,10 +59,6 @@ export const registerPageUpdate = (api: typeof pageApi) => {
|
||||
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
// TODO: implement transaction
|
||||
if (changelogIds) {
|
||||
if (changelogIds.length === 0) {
|
||||
@ -81,8 +79,10 @@ export const registerPageUpdate = (api: typeof pageApi) => {
|
||||
}
|
||||
}
|
||||
|
||||
redis.del(id)
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not Found' })
|
||||
}
|
||||
|
||||
return c.json(PageOutput.parse(result), 200)
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
||||
|
@ -1,111 +0,0 @@
|
||||
import { changelog, db, page } from '@boring.tools/database'
|
||||
import { StatisticOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import type { statisticApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
tags: ['statistic'],
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': { schema: StatisticOutput },
|
||||
},
|
||||
description: 'Return user',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerStatisticGet = (api: typeof statisticApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const pageResult = await db.query.page.findMany({
|
||||
where: eq(page.userId, userId),
|
||||
})
|
||||
const result = await db.query.changelog.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
with: {
|
||||
commits: {
|
||||
columns: {
|
||||
id: true,
|
||||
versionId: true,
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
columns: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const changelog_total = result.length
|
||||
const version_total = result.reduce(
|
||||
(acc, log) => acc + log.versions.length,
|
||||
0,
|
||||
)
|
||||
const version_published = result.reduce(
|
||||
(acc, log) =>
|
||||
acc +
|
||||
log.versions.filter((version) => version.status === 'published').length,
|
||||
0,
|
||||
)
|
||||
const version_review = result.reduce(
|
||||
(acc, log) =>
|
||||
acc +
|
||||
log.versions.filter((version) => version.status === 'review').length,
|
||||
0,
|
||||
)
|
||||
const version_draft = result.reduce(
|
||||
(acc, log) =>
|
||||
acc +
|
||||
log.versions.filter((version) => version.status === 'draft').length,
|
||||
0,
|
||||
)
|
||||
|
||||
const commit_total = result.reduce(
|
||||
(acc, log) => acc + log.commits.length,
|
||||
0,
|
||||
)
|
||||
const commit_unassigned = result.reduce(
|
||||
(acc, log) =>
|
||||
acc + log.commits.filter((commit) => !commit.versionId).length,
|
||||
0,
|
||||
)
|
||||
const commit_assigned = result.reduce(
|
||||
(acc, log) =>
|
||||
acc + log.commits.filter((commit) => commit.versionId).length,
|
||||
0,
|
||||
)
|
||||
|
||||
const mappedData = {
|
||||
changelog: {
|
||||
total: changelog_total,
|
||||
versions: {
|
||||
total: version_total,
|
||||
published: version_published,
|
||||
review: version_review,
|
||||
draft: version_draft,
|
||||
},
|
||||
commits: {
|
||||
total: commit_total,
|
||||
unassigned: commit_unassigned,
|
||||
assigned: commit_assigned,
|
||||
},
|
||||
},
|
||||
page: {
|
||||
total: pageResult.length,
|
||||
},
|
||||
}
|
||||
|
||||
return c.json(StatisticOutput.parse(mappedData), 200)
|
||||
})
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import type { Variables } from '..'
|
||||
import type { ContextModule } from '../utils/sentry'
|
||||
import { registerStatisticGet } from './get'
|
||||
|
||||
export const statisticApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
const module: ContextModule = {
|
||||
name: 'statistic',
|
||||
}
|
||||
|
||||
registerStatisticGet(statisticApi)
|
||||
|
||||
export default statisticApi
|
@ -2,10 +2,7 @@ import { db, user as userDb } from '@boring.tools/database'
|
||||
import { UserOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import type { userApi } from '.'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
@ -18,22 +15,26 @@ const route = createRoute({
|
||||
},
|
||||
description: 'Return user',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
export const registerUserGet = (api: typeof userApi) => {
|
||||
return api.openapi(route, async (c) => {
|
||||
const userId = await verifyAuthentication(c)
|
||||
const user = c.get('user')
|
||||
const result = await db.query.user.findFirst({
|
||||
where: eq(userDb.id, userId),
|
||||
where: eq(userDb.id, user.id),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
return c.json(UserOutput.parse(result), 200)
|
||||
return c.json(result, 200)
|
||||
})
|
||||
}
|
||||
|
@ -4,9 +4,7 @@ import { UserOutput, UserWebhookInput } from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { Webhook } from 'svix'
|
||||
|
||||
import type userApi from '.'
|
||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'post',
|
||||
@ -20,12 +18,19 @@ const route = createRoute({
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': { schema: UserOutput },
|
||||
},
|
||||
description: 'Return success',
|
||||
},
|
||||
...openApiErrorResponses,
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
...openApiSecurity,
|
||||
})
|
||||
|
||||
const userCreate = async ({
|
||||
@ -34,7 +39,7 @@ const userCreate = async ({
|
||||
payload: z.infer<typeof UserWebhookInput>
|
||||
}) => {
|
||||
const data = {
|
||||
providerId: payload.data.id,
|
||||
id: payload.data.id,
|
||||
name: `${payload.data.first_name} ${payload.data.last_name}`,
|
||||
email: payload.data.email_addresses[0].email_address,
|
||||
}
|
||||
@ -45,7 +50,7 @@ const userCreate = async ({
|
||||
...data,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: user.providerId,
|
||||
target: user.id,
|
||||
set: data,
|
||||
})
|
||||
|
||||
@ -67,11 +72,7 @@ export const registerUserWebhook = (api: typeof userApi) => {
|
||||
case 'user.created': {
|
||||
const result = await userCreate({ payload: verifiedPayload })
|
||||
logger.info('Clerk Webhook', result)
|
||||
if (result) {
|
||||
return c.json({}, 204)
|
||||
}
|
||||
|
||||
return c.json({}, 404)
|
||||
return c.json(result, 200)
|
||||
}
|
||||
default:
|
||||
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { access_token, db, user } from '@boring.tools/database'
|
||||
import { logger } from '@boring.tools/logger'
|
||||
import { access_token, db } from '@boring.tools/database'
|
||||
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { Context, Next } from 'hono'
|
||||
@ -25,11 +24,6 @@ const generatedToken = async (c: Context, next: Next) => {
|
||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
await db
|
||||
.update(access_token)
|
||||
.set({ lastUsedOn: new Date() })
|
||||
.where(eq(access_token.id, accessTokenResult.id))
|
||||
|
||||
c.set('user', accessTokenResult.user)
|
||||
|
||||
await next()
|
||||
@ -37,7 +31,7 @@ const generatedToken = async (c: Context, next: Next) => {
|
||||
|
||||
export const authentication = some(generatedToken, clerkMiddleware())
|
||||
|
||||
export const verifyAuthentication = async (c: Context) => {
|
||||
export const verifyAuthentication = (c: Context) => {
|
||||
const auth = getAuth(c)
|
||||
if (!auth?.userId) {
|
||||
const accessTokenUser = c.get('user')
|
||||
@ -46,16 +40,5 @@ export const verifyAuthentication = async (c: Context) => {
|
||||
}
|
||||
return accessTokenUser.id
|
||||
}
|
||||
|
||||
const [userEntry] = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.providerId, auth.userId))
|
||||
|
||||
if (!userEntry) {
|
||||
logger.error('User not found - Unauthorized', { providerId: auth.userId })
|
||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
return userEntry.id
|
||||
return auth.userId
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { type RouteConfig, createRoute as route } from '@hono/zod-openapi'
|
||||
import { openApiErrorResponses } from './openapi'
|
||||
|
||||
type CreateRoute = {
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
tags: string[]
|
||||
description: string
|
||||
path: string
|
||||
responses: RouteConfig['responses']
|
||||
request?: RouteConfig['request']
|
||||
}
|
||||
|
||||
export const createRoute = (input: CreateRoute) =>
|
||||
route({
|
||||
...input,
|
||||
responses: {
|
||||
...input.responses,
|
||||
...openApiErrorResponses,
|
||||
},
|
||||
security: [
|
||||
{
|
||||
Clerk: [],
|
||||
AccessToken: [],
|
||||
},
|
||||
],
|
||||
})
|
@ -1,113 +0,0 @@
|
||||
import type { CommitOutput } from '@boring.tools/schema'
|
||||
import type { z } from '@hono/zod-openapi'
|
||||
import { convert } from 'convert-gitmoji'
|
||||
import { type GitCommit, parseCommits } from './parseCommit'
|
||||
|
||||
export const config = {
|
||||
types: {
|
||||
feat: { title: '🚀 Enhancements', semver: 'minor' },
|
||||
perf: { title: '🔥 Performance', semver: 'patch' },
|
||||
fix: { title: '🩹 Fixes', semver: 'patch' },
|
||||
refactor: { title: '💅 Refactors', semver: 'patch' },
|
||||
docs: { title: '📖 Documentation', semver: 'patch' },
|
||||
build: { title: '📦 Build', semver: 'patch' },
|
||||
types: { title: '🌊 Types', semver: 'patch' },
|
||||
chore: { title: '🏡 Chore' },
|
||||
examples: { title: '🏀 Examples' },
|
||||
test: { title: '✅ Tests' },
|
||||
style: { title: '🎨 Styles' },
|
||||
ci: { title: '🤖 CI' },
|
||||
},
|
||||
}
|
||||
|
||||
type Commit = z.infer<typeof CommitOutput>
|
||||
export const commitsToMarkdown = async (commits: Commit[]) => {
|
||||
const parsedCommits = await parseCommits(commits)
|
||||
|
||||
const typeGroups: Record<string, GitCommit[]> = groupBy(parsedCommits, 'type')
|
||||
|
||||
const markdown: string[] = []
|
||||
const breakingChanges = []
|
||||
|
||||
for (const type in config.types) {
|
||||
const group = typeGroups[type]
|
||||
if (!group || group.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (type in config.types) {
|
||||
markdown.push(
|
||||
'',
|
||||
`### ${config.types[type as keyof typeof config.types].title}`,
|
||||
'',
|
||||
)
|
||||
for (const commit of group.reverse()) {
|
||||
const line = formatCommit(commit)
|
||||
markdown.push(line)
|
||||
if (commit.isBreaking) {
|
||||
breakingChanges.push(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breakingChanges.length > 0) {
|
||||
markdown.push('', '#### ⚠️ Breaking Changes', '', ...breakingChanges)
|
||||
}
|
||||
|
||||
const _authors = new Map<string, { email: Set<string>; github?: string }>()
|
||||
for (const commit of commits) {
|
||||
if (!commit.author) {
|
||||
continue
|
||||
}
|
||||
const name = formatName(commit.author.name)
|
||||
if (!name || name.includes('[bot]')) {
|
||||
continue
|
||||
}
|
||||
if (_authors.has(name)) {
|
||||
const entry = _authors.get(name)
|
||||
entry?.email.add(commit.author.email)
|
||||
} else {
|
||||
_authors.set(name, { email: new Set([commit.author.email]) })
|
||||
}
|
||||
}
|
||||
|
||||
const authors = [..._authors.entries()].map((e) => ({ name: e[0], ...e[1] }))
|
||||
|
||||
if (authors.length > 0) {
|
||||
markdown.push(
|
||||
'',
|
||||
'### ' + '❤️ Contributors',
|
||||
'',
|
||||
...authors.map((i) => {
|
||||
return `- ${i.name} ${[...i.email]}`
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return convert(markdown.join('\n').trim(), true)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
function groupBy(items: any[], key: string): Record<string, any[]> {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
const groups: Record<string, any[]> = {}
|
||||
for (const item of items) {
|
||||
groups[item[key]] = groups[item[key]] || []
|
||||
groups[item[key]].push(item)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
function formatCommit(commit: GitCommit) {
|
||||
return `- ${commit.scope ? `**${commit.scope.trim()}:** ` : ''}${
|
||||
commit.isBreaking ? '⚠️ ' : ''
|
||||
}${commit.description}`
|
||||
}
|
||||
|
||||
function formatName(name = '') {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((p) => p.trim())
|
||||
.join(' ')
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import type { CommitOutput } from '@boring.tools/schema'
|
||||
import type { z } from '@hono/zod-openapi'
|
||||
|
||||
export interface GitCommitAuthor {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface GitCommit {
|
||||
description: string
|
||||
type: string
|
||||
scope: string
|
||||
authors: GitCommitAuthor[]
|
||||
isBreaking: boolean
|
||||
}
|
||||
|
||||
type Commit = z.infer<typeof CommitOutput>
|
||||
|
||||
export function parseCommits(commits: Commit[]): GitCommit[] {
|
||||
return commits.map((commit) => parseGitCommit(commit)).filter(Boolean)
|
||||
}
|
||||
|
||||
// https://www.conventionalcommits.org/en/v1.0.0/
|
||||
// https://regex101.com/r/FSfNvA/1
|
||||
const ConventionalCommitRegex =
|
||||
/(?<emoji>:.+:|(\uD83C[\uDF00-\uDFFF])|(\uD83D[\uDC00-\uDE4F\uDE80-\uDEFF])|[\u2600-\u2B55])?( *)?(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i
|
||||
const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim
|
||||
|
||||
export function parseGitCommit(commit: Commit): GitCommit | null {
|
||||
const match = commit.subject.match(ConventionalCommitRegex)
|
||||
if (!match) {
|
||||
return {
|
||||
...commit,
|
||||
authors: [],
|
||||
description: '',
|
||||
type: 'none',
|
||||
scope: 'none',
|
||||
isBreaking: false,
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
const type = match.groups?.['type'] ?? ''
|
||||
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
const scope = match.groups?.['scope'] ?? ''
|
||||
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
const isBreaking = Boolean(match.groups?.['breaking'])
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
const description = match.groups?.['description'] ?? ''
|
||||
|
||||
// Find all authors
|
||||
const authors: GitCommitAuthor[] = [commit.author]
|
||||
if (commit?.body) {
|
||||
for (const match of commit.body.matchAll(CoAuthoredByRegex)) {
|
||||
authors.push({
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
name: (match.groups?.['name'] ?? '').trim(),
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
email: (match.groups?.['email'] ?? '').trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authors,
|
||||
description,
|
||||
type,
|
||||
scope,
|
||||
isBreaking,
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { db, user } from '@boring.tools/database'
|
||||
import { logger as log } from '@boring.tools/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import { getPath } from 'hono/utils/url'
|
||||
|
||||
export const logger = (): MiddlewareHandler => {
|
||||
return async function logga(c, next) {
|
||||
const { method } = c.req
|
||||
const clerkUser = c.get('clerkAuth')
|
||||
const requestId = c.get('requestId')
|
||||
const [dbUser] = await db
|
||||
.select({ id: user.id, providerId: user.providerId })
|
||||
.from(user)
|
||||
.where(eq(user.providerId, clerkUser?.userId))
|
||||
const path = getPath(c.req.raw)
|
||||
|
||||
log.info('Incoming', {
|
||||
direction: 'in',
|
||||
method,
|
||||
path,
|
||||
userId: dbUser?.id,
|
||||
requestId,
|
||||
})
|
||||
|
||||
await next()
|
||||
|
||||
if (c.res.status <= 399) {
|
||||
log.info('Outgoing', {
|
||||
direction: 'out',
|
||||
method,
|
||||
path,
|
||||
status: c.res.status,
|
||||
userId: dbUser?.id,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
if (c.res.status >= 400 && c.res.status < 499) {
|
||||
log.warn('Outgoing', {
|
||||
direction: 'out',
|
||||
method,
|
||||
path,
|
||||
status: c.res.status,
|
||||
userId: dbUser?.id,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
if (c.res.status >= 500) {
|
||||
log.error('Outgoing', {
|
||||
direction: 'out',
|
||||
method,
|
||||
path,
|
||||
status: c.res.status,
|
||||
userId: dbUser?.id,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import { createErrorSchema } from './errors'
|
||||
|
||||
export const openApiSecurity = {
|
||||
security: [
|
||||
{
|
||||
Clerk: [],
|
||||
AccessToken: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const openApiErrorResponses = {
|
||||
400: {
|
||||
description:
|
||||
'The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('BAD_REQUEST').openapi('ErrBadRequest'),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description:
|
||||
'The client must authenticate itself to get the requested response.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('UNAUTHORIZED').openapi('ErrUnauthorized'),
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description:
|
||||
'The client does not have the necessary permissions to access the resource.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('FORBIDDEN').openapi('ErrForbidden'),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "The server can't find the requested resource.",
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('NOT_FOUND').openapi('ErrNotFound'),
|
||||
},
|
||||
},
|
||||
},
|
||||
409: {
|
||||
description:
|
||||
'The request could not be completed due to a conflict mainly due to unique constraints.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('CONFLICT').openapi('ErrConflict'),
|
||||
},
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description:
|
||||
"The server has encountered a situation it doesn't know how to handle.",
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: createErrorSchema('INTERNAL_SERVER_ERROR').openapi(
|
||||
'ErrInternalServerError',
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { createClient } from 'redis'
|
||||
|
||||
const getRedisOptions = () => {
|
||||
if (import.meta.env.NODE_ENV === 'production') {
|
||||
return {
|
||||
password: import.meta.env.REDIS_PASSWORD,
|
||||
url: import.meta.env.REDIS_URL,
|
||||
}
|
||||
}
|
||||
return {
|
||||
url: import.meta.env.REDIS_URL,
|
||||
}
|
||||
}
|
||||
export const redis = createClient(getRedisOptions())
|
||||
|
||||
redis.on('error', (err) => console.log('Redis Client Error', err))
|
||||
await redis.connect()
|
@ -5,8 +5,6 @@ import { logger } from '@boring.tools/logger'
|
||||
declare module 'bun' {
|
||||
interface Env {
|
||||
POSTGRES_URL: string
|
||||
REDIS_PASSWORD: string
|
||||
REDIS_URL: string
|
||||
CLERK_WEBHOOK_SECRET: string
|
||||
CLERK_SECRET_KEY: string
|
||||
CLERK_PUBLISHABLE_KEY: string
|
||||
@ -14,7 +12,7 @@ declare module 'bun' {
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_VARIABLES = ['POSTGRES_URL', 'REDIS_URL', 'REDIS_PASSWORD']
|
||||
const TEST_VARIABLES = ['POSTGRES_URL']
|
||||
|
||||
const DEVELOPMENT_VARIABLES = [
|
||||
...TEST_VARIABLES,
|
||||
|
@ -20,14 +20,12 @@
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"@tanstack/react-router": "^1.58.15",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"axios": "^1.7.7",
|
||||
"lucide-react": "^0.446.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="80px" height="80px" viewBox="-1.5 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-102.000000, -7439.000000)" fill="#71717a">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M57.5708873,7282.19296 C58.2999598,7281.34797 58.7914012,7280.17098 58.6569121,7279 C57.6062792,7279.04 56.3352055,7279.67099 55.5818643,7280.51498 C54.905374,7281.26397 54.3148354,7282.46095 54.4735932,7283.60894 C55.6455696,7283.69593 56.8418148,7283.03894 57.5708873,7282.19296 M60.1989864,7289.62485 C60.2283111,7292.65181 62.9696641,7293.65879 63,7293.67179 C62.9777537,7293.74279 62.562152,7295.10677 61.5560117,7296.51675 C60.6853718,7297.73474 59.7823735,7298.94772 58.3596204,7298.97372 C56.9621472,7298.99872 56.5121648,7298.17973 54.9134635,7298.17973 C53.3157735,7298.17973 52.8162425,7298.94772 51.4935978,7298.99872 C50.1203933,7299.04772 49.0738052,7297.68074 48.197098,7296.46676 C46.4032359,7293.98379 45.0330649,7289.44985 46.8734421,7286.3899 C47.7875635,7284.87092 49.4206455,7283.90793 51.1942837,7283.88393 C52.5422083,7283.85893 53.8153044,7284.75292 54.6394294,7284.75292 C55.4635543,7284.75292 57.0106846,7283.67793 58.6366882,7283.83593 C59.3172232,7283.86293 61.2283842,7284.09893 62.4549652,7285.8199 C62.355868,7285.8789 60.1747177,7287.09489 60.1989864,7289.62485" id="apple-[#173]">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 10 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-60.000000, -7439.000000)" fill="#71717a">
|
||||
<g transform="translate(56.000000, 160.000000)">
|
||||
<path d="M13.1458647,7289.43426 C13.1508772,7291.43316 13.1568922,7294.82929 13.1619048,7297.46884 C16.7759398,7297.95757 20.3899749,7298.4613 23.997995,7299 C23.997995,7295.84873 24.002005,7292.71146 23.997995,7289.71311 C20.3809524,7289.71311 16.7649123,7289.43426 13.1458647,7289.43426 M4,7289.43526 L4,7296.22153 C6.72581454,7296.58933 9.45162907,7296.94113 12.1724311,7297.34291 C12.1774436,7294.71736 12.1704261,7292.0908 12.1704261,7289.46524 C9.44661654,7289.47024 6.72380952,7289.42627 4,7289.43526 M4,7281.84344 L4,7288.61071 C6.72581454,7288.61771 9.45162907,7288.57673 12.1774436,7288.57973 C12.1754386,7285.96017 12.1754386,7283.34361 12.1724311,7280.72405 C9.44461153,7281.06486 6.71679198,7281.42567 4,7281.84344 M24,7288.47179 C20.3879699,7288.48578 16.7759398,7288.54075 13.1619048,7288.55175 C13.1598997,7285.88921 13.1598997,7283.22967 13.1619048,7280.56914 C16.7689223,7280.01844 20.3839599,7279.50072 23.997995,7279 C24,7282.15826 23.997995,7285.31353 24,7288.47179">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,72 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { Trash2Icon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useAccessTokenDelete } from '../../hooks/useAccessToken'
|
||||
|
||||
export const AccessTokenDelete = ({ id }: { id: string }) => {
|
||||
const accessTokenDelete = useAccessTokenDelete()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const removeChangelog = () => {
|
||||
accessTokenDelete.mutate(
|
||||
{ id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<AlertDialog open={isOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost-destructive'}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Trash2Icon strokeWidth={1.5} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
access token and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button onClick={removeChangelog} variant={'destructive'}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<TooltipContent>
|
||||
<p>Delete access token</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import type { AccessTokenOutput } from '@boring.tools/schema'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { format } from 'date-fns'
|
||||
import type { z } from 'zod'
|
||||
import { AccessTokenDelete } from '../Delete'
|
||||
|
||||
export const AccessTokenColumns: ColumnDef<
|
||||
z.infer<typeof AccessTokenOutput>
|
||||
>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastUsedOn',
|
||||
header: 'Last seen',
|
||||
accessorFn: (row) => {
|
||||
if (!row.lastUsedOn) {
|
||||
return 'Never'
|
||||
}
|
||||
return format(new Date(row.lastUsedOn), 'HH:mm dd.MM.yyyy')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'token',
|
||||
header: 'Token',
|
||||
accessorFn: (row) => `${row.token}...`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: '',
|
||||
size: 20,
|
||||
maxSize: 20,
|
||||
cell: (props) => <AccessTokenDelete id={props.row.original.id} />,
|
||||
},
|
||||
]
|
@ -1,86 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@boring.tools/ui'
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{ width: header.getSize() }}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { format } from 'date-fns'
|
||||
import { useChangelogCommitList } from '../../hooks/useChangelog'
|
||||
|
||||
export const ChangelogCommitList = () => {
|
||||
const { id } = useParams({ from: '/changelog/$id' })
|
||||
const { data } = useChangelogCommitList({ id, limit: 50 })
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<Card className="w-full max-w-screen-sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Commits ({data.length})</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-1">
|
||||
{data?.map((commit) => {
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
|
||||
key={commit.id}
|
||||
>
|
||||
<span className="font-mono font-bold text-muted-foreground">
|
||||
{commit.commit}
|
||||
</span>
|
||||
<p className="w-full">{commit.subject}</p>
|
||||
|
||||
<span className="text-xs">
|
||||
{format(new Date(commit.author.date), 'dd.MM.yyyy')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="flex flex-col gap-5">Not found </div>
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { Link, useParams } from '@tanstack/react-router'
|
||||
import { HandIcon, WorkflowIcon } from 'lucide-react'
|
||||
|
||||
export const ChangelogVersionCreateStep01 = ({
|
||||
nextStep,
|
||||
}: { nextStep: () => void }) => {
|
||||
const { id } = useParams({ from: '/changelog/$id' })
|
||||
|
||||
return (
|
||||
<div className="flex gap-10 mt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex-col hover:border-accent border rounded border-muted p-5 flex items-center justify-center w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
<WorkflowIcon />
|
||||
Automatic
|
||||
</button>
|
||||
<Link
|
||||
className="flex-col hover:border-accent border rounded border-muted p-5 flex items-center justify-center w-full"
|
||||
to="/changelog/$id/versionCreate"
|
||||
params={{ id }}
|
||||
>
|
||||
<HandIcon />
|
||||
Manual
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import { VersionCreateAutoInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useNavigate, useParams } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { z } from 'zod'
|
||||
import { useChangelogVersionCreateAuto } from '../../../../hooks/useChangelog'
|
||||
|
||||
export const ChangelogVersionCreateStep02 = () => {
|
||||
const { id } = useParams({ from: '/changelog/$id' })
|
||||
const navigate = useNavigate({ from: `/changelog/${id}` })
|
||||
const autoVersion = useChangelogVersionCreateAuto()
|
||||
|
||||
const form = useForm<z.infer<typeof VersionCreateAutoInput>>({
|
||||
resolver: zodResolver(VersionCreateAutoInput),
|
||||
defaultValues: {
|
||||
changelogId: id,
|
||||
version: '',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof VersionCreateAutoInput>) => {
|
||||
autoVersion.mutate(values, {
|
||||
onSuccess(data) {
|
||||
navigate({
|
||||
to: '/changelog/$id/version/$versionId',
|
||||
params: { id, versionId: data.id },
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-10 mt-3">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 w-full"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="v1.0.1" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormDescription>
|
||||
Leave blank for auto generating.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-5 mt-5 w-full justify-end items-end md:col-span-6">
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ChangelogVersionCreateStep01 } from './Step01'
|
||||
import { ChangelogVersionCreateStep02 } from './Step02'
|
||||
|
||||
export const ChangelogVersionCreate = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [step, setStep] = useState(1)
|
||||
const nextStep = () => setStep((prev) => prev + 1)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setStep(1)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(state) => setIsOpen(state)}>
|
||||
<DialogTrigger>
|
||||
<PlusCircleIcon />
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>How would you like to create your version?</DialogTitle>
|
||||
<DialogDescription>
|
||||
You can create your version manually. You have to make every entry
|
||||
yourself. However, if you want to create your changelog attachment
|
||||
from your commit messages, select the automatic option.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{step === 1 && <ChangelogVersionCreateStep01 nextStep={nextStep} />}
|
||||
{step === 2 && <ChangelogVersionCreateStep02 />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -38,7 +38,7 @@ export const ChangelogVersionDelete = ({
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Alert className="mt-10 w-full" variant={'destructive'}>
|
||||
<Alert className="mt-10 max-w-screen-md" variant={'destructive'}>
|
||||
<TriangleAlertIcon className="h-4 w-4" />
|
||||
<AlertTitle>Danger Zone</AlertTitle>
|
||||
<AlertDescription className="inline-flex flex-col gap-3">
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
||||
import { Link, useParams } from '@tanstack/react-router'
|
||||
import { useChangelogById } from '../../hooks/useChangelog'
|
||||
import { ChangelogVersionCreate } from './Version/Create'
|
||||
import { VersionStatus } from './VersionStatus'
|
||||
|
||||
export const ChangelogVersionList = () => {
|
||||
const { id } = useParams({ from: '/changelog/$id' })
|
||||
const { data } = useChangelogById({ id })
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<Card className="w-full max-w-screen-sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Versions ({data.versions?.length})</CardTitle>
|
||||
|
||||
<ChangelogVersionCreate />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-1">
|
||||
{data.versions?.map((version) => {
|
||||
return (
|
||||
<Link
|
||||
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
|
||||
to="/changelog/$id/version/$versionId"
|
||||
params={{
|
||||
id,
|
||||
versionId: version.id,
|
||||
}}
|
||||
key={version.id}
|
||||
>
|
||||
<VersionStatus status={version.status} />
|
||||
{version.version}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="flex flex-col gap-5">Not found</div>
|
||||
}
|
97
apps/app/src/components/Navigation.tsx
Normal file
97
apps/app/src/components/Navigation.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { BellIcon } from 'lucide-react'
|
||||
import { NavigationRoutes } from '../utils/navigation-routes'
|
||||
|
||||
export const Navigation = () => {
|
||||
return (
|
||||
<div className="hidden border-r bg-muted/40 md:block">
|
||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
||||
<Link to="/" className="flex items-center gap-2 font-semibold">
|
||||
<span className="">boring.tools</span>
|
||||
</Link>
|
||||
<Button variant="outline" size="icon" className="ml-auto h-8 w-8">
|
||||
<BellIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle notifications</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<nav className="grid items-start px-2 text-sm font-medium lg:px-4 gap-2">
|
||||
{NavigationRoutes.map((route) => {
|
||||
if (!route.childrens) {
|
||||
return (
|
||||
<Link
|
||||
key={route.name}
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
key={route.name}
|
||||
defaultValue="changelog"
|
||||
>
|
||||
<AccordionItem value="changelog">
|
||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
||||
<AccordionContent className="gap-2 flex flex-col">
|
||||
{route.childrens.map((childRoute) => (
|
||||
<Link
|
||||
key={childRoute.name}
|
||||
to={childRoute.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<childRoute.icon className="h-4 w-4" />
|
||||
{childRoute.name}
|
||||
</Link>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="mt-auto p-4">
|
||||
<Card>
|
||||
<CardHeader className="p-2 pt-0 md:p-4">
|
||||
<CardTitle>More Infos</CardTitle>
|
||||
<CardDescription>
|
||||
If you want more information about boring.tools, visit our
|
||||
documentation!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
||||
<a href="https://boring.tools">
|
||||
<Button size="sm" className="w-full">
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
102
apps/app/src/components/NavigationMobile.tsx
Normal file
102
apps/app/src/components/NavigationMobile.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { MenuIcon } from 'lucide-react'
|
||||
import { NavigationRoutes } from '../utils/navigation-routes'
|
||||
|
||||
export const NavigationMobile = () => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0 md:hidden">
|
||||
<MenuIcon className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col">
|
||||
<nav className="grid gap-2 text-lg font-medium">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<span className="sr-only">boring.tools</span>
|
||||
</Link>
|
||||
{NavigationRoutes.map((route) => {
|
||||
if (!route.childrens) {
|
||||
return (
|
||||
<Link
|
||||
key={route.name}
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
key={route.name}
|
||||
defaultValue="changelog"
|
||||
>
|
||||
<AccordionItem value="changelog">
|
||||
<AccordionTrigger>Changelog</AccordionTrigger>
|
||||
<AccordionContent className="gap-2 flex flex-col">
|
||||
{route.childrens.map((childRoute) => (
|
||||
<Link
|
||||
key={childRoute.name}
|
||||
to={childRoute.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<childRoute.icon className="h-4 w-4" />
|
||||
{childRoute.name}
|
||||
</Link>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<div className="mt-auto">
|
||||
<Card>
|
||||
<CardHeader className="p-2 pt-0 md:p-4">
|
||||
<CardTitle>More Infos</CardTitle>
|
||||
<CardDescription>
|
||||
If you want more information about boring.tools, visit our
|
||||
documentation!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
|
||||
<a href="https://boring.tools">
|
||||
<Button size="sm" className="w-full">
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
@ -8,11 +8,7 @@ import {
|
||||
Separator,
|
||||
SidebarTrigger,
|
||||
} from '@boring.tools/ui'
|
||||
import { useAuth } from '@clerk/clerk-react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useUser } from '../hooks/useUser'
|
||||
|
||||
type Breadcrumbs = {
|
||||
name: string
|
||||
@ -23,15 +19,6 @@ export const PageWrapper = ({
|
||||
children,
|
||||
breadcrumbs,
|
||||
}: { children: React.ReactNode; breadcrumbs?: Breadcrumbs[] }) => {
|
||||
const { error } = useUser()
|
||||
const { signOut } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
signOut()
|
||||
}
|
||||
}, [error, signOut])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||
|
@ -34,8 +34,10 @@ export function Sidebar() {
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarChangelog />
|
||||
<SidebarPage />
|
||||
<SidebarMenu>
|
||||
<SidebarChangelog />
|
||||
<SidebarPage />
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@ -10,91 +9,68 @@ import {
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ChevronRightIcon, FileStackIcon, PlusIcon } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocalStorage } from 'usehooks-ts'
|
||||
import { useChangelogList } from '../hooks/useChangelog'
|
||||
|
||||
export const SidebarChangelog = () => {
|
||||
const location = useLocation()
|
||||
const [value, setValue] = useLocalStorage('sidebar-changelog-open', false)
|
||||
const { data, error, isLoading } = useChangelogList()
|
||||
|
||||
useEffect(() => {
|
||||
const firstElement = location.href.split('/')[1]
|
||||
if (firstElement === 'changelog') {
|
||||
setValue(true)
|
||||
}
|
||||
}, [location, setValue])
|
||||
const { data, error } = useChangelogList()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Changelog">
|
||||
<Link
|
||||
to="/changelog"
|
||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||
>
|
||||
<FileStackIcon />
|
||||
<span>Changelog</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<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>
|
||||
{isLoading && !data && (
|
||||
<div className="flex flex-col gap-1 animate-pulse">
|
||||
<SidebarMenuSubItem>
|
||||
<div className="w-[100px] h-[20px] bg-border rounded" />
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<div className="w-[130px] h-[20px] bg-border rounded" />
|
||||
</SidebarMenuSubItem>
|
||||
</div>
|
||||
)}
|
||||
{!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>
|
||||
))}
|
||||
<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>
|
||||
</SidebarMenu>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@ -10,88 +9,65 @@ import {
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ChevronRightIcon, NotebookTextIcon, PlusIcon } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocalStorage } from 'usehooks-ts'
|
||||
import { usePageList } from '../hooks/usePage'
|
||||
|
||||
export const SidebarPage = () => {
|
||||
const location = useLocation()
|
||||
const [value, setValue] = useLocalStorage('sidebar-page-open', false)
|
||||
const { data, error, isLoading } = usePageList()
|
||||
|
||||
useEffect(() => {
|
||||
const firstElement = location.href.split('/')[1]
|
||||
if (firstElement === 'page') {
|
||||
setValue(true)
|
||||
}
|
||||
}, [location, setValue])
|
||||
const { data, error } = usePageList()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Page">
|
||||
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||
<NotebookTextIcon />
|
||||
<span>Page</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<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>
|
||||
{isLoading && !data && (
|
||||
<div className="flex flex-col gap-1 animate-pulse">
|
||||
<SidebarMenuSubItem>
|
||||
<div className="w-[100px] h-[20px] bg-border rounded" />
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<div className="w-[130px] h-[20px] bg-border rounded" />
|
||||
</SidebarMenuSubItem>
|
||||
</div>
|
||||
)}
|
||||
{!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>
|
||||
))}
|
||||
<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>
|
||||
</SidebarMenu>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChevronsUpDown, KeyRoundIcon, TerminalIcon } from 'lucide-react'
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@ -14,7 +14,6 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
useSidebar,
|
||||
} from '@boring.tools/ui'
|
||||
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
||||
@ -26,26 +25,6 @@ export function SidebarUser() {
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Access Tokens">
|
||||
<Link
|
||||
to="/access-tokens"
|
||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
||||
>
|
||||
<KeyRoundIcon />
|
||||
<span>Access Tokens</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Access Tokens">
|
||||
<Link to="/cli" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||
<TerminalIcon />
|
||||
<span>CLI</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarSeparator />
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
@ -1,67 +0,0 @@
|
||||
import type {
|
||||
AccessTokenCreateInput,
|
||||
AccessTokenListOutput,
|
||||
AccessTokenOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import { useAuth } from '@clerk/clerk-react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { z } from 'zod'
|
||||
import { queryFetch } from '../utils/queryFetch'
|
||||
|
||||
type AccessToken = z.infer<typeof AccessTokenOutput>
|
||||
type AccessTokenList = z.infer<typeof AccessTokenListOutput>
|
||||
type AccessTokenCreate = z.infer<typeof AccessTokenCreateInput>
|
||||
|
||||
export const useAccessTokenList = () => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ['accessTokenList'],
|
||||
queryFn: async (): Promise<AccessTokenList> =>
|
||||
await queryFetch({
|
||||
path: 'access-token',
|
||||
method: 'get',
|
||||
token: await getToken(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAccessTokenCreate = () => {
|
||||
const { getToken } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
payload: AccessTokenCreate,
|
||||
): Promise<Readonly<AccessToken>> =>
|
||||
await queryFetch({
|
||||
path: 'access-token',
|
||||
data: payload,
|
||||
method: 'post',
|
||||
token: await getToken(),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accessTokenList'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useAccessTokenDelete = () => {
|
||||
const { getToken } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
}: { id: string }): Promise<Readonly<AccessToken>> =>
|
||||
await queryFetch({
|
||||
path: `access-token/${id}`,
|
||||
method: 'delete',
|
||||
token: await getToken(),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['accessTokenList'],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
@ -2,8 +2,6 @@ import type {
|
||||
ChangelogCreateInput,
|
||||
ChangelogOutput,
|
||||
ChangelogUpdateInput,
|
||||
CommitOutput,
|
||||
VersionCreateAutoInput,
|
||||
VersionCreateInput,
|
||||
VersionOutput,
|
||||
VersionUpdateInput,
|
||||
@ -19,11 +17,8 @@ type ChangelogUpdate = z.infer<typeof ChangelogUpdateInput>
|
||||
|
||||
type Version = z.infer<typeof VersionOutput>
|
||||
type VersionCreate = z.infer<typeof VersionCreateInput>
|
||||
type VersionCreateAuto = z.infer<typeof VersionCreateAutoInput>
|
||||
type VersionUpdate = z.infer<typeof VersionUpdateInput>
|
||||
|
||||
type Commit = z.infer<typeof CommitOutput>
|
||||
|
||||
export const useChangelogList = () => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
@ -37,23 +32,6 @@ export const useChangelogList = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useChangelogCommitList = ({
|
||||
id,
|
||||
limit,
|
||||
offset,
|
||||
}: { id: string; limit?: number; offset?: number }) => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ['changelogCommitList'],
|
||||
queryFn: async (): Promise<ReadonlyArray<Commit>> =>
|
||||
await queryFetch({
|
||||
path: `changelog/commit?changelogId=${id}&limit=${limit}&offset=${offset}`,
|
||||
method: 'get',
|
||||
token: await getToken(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useChangelogById = ({ id }: { id: string }) => {
|
||||
const { getToken } = useAuth()
|
||||
|
||||
@ -157,29 +135,6 @@ export const useChangelogVersionCreate = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useChangelogVersionCreateAuto = () => {
|
||||
const { getToken } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
payload: VersionCreateAuto,
|
||||
): Promise<Readonly<Version>> =>
|
||||
await queryFetch({
|
||||
path: 'changelog/version/auto',
|
||||
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()
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
import type { StatisticOutput } from '@boring.tools/schema'
|
||||
import { useAuth } from '@clerk/clerk-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { z } from 'zod'
|
||||
import { queryFetch } from '../utils/queryFetch'
|
||||
|
||||
type Statistic = z.infer<typeof StatisticOutput>
|
||||
|
||||
export const useStatistic = () => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ['statistic'],
|
||||
queryFn: async (): Promise<Readonly<Statistic>> =>
|
||||
await queryFetch({
|
||||
path: 'statistic',
|
||||
method: 'get',
|
||||
token: await getToken(),
|
||||
}),
|
||||
})
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import type { UserOutput } from '@boring.tools/schema'
|
||||
import { useAuth } from '@clerk/clerk-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { z } from 'zod'
|
||||
import { queryFetch } from '../utils/queryFetch'
|
||||
|
||||
type User = z.infer<typeof UserOutput>
|
||||
|
||||
export const useUser = () => {
|
||||
const { getToken } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ['user'],
|
||||
queryFn: async (): Promise<Readonly<User>> =>
|
||||
await queryFetch({
|
||||
path: 'user',
|
||||
method: 'get',
|
||||
token: await getToken(),
|
||||
}),
|
||||
retry: false,
|
||||
})
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
/* prettier-ignore-start */
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
// This file is auto-generated by TanStack Router
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
@ -17,17 +17,14 @@ import { Route as ChangelogIdVersionVersionIdImport } from './routes/changelog.$
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
const CliLazyImport = createFileRoute('/cli')()
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||
const PageIndexLazyImport = createFileRoute('/page/')()
|
||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||
const AccessTokensIndexLazyImport = createFileRoute('/access-tokens/')()
|
||||
const PageCreateLazyImport = createFileRoute('/page/create')()
|
||||
const PageIdLazyImport = createFileRoute('/page/$id')()
|
||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||
const AccessTokensNewLazyImport = createFileRoute('/access-tokens/new')()
|
||||
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
||||
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
||||
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
||||
@ -38,60 +35,39 @@ const ChangelogIdEditLazyImport = createFileRoute('/changelog/$id/edit')()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const CliLazyRoute = CliLazyImport.update({
|
||||
id: '/cli',
|
||||
path: '/cli',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/cli.lazy').then((d) => d.Route))
|
||||
|
||||
const IndexLazyRoute = IndexLazyImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||
|
||||
const UserIndexLazyRoute = UserIndexLazyImport.update({
|
||||
id: '/user/',
|
||||
path: '/user/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
||||
|
||||
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
||||
id: '/page/',
|
||||
path: '/page/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
||||
|
||||
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||
id: '/changelog/',
|
||||
path: '/changelog/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
||||
id: '/access-tokens/',
|
||||
path: '/access-tokens/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/access-tokens.index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||
id: '/page/create',
|
||||
path: '/page/create',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
||||
|
||||
const PageIdLazyRoute = PageIdLazyImport.update({
|
||||
id: '/page/$id',
|
||||
path: '/page/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
||||
|
||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||
id: '/changelog/create',
|
||||
path: '/changelog/create',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
@ -99,21 +75,11 @@ const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||
)
|
||||
|
||||
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||
id: '/changelog/$id',
|
||||
path: '/changelog/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
||||
|
||||
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
||||
id: '/access-tokens/new',
|
||||
path: '/access-tokens/new',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/access-tokens.new.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => PageIdLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@ -121,7 +87,6 @@ const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||
)
|
||||
|
||||
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => ChangelogIdLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@ -129,14 +94,12 @@ const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
||||
)
|
||||
|
||||
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
||||
id: '/edit',
|
||||
path: '/edit',
|
||||
getParentRoute: () => PageIdLazyRoute,
|
||||
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
||||
|
||||
const ChangelogIdVersionCreateLazyRoute =
|
||||
ChangelogIdVersionCreateLazyImport.update({
|
||||
id: '/versionCreate',
|
||||
path: '/versionCreate',
|
||||
getParentRoute: () => ChangelogIdLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@ -144,7 +107,6 @@ const ChangelogIdVersionCreateLazyRoute =
|
||||
)
|
||||
|
||||
const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
||||
id: '/edit',
|
||||
path: '/edit',
|
||||
getParentRoute: () => ChangelogIdLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@ -153,7 +115,6 @@ const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
||||
|
||||
const ChangelogIdVersionVersionIdRoute =
|
||||
ChangelogIdVersionVersionIdImport.update({
|
||||
id: '/version/$versionId',
|
||||
path: '/version/$versionId',
|
||||
getParentRoute: () => ChangelogIdLazyRoute,
|
||||
} as any)
|
||||
@ -169,20 +130,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/cli': {
|
||||
id: '/cli'
|
||||
path: '/cli'
|
||||
fullPath: '/cli'
|
||||
preLoaderRoute: typeof CliLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/access-tokens/new': {
|
||||
id: '/access-tokens/new'
|
||||
path: '/access-tokens/new'
|
||||
fullPath: '/access-tokens/new'
|
||||
preLoaderRoute: typeof AccessTokensNewLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/$id': {
|
||||
id: '/changelog/$id'
|
||||
path: '/changelog/$id'
|
||||
@ -211,13 +158,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PageCreateLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/access-tokens/': {
|
||||
id: '/access-tokens/'
|
||||
path: '/access-tokens'
|
||||
fullPath: '/access-tokens'
|
||||
preLoaderRoute: typeof AccessTokensIndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/': {
|
||||
id: '/changelog/'
|
||||
path: '/changelog'
|
||||
@ -320,13 +260,10 @@ const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren(
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cli': typeof CliLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/page': typeof PageIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
@ -340,11 +277,8 @@ export interface FileRoutesByFullPath {
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cli': typeof CliLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/page': typeof PageIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
@ -359,13 +293,10 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cli': typeof CliLazyRoute
|
||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||
'/page/create': typeof PageCreateLazyRoute
|
||||
'/access-tokens/': typeof AccessTokensIndexLazyRoute
|
||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||
'/page/': typeof PageIndexLazyRoute
|
||||
'/user/': typeof UserIndexLazyRoute
|
||||
@ -381,13 +312,10 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/cli'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/page/$id'
|
||||
| '/page/create'
|
||||
| '/access-tokens'
|
||||
| '/changelog'
|
||||
| '/page'
|
||||
| '/user'
|
||||
@ -400,11 +328,8 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/cli'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/create'
|
||||
| '/page/create'
|
||||
| '/access-tokens'
|
||||
| '/changelog'
|
||||
| '/page'
|
||||
| '/user'
|
||||
@ -417,13 +342,10 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/cli'
|
||||
| '/access-tokens/new'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/page/$id'
|
||||
| '/page/create'
|
||||
| '/access-tokens/'
|
||||
| '/changelog/'
|
||||
| '/page/'
|
||||
| '/user/'
|
||||
@ -438,13 +360,10 @@ export interface FileRouteTypes {
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexLazyRoute: typeof IndexLazyRoute
|
||||
CliLazyRoute: typeof CliLazyRoute
|
||||
AccessTokensNewLazyRoute: typeof AccessTokensNewLazyRoute
|
||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
||||
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
||||
AccessTokensIndexLazyRoute: typeof AccessTokensIndexLazyRoute
|
||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||
@ -452,13 +371,10 @@ export interface RootRouteChildren {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexLazyRoute: IndexLazyRoute,
|
||||
CliLazyRoute: CliLazyRoute,
|
||||
AccessTokensNewLazyRoute: AccessTokensNewLazyRoute,
|
||||
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
||||
PageCreateLazyRoute: PageCreateLazyRoute,
|
||||
AccessTokensIndexLazyRoute: AccessTokensIndexLazyRoute,
|
||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||
PageIndexLazyRoute: PageIndexLazyRoute,
|
||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||
@ -468,6 +384,8 @@ export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
@ -475,13 +393,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/cli",
|
||||
"/access-tokens/new",
|
||||
"/changelog/$id",
|
||||
"/changelog/create",
|
||||
"/page/$id",
|
||||
"/page/create",
|
||||
"/access-tokens/",
|
||||
"/changelog/",
|
||||
"/page/",
|
||||
"/user/"
|
||||
@ -490,12 +405,6 @@ export const routeTree = rootRoute
|
||||
"/": {
|
||||
"filePath": "index.lazy.tsx"
|
||||
},
|
||||
"/cli": {
|
||||
"filePath": "cli.lazy.tsx"
|
||||
},
|
||||
"/access-tokens/new": {
|
||||
"filePath": "access-tokens.new.lazy.tsx"
|
||||
},
|
||||
"/changelog/$id": {
|
||||
"filePath": "changelog.$id.lazy.tsx",
|
||||
"children": [
|
||||
@ -518,9 +427,6 @@ export const routeTree = rootRoute
|
||||
"/page/create": {
|
||||
"filePath": "page.create.lazy.tsx"
|
||||
},
|
||||
"/access-tokens/": {
|
||||
"filePath": "access-tokens.index.lazy.tsx"
|
||||
},
|
||||
"/changelog/": {
|
||||
"filePath": "changelog.index.lazy.tsx"
|
||||
},
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { Button } from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { AccessTokenColumns } from '../components/AccessToken/Table/Columns'
|
||||
import { DataTable } from '../components/AccessToken/Table/DataTable'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { useAccessTokenList } from '../hooks/useAccessToken'
|
||||
|
||||
const Component = () => {
|
||||
const { data, isPending } = useAccessTokenList()
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Access tokens',
|
||||
to: '/access-tokens',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="flex w-full gap-5 justify-between items-start md:items-center flex-col md:flex-row">
|
||||
<h1 className="text-3xl">Access Tokens</h1>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/access-tokens/new">Generate new token</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{data && !isPending && (
|
||||
<DataTable data={data} columns={AccessTokenColumns} />
|
||||
)}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/access-tokens/')({
|
||||
component: Component,
|
||||
})
|
@ -1,136 +0,0 @@
|
||||
import { AccessTokenCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLazyFileRoute, useRouter } from '@tanstack/react-router'
|
||||
import { AlertCircle, CopyIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useCopyToClipboard } from 'usehooks-ts'
|
||||
import type { z } from 'zod'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { useAccessTokenCreate } from '../hooks/useAccessToken'
|
||||
|
||||
const Component = () => {
|
||||
const router = useRouter()
|
||||
const [, copy] = useCopyToClipboard()
|
||||
const [token, setToken] = useState<null | string>(null)
|
||||
const accessTokenCreate = useAccessTokenCreate()
|
||||
const form = useForm<z.infer<typeof AccessTokenCreateInput>>({
|
||||
resolver: zodResolver(AccessTokenCreateInput),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof AccessTokenCreateInput>) => {
|
||||
accessTokenCreate.mutate(values, {
|
||||
onSuccess(data) {
|
||||
if (data.token) {
|
||||
setToken(data.token)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Access tokens',
|
||||
to: '/access-tokens',
|
||||
},
|
||||
{
|
||||
name: 'New',
|
||||
to: '/access-tokens/new',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="flex w-full gap-5 justify-between items-center">
|
||||
<h1 className="text-3xl">New access token</h1>
|
||||
</div>
|
||||
|
||||
{token && (
|
||||
<div className="flex flex-col gap-3 w-full max-w-screen-md">
|
||||
<h2 className="text-xl">Your token</h2>
|
||||
<pre className="bg-muted text-xl p-3 rounded text-center flex justify-between items-center">
|
||||
{token}
|
||||
<Button
|
||||
onClick={() => copy(token)}
|
||||
size={'icon'}
|
||||
variant={'outline'}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</pre>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Reminder</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your token is only visible this time. Please notify it securely.
|
||||
If you forget it, you have to create a new token.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
type="button"
|
||||
onClick={() => router.history.back()}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!token && (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="CLI Token" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="w-full flex items-center justify-end gap-5">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
type="button"
|
||||
onClick={() => router.history.back()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/access-tokens/new')({
|
||||
component: Component,
|
||||
})
|
@ -1,9 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
@ -62,118 +58,66 @@ const Component = () => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<div className="flex gap-10 w-full max-w-screen-lg">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My changelog"
|
||||
{...field}
|
||||
autoFocus
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My changelog" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="w-full flex flex-col gap-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isConventional"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Conventional Commits</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is using{' '}
|
||||
<a
|
||||
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
conventional commits
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-end gap-5">
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
@ -183,8 +127,7 @@ const Component = () => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">Update</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -1,12 +1,58 @@
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { ChangelogCommitList } from '../components/Changelog/CommitList'
|
||||
import { ChangelogVersionList } from '../components/Changelog/VersionList'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import { VersionStatus } from '../components/Changelog/VersionStatus'
|
||||
import { useChangelogById } from '../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const { id } = Route.useParams()
|
||||
const { data, isPending } = useChangelogById({ id })
|
||||
|
||||
return (
|
||||
<div className="flex gap-5 flex-wrap">
|
||||
<ChangelogVersionList />
|
||||
<ChangelogCommitList />
|
||||
<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>Versions ({data.versions?.length})</CardTitle>
|
||||
|
||||
<Link to="/changelog/$id/versionCreate" params={{ id }}>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-1">
|
||||
{data.versions?.map((version) => {
|
||||
return (
|
||||
<Link
|
||||
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
|
||||
to="/changelog/$id/version/$versionId"
|
||||
params={{
|
||||
id,
|
||||
versionId: version.id,
|
||||
}}
|
||||
key={version.id}
|
||||
>
|
||||
<VersionStatus status={version.status} />
|
||||
{version.version}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { useChangelogById } from '../hooks/useChangelog'
|
||||
const Component = () => {
|
||||
const { id } = Route.useParams()
|
||||
const { data, error, isPending, refetch } = useChangelogById({ id })
|
||||
console.log(data)
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-32 flex-col">
|
||||
@ -64,7 +65,16 @@ const Component = () => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
*/}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant={'ghost'}>
|
||||
<Globe2Icon strokeWidth={1.5} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Public Page</p>
|
||||
</TooltipContent>
|
||||
</Tooltip> */}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
@ -2,30 +2,21 @@ import { VersionUpdateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollArea,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
cn,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
@ -46,19 +37,17 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { z } from 'zod'
|
||||
import {
|
||||
useChangelogCommitList,
|
||||
useChangelogVersionById,
|
||||
useChangelogVersionUpdate,
|
||||
} from '../hooks/useChangelog'
|
||||
import '@mdxeditor/editor/style.css'
|
||||
import { format } from 'date-fns'
|
||||
import { CalendarIcon } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { ChangelogVersionDelete } from '../components/Changelog/VersionDelete'
|
||||
import { VersionStatus } from '../components/Changelog/VersionStatus'
|
||||
|
||||
const Component = () => {
|
||||
const [activeTab, setActiveTab] = useState('assigned')
|
||||
const { id, versionId } = Route.useParams()
|
||||
const mdxEditorRef = useRef<MDXEditorMethods>(null)
|
||||
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
||||
@ -66,8 +55,6 @@ const Component = () => {
|
||||
const { data, error, isPending, refetch } = useChangelogVersionById({
|
||||
id: versionId,
|
||||
})
|
||||
const commitResult = useChangelogCommitList({ id })
|
||||
|
||||
const form = useForm<z.infer<typeof VersionUpdateInput>>({
|
||||
resolver: zodResolver(VersionUpdateInput),
|
||||
defaultValues: data,
|
||||
@ -87,12 +74,9 @@ const Component = () => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
mdxEditorRef.current?.setMarkdown(data.markdown)
|
||||
form.reset({
|
||||
...data,
|
||||
commitIds: data.commits?.map((commit) => commit.id),
|
||||
})
|
||||
form.reset(data)
|
||||
}
|
||||
}, [data, form])
|
||||
}, [data, form.reset])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@ -109,332 +93,152 @@ const Component = () => {
|
||||
<div className="flex flex-col gap-5">
|
||||
<Separator />
|
||||
{!isPending && data && (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex gap-4 w-full flex-col max-w-screen-2xl"
|
||||
>
|
||||
<div className="flex gap-5 w-full">
|
||||
<Card className="w-full h-[700px]">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Details</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-3 flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<div>
|
||||
<h1 className="text-xl mb-2">Version: {data.version}</h1>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="markdown"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<MDXEditor
|
||||
className="dark-theme"
|
||||
contentEditableClassName="prose dark:prose-invert max-w-none"
|
||||
markdown={''}
|
||||
ref={mdxEditorRef}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
listsPlugin(),
|
||||
thematicBreakPlugin(),
|
||||
quotePlugin(),
|
||||
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<BlockTypeSelect />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<ListsToggle />
|
||||
<UndoRedo />
|
||||
</>
|
||||
),
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-5 items-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<Input placeholder="v1.0.1" {...field} />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select your version status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'draft'} />
|
||||
<span>Draft</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="review">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'review'} />
|
||||
<span>Review</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="published">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'published'} />
|
||||
<span>Published</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-5 items-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select your version status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'draft'} />
|
||||
<span>Draft</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="review">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'review'} />
|
||||
<span>Review</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="published">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'published'} />
|
||||
<span>Published</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="releasedAt"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="mb-2">Released at</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
size={'lg'}
|
||||
className={cn(
|
||||
'w-[240px] pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="releasedAt"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="mb-2">Released at</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
size={'lg'}
|
||||
className={cn(
|
||||
'w-[240px] pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => field.onChange(date)}
|
||||
weekStartsOn={1}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="markdown"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<MDXEditor
|
||||
className="dark-theme"
|
||||
contentEditableClassName="prose dark:prose-invert max-w-none max-h-[390px] overflow-scroll"
|
||||
markdown={''}
|
||||
ref={mdxEditorRef}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
listsPlugin(),
|
||||
thematicBreakPlugin(),
|
||||
quotePlugin(),
|
||||
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<BlockTypeSelect />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<ListsToggle />
|
||||
<UndoRedo />
|
||||
</>
|
||||
),
|
||||
}),
|
||||
]}
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => field.onChange(date)}
|
||||
weekStartsOn={1}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full max-w-screen-sm h-[700px]">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Commits ({data.commits?.length})</CardTitle>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={activeTab === 'assigned' ? 'outline' : 'ghost'}
|
||||
size={'sm'}
|
||||
onClick={() => setActiveTab('assigned')}
|
||||
>
|
||||
Assigend
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={
|
||||
activeTab === 'unassigned' ? 'outline' : 'ghost'
|
||||
}
|
||||
size={'sm'}
|
||||
onClick={() => setActiveTab('unassigned')}
|
||||
>
|
||||
Unassigend
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={activeTab} className="w-full">
|
||||
<TabsContent value="assigned">
|
||||
<ScrollArea className="w-full h-[580px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
{data?.commits?.map((commit) => {
|
||||
return (
|
||||
<FormField
|
||||
key={commit.id}
|
||||
control={form.control}
|
||||
name={'commitIds'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
value={commit.id}
|
||||
checked={field.value?.includes(
|
||||
commit.id,
|
||||
)}
|
||||
onCheckedChange={() => {
|
||||
const exist = field.value?.includes(
|
||||
commit.id,
|
||||
)
|
||||
if (exist) {
|
||||
return field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== commit.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
return field.onChange([
|
||||
...(field.value as string[]),
|
||||
commit.id,
|
||||
])
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none w-full">
|
||||
<FormLabel className="flex gap-2 w-full">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{commit.commit}{' '}
|
||||
</span>
|
||||
<span className="w-full">
|
||||
{commit.subject}
|
||||
</span>
|
||||
<span>
|
||||
{format(
|
||||
new Date(commit.commiter.date),
|
||||
'dd.MM.',
|
||||
)}
|
||||
</span>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
<TabsContent value="unassigned">
|
||||
<ScrollArea className="w-full h-[350px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
{commitResult.data?.map((commit) => {
|
||||
return (
|
||||
<FormField
|
||||
key={commit.id}
|
||||
control={form.control}
|
||||
name={'commitIds'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
value={commit.id}
|
||||
checked={field.value?.includes(
|
||||
commit.id,
|
||||
)}
|
||||
onCheckedChange={() => {
|
||||
const exist = field.value?.includes(
|
||||
commit.id,
|
||||
)
|
||||
if (exist) {
|
||||
return field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== commit.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
return field.onChange([
|
||||
...(field.value as string[]),
|
||||
commit.id,
|
||||
])
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none w-full">
|
||||
<FormLabel className="flex gap-2 w-full">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{commit.commit}
|
||||
</span>
|
||||
<span className="w-full">
|
||||
{commit.subject}
|
||||
</span>
|
||||
<span>
|
||||
{format(
|
||||
new Date(commit.commiter.date),
|
||||
'dd.MM.',
|
||||
)}
|
||||
</span>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex gap-5 w-full items-end justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() =>
|
||||
navigate({ to: '/changelog/$id', params: { id } })
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Update</Button>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() =>
|
||||
navigate({ to: '/changelog/$id', params: { id } })
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Update</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<ChangelogVersionDelete id={id} versionId={versionId} />
|
||||
</form>
|
||||
</Form>
|
||||
<ChangelogVersionDelete id={id} versionId={versionId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
@ -2,11 +2,6 @@ import { VersionCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
@ -17,12 +12,12 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollArea,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
cn,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
@ -42,10 +37,7 @@ import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { z } from 'zod'
|
||||
import {
|
||||
useChangelogCommitList,
|
||||
useChangelogVersionCreate,
|
||||
} from '../hooks/useChangelog'
|
||||
import { useChangelogVersionCreate } from '../hooks/useChangelog'
|
||||
import '@mdxeditor/editor/style.css'
|
||||
import { format } from 'date-fns'
|
||||
import { CalendarIcon } from 'lucide-react'
|
||||
@ -54,9 +46,7 @@ import { VersionStatus } from '../components/Changelog/VersionStatus'
|
||||
const Component = () => {
|
||||
const { id } = Route.useParams()
|
||||
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
||||
const changelogCommit = useChangelogCommitList({ id })
|
||||
const versionCreate = useChangelogVersionCreate()
|
||||
const { data } = useChangelogCommitList({ id })
|
||||
const form = useForm<z.infer<typeof VersionCreateInput>>({
|
||||
resolver: zodResolver(VersionCreateInput),
|
||||
defaultValues: {
|
||||
@ -64,18 +54,9 @@ const Component = () => {
|
||||
version: '',
|
||||
markdown: '',
|
||||
status: 'draft',
|
||||
commitIds: [],
|
||||
},
|
||||
})
|
||||
|
||||
const selectAllCommits = () => {
|
||||
const commitIds = data?.map((commit) => commit.id)
|
||||
if (!commitIds) {
|
||||
return form.setValue('commitIds', [])
|
||||
}
|
||||
form.setValue('commitIds', commitIds)
|
||||
}
|
||||
|
||||
const onSubmit = (values: z.infer<typeof VersionCreateInput>) => {
|
||||
versionCreate.mutate(values, {
|
||||
onSuccess(data) {
|
||||
@ -85,213 +66,149 @@ const Component = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full">
|
||||
<h1 className="text-2xl">New version</h1>
|
||||
<div className="grid md:grid-cols-6 gap-5 w-full md:max-w-screen-xl grid-flow-row grid-cols-1">
|
||||
<Card className="md:col-span-4 col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<div className="flex flex-col gap-5">
|
||||
<Separator />
|
||||
<h1 className="text-xl mb-2">New version</h1>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="v1.0.1" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="markdown"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<MDXEditor
|
||||
className="dark-theme"
|
||||
contentEditableClassName="prose dark:prose-invert max-w-none"
|
||||
markdown={field.value}
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
listsPlugin(),
|
||||
thematicBreakPlugin(),
|
||||
quotePlugin(),
|
||||
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<BlockTypeSelect />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<ListsToggle />
|
||||
<UndoRedo />
|
||||
</>
|
||||
),
|
||||
}),
|
||||
]}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-5 items-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<Input placeholder="v1.0.1" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select your version status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'draft'} />
|
||||
<span>Draft</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="review">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'review'} />
|
||||
<span>Review</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="published">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'published'} />
|
||||
<span>Published</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-5 md:items-center flex-col md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select your version status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'draft'} />
|
||||
<span>Draft</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="review">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'review'} />
|
||||
<span>Review</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="published">
|
||||
<div className="flex gap-2 items-center">
|
||||
<VersionStatus status={'published'} />
|
||||
<span>Published</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="releasedAt"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="mb-2">Released at</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
size={'lg'}
|
||||
className={cn(
|
||||
'w-[240px] pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => field.onChange(date)}
|
||||
weekStartsOn={1}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="markdown"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Changes</FormLabel>
|
||||
<FormControl>
|
||||
<MDXEditor
|
||||
className="dark-theme h-56"
|
||||
contentEditableClassName="prose dark:prose-invert max-w-none"
|
||||
markdown={field.value}
|
||||
plugins={[
|
||||
headingsPlugin(),
|
||||
listsPlugin(),
|
||||
thematicBreakPlugin(),
|
||||
quotePlugin(),
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<BlockTypeSelect />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<ListsToggle />
|
||||
<UndoRedo />
|
||||
</>
|
||||
),
|
||||
}),
|
||||
]}
|
||||
{...field}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="releasedAt"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="mb-2">Released at</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
size={'lg'}
|
||||
className={cn(
|
||||
'w-[240px] pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => field.onChange(date)}
|
||||
weekStartsOn={1}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="md:col-span-2 col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Associated commits</CardTitle>
|
||||
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'sm'}
|
||||
onClick={selectAllCommits}
|
||||
type="button"
|
||||
>
|
||||
Add all commits
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="w-full h-[350px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
{changelogCommit.data?.map((commit) => {
|
||||
return (
|
||||
<FormField
|
||||
key={commit.id}
|
||||
control={form.control}
|
||||
name={'commitIds'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
value={commit.id}
|
||||
checked={field.value?.includes(commit.id)}
|
||||
onCheckedChange={() => {
|
||||
const exist = field.value.includes(
|
||||
commit.id,
|
||||
)
|
||||
if (exist) {
|
||||
return field.onChange(
|
||||
field.value.filter(
|
||||
(value) => value !== commit.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
return field.onChange([
|
||||
...field.value,
|
||||
commit.id,
|
||||
])
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{commit.subject}</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex gap-5 mt-5 w-full justify-end items-end md:col-span-6">
|
||||
<div className="flex gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
@ -301,9 +218,9 @@ const Component = () => {
|
||||
</Button>
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { ChangelogCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
@ -33,7 +29,6 @@ const Component = () => {
|
||||
title: '',
|
||||
description: '',
|
||||
isSemver: true,
|
||||
isConventional: true,
|
||||
},
|
||||
})
|
||||
|
||||
@ -57,131 +52,70 @@ const Component = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">New changelog</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<div className="flex gap-10 w-full max-w-screen-lg">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My changelog"
|
||||
{...field}
|
||||
autoFocus
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My changelog" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="w-full flex flex-col gap-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isConventional"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Conventional Commits</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is using{' '}
|
||||
<a
|
||||
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
conventional commits
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-end gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => navigate({ to: '/changelog' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Create</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -26,45 +26,47 @@ const Component = () => {
|
||||
|
||||
return (
|
||||
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelog</h1>
|
||||
<>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelog</h1>
|
||||
|
||||
<div className="flex gap-10 w-full flex-wrap">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed?.versionCount}</span>
|
||||
<div className="flex gap-10 w-full">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed.versionCount}</span>
|
||||
|
||||
<span>Commits: {changelog.computed?.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
<span>Commits: {changelog.computed.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1,121 +0,0 @@
|
||||
import { Button, Card, CardContent, CardHeader, cn } from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
|
||||
const Platforms = [
|
||||
{
|
||||
name: 'Linux',
|
||||
arch: 'x86',
|
||||
svg: '/linux.svg',
|
||||
path: '/cli/linux/bt-cli',
|
||||
filename: 'bt-cli',
|
||||
},
|
||||
{
|
||||
name: 'Apple Intel',
|
||||
arch: 'Intel',
|
||||
svg: '/apple.svg',
|
||||
path: '/cli/mac-intel/bt-cli',
|
||||
filename: 'bt-cli',
|
||||
},
|
||||
{
|
||||
name: 'Apple ARM',
|
||||
arch: 'ARM',
|
||||
svg: '/apple.svg',
|
||||
path: '/cli/mac-arm/bt-cli',
|
||||
filename: 'bt-cli',
|
||||
},
|
||||
{
|
||||
name: 'Windows',
|
||||
arch: 'x86',
|
||||
svg: '/windows.svg',
|
||||
path: '/cli/windows/bt-cli.exe',
|
||||
filename: 'bt-cli.exe',
|
||||
},
|
||||
]
|
||||
|
||||
const Component = () => {
|
||||
const [activePlatform, setPlatform] = useState('Linux')
|
||||
const getPlatform = Platforms.find((p) => p.name === activePlatform)
|
||||
return (
|
||||
<PageWrapper breadcrumbs={[{ name: 'CLI', to: '/cli' }]}>
|
||||
<div className="flex flex-col gap-5 w-full md:max-w-screen-lg">
|
||||
<h1 className="text-3xl">CLI</h1>
|
||||
<p className="text-muted-foreground">
|
||||
With our CLI you can upload your commits for your changelog in just a
|
||||
few seconds.
|
||||
</p>
|
||||
<h2 className="text-xl">Platform</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-10">
|
||||
{Platforms.map((platform) => {
|
||||
return (
|
||||
// <a
|
||||
// href={`https://cdn.boring.tools${platform.path}`}
|
||||
// key={`${platform.arch}-${platform.name}`}
|
||||
// download={platform.filename}
|
||||
// >
|
||||
<Card
|
||||
className={cn('hover:border-accent transition', {
|
||||
'border-accent': platform.name === activePlatform,
|
||||
})}
|
||||
key={`${platform.arch}-${platform.name}`}
|
||||
onClick={() => setPlatform(platform.name)}
|
||||
>
|
||||
<CardHeader>
|
||||
<img
|
||||
src={platform.svg}
|
||||
alt={platform.name}
|
||||
className="h-10 md:h-20"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
{platform.arch}
|
||||
</CardContent>
|
||||
</Card>
|
||||
// </a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h2 className="text-xl">Download</h2>
|
||||
<Button asChild variant={'outline'}>
|
||||
<a
|
||||
href={`https://cdn.boring.tools${getPlatform?.path}`}
|
||||
download={getPlatform?.filename}
|
||||
>
|
||||
https://cdn.boring.tools{getPlatform?.path}
|
||||
</a>
|
||||
</Button>
|
||||
<h2 className="text-xl">WGET</h2>
|
||||
<pre className="bg-muted text-xs md:text-xl p-3 rounded text-center flex justify-between items-center">
|
||||
wget https://cdn.boring.tools{getPlatform?.path}
|
||||
</pre>
|
||||
<h2 className="text-xl">Usage</h2>
|
||||
<pre className="bg-muted text-xs md:text-xl p-3 rounded text-center flex justify-between items-center">
|
||||
{getPlatform?.filename} --help
|
||||
</pre>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
Alternatively, you can use an .env file:
|
||||
</p>
|
||||
|
||||
<pre className="bg-muted text-xs md:text-xl p-3 rounded text-center flex justify-between items-center">
|
||||
BT_CHANGELOG_ID=...
|
||||
<br />
|
||||
BT_ACCESS_TOKEN=bt_...
|
||||
</pre>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
If you have not yet created an Access Token, you can do so{' '}
|
||||
<Link to="/access-tokens/new" className="text-accent font-bold">
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/cli')({
|
||||
component: Component,
|
||||
})
|
@ -1,71 +1,13 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
||||
import { useUser } from '@clerk/clerk-react'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { useStatistic } from '../hooks/useStatistic'
|
||||
|
||||
const Component = () => {
|
||||
const { data } = useStatistic()
|
||||
const user = useUser()
|
||||
export const Route = createLazyFileRoute('/')({
|
||||
component: Index,
|
||||
})
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Dashboard',
|
||||
to: '/',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h1 className="text-3xl">Welcome back, {user.user?.fullName}</h1>
|
||||
<div className="grid w-full max-w-screen-md gap-10 grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Changelogs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">
|
||||
{data?.changelog.total}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Versions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">
|
||||
{data?.changelog.versions.total}
|
||||
<div className="text-xs text-muted-foreground tracking-normal flex gap-3 flex-wrap">
|
||||
<span>{data?.changelog.versions.published} Published</span>
|
||||
<span>{data?.changelog.versions.review} Review</span>
|
||||
<span>{data?.changelog.versions.draft} Draft</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Commits</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">
|
||||
{data?.changelog.commits.total}
|
||||
<div className="text-xs text-muted-foreground tracking-normal flex gap-3 flex-wrap">
|
||||
<span>{data?.changelog.commits.assigned} Assigned</span>
|
||||
<span>{data?.changelog.commits.unassigned} Unassigned</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Pages</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">
|
||||
{data?.page.total}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const Route = createLazyFileRoute('/')({
|
||||
component: Component,
|
||||
})
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { PageUpdateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
@ -44,7 +40,7 @@ const Component = () => {
|
||||
resolver: zodResolver(PageUpdateInput),
|
||||
defaultValues: {
|
||||
...page.data,
|
||||
changelogIds: page.data?.changelogs?.map((log) => log.id),
|
||||
changelogIds: page.data?.changelogs.map((log) => log.id),
|
||||
},
|
||||
})
|
||||
const onSubmit = (values: z.infer<typeof PageUpdateInput>) => {
|
||||
@ -61,147 +57,120 @@ const Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Edit page</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<div className="flex gap-10 w-full">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<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="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}
|
||||
value={field.value ?? ''}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the page..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<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',
|
||||
)}
|
||||
<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())
|
||||
}}
|
||||
>
|
||||
{field?.value?.length === 1 &&
|
||||
changelogList.data?.find((changelog) =>
|
||||
field.value?.includes(changelog.id),
|
||||
)?.title}
|
||||
{field?.value &&
|
||||
field.value.length <= 0 &&
|
||||
'No changelog selected'}
|
||||
{field?.value &&
|
||||
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) {
|
||||
return [changelog.id]
|
||||
}
|
||||
|
||||
if (
|
||||
field.value?.includes(changelog.id)
|
||||
) {
|
||||
return field.value.filter(
|
||||
(id) => id !== changelog.id,
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
...(field?.value as string[]),
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-end gap-5">
|
||||
<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'}
|
||||
@ -209,7 +178,7 @@ const Component = () => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">Update</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { CircleMinusIcon } from 'lucide-react'
|
||||
import { CircleMinusIcon, PlusCircleIcon } from 'lucide-react'
|
||||
import { usePageById, usePageUpdate } from '../hooks/usePage'
|
||||
|
||||
const Component = () => {
|
||||
@ -31,14 +31,12 @@ const Component = () => {
|
||||
<Card className="w-full max-w-screen-sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>
|
||||
Changelogs ({data.changelogs?.length ?? 0})
|
||||
</CardTitle>
|
||||
<CardTitle>Changelogs ({data.changelogs?.length})</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-1">
|
||||
{data.changelogs?.map((changelog) => {
|
||||
{data.changelogs.map((changelog) => {
|
||||
return (
|
||||
<div className="flex gap-3" key={changelog.id}>
|
||||
<Link
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { PageCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
@ -73,146 +69,115 @@ const Component = () => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<div className="flex gap-10 w-full">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Details</CardTitle>
|
||||
</CardHeader>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My page" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<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="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the page..."
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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
|
||||
}
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<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',
|
||||
)}
|
||||
return [...field.value, changelog.id]
|
||||
}
|
||||
form.setValue('changelogIds', getIds())
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => navigate({ to: '/page' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -1,51 +1,10 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import { PageWrapper } from '../components/PageWrapper'
|
||||
import { usePageList } from '../hooks/usePage'
|
||||
//import { usePageById, usePageList } from '../hooks/usePage'
|
||||
|
||||
const Component = () => {
|
||||
const { data, isPending } = usePageList()
|
||||
//const { data, error } = usePageList()
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Page',
|
||||
to: '/page',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h1 className="text-3xl">Page</h1>
|
||||
<div className="flex gap-10 w-full flex-wrap">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((page) => {
|
||||
return (
|
||||
<Link to="/page/$id" params={{ id: page.id }} key={page.id}>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{page.title}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<Link to="/page/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New page</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
return <div>some</div>
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/page/')({
|
||||
|
@ -12,17 +12,13 @@ const url = import.meta.env.PROD
|
||||
: 'http://localhost:3000'
|
||||
|
||||
export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
method,
|
||||
url: `${url}/v1/${path}`,
|
||||
data,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error('Somethind went wrong.')
|
||||
}
|
||||
const response = await axios({
|
||||
method,
|
||||
url: `${url}/v1/${path}`,
|
||||
data,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/AccessToken/Delete.tsx","./src/components/AccessToken/Table/Columns.tsx","./src/components/AccessToken/Table/DataTable.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/Step02.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useAccessToken.ts","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/hooks/useStatistic.ts","./src/routes/__root.tsx","./src/routes/access-tokens.index.lazy.tsx","./src/routes/access-tokens.new.lazy.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/cli.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
|
||||
{"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"}
|
175
apps/cli/.gitignore
vendored
175
apps/cli/.gitignore
vendored
@ -1,175 +0,0 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
@ -1,15 +0,0 @@
|
||||
# cli
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run src/index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.1.21. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@boring.tools/cli",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:linux": "bun build --compile --target=bun-linux-x64-modern ./src/index.ts --outfile ../../build/cli/linux/bt-cli",
|
||||
"build:mac:arm": "bun build --compile --target=bun-darwin-arm64 ./src/index.ts --outfile ../../build/cli/mac-arm/bt-cli",
|
||||
"build:mac:intel": "bun build --compile --target=bun-darwin-x64 ./src/index.ts --outfile ../../build/cli/mac-intel/bt-cli",
|
||||
"build:win": "bun build --compile --target=bun-windows-x64-modern ./src/index.ts --outfile ../../build/cli/windows/bt-cli",
|
||||
"build": "bun run build:linux && bun run build:mac:arm && bun run build:mac:intel && bun run build:win"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@commander-js/extra-typings": "^12.1.0",
|
||||
"commander": "^12.1.0"
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#! /usr/bin/env bun
|
||||
import { program } from '@commander-js/extra-typings'
|
||||
import { upload_commits } from './upload_commits'
|
||||
import type { Arguments } from './utils/arguments'
|
||||
|
||||
const ENV_CHANGELOG_ID = Bun.env.BT_CHANGELOG_ID
|
||||
const ENV_ACCESS_TOKEN = Bun.env.BT_ACCESS_TOKEN
|
||||
|
||||
program.name('bt-cli').description('boring.tools CLI').version('0.8.0')
|
||||
|
||||
const commit = program.command('commit').description('Commits')
|
||||
commit
|
||||
.command('upload')
|
||||
.description('Upload commit messages')
|
||||
.requiredOption('--changelogId <id>', 'Changelog Id', ENV_CHANGELOG_ID)
|
||||
.requiredOption('--accessToken <string>', 'Access Token', ENV_ACCESS_TOKEN)
|
||||
.action((data) => {
|
||||
upload_commits(data as Arguments)
|
||||
})
|
||||
|
||||
program.parse()
|
@ -1,49 +0,0 @@
|
||||
import type { Arguments } from './utils/arguments'
|
||||
import { fetchAPI } from './utils/fetch_api'
|
||||
import { git_log } from './utils/git_log'
|
||||
|
||||
const getLastCommitHash = async (args: Arguments) => {
|
||||
const result = await fetchAPI(
|
||||
`/v1/changelog/commit?changelogId=${args.changelogId}&limit=1`,
|
||||
{},
|
||||
args.accessToken,
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return result[0].commit
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export const upload_commits = async (arguemnts: Arguments) => {
|
||||
const hash = await getLastCommitHash(arguemnts)
|
||||
const commits = await git_log(hash)
|
||||
|
||||
if (commits.length === 0) {
|
||||
console.info('No new commits found')
|
||||
return
|
||||
}
|
||||
|
||||
console.info(`Pushing ${commits.length} commits`)
|
||||
const mappedCommits = commits.map((commit) => ({
|
||||
...commit,
|
||||
changelogId: arguemnts.changelogId,
|
||||
}))
|
||||
|
||||
await fetchAPI(
|
||||
'/v1/changelog/commit',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(mappedCommits),
|
||||
},
|
||||
arguemnts.accessToken,
|
||||
)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
accessToken: z.string().startsWith('bt_'),
|
||||
changelogId: z.string(),
|
||||
})
|
||||
|
||||
export type Arguments = z.infer<typeof schema>
|
@ -1,27 +0,0 @@
|
||||
const getAPIUrl = () => {
|
||||
if (Bun.env.NODE_ENV === 'development') {
|
||||
return 'http://localhost:3000'
|
||||
}
|
||||
|
||||
return 'https://api.boring.tools'
|
||||
}
|
||||
|
||||
export const fetchAPI = async (
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
token: string,
|
||||
) => {
|
||||
const response = await fetch(`${getAPIUrl()}${url}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
const GITFORMAT =
|
||||
'--pretty=format:{"commit": "%h", "parent": "%p", "refs": "%D", "subject": "%s", "author": { "name": "%aN", "email": "%aE", "date": "%ad" }, "commiter": { "name": "%cN", "email": "%cE", "date": "%cd" }},'
|
||||
|
||||
export const git_log = async (
|
||||
from: string | undefined,
|
||||
to = 'HEAD',
|
||||
): Promise<Array<Record<string, unknown>>> => {
|
||||
// https://git-scm.com/docs/pretty-formats
|
||||
const process = Bun.spawn([
|
||||
'git',
|
||||
'log',
|
||||
`${from ? `${from}...` : ''}${to}`,
|
||||
GITFORMAT,
|
||||
'--date=iso',
|
||||
])
|
||||
|
||||
const output = await new Response(process.stdout).text()
|
||||
const jsonString = `[${output.slice(0, -1)}]`
|
||||
return JSON.parse(jsonString)
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"astro": "^4.16.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"marked": "^14.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^3.4.14",
|
||||
|
@ -3,7 +3,6 @@ import type { PageByIdOutput } from '@boring.tools/schema'
|
||||
import { Separator } from '@boring.tools/ui'
|
||||
import type { z } from 'astro/zod'
|
||||
import { format } from 'date-fns'
|
||||
import { marked } from 'marked'
|
||||
|
||||
type PageById = z.infer<typeof PageByIdOutput>
|
||||
const url = import.meta.env.DEV
|
||||
@ -30,9 +29,9 @@ const data: PageById = await response.json()
|
||||
<p class="prose text-sm">{data.description}</p>
|
||||
</div>
|
||||
|
||||
{data.changelogs?.length >= 2 && <div class="flex flex-col">
|
||||
{data.changelogs.length >= 2 && <div class="flex flex-col">
|
||||
<h2 class="prose prose-xl">Changelogs</h2>
|
||||
{data.changelogs?.map((changelog) => {
|
||||
{data.changelogs.map((changelog) => {
|
||||
if (changelog.versions && changelog.versions?.length < 1) {
|
||||
return null
|
||||
}
|
||||
@ -56,7 +55,9 @@ const data: PageById = await response.json()
|
||||
}
|
||||
</div>
|
||||
|
||||
<div set:html={marked.parse(version.markdown ?? "")} />
|
||||
<p>
|
||||
{version.markdown}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -65,7 +66,7 @@ const data: PageById = await response.json()
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
{data.changelogs?.length === 1 && <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) {
|
||||
@ -88,7 +89,10 @@ const data: PageById = await response.json()
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="page" set:html={marked.parse(version.markdown ?? "")} />
|
||||
|
||||
<p>
|
||||
{version.markdown}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
@ -25,18 +25,8 @@ export default defineConfig({
|
||||
favicon: '/favicon.svg',
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Introduction', slug: 'docs' },
|
||||
{ label: 'Motivation', slug: 'docs/motivation' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
items: [
|
||||
{ label: 'Getting started', slug: 'docs/changelog' },
|
||||
{ label: 'Features', slug: 'docs/changelog/features' },
|
||||
],
|
||||
label: 'Guides',
|
||||
items: [{ label: 'Getting started', slug: 'guides/getting-started' }],
|
||||
},
|
||||
{ label: 'API', link: 'https://api.boring.tools' },
|
||||
{ label: 'Status', link: 'https://status.boring.tools' },
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user