Compare commits

..

No commits in common. "main" and "feature/changelog-public-page" have entirely different histories.

169 changed files with 2658 additions and 6891 deletions

View File

@ -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

View File

@ -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
View File

@ -0,0 +1 @@
POSTGRES_URL=postgres://postgres:postgres@localhost:5432/postgres

View File

@ -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",

View File

@ -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)
})
})
})

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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)
})
}

View File

@ -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,
} }

View File

@ -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)

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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)
})
}

View File

@ -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,
} }

View File

@ -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,
} }

View File

@ -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

View File

@ -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,
} }

View File

@ -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,
} }

View File

@ -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
})
} }

View File

@ -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)
})
} }

View File

@ -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)
})
}

View File

@ -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)
})
} }

View File

@ -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

View File

@ -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)
})
} }

View File

@ -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',

View File

@ -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)
}) })
} }

View File

@ -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)
}) })
} }

View File

@ -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)
}) })
} }

View File

@ -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',
} }

View File

@ -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)
}) })
} }

View File

@ -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)
})
})
})

View File

@ -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)
}) })
} }

View File

@ -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)
}) })
} }

View File

@ -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)
})
}

View File

@ -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

View File

@ -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)
}) })
} }

View File

@ -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' })

View File

@ -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
} }

View File

@ -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: [],
},
],
})

View File

@ -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(' ')
}

View File

@ -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,
}
}

View File

@ -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,
})
}
}
}

View File

@ -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',
),
},
},
},
}

View File

@ -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()

View File

@ -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,

View File

@ -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": {

View File

@ -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

View File

@ -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

View File

@ -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>
)
}

View File

@ -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} />,
},
]

View File

@ -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>
)
}

View File

@ -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>
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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">

View File

@ -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>
}

View 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>
)
}

View 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>
)
}

View File

@ -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">

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>

View File

@ -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'],
})
},
})
}

View File

@ -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()

View File

@ -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(),
}),
})
}

View File

@ -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,
})
}

View File

@ -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"
}, },

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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>

View File

@ -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>
) )

View File

@ -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>
) )
} }

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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/')({

View File

@ -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.')
}
} }

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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()

View File

@ -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,
)
}

View File

@ -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>

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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",

View File

@ -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>
) )
})} })}

View File

@ -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