Compare commits
1 Commits
main
...
feat/feedb
Author | SHA1 | Date | |
---|---|---|---|
091cfe9eee |
@ -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": {
|
||||||
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -29,7 +29,7 @@ export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
|
|||||||
const id = c.req.param('id')
|
const id = c.req.param('id')
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = await verifyAuthentication(c)
|
||||||
|
|
||||||
const [result] = await db
|
const result = await db
|
||||||
.delete(access_token)
|
.delete(access_token)
|
||||||
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
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 user from './user'
|
||||||
|
|
||||||
import { accessTokenApi } from './access-token'
|
import { accessTokenApi } from './access-token'
|
||||||
import pageApi from './page'
|
import pageApi from './page'
|
||||||
import statisticApi from './statistic'
|
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>
|
||||||
@ -33,12 +31,9 @@ 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', {
|
app.openAPIRegistry.registerComponent('securitySchemes', 'AccessToken', {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
@ -48,7 +43,7 @@ app.openAPIRegistry.registerComponent('securitySchemes', 'Clerk', {
|
|||||||
scheme: 'bearer',
|
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/page', pageApi)
|
app.route('/v1/page', pageApi)
|
||||||
app.route('/v1/access-token', accessTokenApi)
|
app.route('/v1/access-token', accessTokenApi)
|
||||||
|
@ -20,7 +20,7 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
201: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: PageOutput,
|
schema: PageOutput,
|
||||||
@ -62,6 +62,6 @@ export const registerPageCreate = (api: typeof pageApi) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(PageOutput.parse(result), 201)
|
return c.json(PageOutput.parse(result), 200)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export const registerPageDelete = (api: typeof pageApi) => {
|
|||||||
const userId = await verifyAuthentication(c)
|
const userId = await 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()
|
||||||
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -91,7 +91,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
||||||
const asd = PagePublicOutput.parse(mappedResult)
|
return c.json(PagePublicOutput.parse(mappedResult), 200)
|
||||||
return c.json(asd, 200)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ 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'
|
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
@ -25,9 +24,9 @@ const route = createRoute({
|
|||||||
|
|
||||||
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) {
|
||||||
|
@ -20,7 +20,10 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
204: {
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': { schema: UserOutput },
|
||||||
|
},
|
||||||
description: 'Return success',
|
description: 'Return success',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
...openApiErrorResponses,
|
||||||
@ -67,11 +70,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(UserOutput.parse(result), 200)
|
||||||
return c.json({}, 204)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({}, 404)
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { access_token, db, user } from '@boring.tools/database'
|
import { access_token, db, user } 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'
|
||||||
@ -53,9 +52,8 @@ export const verifyAuthentication = async (c: Context) => {
|
|||||||
.where(eq(user.providerId, auth.userId))
|
.where(eq(user.providerId, auth.userId))
|
||||||
|
|
||||||
if (!userEntry) {
|
if (!userEntry) {
|
||||||
logger.error('User not found - Unauthorized', { providerId: auth.userId })
|
|
||||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||||
}
|
}
|
||||||
|
// console.log(userEntry)
|
||||||
return userEntry.id
|
return userEntry.id
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,7 @@ export function handleZodError(
|
|||||||
},
|
},
|
||||||
c: Context,
|
c: Context,
|
||||||
) {
|
) {
|
||||||
|
console.log('asdasasdas')
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const error = SchemaError.fromZod(result.error, c)
|
const error = SchemaError.fromZod(result.error, c)
|
||||||
return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
|
return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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">
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import type { UserOutput } from '@boring.tools/schema'
|
|
||||||
import { useAuth } from '@clerk/clerk-react'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import type { z } from 'zod'
|
|
||||||
import { queryFetch } from '../utils/queryFetch'
|
|
||||||
|
|
||||||
type User = z.infer<typeof UserOutput>
|
|
||||||
|
|
||||||
export const useUser = () => {
|
|
||||||
const { getToken } = useAuth()
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['user'],
|
|
||||||
queryFn: async (): Promise<Readonly<User>> =>
|
|
||||||
await queryFetch({
|
|
||||||
path: 'user',
|
|
||||||
method: 'get',
|
|
||||||
token: await getToken(),
|
|
||||||
}),
|
|
||||||
retry: false,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,12 +1,12 @@
|
|||||||
|
/* prettier-ignore-start */
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
// This file was automatically generated by TanStack Router.
|
// This file is auto-generated by TanStack Router
|
||||||
// You should NOT make any changes in this file as it will be overwritten.
|
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
||||||
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
@ -39,31 +39,26 @@ const ChangelogIdEditLazyImport = createFileRoute('/changelog/$id/edit')()
|
|||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const CliLazyRoute = CliLazyImport.update({
|
const CliLazyRoute = CliLazyImport.update({
|
||||||
id: '/cli',
|
|
||||||
path: '/cli',
|
path: '/cli',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/cli.lazy').then((d) => d.Route))
|
} 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(() =>
|
||||||
@ -71,7 +66,6 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
||||||
id: '/access-tokens/',
|
|
||||||
path: '/access-tokens/',
|
path: '/access-tokens/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -79,19 +73,16 @@ const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
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,13 +90,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({
|
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
||||||
id: '/access-tokens/new',
|
|
||||||
path: '/access-tokens/new',
|
path: '/access-tokens/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -113,7 +102,6 @@ const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||||
id: '/',
|
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -121,7 +109,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 +116,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 +129,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 +137,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)
|
||||||
@ -468,6 +451,8 @@ export const routeTree = rootRoute
|
|||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
/* prettier-ignore-end */
|
||||||
|
|
||||||
/* ROUTE_MANIFEST_START
|
/* ROUTE_MANIFEST_START
|
||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -62,118 +58,66 @@ 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">
|
<FormField
|
||||||
<Card className="w-full">
|
control={form.control}
|
||||||
<CardHeader>
|
name="title"
|
||||||
<CardTitle>Details</CardTitle>
|
render={({ field }) => (
|
||||||
</CardHeader>
|
<FormItem>
|
||||||
<CardContent>
|
<FormLabel>Title</FormLabel>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<FormControl>
|
||||||
<FormField
|
<Input placeholder="My changelog" {...field} autoFocus />
|
||||||
control={form.control}
|
</FormControl>{' '}
|
||||||
name="title"
|
<FormMessage />
|
||||||
render={({ field }) => (
|
</FormItem>
|
||||||
<FormItem>
|
)}
|
||||||
<FormLabel>Title</FormLabel>
|
/>
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="My changelog"
|
|
||||||
{...field}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Some details about the changelog..."
|
placeholder="Some details about the changelog..."
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isSemver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Semver</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is following the{' '}
|
||||||
|
<a
|
||||||
|
href="https://semver.org/lang/de/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
semantic versioning?
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</FormItem>
|
||||||
</Card>
|
)}
|
||||||
|
/>
|
||||||
<Card className="w-full">
|
<div className="flex gap-5">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Options</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="w-full flex flex-col gap-5">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isSemver"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Semver</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is following the{' '}
|
|
||||||
<a
|
|
||||||
href="https://semver.org/lang/de/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
semantic versioning?
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isConventional"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Conventional Commits</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is using{' '}
|
|
||||||
<a
|
|
||||||
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
conventional commits
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end justify-end gap-5">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -183,8 +127,7 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="submit">Update</Button>
|
||||||
<Button type="submit">Save</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,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,
|
||||||
@ -57,131 +53,97 @@ 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">
|
<FormField
|
||||||
<Card className="w-full">
|
control={form.control}
|
||||||
<CardHeader>
|
name="title"
|
||||||
<CardTitle>Details</CardTitle>
|
render={({ field }) => (
|
||||||
</CardHeader>
|
<FormItem>
|
||||||
<CardContent>
|
<FormLabel>Title</FormLabel>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<FormControl>
|
||||||
<FormField
|
<Input placeholder="My changelog" {...field} autoFocus />
|
||||||
control={form.control}
|
</FormControl>{' '}
|
||||||
name="title"
|
<FormMessage />
|
||||||
render={({ field }) => (
|
</FormItem>
|
||||||
<FormItem>
|
)}
|
||||||
<FormLabel>Title</FormLabel>
|
/>
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="My changelog"
|
|
||||||
{...field}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Some details about the changelog..."
|
placeholder="Some details about the changelog..."
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isSemver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Semver</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is following the{' '}
|
||||||
|
<a
|
||||||
|
href="https://semver.org/lang/de/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
semantic versioning?
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</FormItem>
|
||||||
</Card>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="w-full">
|
<FormField
|
||||||
<CardHeader>
|
control={form.control}
|
||||||
<CardTitle>Options</CardTitle>
|
name="isConventional"
|
||||||
</CardHeader>
|
render={({ field }) => (
|
||||||
<CardContent>
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
<div className="w-full flex flex-col gap-5">
|
<FormControl>
|
||||||
<FormField
|
<Checkbox
|
||||||
control={form.control}
|
checked={field.value}
|
||||||
name="isSemver"
|
onCheckedChange={field.onChange}
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Semver</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is following the{' '}
|
|
||||||
<a
|
|
||||||
href="https://semver.org/lang/de/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
semantic versioning?
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isConventional"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Conventional Commits</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is using{' '}
|
|
||||||
<a
|
|
||||||
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
conventional commits
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
</CardContent>
|
</FormItem>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
<div className="flex items-end justify-end gap-5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={'ghost'}
|
|
||||||
onClick={() => navigate({ to: '/changelog' })}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit">Create</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,10 @@ const Component = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<h1 className="text-3xl">Welcome back, {user.user?.fullName}</h1>
|
<h1 className="text-3xl">Welcome back, {user.user?.fullName}</h1>
|
||||||
|
<iframe
|
||||||
|
src="http://localhost:4030"
|
||||||
|
title="W3Schools Free Online Web Tutorials"
|
||||||
|
/>
|
||||||
<div className="grid w-full max-w-screen-md gap-10 grid-cols-2">
|
<div className="grid w-full max-w-screen-md gap-10 grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -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,
|
||||||
@ -61,147 +57,129 @@ 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">
|
<FormField
|
||||||
<Card className="w-full">
|
control={form.control}
|
||||||
<CardHeader>
|
name="title"
|
||||||
<CardTitle>Details</CardTitle>
|
render={({ field }) => (
|
||||||
</CardHeader>
|
<FormItem>
|
||||||
<CardContent className="flex flex-col gap-5">
|
<FormLabel>Title</FormLabel>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
name="title"
|
</FormControl>{' '}
|
||||||
render={({ field }) => (
|
<FormMessage />
|
||||||
<FormItem>
|
</FormItem>
|
||||||
<FormLabel>Title</FormLabel>
|
)}
|
||||||
<FormControl>
|
/>
|
||||||
<Input placeholder="My page" {...field} autoFocus />
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<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">
|
<FormField
|
||||||
<CardHeader>
|
control={form.control}
|
||||||
<CardTitle>Options</CardTitle>
|
name="changelogIds"
|
||||||
</CardHeader>
|
render={({ field }) => (
|
||||||
<CardContent className="flex flex-col gap-5">
|
<FormItem className="flex flex-col">
|
||||||
<FormField
|
<FormLabel>Changelogs</FormLabel>
|
||||||
control={form.control}
|
<Popover>
|
||||||
name="changelogIds"
|
<PopoverTrigger asChild>
|
||||||
render={({ field }) => (
|
<FormControl>
|
||||||
<FormItem className="flex flex-col">
|
<Button
|
||||||
<FormLabel>Changelogs</FormLabel>
|
variant="outline"
|
||||||
<Popover>
|
role="combobox"
|
||||||
<PopoverTrigger asChild>
|
className={cn(
|
||||||
<FormControl>
|
'w-[200px] justify-between',
|
||||||
<Button
|
!field.value && 'text-muted-foreground',
|
||||||
variant="outline"
|
)}
|
||||||
role="combobox"
|
>
|
||||||
className={cn(
|
{field?.value?.length === 1 &&
|
||||||
'w-[200px] justify-between',
|
changelogList.data?.find((changelog) =>
|
||||||
!field.value && 'text-muted-foreground',
|
field.value?.includes(changelog.id),
|
||||||
)}
|
)?.title}
|
||||||
|
{field?.value &&
|
||||||
|
field.value.length <= 0 &&
|
||||||
|
'No changelog selected'}
|
||||||
|
{field?.value &&
|
||||||
|
field.value.length > 1 &&
|
||||||
|
`${field?.value?.length} selected`}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{changelogList.data?.map((changelog) => (
|
||||||
|
<CommandItem
|
||||||
|
value={changelog.title}
|
||||||
|
key={changelog.id}
|
||||||
|
onSelect={() => {
|
||||||
|
const getIds = () => {
|
||||||
|
if (!field.value) {
|
||||||
|
return [changelog.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.value?.includes(changelog.id)) {
|
||||||
|
return field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(field?.value as string[]),
|
||||||
|
changelog.id,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
form.setValue('changelogIds', getIds())
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{field?.value?.length === 1 &&
|
<Check
|
||||||
changelogList.data?.find((changelog) =>
|
className={cn(
|
||||||
field.value?.includes(changelog.id),
|
'mr-2 h-4 w-4',
|
||||||
)?.title}
|
field.value?.includes(changelog.id)
|
||||||
{field?.value &&
|
? 'opacity-100'
|
||||||
field.value.length <= 0 &&
|
: 'opacity-0',
|
||||||
'No changelog selected'}
|
)}
|
||||||
{field?.value &&
|
/>
|
||||||
field.value.length > 1 &&
|
{changelog.title}
|
||||||
`${field?.value?.length} selected`}
|
</CommandItem>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
))}
|
||||||
</Button>
|
</CommandGroup>
|
||||||
</FormControl>
|
</CommandList>
|
||||||
</PopoverTrigger>
|
</Command>
|
||||||
<PopoverContent className="w-[200px] p-0">
|
</PopoverContent>
|
||||||
<Command>
|
</Popover>
|
||||||
<CommandInput placeholder="Search changelogs..." />
|
<FormDescription>
|
||||||
<CommandList>
|
This changelogs are shown on this page.
|
||||||
<CommandEmpty>No changelog found.</CommandEmpty>
|
</FormDescription>
|
||||||
<CommandGroup>
|
<FormMessage />
|
||||||
{changelogList.data?.map((changelog) => (
|
</FormItem>
|
||||||
<CommandItem
|
)}
|
||||||
value={changelog.title}
|
/>
|
||||||
key={changelog.id}
|
<div className="flex gap-5">
|
||||||
onSelect={() => {
|
|
||||||
const getIds = () => {
|
|
||||||
if (!field.value) {
|
|
||||||
return [changelog.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
field.value?.includes(changelog.id)
|
|
||||||
) {
|
|
||||||
return field.value.filter(
|
|
||||||
(id) => id !== changelog.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
...(field?.value as string[]),
|
|
||||||
changelog.id,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
form.setValue('changelogIds', getIds())
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
field.value?.includes(changelog.id)
|
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{changelog.title}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormDescription>
|
|
||||||
This changelogs are shown on this page.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end justify-end gap-5">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -209,7 +187,7 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">Update</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -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,146 +69,115 @@ 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">
|
<FormField
|
||||||
<Card className="w-full">
|
control={form.control}
|
||||||
<CardHeader>
|
name="title"
|
||||||
<CardTitle>Details</CardTitle>
|
render={({ field }) => (
|
||||||
</CardHeader>
|
<FormItem>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-5">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="description"
|
||||||
name="title"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormLabel>Title</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Textarea
|
||||||
<Input placeholder="My page" {...field} autoFocus />
|
placeholder="Some details about the page..."
|
||||||
</FormControl>{' '}
|
{...field}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</FormControl>{' '}
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="changelogIds"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Changelogs</FormLabel>
|
||||||
<FormControl>
|
<Popover>
|
||||||
<Textarea
|
<PopoverTrigger asChild>
|
||||||
placeholder="Some details about the page..."
|
<FormControl>
|
||||||
{...field}
|
<Button
|
||||||
value={field.value ?? ''}
|
variant="outline"
|
||||||
/>
|
role="combobox"
|
||||||
</FormControl>{' '}
|
className={cn(
|
||||||
<FormMessage />
|
'w-[200px] justify-between',
|
||||||
</FormItem>
|
!field.value && 'text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</CardContent>
|
{field.value.length === 1 &&
|
||||||
</Card>
|
changelogList.data?.find((changelog) =>
|
||||||
|
field.value?.includes(changelog.id),
|
||||||
|
)?.title}
|
||||||
|
{field.value.length <= 0 && 'No changelog selected'}
|
||||||
|
{field.value.length > 1 &&
|
||||||
|
`${field.value.length} selected`}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{changelogList.data?.map((changelog) => (
|
||||||
|
<CommandItem
|
||||||
|
value={changelog.title}
|
||||||
|
key={changelog.id}
|
||||||
|
onSelect={() => {
|
||||||
|
const getIds = () => {
|
||||||
|
if (field.value.includes(changelog.id)) {
|
||||||
|
const asd = field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
return asd
|
||||||
|
}
|
||||||
|
|
||||||
<Card className="w-full">
|
return [...field.value, changelog.id]
|
||||||
<CardHeader>
|
}
|
||||||
<CardTitle>Options</CardTitle>
|
form.setValue('changelogIds', getIds())
|
||||||
</CardHeader>
|
}}
|
||||||
<CardContent className="flex flex-col gap-5">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="changelogIds"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col">
|
|
||||||
<FormLabel>Changelogs</FormLabel>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<FormControl>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
'w-[200px] justify-between',
|
|
||||||
!field.value && 'text-muted-foreground',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{field.value.length === 1 &&
|
<Check
|
||||||
changelogList.data?.find((changelog) =>
|
className={cn(
|
||||||
field.value?.includes(changelog.id),
|
'mr-2 h-4 w-4',
|
||||||
)?.title}
|
field.value.includes(changelog.id)
|
||||||
{field.value.length <= 0 &&
|
? 'opacity-100'
|
||||||
'No changelog selected'}
|
: 'opacity-0',
|
||||||
{field.value.length > 1 &&
|
)}
|
||||||
`${field.value.length} selected`}
|
/>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
{changelog.title}
|
||||||
</Button>
|
</CommandItem>
|
||||||
</FormControl>
|
))}
|
||||||
</PopoverTrigger>
|
</CommandGroup>
|
||||||
<PopoverContent className="w-[200px] p-0">
|
</CommandList>
|
||||||
<Command>
|
</Command>
|
||||||
<CommandInput placeholder="Search changelogs..." />
|
</PopoverContent>
|
||||||
<CommandList>
|
</Popover>
|
||||||
<CommandEmpty>No changelog found.</CommandEmpty>
|
<FormDescription>
|
||||||
<CommandGroup>
|
This changelogs are shown on this page.
|
||||||
{changelogList.data?.map((changelog) => (
|
</FormDescription>
|
||||||
<CommandItem
|
<FormMessage />
|
||||||
value={changelog.title}
|
</FormItem>
|
||||||
key={changelog.id}
|
)}
|
||||||
onSelect={() => {
|
/>
|
||||||
const getIds = () => {
|
<Button type="submit">Create</Button>
|
||||||
if (
|
|
||||||
field.value.includes(changelog.id)
|
|
||||||
) {
|
|
||||||
const asd = field.value.filter(
|
|
||||||
(id) => id !== changelog.id,
|
|
||||||
)
|
|
||||||
return asd
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...field.value, changelog.id]
|
|
||||||
}
|
|
||||||
form.setValue('changelogIds', getIds())
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
field.value.includes(changelog.id)
|
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{changelog.title}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormDescription>
|
|
||||||
This changelogs are shown on this page.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end justify-end gap-5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={'ghost'}
|
|
||||||
onClick={() => navigate({ to: '/page' })}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit">Create</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,17 +12,13 @@ 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}`,
|
data,
|
||||||
data,
|
headers: {
|
||||||
headers: {
|
Authorization: `Bearer ${token}`,
|
||||||
Authorization: `Bearer ${token}`,
|
},
|
||||||
},
|
})
|
||||||
})
|
return response.data
|
||||||
return response.data
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Somethind went wrong.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
24
apps/feedback_widget/.gitignore
vendored
Normal file
24
apps/feedback_widget/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
47
apps/feedback_widget/README.md
Normal file
47
apps/feedback_widget/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
24
apps/feedback_widget/astro.config.mjs
Normal file
24
apps/feedback_widget/astro.config.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
|
import react from '@astrojs/react'
|
||||||
|
|
||||||
|
import tailwind from '@astrojs/tailwind'
|
||||||
|
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server',
|
||||||
|
outDir: '../../build/page',
|
||||||
|
integrations: [react(), tailwind({ nesting: true })],
|
||||||
|
server: {
|
||||||
|
port: 4030,
|
||||||
|
},
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
36
apps/feedback_widget/package.json
Normal file
36
apps/feedback_widget/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@boring.tools/feedback_widget",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/node": "^8.3.4",
|
||||||
|
"@astrojs/react": "^3.6.2",
|
||||||
|
"@astrojs/tailwind": "^5.1.2",
|
||||||
|
"@boring.tools/schema": "workspace:*",
|
||||||
|
"@boring.tools/ui": "workspace:*",
|
||||||
|
"@fingerprintjs/fingerprintjs": "^4.5.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"astro": "^4.16.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"marked": "^14.1.3",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"webcoreui": "^0.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"sass-embedded": "^1.80.6"
|
||||||
|
}
|
||||||
|
}
|
9
apps/feedback_widget/public/favicon.svg
Normal file
9
apps/feedback_widget/public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
1
apps/feedback_widget/src/env.d.ts
vendored
Normal file
1
apps/feedback_widget/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
104
apps/feedback_widget/src/pages/[id].astro
Normal file
104
apps/feedback_widget/src/pages/[id].astro
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
import type { PageByIdOutput } from '@boring.tools/schema'
|
||||||
|
import { Separator } from '@boring.tools/ui'
|
||||||
|
import type { z } from 'astro/zod'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
type PageById = z.infer<typeof PageByIdOutput>
|
||||||
|
const url = import.meta.env.DEV
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://api.boring.tools'
|
||||||
|
const { id } = Astro.params
|
||||||
|
const response = await fetch(`${url}/v1/page/${id}/public`)
|
||||||
|
const data: PageById = await response.json()
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>{data.title} | boring.tools</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-neutral-100">
|
||||||
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-screen-sm flex my-6 flex-col gap-8">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="prose prose-2xl">{data.title}</h1>
|
||||||
|
<p class="prose text-sm">{data.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.changelogs?.length >= 2 && <div class="flex flex-col">
|
||||||
|
<h2 class="prose prose-xl">Changelogs</h2>
|
||||||
|
{data.changelogs?.map((changelog) => {
|
||||||
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<h3 class="font-bold">{changelog.title}</h3>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex flex-col gap-5 my-6">
|
||||||
|
|
||||||
|
{changelog.versions?.map((version) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-10 bg-white rounded p-3 border border-neutral-300">
|
||||||
|
<div>
|
||||||
|
<h2 class="prose text-3xl font-bold">
|
||||||
|
{version.version}
|
||||||
|
</h2>
|
||||||
|
{version.releasedAt &&
|
||||||
|
|
||||||
|
<p class="prose text-xs text-center">
|
||||||
|
{format(new Date(String(version.releasedAt)), "dd-MM-yy")}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div set:html={marked.parse(version.markdown ?? "")} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
|
{data.changelogs?.length === 1 && <div>
|
||||||
|
<h2 class="uppercase text-sm prose tracking-widest">Changelog</h2>
|
||||||
|
{data.changelogs.map((changelog) => {
|
||||||
|
if (changelog.versions && changelog.versions?.length < 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<div class="flex flex-col gap-5 my-3">
|
||||||
|
|
||||||
|
{changelog.versions?.map((version) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-10 bg-white rounded p-3 border border-neutral-300">
|
||||||
|
<div>
|
||||||
|
<h2 class="prose text-3xl font-bold">
|
||||||
|
{version.version}
|
||||||
|
</h2>
|
||||||
|
{version.releasedAt &&
|
||||||
|
|
||||||
|
<p class="prose text-xs text-center">
|
||||||
|
{format(new Date(String(version.releasedAt)), "dd-MM-yy")}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="page" set:html={marked.parse(version.markdown ?? "")} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
127
apps/feedback_widget/src/pages/index.astro
Normal file
127
apps/feedback_widget/src/pages/index.astro
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
import '../style.css'
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
visitorId: '',
|
||||||
|
feedback: '',
|
||||||
|
feedbackId: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Astro.request.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const data = await Astro.request.formData()
|
||||||
|
const feedback = data.get('feedback')
|
||||||
|
const visitorId = data.get('visitor-id')
|
||||||
|
|
||||||
|
console.log(feedback, visitorId, data)
|
||||||
|
// const validation = AccessTokenCreateInput.safeParse({ name })
|
||||||
|
// if (!validation.success) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const response = await apiFetch({
|
||||||
|
// path: 'accessToken',
|
||||||
|
// method: 'post',
|
||||||
|
// body: { name },
|
||||||
|
// token: session.jwt,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (response.status === 201) {
|
||||||
|
// isCreating = true
|
||||||
|
// token = response.json.token
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>Astro</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center justify-center w-full h-screen flex-col">
|
||||||
|
<h1 class="prose prose-2xl">boring.tools</h1>
|
||||||
|
<p class="prose prose-sm">CHANGELOG</p>
|
||||||
|
<div class="">
|
||||||
|
<button popovertarget="my-popover" class="p-2 hover:bg-neutral-900 rounded hover:text-neutral-100 transition">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-vote"><path d="m9 12 2 2 4-4"/><path d="M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z"/><path d="M22 19H2"/></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div popover id="my-popover" class="bg-transparent" >
|
||||||
|
<div class="border rounded-md shadow-md p-1 flex flex-col gap-2 bg-white">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 p-3">
|
||||||
|
<h2 class=" font-bold text-neutral-800">Your feedback matters!</h2>
|
||||||
|
<form method="POST" class="flex flex-col gap-5">
|
||||||
|
|
||||||
|
<textarea class="border rounded p-2" name="feedback" placeholder="Tell us your feedback ..." />
|
||||||
|
<input type="hidden" id="visitor-id" name="visitor-id" />
|
||||||
|
<button class="bg-neutral-900 hover:bg-neutral-800 text-white px-3 py-1 rounded">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-right">
|
||||||
|
powered by <a href="https://boring.tools" class="text-blue-500">boring.tools</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div >
|
||||||
|
|
||||||
|
|
||||||
|
</div> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<script>
|
||||||
|
// 1. get body element
|
||||||
|
// 2. inject custom element absolute positioning
|
||||||
|
// 3. add event listener to close on click outside
|
||||||
|
const body = document.querySelector('body')
|
||||||
|
console.log(body)
|
||||||
|
import print from "@fingerprintjs/fingerprintjs"
|
||||||
|
const visitorIdInput = document.querySelector("#visitor-id");
|
||||||
|
|
||||||
|
if (body instanceof HTMLBodyElement) {
|
||||||
|
|
||||||
|
const btn = body.appendChild(document.createElement('div'))
|
||||||
|
const popover = body.appendChild(document.createElement('div'))
|
||||||
|
btn.innerHTML = `
|
||||||
|
<button popovertarget="my-popover" class="p-2 hover:bg-neutral-900 rounded hover:text-neutral-100 transition">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-vote"><path d="m9 12 2 2 4-4"/><path d="M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z"/><path "M22 19H2"/></svg>
|
||||||
|
</button>`
|
||||||
|
|
||||||
|
popover.innerHTML = `<div popover id="my-popover" class="bg-transparent" >
|
||||||
|
<div class="border rounded-md shadow-md p-1 flex flex-col gap-2 bg-white">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 p-3">
|
||||||
|
<h2 class=" font-bold text-neutral-800">Your feedback matters!</h2>
|
||||||
|
<form method="POST" class="flex flex-col gap-5">
|
||||||
|
|
||||||
|
<textarea class="border rounded p-2" name="feedback" placeholder="Tell us your feedback ..." />
|
||||||
|
<input type="hidden" id="visitor-id" name="visitor-id" />
|
||||||
|
<button class="bg-neutral-900 hover:bg-neutral-800 text-white px-3 py-1 rounded">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
if (visitorIdInput instanceof HTMLInputElement) {
|
||||||
|
print.load()
|
||||||
|
.then(fp => fp.get())
|
||||||
|
.then(result => {
|
||||||
|
const visitorId = result.visitorId
|
||||||
|
visitorIdInput.setAttribute("value", visitorId)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
3
apps/feedback_widget/src/style.css
Normal file
3
apps/feedback_widget/src/style.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
::backdrop {
|
||||||
|
@apply bg-neutral-600 opacity-50;
|
||||||
|
}
|
86
apps/feedback_widget/tailwind.config.mjs
Normal file
86
apps/feedback_widget/tailwind.config.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
|
||||||
|
'../../packages/ui/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||||
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
|
primary: 'hsl(var(--sidebar-primary))',
|
||||||
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
|
border: 'hsl(var(--sidebar-border))',
|
||||||
|
ring: 'hsl(var(--sidebar-ring))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// borderRadius: {
|
||||||
|
// lg: 'var(--radius)',
|
||||||
|
// md: 'calc(var(--radius) - 2px)',
|
||||||
|
// sm: 'calc(var(--radius) - 4px)',
|
||||||
|
// },
|
||||||
|
/* fontFamily: {
|
||||||
|
sans: ['var(--font-sans)', ...fontFamily.sans],
|
||||||
|
}, */
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: { height: '0' },
|
||||||
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
to: { height: '0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
||||||
|
}
|
7
apps/feedback_widget/tsconfig.json
Normal file
7
apps/feedback_widget/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
0
apps/feedback_widget/webcore.config.scss
Normal file
0
apps/feedback_widget/webcore.config.scss
Normal file
@ -24,6 +24,7 @@
|
|||||||
"formatter": {
|
"formatter": {
|
||||||
"jsxQuoteStyle": "double",
|
"jsxQuoteStyle": "double",
|
||||||
"quoteProperties": "asNeeded",
|
"quoteProperties": "asNeeded",
|
||||||
|
"trailingCommas": "all",
|
||||||
"semicolons": "asNeeded",
|
"semicolons": "asNeeded",
|
||||||
"arrowParentheses": "always",
|
"arrowParentheses": "always",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:17-alpine
|
|
||||||
container_name: boring_services_postgres
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=postgres
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_DB=postgres
|
|
||||||
|
|
||||||
cache:
|
|
||||||
container_name: boring_services_redis
|
|
||||||
image: redis:7.4.1-alpine
|
|
||||||
ports:
|
|
||||||
- '6379:6379'
|
|
||||||
command: redis-server --save 20 1 --loglevel warning --requirepass development
|
|
||||||
|
|
||||||
loki:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
container_name: boring_services_loki
|
|
||||||
ports:
|
|
||||||
- "9100:3100"
|
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
container_name: boring_services_grafana
|
|
||||||
environment:
|
|
||||||
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
|
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
||||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
|
||||||
volumes:
|
|
||||||
- ./grafana/dashboard.yaml:/etc/grafana/provisioning/dashboards/main.yaml
|
|
||||||
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
|
||||||
- ./grafana/datasources:/etc/grafana/provisioning/datasources
|
|
||||||
# entrypoint:
|
|
||||||
# - sh
|
|
||||||
# - -euc
|
|
||||||
# - |
|
|
||||||
# mkdir -p /etc/grafana/provisioning/datasources
|
|
||||||
# cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
|
|
||||||
# apiVersion: 1
|
|
||||||
# datasources:
|
|
||||||
# - name: Loki
|
|
||||||
# type: loki
|
|
||||||
# access: proxy
|
|
||||||
# orgId: 1
|
|
||||||
# url: http://loki:3100
|
|
||||||
# basicAuth: false
|
|
||||||
# isDefault: true
|
|
||||||
# version: 1
|
|
||||||
# editable: false
|
|
||||||
# EOF
|
|
||||||
# /run.sh
|
|
||||||
ports:
|
|
||||||
- "9000:3000"
|
|
@ -1,12 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
providers:
|
|
||||||
- name: "Dashboard provider"
|
|
||||||
orgId: 1
|
|
||||||
type: file
|
|
||||||
disableDeletion: false
|
|
||||||
updateIntervalSeconds: 10
|
|
||||||
allowUiUpdates: false
|
|
||||||
options:
|
|
||||||
path: /var/lib/grafana/dashboards
|
|
||||||
foldersFromFilesStructure: true
|
|
@ -1,546 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": 1,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"collapsed": false,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 1,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 6,
|
|
||||||
"panels": [],
|
|
||||||
"title": "Service: API",
|
|
||||||
"type": "row"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "points",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "smooth",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"spanNulls": 5000,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "#73BF69",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 7,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"id": 11,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status <= 399 [$__auto]))",
|
|
||||||
"legendFormat": "<= 300",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status >= 400 and status <= 499 [$__auto]))",
|
|
||||||
"hide": false,
|
|
||||||
"legendFormat": "400 - 499",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "B"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status >= 500 [$__auto]))",
|
|
||||||
"hide": false,
|
|
||||||
"legendFormat": ">= 500",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "C"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Requests",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 0,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 10,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status <= 399 [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "<= 399",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "yellow",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 4,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status >= 400 and status <= 499 [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "400 - 499",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 8,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | status >= 500 [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "400 - 499",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "blue",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 0,
|
|
||||||
"y": 16
|
|
||||||
},
|
|
||||||
"id": 7,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | level = `info` [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Info",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "yellow",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 4,
|
|
||||||
"y": 16
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | level = `warn` [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Warning",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 4,
|
|
||||||
"x": 8,
|
|
||||||
"y": 16
|
|
||||||
},
|
|
||||||
"id": 9,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "value",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "auto",
|
|
||||||
"orientation": "auto",
|
|
||||||
"percentChangeColorMode": "standard",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": ["sum"],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "count(rate({service=\"api\"} | json | level = `error` [$__auto]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Error",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 17,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 24
|
|
||||||
},
|
|
||||||
"id": 5,
|
|
||||||
"options": {
|
|
||||||
"dedupStrategy": "none",
|
|
||||||
"enableLogDetails": true,
|
|
||||||
"prettifyLogMessage": false,
|
|
||||||
"showCommonLabels": false,
|
|
||||||
"showLabels": false,
|
|
||||||
"showTime": true,
|
|
||||||
"sortOrder": "Descending",
|
|
||||||
"wrapLogMessage": false
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.3.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "{service=\"api\"} | json",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Logs",
|
|
||||||
"type": "logs"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"preload": false,
|
|
||||||
"schemaVersion": 40,
|
|
||||||
"tags": [],
|
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-1h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "Logging",
|
|
||||||
"uid": "be3r0x46iwlq8f",
|
|
||||||
"version": 2,
|
|
||||||
"weekStart": "monday"
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
providers:
|
|
||||||
- name: "Dashboard provider"
|
|
||||||
orgId: 1
|
|
||||||
type: file
|
|
||||||
disableDeletion: false
|
|
||||||
updateIntervalSeconds: 10
|
|
||||||
allowUiUpdates: false
|
|
||||||
options:
|
|
||||||
path: /var/lib/grafana/dashboards
|
|
||||||
foldersFromFilesStructure: true
|
|
@ -1,11 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
datasources:
|
|
||||||
- name: Loki
|
|
||||||
type: loki
|
|
||||||
access: proxy
|
|
||||||
orgId: 1
|
|
||||||
url: http://loki:3100
|
|
||||||
basicAuth: false
|
|
||||||
isDefault: true
|
|
||||||
version: 1
|
|
||||||
editable: false
|
|
58
docker-compose.yaml
Normal file
58
docker-compose.yaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: boring_postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
|
||||||
|
cache:
|
||||||
|
container_name: boring_redis
|
||||||
|
image: redis:7.4.1-alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
# command: redis-server --save 20 1 --loglevel warning --requirepass development
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
ports:
|
||||||
|
- "9100:3100"
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
|
||||||
|
promtail:
|
||||||
|
image: grafana/promtail:latest
|
||||||
|
volumes:
|
||||||
|
- /var/log:/var/log
|
||||||
|
command: -config.file=/etc/promtail/config.yml
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
environment:
|
||||||
|
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
|
||||||
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||||
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
||||||
|
entrypoint:
|
||||||
|
- sh
|
||||||
|
- -euc
|
||||||
|
- |
|
||||||
|
mkdir -p /etc/grafana/provisioning/datasources
|
||||||
|
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
|
||||||
|
apiVersion: 1
|
||||||
|
datasources:
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://loki:3100
|
||||||
|
basicAuth: false
|
||||||
|
isDefault: true
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
|
EOF
|
||||||
|
/run.sh
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- "9000:3000"
|
@ -2,5 +2,5 @@ pre-commit:
|
|||||||
commands:
|
commands:
|
||||||
check:
|
check:
|
||||||
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,astro}"
|
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,astro}"
|
||||||
run: /run/current-system/sw/bin/biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||||
stage_fixed: true
|
stage_fixed: true
|
10
package.json
10
package.json
@ -6,9 +6,6 @@
|
|||||||
"check:apps": "bunx biome check --write --config-path ./biome.json ./apps",
|
"check:apps": "bunx biome check --write --config-path ./biome.json ./apps",
|
||||||
"check:packages": "bunx biome check --write --config-path ./biome.json ./packages",
|
"check:packages": "bunx biome check --write --config-path ./biome.json ./packages",
|
||||||
"dev": "bun --filter '*' dev",
|
"dev": "bun --filter '*' dev",
|
||||||
"dev:docker:up": "docker compose -f ci/development/docker-compose.yaml up -d",
|
|
||||||
"dev:docker:down": "docker compose -f ci/development/docker-compose.yaml down",
|
|
||||||
"dev:docker:stop": "docker compose -f ci/development/docker-compose.yaml stop",
|
|
||||||
"build": "bun --filter '*' build",
|
"build": "bun --filter '*' build",
|
||||||
"db:generate": "bun --filter '@boring.tools/database' db:generate",
|
"db:generate": "bun --filter '@boring.tools/database' db:generate",
|
||||||
"db:migrate": "bun --filter '@boring.tools/database' db:migrate",
|
"db:migrate": "bun --filter '@boring.tools/database' db:migrate",
|
||||||
@ -29,9 +26,12 @@
|
|||||||
"docker:page": "bun docker:page:build && bun docker:page:push"
|
"docker:page": "bun docker:page:build && bun docker:page:push"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.29",
|
"packageManager": "bun@1.1.29",
|
||||||
"workspaces": ["apps/*", "packages/*"],
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.8.3",
|
||||||
"lefthook": "^1.7.15"
|
"lefthook": "^1.7.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
"@logtail/node": "^0.5.0",
|
"@logtail/node": "^0.5.0",
|
||||||
"@logtail/winston": "^0.5.0",
|
"@logtail/winston": "^0.5.0",
|
||||||
"winston": "^3.14.2",
|
"winston": "^3.14.2",
|
||||||
"winston-console-format": "^1.0.8",
|
|
||||||
"winston-loki": "^6.1.3"
|
"winston-loki": "^6.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,43 +1,20 @@
|
|||||||
import { Logtail } from '@logtail/node'
|
import { Logtail } from '@logtail/node'
|
||||||
import { LogtailTransport } from '@logtail/winston'
|
import { LogtailTransport } from '@logtail/winston'
|
||||||
import { createLogger, format, transports } from 'winston'
|
import winston from 'winston'
|
||||||
import { consoleFormat } from 'winston-console-format'
|
|
||||||
import LokiTransport from 'winston-loki'
|
import LokiTransport from 'winston-loki'
|
||||||
|
|
||||||
// Create a Winston logger - passing in the Logtail transport
|
// Create a Winston logger - passing in the Logtail transport
|
||||||
export const logger = createLogger({
|
export const logger = winston.createLogger({
|
||||||
format: format.combine(
|
format: winston.format.json(),
|
||||||
format.timestamp(),
|
|
||||||
format.ms(),
|
|
||||||
format.errors({ stack: true }),
|
|
||||||
format.splat(),
|
|
||||||
format.json(),
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
new transports.Console({
|
new winston.transports.Console({
|
||||||
silent: import.meta.env.NODE_ENV === 'test',
|
format: winston.format.json(),
|
||||||
format: format.combine(
|
|
||||||
format.colorize({ all: true }),
|
|
||||||
format.padLevels(),
|
|
||||||
consoleFormat({
|
|
||||||
showMeta: true,
|
|
||||||
metaStrip: ['timestamp', 'service'],
|
|
||||||
inspectOptions: {
|
|
||||||
depth: Number.POSITIVE_INFINITY,
|
|
||||||
colors: true,
|
|
||||||
maxArrayLength: Number.POSITIVE_INFINITY,
|
|
||||||
breakLength: 120,
|
|
||||||
compact: Number.POSITIVE_INFINITY,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
new LokiTransport({
|
new LokiTransport({
|
||||||
silent: import.meta.env.NODE_ENV === 'test',
|
|
||||||
host: 'http://localhost:9100',
|
host: 'http://localhost:9100',
|
||||||
|
labels: { app: 'api' },
|
||||||
json: true,
|
json: true,
|
||||||
labels: { service: import.meta.env.SERVICE_NAME ?? 'unknown' },
|
format: winston.format.json(),
|
||||||
format: format.json(),
|
|
||||||
replaceTimestamp: true,
|
replaceTimestamp: true,
|
||||||
onConnectionError: (err) => console.error(err),
|
onConnectionError: (err) => console.error(err),
|
||||||
}),
|
}),
|
||||||
|
@ -7,8 +7,8 @@ export const PageOutput = z
|
|||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional(),
|
||||||
icon: z.string().optional().nullable(),
|
icon: z.string(),
|
||||||
changelogs: z.array(ChangelogOutput).optional(),
|
changelogs: z.array(ChangelogOutput).optional(),
|
||||||
})
|
})
|
||||||
.openapi('Page')
|
.openapi('Page')
|
||||||
|
@ -5,10 +5,10 @@ export const PageCreateInput = z
|
|||||||
title: z.string().min(3).openapi({
|
title: z.string().min(3).openapi({
|
||||||
example: 'My page',
|
example: 'My page',
|
||||||
}),
|
}),
|
||||||
description: z.string().optional().nullable().openapi({
|
description: z.string().optional().openapi({
|
||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
icon: z.string().optional().nullable().openapi({
|
icon: z.string().optional().openapi({
|
||||||
example: 'base64...',
|
example: 'base64...',
|
||||||
}),
|
}),
|
||||||
changelogIds: z.array(z.string().uuid()),
|
changelogIds: z.array(z.string().uuid()),
|
||||||
|
@ -2,23 +2,21 @@ import { z } from '@hono/zod-openapi'
|
|||||||
|
|
||||||
export const PagePublicOutput = z.object({
|
export const PagePublicOutput = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string(),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
changelogs: z
|
changelogs: z.array(
|
||||||
.array(
|
z.object({
|
||||||
z.object({
|
title: z.string(),
|
||||||
title: z.string(),
|
description: z.string(),
|
||||||
description: z.string(),
|
versions: z.array(
|
||||||
versions: z.array(
|
z.object({
|
||||||
z.object({
|
markdown: z.string(),
|
||||||
markdown: z.string(),
|
version: z.string(),
|
||||||
version: z.string(),
|
releasedAt: z.date(),
|
||||||
releasedAt: z.date(),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
}),
|
||||||
}),
|
),
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PagePublicParams = z.object({
|
export const PagePublicParams = z.object({
|
||||||
|
@ -7,10 +7,10 @@ export const PageUpdateInput = z
|
|||||||
title: z.string().min(3).optional().openapi({
|
title: z.string().min(3).optional().openapi({
|
||||||
example: 'My page',
|
example: 'My page',
|
||||||
}),
|
}),
|
||||||
description: z.string().nullable().optional().openapi({
|
description: z.string().optional().openapi({
|
||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
icon: z.string().optional().nullable().openapi({
|
icon: z.string().optional().openapi({
|
||||||
example: 'base64...',
|
example: 'base64...',
|
||||||
}),
|
}),
|
||||||
changelogIds: z.array(z.string().uuid()).optional(),
|
changelogIds: z.array(z.string().uuid()).optional(),
|
||||||
|
Loading…
Reference in New Issue
Block a user