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
|
# environment: production
|
||||||
# sourcemaps: ./build/app/
|
# 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)
|
- name: Set docker chmod (temp solution)
|
||||||
run: sudo chmod 666 /var/run/docker.sock
|
run: sudo chmod 666 /var/run/docker.sock
|
||||||
|
@ -6,12 +6,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
redis:
|
|
||||||
image: redis:7.4.1-alpine
|
|
||||||
# command: redis-server --save 20 1 --loglevel warning --requirepass development
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
|
|
||||||
@ -51,5 +45,3 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
POSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres
|
POSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres
|
||||||
REDIS_URL: redis://redis:6379
|
|
||||||
REDIS_PASSWORD: development
|
|
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",
|
"name": "@boring.tools/api",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot src/index.ts",
|
"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"
|
"test": "bun test --preload ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -13,12 +13,9 @@
|
|||||||
"@hono/sentry": "^1.2.0",
|
"@hono/sentry": "^1.2.0",
|
||||||
"@hono/zod-openapi": "^0.16.2",
|
"@hono/zod-openapi": "^0.16.2",
|
||||||
"@scalar/hono-api-reference": "^0.5.149",
|
"@scalar/hono-api-reference": "^0.5.149",
|
||||||
"convert-gitmoji": "^0.1.5",
|
|
||||||
"hono": "^4.6.3",
|
"hono": "^4.6.3",
|
||||||
"redis": "^4.7.0",
|
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"svix": "^1.36.0",
|
"svix": "^1.36.0"
|
||||||
"ts-pattern": "^5.5.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@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 { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
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({
|
export const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
tags: ['changelog'],
|
|
||||||
request: {
|
request: {
|
||||||
params: ChangelogByIdParams,
|
params: ChangelogByIdParams,
|
||||||
},
|
},
|
||||||
@ -24,16 +19,16 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelog by id',
|
description: 'Return changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerChangelogById = (api: typeof changelogApi) => {
|
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||||
return api.openapi(route, async (c) => {
|
|
||||||
const userId = await verifyAuthentication(c)
|
|
||||||
const { id } = c.req.valid('param')
|
|
||||||
|
|
||||||
const result = await db.query.changelog.findFirst({
|
const result = await db.query.changelog.findFirst({
|
||||||
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
||||||
with: {
|
with: {
|
||||||
@ -53,6 +48,10 @@ export const registerChangelogById = (api: typeof changelogApi) => {
|
|||||||
if (!result) {
|
if (!result) {
|
||||||
throw new HTTPException(404, { message: 'Not found' })
|
throw new HTTPException(404, { message: 'Not found' })
|
||||||
}
|
}
|
||||||
return c.json(ChangelogOutput.parse(result), 200)
|
return result
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
route,
|
||||||
|
func,
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,18 @@ import { eq } from 'drizzle-orm'
|
|||||||
import { fetch } from '../utils/testing/fetch'
|
import { fetch } from '../utils/testing/fetch'
|
||||||
|
|
||||||
describe('Changelog', () => {
|
describe('Changelog', () => {
|
||||||
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
let testAccessToken: AccessTokenOutput
|
||||||
let testChangelog: z.infer<typeof ChangelogOutput>
|
let testChangelog: z.infer<typeof ChangelogOutput>
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const createdUser = await db
|
await db
|
||||||
.insert(user)
|
.insert(user)
|
||||||
.values({ email: 'changelog@test.local', providerId: 'test_000' })
|
.values({ email: 'changelog@test.local', id: 'test_000' })
|
||||||
.returning()
|
|
||||||
const tAccessToken = await db
|
const tAccessToken = await db
|
||||||
.insert(access_token)
|
.insert(access_token)
|
||||||
.values({
|
.values({ token: 'test123', userId: 'test_000', name: 'testtoken' })
|
||||||
token: 'test123',
|
|
||||||
userId: createdUser[0].id,
|
|
||||||
name: 'testtoken',
|
|
||||||
})
|
|
||||||
.returning()
|
.returning()
|
||||||
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
testAccessToken = tAccessToken[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -40,8 +35,6 @@ describe('Changelog', () => {
|
|||||||
const payload: z.infer<typeof ChangelogCreateInput> = {
|
const payload: z.infer<typeof ChangelogCreateInput> = {
|
||||||
title: 'changelog',
|
title: 'changelog',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
isSemver: true,
|
|
||||||
isConventional: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@ -50,10 +43,10 @@ describe('Changelog', () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: payload,
|
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
|
testChangelog = json
|
||||||
|
|
||||||
expect(res.status).toBe(201)
|
expect(res.status).toBe(201)
|
||||||
@ -68,7 +61,7 @@ describe('Changelog', () => {
|
|||||||
path: `/v1/changelog/${testChangelog.id}`,
|
path: `/v1/changelog/${testChangelog.id}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
@ -80,7 +73,7 @@ describe('Changelog', () => {
|
|||||||
path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(404)
|
expect(res.status).toBe(404)
|
||||||
@ -92,12 +85,12 @@ describe('Changelog', () => {
|
|||||||
path: '/v1/changelog/some',
|
path: '/v1/changelog/some',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(400)
|
expect(res.status).toBe(400)
|
||||||
|
|
||||||
const json = (await res.json()) as { success: boolean }
|
const json = await res.json()
|
||||||
expect(json.success).toBeFalse()
|
expect(json.success).toBeFalse()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -109,12 +102,12 @@ describe('Changelog', () => {
|
|||||||
path: '/v1/changelog',
|
path: '/v1/changelog',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(200)
|
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)
|
expect(json).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -126,7 +119,7 @@ describe('Changelog', () => {
|
|||||||
path: `/v1/changelog/${testChangelog.id}`,
|
path: `/v1/changelog/${testChangelog.id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
@ -138,7 +131,7 @@ describe('Changelog', () => {
|
|||||||
path: `/v1/changelog/${testChangelog.id}`,
|
path: `/v1/changelog/${testChangelog.id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
testAccessToken.token as string,
|
testAccessToken.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.status).toBe(404)
|
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'
|
} from '@boring.tools/schema'
|
||||||
import { createRoute, type z } from '@hono/zod-openapi'
|
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({
|
export const route = createRoute({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/',
|
path: '/',
|
||||||
tags: ['changelog'],
|
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
@ -27,24 +22,32 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return created changelog',
|
description: 'Return created changelog',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerChangelogCreate = (api: typeof changelogApi) => {
|
export const func = async ({
|
||||||
return api.openapi(route, async (c) => {
|
userId,
|
||||||
const userId = await verifyAuthentication(c)
|
payload,
|
||||||
const payload: z.infer<typeof ChangelogCreateInput> = await c.req.json()
|
}: {
|
||||||
|
userId: string
|
||||||
const [result] = await db
|
payload: z.infer<typeof ChangelogCreateInput>
|
||||||
|
}) => {
|
||||||
|
return await db
|
||||||
.insert(changelog)
|
.insert(changelog)
|
||||||
.values({
|
.values({
|
||||||
...payload,
|
...payload,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
}
|
||||||
return c.json(ChangelogCreateOutput.parse(result), 201)
|
|
||||||
})
|
export default {
|
||||||
|
route,
|
||||||
|
func,
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,9 @@ import { createRoute } from '@hono/zod-openapi'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
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({
|
export const route = createRoute({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
tags: ['changelog'],
|
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@ -21,17 +16,17 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Removes a changelog by id',
|
description: 'Removes a changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerChangelogDelete = async (api: typeof changelogApi) => {
|
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||||
return api.openapi(route, async (c) => {
|
const result = await db
|
||||||
const userId = await verifyAuthentication(c)
|
|
||||||
const id = c.req.param('id')
|
|
||||||
|
|
||||||
const [result] = await db
|
|
||||||
.delete(changelog)
|
.delete(changelog)
|
||||||
.where(and(eq(changelog.userId, userId), eq(changelog.id, id)))
|
.where(and(eq(changelog.userId, userId), eq(changelog.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
@ -40,6 +35,10 @@ export const registerChangelogDelete = async (api: typeof changelogApi) => {
|
|||||||
throw new HTTPException(404, { message: 'Not found' })
|
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 { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import { cors } from 'hono/cors'
|
|
||||||
import type { Variables } from '..'
|
import type { Variables } from '..'
|
||||||
import type { ContextModule } from '../utils/sentry'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { registerChangelogById } from './byId'
|
import { type ContextModule, captureSentry } from '../utils/sentry'
|
||||||
import { changelogCommitApi } from './commit'
|
import ById from './byId'
|
||||||
import { registerChangelogCreate } from './create'
|
import Create from './create'
|
||||||
import { registerChangelogDelete } from './delete'
|
import Delete from './delete'
|
||||||
import { registerChangelogList } from './list'
|
import List from './list'
|
||||||
import { registerChangelogUpdate } from './update'
|
import Update from './update'
|
||||||
import version from './version'
|
|
||||||
|
|
||||||
export const changelogApi = new OpenAPIHono<{ Variables: Variables }>()
|
const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
|
|
||||||
const module: ContextModule = {
|
const module: ContextModule = {
|
||||||
name: 'changelog',
|
name: 'changelog',
|
||||||
}
|
}
|
||||||
changelogApi.use('*', cors())
|
|
||||||
changelogApi.route('/commit', changelogCommitApi)
|
|
||||||
changelogApi.route('/version', version)
|
|
||||||
|
|
||||||
registerChangelogById(changelogApi)
|
app.openapi(ById.route, async (c) => {
|
||||||
registerChangelogCreate(changelogApi)
|
const userId = verifyAuthentication(c)
|
||||||
registerChangelogDelete(changelogApi)
|
try {
|
||||||
registerChangelogUpdate(changelogApi)
|
const id = c.req.param('id')
|
||||||
registerChangelogList(changelogApi)
|
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 { createRoute } from '@hono/zod-openapi'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
import type { changelogApi } from '.'
|
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
|
|
||||||
export const route = createRoute({
|
export const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/',
|
path: '/',
|
||||||
tags: ['changelog'],
|
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@ -20,15 +15,16 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelogs for current user',
|
description: 'Return changelogs for current user',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerChangelogList = (api: typeof changelogApi) => {
|
export const func = async ({ userId }: { userId: string }) => {
|
||||||
return api.openapi(route, async (c) => {
|
|
||||||
const userId = await verifyAuthentication(c)
|
|
||||||
|
|
||||||
const result = await db.query.changelog.findMany({
|
const result = await db.query.changelog.findMany({
|
||||||
where: eq(changelog.userId, userId),
|
where: eq(changelog.userId, userId),
|
||||||
with: {
|
with: {
|
||||||
@ -40,7 +36,7 @@ export const registerChangelogList = (api: typeof changelogApi) => {
|
|||||||
orderBy: (changelog, { asc }) => [asc(changelog.createdAt)],
|
orderBy: (changelog, { asc }) => [asc(changelog.createdAt)],
|
||||||
})
|
})
|
||||||
|
|
||||||
const mappedData = result.map((changelog) => {
|
return result.map((changelog) => {
|
||||||
const { versions, commits, ...rest } = changelog
|
const { versions, commits, ...rest } = changelog
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
@ -50,7 +46,9 @@ export const registerChangelogList = (api: typeof changelogApi) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return c.json(ChangelogListOutput.parse(mappedData), 200)
|
|
||||||
})
|
export default {
|
||||||
|
route,
|
||||||
|
func,
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,6 @@ import { createRoute, type z } from '@hono/zod-openapi'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
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({
|
export const route = createRoute({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
@ -30,17 +25,24 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return updated changelog',
|
description: 'Return updated changelog',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerChangelogUpdate = (api: typeof changelogApi) => {
|
export const func = async ({
|
||||||
return api.openapi(route, async (c) => {
|
userId,
|
||||||
const userId = await verifyAuthentication(c)
|
payload,
|
||||||
const id = c.req.param('id')
|
id,
|
||||||
const payload: z.infer<typeof ChangelogUpdateInput> = await c.req.json()
|
}: {
|
||||||
|
userId: string
|
||||||
|
payload: z.infer<typeof ChangelogUpdateInput>
|
||||||
|
id: string
|
||||||
|
}) => {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.update(changelog)
|
.update(changelog)
|
||||||
.set({
|
.set({
|
||||||
@ -53,9 +55,10 @@ export const registerChangelogUpdate = (api: typeof changelogApi) => {
|
|||||||
throw new HTTPException(404, { message: 'Not found' })
|
throw new HTTPException(404, { message: 'Not found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.pageId) {
|
return result
|
||||||
redis.del(result.pageId)
|
}
|
||||||
}
|
|
||||||
return c.json(ChangelogUpdateOutput.parse(result), 200)
|
export default {
|
||||||
})
|
route,
|
||||||
|
func,
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,11 @@
|
|||||||
import {
|
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||||
changelog,
|
|
||||||
changelog_commit,
|
|
||||||
changelog_version,
|
|
||||||
db,
|
|
||||||
} from '@boring.tools/database'
|
|
||||||
import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
|
import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { and, desc, eq } 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'
|
|
||||||
|
|
||||||
export const byId = createRoute({
|
export const byId = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
tags: ['version'],
|
|
||||||
request: {
|
request: {
|
||||||
params: VersionByIdParams,
|
params: VersionByIdParams,
|
||||||
},
|
},
|
||||||
@ -29,31 +18,35 @@ export const byId = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return version by id',
|
description: 'Return version by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerVersionById = (api: typeof changelogVersionApi) => {
|
export const byIdFunc = async ({
|
||||||
return api.openapi(byId, async (c) => {
|
userId,
|
||||||
const userId = await verifyAuthentication(c)
|
id,
|
||||||
const { id } = c.req.valid('param')
|
}: {
|
||||||
|
userId: string
|
||||||
|
id: string
|
||||||
|
}) => {
|
||||||
const versionResult = await db.query.changelog_version.findFirst({
|
const versionResult = await db.query.changelog_version.findFirst({
|
||||||
where: eq(changelog_version.id, id),
|
where: eq(changelog_version.id, id),
|
||||||
with: {
|
with: {
|
||||||
commits: {
|
commits: true,
|
||||||
orderBy: () => desc(changelog_commit.createdAt),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!versionResult) {
|
if (!versionResult) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!versionResult.changelogId) {
|
if (!versionResult.changelogId) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const changelogResult = await db.query.changelog.findMany({
|
const changelogResult = await db.query.changelog.findMany({
|
||||||
@ -66,9 +59,8 @@ export const registerVersionById = (api: typeof changelogVersionApi) => {
|
|||||||
const changelogIds = changelogResult.map((cl) => cl.id)
|
const changelogIds = changelogResult.map((cl) => cl.id)
|
||||||
|
|
||||||
if (!changelogIds.includes(versionResult.changelogId)) {
|
if (!changelogIds.includes(versionResult.changelogId)) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(VersionOutput.parse(versionResult), 200)
|
return versionResult
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
import {
|
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||||
changelog,
|
|
||||||
changelog_commit,
|
|
||||||
changelog_version,
|
|
||||||
db,
|
|
||||||
} from '@boring.tools/database'
|
|
||||||
import { VersionCreateInput, VersionCreateOutput } from '@boring.tools/schema'
|
import { VersionCreateInput, VersionCreateOutput } from '@boring.tools/schema'
|
||||||
import { createRoute, type z } from '@hono/zod-openapi'
|
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 { HTTPException } from 'hono/http-exception'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
|
||||||
import type changelogVersionApi from '.'
|
export const create = createRoute({
|
||||||
import { verifyAuthentication } from '../../utils/authentication'
|
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
|
||||||
import { redis } from '../../utils/redis'
|
|
||||||
|
|
||||||
export const route = createRoute({
|
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/',
|
path: '/',
|
||||||
tags: ['version'],
|
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
@ -33,52 +22,58 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Version created',
|
description: 'Version created',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerVersionCreate = (api: typeof changelogVersionApi) => {
|
export const createFunc = async ({
|
||||||
return api.openapi(route, async (c) => {
|
userId,
|
||||||
const userId = await verifyAuthentication(c)
|
payload,
|
||||||
const payload: z.infer<typeof VersionCreateInput> = await c.req.json()
|
}: {
|
||||||
|
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({
|
const changelogResult = await db.query.changelog.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(changelog.userId, userId),
|
eq(changelog.userId, userId),
|
||||||
eq(changelog.id, payload.changelogId),
|
eq(changelog.id, payload.changelogId),
|
||||||
),
|
),
|
||||||
with: {
|
|
||||||
versions: {
|
|
||||||
where: and(
|
|
||||||
eq(changelog_version.changelogId, payload.changelogId),
|
|
||||||
eq(changelog_version.version, payload.version),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!changelogResult) {
|
if (!changelogResult) {
|
||||||
throw new HTTPException(404, {
|
throw new HTTPException(404, {
|
||||||
message: 'Changelog not found',
|
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) {
|
if (validVersion === null) {
|
||||||
throw new HTTPException(409, {
|
throw new HTTPException(409, {
|
||||||
message: 'Version is not semver compatible',
|
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
|
const [versionCreateResult] = await db
|
||||||
.insert(changelog_version)
|
.insert(changelog_version)
|
||||||
.values({
|
.values({
|
||||||
@ -89,15 +84,5 @@ export const registerVersionCreate = (api: typeof changelogVersionApi) => {
|
|||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (changelogResult.pageId) {
|
return versionCreateResult
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||||
changelog,
|
|
||||||
changelog_commit,
|
|
||||||
changelog_version,
|
|
||||||
db,
|
|
||||||
} from '@boring.tools/database'
|
|
||||||
import { GeneralOutput } from '@boring.tools/schema'
|
import { GeneralOutput } from '@boring.tools/schema'
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
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',
|
method: 'delete',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
tags: ['version'],
|
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@ -26,18 +16,27 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Removes a version by id',
|
description: 'Removes a version by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
404: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: GeneralOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Version not found',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerVersionDelete = async (
|
export const removeFunc = async ({
|
||||||
api: typeof changelogVersionApi,
|
userId,
|
||||||
) => {
|
id,
|
||||||
return api.openapi(route, async (c) => {
|
}: {
|
||||||
const userId = await verifyAuthentication(c)
|
userId: string
|
||||||
const id = c.req.param('id')
|
id: string
|
||||||
|
}) => {
|
||||||
const changelogResult = await db.query.changelog.findMany({
|
const changelogResult = await db.query.changelog.findMany({
|
||||||
where: and(eq(changelog.userId, userId)),
|
where: and(eq(changelog.userId, userId)),
|
||||||
with: {
|
with: {
|
||||||
@ -47,10 +46,6 @@ export const registerVersionDelete = async (
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!changelogResult) {
|
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const findChangelog = changelogResult.find((change) =>
|
const findChangelog = changelogResult.find((change) =>
|
||||||
change.versions.find((ver) => ver.id === id),
|
change.versions.find((ver) => ver.id === id),
|
||||||
)
|
)
|
||||||
@ -61,20 +56,8 @@ export const registerVersionDelete = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
return 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)
|
.delete(changelog_version)
|
||||||
.where(and(eq(changelog_version.id, id)))
|
.where(and(eq(changelog_version.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
return c.json(GeneralOutput.parse({ message: 'Version deleted' }), 200)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,126 @@
|
|||||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import type { Variables } from '../..'
|
import type { Variables } from '../..'
|
||||||
import type { ContextModule } from '../../utils/sentry'
|
import { verifyAuthentication } from '../../utils/authentication'
|
||||||
import { registerVersionById } from './byId'
|
import { type ContextModule, captureSentry } from '../../utils/sentry'
|
||||||
import { registerVersionCreate } from './create'
|
import { byId, byIdFunc } from './byId'
|
||||||
import { registerVersionCreateAuto } from './createAuto'
|
import { create, createFunc } from './create'
|
||||||
import { registerVersionDelete } from './delete'
|
import { remove, removeFunc } from './delete'
|
||||||
import { registerVersionUpdate } from './update'
|
import { update, updateFunc } from './update'
|
||||||
|
|
||||||
export const changelogVersionApi = new OpenAPIHono<{ Variables: Variables }>()
|
const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
|
|
||||||
const module: ContextModule = {
|
const module: ContextModule = {
|
||||||
name: 'changelog',
|
name: 'changelog',
|
||||||
sub_module: 'version',
|
sub_module: 'version',
|
||||||
}
|
}
|
||||||
|
|
||||||
registerVersionCreateAuto(changelogVersionApi)
|
app.openapi(create, async (c) => {
|
||||||
registerVersionById(changelogVersionApi)
|
const userId = verifyAuthentication(c)
|
||||||
registerVersionCreate(changelogVersionApi)
|
try {
|
||||||
registerVersionDelete(changelogVersionApi)
|
const payload = await c.req.json()
|
||||||
registerVersionUpdate(changelogVersionApi)
|
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 {
|
import { changelog, changelog_version, db } from '@boring.tools/database'
|
||||||
changelog,
|
|
||||||
changelog_commit,
|
|
||||||
changelog_version,
|
|
||||||
db,
|
|
||||||
} from '@boring.tools/database'
|
|
||||||
import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
|
import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
|
||||||
import { createRoute, type z } from '@hono/zod-openapi'
|
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 { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
import type changelogVersionApi from '.'
|
export const update = createRoute({
|
||||||
import { verifyAuthentication } from '../../utils/authentication'
|
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
|
|
||||||
import { redis } from '../../utils/redis'
|
|
||||||
|
|
||||||
export const route = createRoute({
|
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
tags: ['version'],
|
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
@ -32,17 +21,24 @@ export const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return updated version',
|
description: 'Return updated version',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
|
export const updateFunc = async ({
|
||||||
return api.openapi(route, async (c) => {
|
userId,
|
||||||
const userId = await verifyAuthentication(c)
|
payload,
|
||||||
const id = c.req.param('id')
|
id,
|
||||||
const payload: z.infer<typeof VersionUpdateInput> = await c.req.json()
|
}: {
|
||||||
|
userId: string
|
||||||
|
payload: z.infer<typeof VersionUpdateInput>
|
||||||
|
id: string
|
||||||
|
}) => {
|
||||||
const changelogResult = await db.query.changelog.findMany({
|
const changelogResult = await db.query.changelog.findMany({
|
||||||
where: and(eq(changelog.userId, userId)),
|
where: and(eq(changelog.userId, userId)),
|
||||||
with: {
|
with: {
|
||||||
@ -52,12 +48,6 @@ export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!changelogResult) {
|
|
||||||
throw new HTTPException(404, {
|
|
||||||
message: 'Version not found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const findChangelog = changelogResult.find((change) =>
|
const findChangelog = changelogResult.find((change) =>
|
||||||
change.versions.find((ver) => ver.id === id),
|
change.versions.find((ver) => ver.id === id),
|
||||||
)
|
)
|
||||||
@ -71,7 +61,6 @@ export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
|
|||||||
const [versionUpdateResult] = await db
|
const [versionUpdateResult] = await db
|
||||||
.update(changelog_version)
|
.update(changelog_version)
|
||||||
.set({
|
.set({
|
||||||
version: payload.version,
|
|
||||||
status: payload.status,
|
status: payload.status,
|
||||||
markdown: payload.markdown,
|
markdown: payload.markdown,
|
||||||
releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
|
releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
|
||||||
@ -79,22 +68,5 @@ export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
|
|||||||
.where(and(eq(changelog_version.id, id)))
|
.where(and(eq(changelog_version.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (payload.commitIds) {
|
return versionUpdateResult
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import type { UserOutput } from '@boring.tools/schema'
|
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 { OpenAPIHono, type z } from '@hono/zod-openapi'
|
||||||
import { apiReference } from '@scalar/hono-api-reference'
|
import { apiReference } from '@scalar/hono-api-reference'
|
||||||
import { cors } from 'hono/cors'
|
import { cors } from 'hono/cors'
|
||||||
import { requestId } from 'hono/request-id'
|
|
||||||
|
|
||||||
import changelog from './changelog'
|
import changelog from './changelog'
|
||||||
|
import version from './changelog/version'
|
||||||
|
import user from './user'
|
||||||
|
|
||||||
import { accessTokenApi } from './access-token'
|
|
||||||
import pageApi from './page'
|
import pageApi from './page'
|
||||||
import statisticApi from './statistic'
|
|
||||||
import userApi from './user'
|
|
||||||
import { authentication } from './utils/authentication'
|
import { authentication } from './utils/authentication'
|
||||||
import { handleError, handleZodError } from './utils/errors'
|
import { handleError, handleZodError } from './utils/errors'
|
||||||
import { logger } from './utils/logger'
|
|
||||||
import { startup } from './utils/startup'
|
import { startup } from './utils/startup'
|
||||||
|
|
||||||
type User = z.infer<typeof UserOutput>
|
type User = z.infer<typeof UserOutput>
|
||||||
@ -24,7 +21,6 @@ export type Variables = {
|
|||||||
|
|
||||||
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
export const app = new OpenAPIHono<{ Variables: Variables }>({
|
||||||
defaultHook: handleZodError,
|
defaultHook: handleZodError,
|
||||||
strict: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// app.use(
|
// app.use(
|
||||||
@ -33,26 +29,14 @@ export const app = new OpenAPIHono<{ Variables: Variables }>({
|
|||||||
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
||||||
// }),
|
// }),
|
||||||
// )
|
// )
|
||||||
|
|
||||||
app.onError(handleError)
|
app.onError(handleError)
|
||||||
app.use('*', cors())
|
app.use('*', cors())
|
||||||
app.use('/v1/*', authentication)
|
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', changelog)
|
||||||
|
app.route('/v1/changelog/version', version)
|
||||||
app.route('/v1/page', pageApi)
|
app.route('/v1/page', pageApi)
|
||||||
app.route('/v1/access-token', accessTokenApi)
|
|
||||||
app.route('/v1/statistic', statisticApi)
|
|
||||||
|
|
||||||
app.doc('/openapi.json', {
|
app.doc('/openapi.json', {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.0',
|
||||||
|
@ -5,7 +5,6 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
import type { pageApi } from './index'
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
@ -25,14 +24,18 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelog by id',
|
description: 'Return changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerPageById = (api: typeof pageApi) => {
|
export const registerPageById = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = verifyAuthentication(c)
|
||||||
const { id } = c.req.valid('param')
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
const result = await db.query.page.findFirst({
|
const result = await db.query.page.findFirst({
|
||||||
@ -57,6 +60,6 @@ export const registerPageById = (api: typeof pageApi) => {
|
|||||||
changelogs: changelogs.map((log) => log.changelog),
|
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 { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
import type { pageApi } from './index'
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
@ -20,7 +19,7 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
201: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: PageOutput,
|
schema: PageOutput,
|
||||||
@ -28,14 +27,18 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelog by id',
|
description: 'Return changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerPageCreate = (api: typeof pageApi) => {
|
export const registerPageCreate = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = verifyAuthentication(c)
|
||||||
|
|
||||||
const { changelogIds, ...rest }: z.infer<typeof PageCreateInput> =
|
const { changelogIds, ...rest }: z.infer<typeof PageCreateInput> =
|
||||||
await c.req.json()
|
await c.req.json()
|
||||||
@ -48,12 +51,8 @@ export const registerPageCreate = (api: typeof pageApi) => {
|
|||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement transaction
|
// TODO: implement transaction
|
||||||
if (changelogIds.length > 0) {
|
if (changelogIds) {
|
||||||
await db.insert(changelogs_to_pages).values(
|
await db.insert(changelogs_to_pages).values(
|
||||||
changelogIds.map((changelogId) => ({
|
changelogIds.map((changelogId) => ({
|
||||||
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 { createRoute } from '@hono/zod-openapi'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
import type { pageApi } from '.'
|
import type { pageApi } from '.'
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
tags: ['page'],
|
|
||||||
path: '/:id',
|
path: '/:id',
|
||||||
request: {
|
request: {
|
||||||
params: PageByIdParams,
|
params: PageByIdParams,
|
||||||
@ -25,25 +22,27 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Removes a changelog by id',
|
description: 'Removes a changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerPageDelete = (api: typeof pageApi) => {
|
export const registerPageDelete = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = verifyAuthentication(c)
|
||||||
const { id } = c.req.valid('param')
|
const { id } = c.req.valid('param')
|
||||||
|
const result = await db
|
||||||
const [result] = await db
|
|
||||||
.delete(page)
|
.delete(page)
|
||||||
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
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 { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import { timing } from 'hono/timing'
|
|
||||||
import type { Variables } from '..'
|
import type { Variables } from '..'
|
||||||
import { handleZodError } from '../utils/errors'
|
|
||||||
import type { ContextModule } from '../utils/sentry'
|
import type { ContextModule } from '../utils/sentry'
|
||||||
import { registerPageById } from './byId'
|
import { registerPageById } from './byId'
|
||||||
import { registerPageCreate } from './create'
|
import { registerPageCreate } from './create'
|
||||||
@ -10,10 +8,8 @@ import { registerPageList } from './list'
|
|||||||
import { registerPagePublic } from './public'
|
import { registerPagePublic } from './public'
|
||||||
import { registerPageUpdate } from './update'
|
import { registerPageUpdate } from './update'
|
||||||
|
|
||||||
export const pageApi = new OpenAPIHono<{ Variables: Variables }>({
|
export const pageApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
defaultHook: handleZodError,
|
|
||||||
})
|
|
||||||
pageApi.use('*', timing())
|
|
||||||
const module: ContextModule = {
|
const module: ContextModule = {
|
||||||
name: 'page',
|
name: 'page',
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import { db, page } from '@boring.tools/database'
|
import { db, page } from '@boring.tools/database'
|
||||||
import { PageListOutput } from '@boring.tools/schema'
|
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { and, desc, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
|
||||||
|
|
||||||
|
import { PageListOutput } from '@boring.tools/schema'
|
||||||
|
import { HTTPException } from 'hono/http-exception'
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
import type { pageApi } from './index'
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
tags: ['page'],
|
tags: ['page'],
|
||||||
description: 'Get a list of pages',
|
description: 'Get a page list',
|
||||||
path: '/',
|
path: '/',
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
@ -22,24 +21,27 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelog by id',
|
description: 'Return changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerPageList = (api: typeof pageApi) => {
|
export const registerPageList = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = verifyAuthentication(c)
|
||||||
|
|
||||||
const result = await db.query.page.findMany({
|
const result = await db.query.page.findMany({
|
||||||
where: and(eq(page.userId, userId)),
|
where: and(eq(page.userId, userId)),
|
||||||
orderBy: () => desc(page.createdAt),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
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 { changelog_version, db, page } from '@boring.tools/database'
|
||||||
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
|
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
|
||||||
import { endTime, startTime } from 'hono/timing'
|
|
||||||
|
|
||||||
import { openApiErrorResponses } from '../utils/openapi'
|
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
|
||||||
import { redis } from '../utils/redis'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
import type { pageApi } from './index'
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
tags: ['page'],
|
tags: ['page'],
|
||||||
description: 'Get a page by id for public view',
|
description: 'Get a page',
|
||||||
path: '/:id/public',
|
path: '/:id/public',
|
||||||
request: {
|
request: {
|
||||||
params: PagePublicParams,
|
params: PagePublicParams,
|
||||||
@ -24,24 +21,20 @@ const route = createRoute({
|
|||||||
schema: PagePublicOutput,
|
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) => {
|
export const registerPagePublic = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const { id } = c.req.valid('param')
|
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({
|
const result = await db.query.page.findFirst({
|
||||||
where: eq(page.id, id),
|
where: eq(page.id, id),
|
||||||
@ -77,8 +70,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
endTime(c, 'database')
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
throw new HTTPException(404, { message: 'Not Found' })
|
||||||
}
|
}
|
||||||
@ -90,8 +81,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
|||||||
changelogs: changelogs.map((log) => log.changelog),
|
changelogs: changelogs.map((log) => log.changelog),
|
||||||
}
|
}
|
||||||
|
|
||||||
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
return c.json(mappedResult, 200)
|
||||||
const asd = PagePublicOutput.parse(mappedResult)
|
|
||||||
return c.json(asd, 200)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,6 @@ import {
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
import { redis } from '../utils/redis'
|
|
||||||
import type { pageApi } from './index'
|
import type { pageApi } from './index'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
@ -35,14 +33,18 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return changelog by id',
|
description: 'Return changelog by id',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerPageUpdate = (api: typeof pageApi) => {
|
export const registerPageUpdate = (api: typeof pageApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = verifyAuthentication(c)
|
||||||
const { id } = c.req.valid('param')
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
const { changelogIds, ...rest }: z.infer<typeof PageUpdateInput> =
|
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)))
|
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new HTTPException(404, { message: 'Not Found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement transaction
|
// TODO: implement transaction
|
||||||
if (changelogIds) {
|
if (changelogIds) {
|
||||||
if (changelogIds.length === 0) {
|
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 { UserOutput } from '@boring.tools/schema'
|
||||||
import { createRoute } from '@hono/zod-openapi'
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
import type { userApi } from '.'
|
import type { userApi } from '.'
|
||||||
import { verifyAuthentication } from '../utils/authentication'
|
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -18,22 +15,26 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
description: 'Return user',
|
description: 'Return user',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const registerUserGet = (api: typeof userApi) => {
|
export const registerUserGet = (api: typeof userApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const userId = await verifyAuthentication(c)
|
const user = c.get('user')
|
||||||
const result = await db.query.user.findFirst({
|
const result = await db.query.user.findFirst({
|
||||||
where: eq(userDb.id, userId),
|
where: eq(userDb.id, user.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('User not found')
|
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 { createRoute, type z } from '@hono/zod-openapi'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
import { Webhook } from 'svix'
|
import { Webhook } from 'svix'
|
||||||
|
|
||||||
import type userApi from '.'
|
import type userApi from '.'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -20,12 +18,19 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
204: {
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': { schema: UserOutput },
|
||||||
|
},
|
||||||
description: 'Return success',
|
description: 'Return success',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...openApiSecurity,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const userCreate = async ({
|
const userCreate = async ({
|
||||||
@ -34,7 +39,7 @@ const userCreate = async ({
|
|||||||
payload: z.infer<typeof UserWebhookInput>
|
payload: z.infer<typeof UserWebhookInput>
|
||||||
}) => {
|
}) => {
|
||||||
const data = {
|
const data = {
|
||||||
providerId: payload.data.id,
|
id: payload.data.id,
|
||||||
name: `${payload.data.first_name} ${payload.data.last_name}`,
|
name: `${payload.data.first_name} ${payload.data.last_name}`,
|
||||||
email: payload.data.email_addresses[0].email_address,
|
email: payload.data.email_addresses[0].email_address,
|
||||||
}
|
}
|
||||||
@ -45,7 +50,7 @@ const userCreate = async ({
|
|||||||
...data,
|
...data,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: user.providerId,
|
target: user.id,
|
||||||
set: data,
|
set: data,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -67,11 +72,7 @@ export const registerUserWebhook = (api: typeof userApi) => {
|
|||||||
case 'user.created': {
|
case 'user.created': {
|
||||||
const result = await userCreate({ payload: verifiedPayload })
|
const result = await userCreate({ payload: verifiedPayload })
|
||||||
logger.info('Clerk Webhook', result)
|
logger.info('Clerk Webhook', result)
|
||||||
if (result) {
|
return c.json(result, 200)
|
||||||
return c.json({}, 204)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({}, 404)
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { access_token, db, user } from '@boring.tools/database'
|
import { access_token, db } from '@boring.tools/database'
|
||||||
import { logger } from '@boring.tools/logger'
|
|
||||||
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
|
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { Context, Next } from 'hono'
|
import type { Context, Next } from 'hono'
|
||||||
@ -25,11 +24,6 @@ const generatedToken = async (c: Context, next: Next) => {
|
|||||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
|
||||||
.update(access_token)
|
|
||||||
.set({ lastUsedOn: new Date() })
|
|
||||||
.where(eq(access_token.id, accessTokenResult.id))
|
|
||||||
|
|
||||||
c.set('user', accessTokenResult.user)
|
c.set('user', accessTokenResult.user)
|
||||||
|
|
||||||
await next()
|
await next()
|
||||||
@ -37,7 +31,7 @@ const generatedToken = async (c: Context, next: Next) => {
|
|||||||
|
|
||||||
export const authentication = some(generatedToken, clerkMiddleware())
|
export const authentication = some(generatedToken, clerkMiddleware())
|
||||||
|
|
||||||
export const verifyAuthentication = async (c: Context) => {
|
export const verifyAuthentication = (c: Context) => {
|
||||||
const auth = getAuth(c)
|
const auth = getAuth(c)
|
||||||
if (!auth?.userId) {
|
if (!auth?.userId) {
|
||||||
const accessTokenUser = c.get('user')
|
const accessTokenUser = c.get('user')
|
||||||
@ -46,16 +40,5 @@ export const verifyAuthentication = async (c: Context) => {
|
|||||||
}
|
}
|
||||||
return accessTokenUser.id
|
return accessTokenUser.id
|
||||||
}
|
}
|
||||||
|
return auth.userId
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -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' {
|
declare module 'bun' {
|
||||||
interface Env {
|
interface Env {
|
||||||
POSTGRES_URL: string
|
POSTGRES_URL: string
|
||||||
REDIS_PASSWORD: string
|
|
||||||
REDIS_URL: string
|
|
||||||
CLERK_WEBHOOK_SECRET: string
|
CLERK_WEBHOOK_SECRET: string
|
||||||
CLERK_SECRET_KEY: string
|
CLERK_SECRET_KEY: string
|
||||||
CLERK_PUBLISHABLE_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 = [
|
const DEVELOPMENT_VARIABLES = [
|
||||||
...TEST_VARIABLES,
|
...TEST_VARIABLES,
|
||||||
|
@ -20,14 +20,12 @@
|
|||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-router": "^1.58.15",
|
"@tanstack/react-router": "^1.58.15",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usehooks-ts": "^3.1.0",
|
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 (
|
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" />
|
<TriangleAlertIcon className="h-4 w-4" />
|
||||||
<AlertTitle>Danger Zone</AlertTitle>
|
<AlertTitle>Danger Zone</AlertTitle>
|
||||||
<AlertDescription className="inline-flex flex-col gap-3">
|
<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,
|
Separator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { useAuth } from '@clerk/clerk-react'
|
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import { useUser } from '../hooks/useUser'
|
|
||||||
|
|
||||||
type Breadcrumbs = {
|
type Breadcrumbs = {
|
||||||
name: string
|
name: string
|
||||||
@ -23,15 +19,6 @@ export const PageWrapper = ({
|
|||||||
children,
|
children,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
}: { children: React.ReactNode; breadcrumbs?: Breadcrumbs[] }) => {
|
}: { children: React.ReactNode; breadcrumbs?: Breadcrumbs[] }) => {
|
||||||
const { error } = useUser()
|
|
||||||
const { signOut } = useAuth()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
signOut()
|
|
||||||
}
|
|
||||||
}, [error, signOut])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||||
|
@ -34,8 +34,10 @@ export function Sidebar() {
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
|
<SidebarMenu>
|
||||||
<SidebarChangelog />
|
<SidebarChangelog />
|
||||||
<SidebarPage />
|
<SidebarPage />
|
||||||
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -10,27 +9,15 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@boring.tools/ui'
|
} 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 { ChevronRightIcon, FileStackIcon, PlusIcon } from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useLocalStorage } from 'usehooks-ts'
|
|
||||||
import { useChangelogList } from '../hooks/useChangelog'
|
import { useChangelogList } from '../hooks/useChangelog'
|
||||||
|
|
||||||
export const SidebarChangelog = () => {
|
export const SidebarChangelog = () => {
|
||||||
const location = useLocation()
|
const { data, error } = useChangelogList()
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<Collapsible asChild>
|
||||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Changelog">
|
<SidebarMenuButton asChild tooltip="Changelog">
|
||||||
<Link
|
<Link
|
||||||
@ -50,16 +37,6 @@ export const SidebarChangelog = () => {
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<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 &&
|
{!error &&
|
||||||
data?.map((changelog) => (
|
data?.map((changelog) => (
|
||||||
<SidebarMenuSubItem key={changelog.id}>
|
<SidebarMenuSubItem key={changelog.id}>
|
||||||
@ -95,6 +72,5 @@ export const SidebarChangelog = () => {
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</SidebarMenu>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -10,27 +9,15 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@boring.tools/ui'
|
} 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 { ChevronRightIcon, NotebookTextIcon, PlusIcon } from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useLocalStorage } from 'usehooks-ts'
|
|
||||||
import { usePageList } from '../hooks/usePage'
|
import { usePageList } from '../hooks/usePage'
|
||||||
|
|
||||||
export const SidebarPage = () => {
|
export const SidebarPage = () => {
|
||||||
const location = useLocation()
|
const { data, error } = usePageList()
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<Collapsible asChild>
|
||||||
<Collapsible asChild open={value} onOpenChange={() => setValue(!value)}>
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Page">
|
<SidebarMenuButton asChild tooltip="Page">
|
||||||
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
<Link to="/page" activeProps={{ className: 'bg-sidebar-accent' }}>
|
||||||
@ -47,16 +34,6 @@ export const SidebarPage = () => {
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<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 &&
|
{!error &&
|
||||||
data?.map((page) => (
|
data?.map((page) => (
|
||||||
<SidebarMenuSubItem key={page.id}>
|
<SidebarMenuSubItem key={page.id}>
|
||||||
@ -92,6 +69,5 @@ export const SidebarPage = () => {
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</SidebarMenu>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChevronsUpDown, KeyRoundIcon, TerminalIcon } from 'lucide-react'
|
import { ChevronsUpDown } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarSeparator,
|
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
import { SignOutButton, useUser } from '@clerk/clerk-react'
|
||||||
@ -26,26 +25,6 @@ export function SidebarUser() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild tooltip="Access Tokens">
|
|
||||||
<Link
|
|
||||||
to="/access-tokens"
|
|
||||||
activeProps={{ className: 'bg-sidebar-accent' }}
|
|
||||||
>
|
|
||||||
<KeyRoundIcon />
|
|
||||||
<span>Access Tokens</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild tooltip="Access Tokens">
|
|
||||||
<Link to="/cli" activeProps={{ className: 'bg-sidebar-accent' }}>
|
|
||||||
<TerminalIcon />
|
|
||||||
<span>CLI</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
<SidebarSeparator />
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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,
|
ChangelogCreateInput,
|
||||||
ChangelogOutput,
|
ChangelogOutput,
|
||||||
ChangelogUpdateInput,
|
ChangelogUpdateInput,
|
||||||
CommitOutput,
|
|
||||||
VersionCreateAutoInput,
|
|
||||||
VersionCreateInput,
|
VersionCreateInput,
|
||||||
VersionOutput,
|
VersionOutput,
|
||||||
VersionUpdateInput,
|
VersionUpdateInput,
|
||||||
@ -19,11 +17,8 @@ type ChangelogUpdate = z.infer<typeof ChangelogUpdateInput>
|
|||||||
|
|
||||||
type Version = z.infer<typeof VersionOutput>
|
type Version = z.infer<typeof VersionOutput>
|
||||||
type VersionCreate = z.infer<typeof VersionCreateInput>
|
type VersionCreate = z.infer<typeof VersionCreateInput>
|
||||||
type VersionCreateAuto = z.infer<typeof VersionCreateAutoInput>
|
|
||||||
type VersionUpdate = z.infer<typeof VersionUpdateInput>
|
type VersionUpdate = z.infer<typeof VersionUpdateInput>
|
||||||
|
|
||||||
type Commit = z.infer<typeof CommitOutput>
|
|
||||||
|
|
||||||
export const useChangelogList = () => {
|
export const useChangelogList = () => {
|
||||||
const { getToken } = useAuth()
|
const { getToken } = useAuth()
|
||||||
return useQuery({
|
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 }) => {
|
export const useChangelogById = ({ id }: { id: string }) => {
|
||||||
const { getToken } = useAuth()
|
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 }) => {
|
export const useChangelogVersionById = ({ id }: { id: string }) => {
|
||||||
const { getToken } = useAuth()
|
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 */
|
/* eslint-disable */
|
||||||
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
// This file was automatically generated by TanStack Router.
|
// This file is auto-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.
|
|
||||||
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
@ -17,17 +17,14 @@ import { Route as ChangelogIdVersionVersionIdImport } from './routes/changelog.$
|
|||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
const CliLazyImport = createFileRoute('/cli')()
|
|
||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||||
const PageIndexLazyImport = createFileRoute('/page/')()
|
const PageIndexLazyImport = createFileRoute('/page/')()
|
||||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||||
const AccessTokensIndexLazyImport = createFileRoute('/access-tokens/')()
|
|
||||||
const PageCreateLazyImport = createFileRoute('/page/create')()
|
const PageCreateLazyImport = createFileRoute('/page/create')()
|
||||||
const PageIdLazyImport = createFileRoute('/page/$id')()
|
const PageIdLazyImport = createFileRoute('/page/$id')()
|
||||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||||
const AccessTokensNewLazyImport = createFileRoute('/access-tokens/new')()
|
|
||||||
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
const PageIdIndexLazyImport = createFileRoute('/page/$id/')()
|
||||||
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
const ChangelogIdIndexLazyImport = createFileRoute('/changelog/$id/')()
|
||||||
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
const PageIdEditLazyImport = createFileRoute('/page/$id/edit')()
|
||||||
@ -38,60 +35,39 @@ const ChangelogIdEditLazyImport = createFileRoute('/changelog/$id/edit')()
|
|||||||
|
|
||||||
// Create/Update Routes
|
// 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({
|
const IndexLazyRoute = IndexLazyImport.update({
|
||||||
id: '/',
|
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const UserIndexLazyRoute = UserIndexLazyImport.update({
|
const UserIndexLazyRoute = UserIndexLazyImport.update({
|
||||||
id: '/user/',
|
|
||||||
path: '/user/',
|
path: '/user/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
||||||
id: '/page/',
|
|
||||||
path: '/page/',
|
path: '/page/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||||
id: '/changelog/',
|
|
||||||
path: '/changelog/',
|
path: '/changelog/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
import('./routes/changelog.index.lazy').then((d) => d.Route),
|
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({
|
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||||
id: '/page/create',
|
|
||||||
path: '/page/create',
|
path: '/page/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const PageIdLazyRoute = PageIdLazyImport.update({
|
const PageIdLazyRoute = PageIdLazyImport.update({
|
||||||
id: '/page/$id',
|
|
||||||
path: '/page/$id',
|
path: '/page/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||||
id: '/changelog/create',
|
|
||||||
path: '/changelog/create',
|
path: '/changelog/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -99,21 +75,11 @@ const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||||
id: '/changelog/$id',
|
|
||||||
path: '/changelog/$id',
|
path: '/changelog/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
|
||||||
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({
|
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||||
id: '/',
|
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -121,7 +87,6 @@ const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
||||||
id: '/',
|
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -129,14 +94,12 @@ const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
||||||
id: '/edit',
|
|
||||||
path: '/edit',
|
path: '/edit',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIdVersionCreateLazyRoute =
|
const ChangelogIdVersionCreateLazyRoute =
|
||||||
ChangelogIdVersionCreateLazyImport.update({
|
ChangelogIdVersionCreateLazyImport.update({
|
||||||
id: '/versionCreate',
|
|
||||||
path: '/versionCreate',
|
path: '/versionCreate',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -144,7 +107,6 @@ const ChangelogIdVersionCreateLazyRoute =
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
||||||
id: '/edit',
|
|
||||||
path: '/edit',
|
path: '/edit',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -153,7 +115,6 @@ const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
|||||||
|
|
||||||
const ChangelogIdVersionVersionIdRoute =
|
const ChangelogIdVersionVersionIdRoute =
|
||||||
ChangelogIdVersionVersionIdImport.update({
|
ChangelogIdVersionVersionIdImport.update({
|
||||||
id: '/version/$versionId',
|
|
||||||
path: '/version/$versionId',
|
path: '/version/$versionId',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any)
|
} as any)
|
||||||
@ -169,20 +130,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexLazyImport
|
preLoaderRoute: typeof IndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/changelog/$id': {
|
||||||
id: '/changelog/$id'
|
id: '/changelog/$id'
|
||||||
path: '/changelog/$id'
|
path: '/changelog/$id'
|
||||||
@ -211,13 +158,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PageCreateLazyImport
|
preLoaderRoute: typeof PageCreateLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/access-tokens/': {
|
|
||||||
id: '/access-tokens/'
|
|
||||||
path: '/access-tokens'
|
|
||||||
fullPath: '/access-tokens'
|
|
||||||
preLoaderRoute: typeof AccessTokensIndexLazyImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/changelog/': {
|
'/changelog/': {
|
||||||
id: '/changelog/'
|
id: '/changelog/'
|
||||||
path: '/changelog'
|
path: '/changelog'
|
||||||
@ -320,13 +260,10 @@ const PageIdLazyRouteWithChildren = PageIdLazyRoute._addFileChildren(
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/cli': typeof CliLazyRoute
|
|
||||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/page': typeof PageIndexLazyRoute
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
@ -340,11 +277,8 @@ export interface FileRoutesByFullPath {
|
|||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/cli': typeof CliLazyRoute
|
|
||||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/access-tokens': typeof AccessTokensIndexLazyRoute
|
|
||||||
'/changelog': typeof ChangelogIndexLazyRoute
|
'/changelog': typeof ChangelogIndexLazyRoute
|
||||||
'/page': typeof PageIndexLazyRoute
|
'/page': typeof PageIndexLazyRoute
|
||||||
'/user': typeof UserIndexLazyRoute
|
'/user': typeof UserIndexLazyRoute
|
||||||
@ -359,13 +293,10 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/cli': typeof CliLazyRoute
|
|
||||||
'/access-tokens/new': typeof AccessTokensNewLazyRoute
|
|
||||||
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
'/changelog/$id': typeof ChangelogIdLazyRouteWithChildren
|
||||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||||
'/page/$id': typeof PageIdLazyRouteWithChildren
|
'/page/$id': typeof PageIdLazyRouteWithChildren
|
||||||
'/page/create': typeof PageCreateLazyRoute
|
'/page/create': typeof PageCreateLazyRoute
|
||||||
'/access-tokens/': typeof AccessTokensIndexLazyRoute
|
|
||||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||||
'/page/': typeof PageIndexLazyRoute
|
'/page/': typeof PageIndexLazyRoute
|
||||||
'/user/': typeof UserIndexLazyRoute
|
'/user/': typeof UserIndexLazyRoute
|
||||||
@ -381,13 +312,10 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/cli'
|
|
||||||
| '/access-tokens/new'
|
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/$id'
|
| '/page/$id'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
| '/access-tokens'
|
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/page'
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
@ -400,11 +328,8 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/cli'
|
|
||||||
| '/access-tokens/new'
|
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
| '/access-tokens'
|
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/page'
|
| '/page'
|
||||||
| '/user'
|
| '/user'
|
||||||
@ -417,13 +342,10 @@ export interface FileRouteTypes {
|
|||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/cli'
|
|
||||||
| '/access-tokens/new'
|
|
||||||
| '/changelog/$id'
|
| '/changelog/$id'
|
||||||
| '/changelog/create'
|
| '/changelog/create'
|
||||||
| '/page/$id'
|
| '/page/$id'
|
||||||
| '/page/create'
|
| '/page/create'
|
||||||
| '/access-tokens/'
|
|
||||||
| '/changelog/'
|
| '/changelog/'
|
||||||
| '/page/'
|
| '/page/'
|
||||||
| '/user/'
|
| '/user/'
|
||||||
@ -438,13 +360,10 @@ export interface FileRouteTypes {
|
|||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
CliLazyRoute: typeof CliLazyRoute
|
|
||||||
AccessTokensNewLazyRoute: typeof AccessTokensNewLazyRoute
|
|
||||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
ChangelogIdLazyRoute: typeof ChangelogIdLazyRouteWithChildren
|
||||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||||
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
PageIdLazyRoute: typeof PageIdLazyRouteWithChildren
|
||||||
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
PageCreateLazyRoute: typeof PageCreateLazyRoute
|
||||||
AccessTokensIndexLazyRoute: typeof AccessTokensIndexLazyRoute
|
|
||||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||||
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
PageIndexLazyRoute: typeof PageIndexLazyRoute
|
||||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||||
@ -452,13 +371,10 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
CliLazyRoute: CliLazyRoute,
|
|
||||||
AccessTokensNewLazyRoute: AccessTokensNewLazyRoute,
|
|
||||||
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
ChangelogIdLazyRoute: ChangelogIdLazyRouteWithChildren,
|
||||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||||
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
PageIdLazyRoute: PageIdLazyRouteWithChildren,
|
||||||
PageCreateLazyRoute: PageCreateLazyRoute,
|
PageCreateLazyRoute: PageCreateLazyRoute,
|
||||||
AccessTokensIndexLazyRoute: AccessTokensIndexLazyRoute,
|
|
||||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||||
PageIndexLazyRoute: PageIndexLazyRoute,
|
PageIndexLazyRoute: PageIndexLazyRoute,
|
||||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||||
@ -468,6 +384,8 @@ export const routeTree = rootRoute
|
|||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
/* prettier-ignore-end */
|
||||||
|
|
||||||
/* ROUTE_MANIFEST_START
|
/* ROUTE_MANIFEST_START
|
||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
@ -475,13 +393,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/cli",
|
|
||||||
"/access-tokens/new",
|
|
||||||
"/changelog/$id",
|
"/changelog/$id",
|
||||||
"/changelog/create",
|
"/changelog/create",
|
||||||
"/page/$id",
|
"/page/$id",
|
||||||
"/page/create",
|
"/page/create",
|
||||||
"/access-tokens/",
|
|
||||||
"/changelog/",
|
"/changelog/",
|
||||||
"/page/",
|
"/page/",
|
||||||
"/user/"
|
"/user/"
|
||||||
@ -490,12 +405,6 @@ export const routeTree = rootRoute
|
|||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.lazy.tsx"
|
"filePath": "index.lazy.tsx"
|
||||||
},
|
},
|
||||||
"/cli": {
|
|
||||||
"filePath": "cli.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/access-tokens/new": {
|
|
||||||
"filePath": "access-tokens.new.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/changelog/$id": {
|
"/changelog/$id": {
|
||||||
"filePath": "changelog.$id.lazy.tsx",
|
"filePath": "changelog.$id.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
@ -518,9 +427,6 @@ export const routeTree = rootRoute
|
|||||||
"/page/create": {
|
"/page/create": {
|
||||||
"filePath": "page.create.lazy.tsx"
|
"filePath": "page.create.lazy.tsx"
|
||||||
},
|
},
|
||||||
"/access-tokens/": {
|
|
||||||
"filePath": "access-tokens.index.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/changelog/": {
|
"/changelog/": {
|
||||||
"filePath": "changelog.index.lazy.tsx"
|
"filePath": "changelog.index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -62,15 +58,8 @@ const Component = () => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@ -78,11 +67,7 @@ const Component = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Title</FormLabel>
|
<FormLabel>Title</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="My changelog" {...field} autoFocus />
|
||||||
placeholder="My changelog"
|
|
||||||
{...field}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
</FormControl>{' '}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -105,16 +90,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Options</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="w-full flex flex-col gap-5">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="isSemver"
|
name="isSemver"
|
||||||
@ -141,39 +117,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex gap-5">
|
||||||
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -183,8 +127,7 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="submit">Update</Button>
|
||||||
<Button type="submit">Save</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,12 +1,58 @@
|
|||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
import {
|
||||||
import { ChangelogCommitList } from '../components/Changelog/CommitList'
|
Button,
|
||||||
import { ChangelogVersionList } from '../components/Changelog/VersionList'
|
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 Component = () => {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const { data, isPending } = useChangelogById({ id })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-5 flex-wrap">
|
<div className="flex flex-col gap-5">
|
||||||
<ChangelogVersionList />
|
{!isPending && data && (
|
||||||
<ChangelogCommitList />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { useChangelogById } from '../hooks/useChangelog'
|
|||||||
const Component = () => {
|
const Component = () => {
|
||||||
const { id } = Route.useParams()
|
const { id } = Route.useParams()
|
||||||
const { data, error, isPending, refetch } = useChangelogById({ id })
|
const { data, error, isPending, refetch } = useChangelogById({ id })
|
||||||
|
console.log(data)
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center mt-32 flex-col">
|
<div className="flex items-center justify-center mt-32 flex-col">
|
||||||
@ -64,7 +65,16 @@ const Component = () => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
*/}
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant={'ghost'}>
|
||||||
|
<Globe2Icon strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Public Page</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip> */}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
@ -2,30 +2,21 @@ import { VersionUpdateInput } from '@boring.tools/schema'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Calendar,
|
Calendar,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Checkbox,
|
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
Input,
|
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
ScrollArea,
|
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
Separator,
|
Separator,
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
cn,
|
cn,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
@ -46,19 +37,17 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
|||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import type { z } from 'zod'
|
import type { z } from 'zod'
|
||||||
import {
|
import {
|
||||||
useChangelogCommitList,
|
|
||||||
useChangelogVersionById,
|
useChangelogVersionById,
|
||||||
useChangelogVersionUpdate,
|
useChangelogVersionUpdate,
|
||||||
} from '../hooks/useChangelog'
|
} from '../hooks/useChangelog'
|
||||||
import '@mdxeditor/editor/style.css'
|
import '@mdxeditor/editor/style.css'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { CalendarIcon } from 'lucide-react'
|
import { CalendarIcon } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { ChangelogVersionDelete } from '../components/Changelog/VersionDelete'
|
import { ChangelogVersionDelete } from '../components/Changelog/VersionDelete'
|
||||||
import { VersionStatus } from '../components/Changelog/VersionStatus'
|
import { VersionStatus } from '../components/Changelog/VersionStatus'
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
const [activeTab, setActiveTab] = useState('assigned')
|
|
||||||
const { id, versionId } = Route.useParams()
|
const { id, versionId } = Route.useParams()
|
||||||
const mdxEditorRef = useRef<MDXEditorMethods>(null)
|
const mdxEditorRef = useRef<MDXEditorMethods>(null)
|
||||||
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
||||||
@ -66,8 +55,6 @@ const Component = () => {
|
|||||||
const { data, error, isPending, refetch } = useChangelogVersionById({
|
const { data, error, isPending, refetch } = useChangelogVersionById({
|
||||||
id: versionId,
|
id: versionId,
|
||||||
})
|
})
|
||||||
const commitResult = useChangelogCommitList({ id })
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof VersionUpdateInput>>({
|
const form = useForm<z.infer<typeof VersionUpdateInput>>({
|
||||||
resolver: zodResolver(VersionUpdateInput),
|
resolver: zodResolver(VersionUpdateInput),
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
@ -87,12 +74,9 @@ const Component = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
mdxEditorRef.current?.setMarkdown(data.markdown)
|
mdxEditorRef.current?.setMarkdown(data.markdown)
|
||||||
form.reset({
|
form.reset(data)
|
||||||
...data,
|
|
||||||
commitIds: data.commits?.map((commit) => commit.id),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [data, form])
|
}, [data, form.reset])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -109,27 +93,45 @@ const Component = () => {
|
|||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<Separator />
|
<Separator />
|
||||||
{!isPending && data && (
|
{!isPending && data && (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl mb-2">Version: {data.version}</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex gap-4 w-full flex-col max-w-screen-2xl"
|
className="space-y-8 max-w-screen-md"
|
||||||
>
|
>
|
||||||
<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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="version"
|
name="markdown"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Version</FormLabel>
|
<FormLabel>Notes</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="v1.0.1" {...field} />
|
<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>{' '}
|
</FormControl>{' '}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -205,10 +207,7 @@ const Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
className="w-auto p-0"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={field.value as Date}
|
selected={field.value as Date}
|
||||||
@ -223,203 +222,7 @@ const Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div className="flex gap-5">
|
||||||
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 />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -431,10 +234,11 @@ const Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Update</Button>
|
<Button type="submit">Update</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChangelogVersionDelete id={id} versionId={versionId} />
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<ChangelogVersionDelete id={id} versionId={versionId} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -2,11 +2,6 @@ import { VersionCreateInput } from '@boring.tools/schema'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Calendar,
|
Calendar,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Checkbox,
|
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
@ -17,12 +12,12 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
ScrollArea,
|
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
|
Separator,
|
||||||
cn,
|
cn,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
@ -42,10 +37,7 @@ import { createLazyFileRoute } from '@tanstack/react-router'
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import type { z } from 'zod'
|
import type { z } from 'zod'
|
||||||
import {
|
import { useChangelogVersionCreate } from '../hooks/useChangelog'
|
||||||
useChangelogCommitList,
|
|
||||||
useChangelogVersionCreate,
|
|
||||||
} from '../hooks/useChangelog'
|
|
||||||
import '@mdxeditor/editor/style.css'
|
import '@mdxeditor/editor/style.css'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { CalendarIcon } from 'lucide-react'
|
import { CalendarIcon } from 'lucide-react'
|
||||||
@ -54,9 +46,7 @@ import { VersionStatus } from '../components/Changelog/VersionStatus'
|
|||||||
const Component = () => {
|
const Component = () => {
|
||||||
const { id } = Route.useParams()
|
const { id } = Route.useParams()
|
||||||
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
|
||||||
const changelogCommit = useChangelogCommitList({ id })
|
|
||||||
const versionCreate = useChangelogVersionCreate()
|
const versionCreate = useChangelogVersionCreate()
|
||||||
const { data } = useChangelogCommitList({ id })
|
|
||||||
const form = useForm<z.infer<typeof VersionCreateInput>>({
|
const form = useForm<z.infer<typeof VersionCreateInput>>({
|
||||||
resolver: zodResolver(VersionCreateInput),
|
resolver: zodResolver(VersionCreateInput),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -64,18 +54,9 @@ const Component = () => {
|
|||||||
version: '',
|
version: '',
|
||||||
markdown: '',
|
markdown: '',
|
||||||
status: 'draft',
|
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>) => {
|
const onSubmit = (values: z.infer<typeof VersionCreateInput>) => {
|
||||||
versionCreate.mutate(values, {
|
versionCreate.mutate(values, {
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
@ -85,15 +66,14 @@ const Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<Separator />
|
||||||
|
<h1 className="text-xl mb-2">New version</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full">
|
<form
|
||||||
<h1 className="text-2xl">New version</h1>
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="grid md:grid-cols-6 gap-5 w-full md:max-w-screen-xl grid-flow-row grid-cols-1">
|
className="space-y-8 max-w-screen-md"
|
||||||
<Card className="md:col-span-4 col-span-1">
|
>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Details</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-5">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="version"
|
name="version"
|
||||||
@ -108,7 +88,43 @@ const Component = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-5 md:items-center flex-col md:flex-row">
|
<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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="status"
|
name="status"
|
||||||
@ -192,106 +208,7 @@ const Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div className="flex gap-5">
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -301,9 +218,9 @@ const Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Create</Button>
|
<Button type="submit">Create</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { ChangelogCreateInput } from '@boring.tools/schema'
|
import { ChangelogCreateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -33,7 +29,6 @@ const Component = () => {
|
|||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
isSemver: true,
|
isSemver: true,
|
||||||
isConventional: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -57,18 +52,12 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="text-3xl">New changelog</h1>
|
<h1 className="text-3xl">New changelog</h1>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@ -76,11 +65,7 @@ const Component = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Title</FormLabel>
|
<FormLabel>Title</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="My changelog" {...field} autoFocus />
|
||||||
placeholder="My changelog"
|
|
||||||
{...field}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
</FormControl>{' '}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -103,16 +88,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Options</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="w-full flex flex-col gap-5">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="isSemver"
|
name="isSemver"
|
||||||
@ -139,49 +115,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</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>
|
<Button type="submit">Create</Button>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,10 +26,11 @@ const Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
<PageWrapper breadcrumbs={[{ name: 'Changelog', to: '/changelog' }]}>
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="text-3xl">Changelog</h1>
|
<h1 className="text-3xl">Changelog</h1>
|
||||||
|
|
||||||
<div className="flex gap-10 w-full flex-wrap">
|
<div className="flex gap-10 w-full">
|
||||||
{!isPending &&
|
{!isPending &&
|
||||||
data &&
|
data &&
|
||||||
data.map((changelog) => {
|
data.map((changelog) => {
|
||||||
@ -44,9 +45,9 @@ const Component = () => {
|
|||||||
<CardTitle>{changelog.title}</CardTitle>
|
<CardTitle>{changelog.title}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-center flex-col">
|
<CardContent className="flex items-center justify-center flex-col">
|
||||||
<span>Versions: {changelog.computed?.versionCount}</span>
|
<span>Versions: {changelog.computed.versionCount}</span>
|
||||||
|
|
||||||
<span>Commits: {changelog.computed?.commitCount}</span>
|
<span>Commits: {changelog.computed.commitCount}</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
@ -65,6 +66,7 @@ const Component = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
</PageWrapper>
|
</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 { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
import { PageWrapper } from '../components/PageWrapper'
|
|
||||||
import { useStatistic } from '../hooks/useStatistic'
|
|
||||||
|
|
||||||
const Component = () => {
|
export const Route = createLazyFileRoute('/')({
|
||||||
const { data } = useStatistic()
|
component: Index,
|
||||||
const user = useUser()
|
})
|
||||||
|
|
||||||
|
function Index() {
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<div className="p-2">
|
||||||
breadcrumbs={[
|
<h3>Welcome Home!</h3>
|
||||||
{
|
|
||||||
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>
|
</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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export const Route = createLazyFileRoute('/')({
|
|
||||||
component: Component,
|
|
||||||
})
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { PageUpdateInput } from '@boring.tools/schema'
|
import { PageUpdateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@ -44,7 +40,7 @@ const Component = () => {
|
|||||||
resolver: zodResolver(PageUpdateInput),
|
resolver: zodResolver(PageUpdateInput),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...page.data,
|
...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>) => {
|
const onSubmit = (values: z.infer<typeof PageUpdateInput>) => {
|
||||||
@ -61,17 +57,13 @@ const Component = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
|
<h1 className="text-3xl">Edit page</h1>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@ -96,21 +88,13 @@ const Component = () => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Some details about the page..."
|
placeholder="Some details about the page..."
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ''}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>{' '}
|
</FormControl>{' '}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Options</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-5">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="changelogIds"
|
name="changelogIds"
|
||||||
@ -128,16 +112,13 @@ const Component = () => {
|
|||||||
!field.value && 'text-muted-foreground',
|
!field.value && 'text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field?.value?.length === 1 &&
|
{field.value.length === 1 &&
|
||||||
changelogList.data?.find((changelog) =>
|
changelogList.data?.find((changelog) =>
|
||||||
field.value?.includes(changelog.id),
|
field.value?.includes(changelog.id),
|
||||||
)?.title}
|
)?.title}
|
||||||
{field?.value &&
|
{field.value.length <= 0 && 'No changelog selected'}
|
||||||
field.value.length <= 0 &&
|
{field.value.length > 1 &&
|
||||||
'No changelog selected'}
|
`${field.value.length} selected`}
|
||||||
{field?.value &&
|
|
||||||
field.value.length > 1 &&
|
|
||||||
`${field?.value?.length} selected`}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -154,22 +135,14 @@ const Component = () => {
|
|||||||
key={changelog.id}
|
key={changelog.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const getIds = () => {
|
const getIds = () => {
|
||||||
if (!field.value) {
|
if (field.value.includes(changelog.id)) {
|
||||||
return [changelog.id]
|
const asd = field.value.filter(
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
field.value?.includes(changelog.id)
|
|
||||||
) {
|
|
||||||
return field.value.filter(
|
|
||||||
(id) => id !== changelog.id,
|
(id) => id !== changelog.id,
|
||||||
)
|
)
|
||||||
|
return asd
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [...field.value, changelog.id]
|
||||||
...(field?.value as string[]),
|
|
||||||
changelog.id,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
form.setValue('changelogIds', getIds())
|
form.setValue('changelogIds', getIds())
|
||||||
}}
|
}}
|
||||||
@ -177,7 +150,7 @@ const Component = () => {
|
|||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-2 h-4 w-4',
|
'mr-2 h-4 w-4',
|
||||||
field.value?.includes(changelog.id)
|
field.value.includes(changelog.id)
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0',
|
: 'opacity-0',
|
||||||
)}
|
)}
|
||||||
@ -197,11 +170,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
<div className="flex gap-5">
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end justify-end gap-5">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -209,7 +178,7 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">Update</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||||
import { CircleMinusIcon } from 'lucide-react'
|
import { CircleMinusIcon, PlusCircleIcon } from 'lucide-react'
|
||||||
import { usePageById, usePageUpdate } from '../hooks/usePage'
|
import { usePageById, usePageUpdate } from '../hooks/usePage'
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
@ -31,14 +31,12 @@ const Component = () => {
|
|||||||
<Card className="w-full max-w-screen-sm">
|
<Card className="w-full max-w-screen-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>
|
<CardTitle>Changelogs ({data.changelogs?.length})</CardTitle>
|
||||||
Changelogs ({data.changelogs?.length ?? 0})
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{data.changelogs?.map((changelog) => {
|
{data.changelogs.map((changelog) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3" key={changelog.id}>
|
<div className="flex gap-3" key={changelog.id}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { PageCreateInput } from '@boring.tools/schema'
|
import { PageCreateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@ -73,15 +69,8 @@ const Component = () => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@ -106,21 +95,13 @@ const Component = () => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Some details about the page..."
|
placeholder="Some details about the page..."
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ''}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>{' '}
|
</FormControl>{' '}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Options</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-5">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="changelogIds"
|
name="changelogIds"
|
||||||
@ -142,8 +123,7 @@ const Component = () => {
|
|||||||
changelogList.data?.find((changelog) =>
|
changelogList.data?.find((changelog) =>
|
||||||
field.value?.includes(changelog.id),
|
field.value?.includes(changelog.id),
|
||||||
)?.title}
|
)?.title}
|
||||||
{field.value.length <= 0 &&
|
{field.value.length <= 0 && 'No changelog selected'}
|
||||||
'No changelog selected'}
|
|
||||||
{field.value.length > 1 &&
|
{field.value.length > 1 &&
|
||||||
`${field.value.length} selected`}
|
`${field.value.length} selected`}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
@ -162,9 +142,7 @@ const Component = () => {
|
|||||||
key={changelog.id}
|
key={changelog.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const getIds = () => {
|
const getIds = () => {
|
||||||
if (
|
if (field.value.includes(changelog.id)) {
|
||||||
field.value.includes(changelog.id)
|
|
||||||
) {
|
|
||||||
const asd = field.value.filter(
|
const asd = field.value.filter(
|
||||||
(id) => id !== changelog.id,
|
(id) => id !== changelog.id,
|
||||||
)
|
)
|
||||||
@ -199,20 +177,7 @@ const Component = () => {
|
|||||||
</FormItem>
|
</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>
|
<Button type="submit">Create</Button>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,51 +1,10 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
|
||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
import { Link } from '@tanstack/react-router'
|
//import { usePageById, usePageList } from '../hooks/usePage'
|
||||||
import { PlusCircleIcon } from 'lucide-react'
|
|
||||||
import { PageWrapper } from '../components/PageWrapper'
|
|
||||||
import { usePageList } from '../hooks/usePage'
|
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
const { data, isPending } = usePageList()
|
//const { data, error } = usePageList()
|
||||||
|
|
||||||
return (
|
return <div>some</div>
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/page/')({
|
export const Route = createLazyFileRoute('/page/')({
|
||||||
|
@ -12,7 +12,6 @@ const url = import.meta.env.PROD
|
|||||||
: 'http://localhost:3000'
|
: 'http://localhost:3000'
|
||||||
|
|
||||||
export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
||||||
try {
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method,
|
method,
|
||||||
url: `${url}/v1/${path}`,
|
url: `${url}/v1/${path}`,
|
||||||
@ -22,7 +21,4 @@ export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Somethind went wrong.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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",
|
"@types/react-dom": "^18.3.1",
|
||||||
"astro": "^4.16.7",
|
"astro": "^4.16.7",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"marked": "^14.1.3",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
|
@ -3,7 +3,6 @@ import type { PageByIdOutput } from '@boring.tools/schema'
|
|||||||
import { Separator } from '@boring.tools/ui'
|
import { Separator } from '@boring.tools/ui'
|
||||||
import type { z } from 'astro/zod'
|
import type { z } from 'astro/zod'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { marked } from 'marked'
|
|
||||||
|
|
||||||
type PageById = z.infer<typeof PageByIdOutput>
|
type PageById = z.infer<typeof PageByIdOutput>
|
||||||
const url = import.meta.env.DEV
|
const url = import.meta.env.DEV
|
||||||
@ -30,9 +29,9 @@ const data: PageById = await response.json()
|
|||||||
<p class="prose text-sm">{data.description}</p>
|
<p class="prose text-sm">{data.description}</p>
|
||||||
</div>
|
</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>
|
<h2 class="prose prose-xl">Changelogs</h2>
|
||||||
{data.changelogs?.map((changelog) => {
|
{data.changelogs.map((changelog) => {
|
||||||
if (changelog.versions && changelog.versions?.length < 1) {
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -56,7 +55,9 @@ const data: PageById = await response.json()
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div set:html={marked.parse(version.markdown ?? "")} />
|
<p>
|
||||||
|
{version.markdown}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -65,7 +66,7 @@ const data: PageById = await response.json()
|
|||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
</div>}
|
</div>}
|
||||||
{data.changelogs?.length === 1 && <div>
|
{data.changelogs.length === 1 && <div>
|
||||||
<h2 class="uppercase text-sm prose tracking-widest">Changelog</h2>
|
<h2 class="uppercase text-sm prose tracking-widest">Changelog</h2>
|
||||||
{data.changelogs.map((changelog) => {
|
{data.changelogs.map((changelog) => {
|
||||||
if (changelog.versions && changelog.versions?.length < 1) {
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
@ -88,7 +89,10 @@ const data: PageById = await response.json()
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="page" set:html={marked.parse(version.markdown ?? "")} />
|
|
||||||
|
<p>
|
||||||
|
{version.markdown}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -25,18 +25,8 @@ export default defineConfig({
|
|||||||
favicon: '/favicon.svg',
|
favicon: '/favicon.svg',
|
||||||
sidebar: [
|
sidebar: [
|
||||||
{
|
{
|
||||||
label: 'Getting Started',
|
label: 'Guides',
|
||||||
items: [
|
items: [{ label: 'Getting started', slug: 'guides/getting-started' }],
|
||||||
{ 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: 'API', link: 'https://api.boring.tools' },
|
{ label: 'API', link: 'https://api.boring.tools' },
|
||||||
{ label: 'Status', link: 'https://status.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