feat: add dashboard statistics
This commit is contained in:
parent
af2d0ec4aa
commit
81baf571f9
@ -9,6 +9,7 @@ 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 { authentication } from './utils/authentication'
|
import { authentication } from './utils/authentication'
|
||||||
import { handleError, handleZodError } from './utils/errors'
|
import { handleError, handleZodError } from './utils/errors'
|
||||||
import { startup } from './utils/startup'
|
import { startup } from './utils/startup'
|
||||||
@ -37,6 +38,7 @@ 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)
|
||||||
|
app.route('/v1/statistic', statisticApi)
|
||||||
|
|
||||||
app.doc('/openapi.json', {
|
app.doc('/openapi.json', {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.0',
|
||||||
|
113
apps/api/src/statistic/get.ts
Normal file
113
apps/api/src/statistic/get.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { changelog, db, page } from '@boring.tools/database'
|
||||||
|
import { StatisticOutput } from '@boring.tools/schema'
|
||||||
|
import { createRoute } from '@hono/zod-openapi'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import type { statisticApi } from '.'
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: '/',
|
||||||
|
tags: ['statistic'],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': { schema: StatisticOutput },
|
||||||
|
},
|
||||||
|
description: 'Return user',
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Bad Request',
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerStatisticGet = (api: typeof statisticApi) => {
|
||||||
|
return api.openapi(route, async (c) => {
|
||||||
|
const userId = verifyAuthentication(c)
|
||||||
|
const pageResult = await db.query.page.findMany({
|
||||||
|
where: eq(page.userId, userId),
|
||||||
|
})
|
||||||
|
const result = await db.query.changelog.findMany({
|
||||||
|
where: eq(changelog.userId, userId),
|
||||||
|
with: {
|
||||||
|
commits: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
versionId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const changelog_total = result.length
|
||||||
|
const version_total = result.reduce(
|
||||||
|
(acc, log) => acc + log.versions.length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const version_published = result.reduce(
|
||||||
|
(acc, log) =>
|
||||||
|
acc +
|
||||||
|
log.versions.filter((version) => version.status === 'published').length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const version_review = result.reduce(
|
||||||
|
(acc, log) =>
|
||||||
|
acc +
|
||||||
|
log.versions.filter((version) => version.status === 'review').length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const version_draft = result.reduce(
|
||||||
|
(acc, log) =>
|
||||||
|
acc +
|
||||||
|
log.versions.filter((version) => version.status === 'draft').length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const commit_total = result.reduce(
|
||||||
|
(acc, log) => acc + log.commits.length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const commit_unassigned = result.reduce(
|
||||||
|
(acc, log) =>
|
||||||
|
acc + log.commits.filter((commit) => !commit.versionId).length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const commit_assigned = result.reduce(
|
||||||
|
(acc, log) =>
|
||||||
|
acc + log.commits.filter((commit) => commit.versionId).length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const mappedData = {
|
||||||
|
changelog: {
|
||||||
|
total: changelog_total,
|
||||||
|
versions: {
|
||||||
|
total: version_total,
|
||||||
|
published: version_published,
|
||||||
|
review: version_review,
|
||||||
|
draft: version_draft,
|
||||||
|
},
|
||||||
|
commits: {
|
||||||
|
total: commit_total,
|
||||||
|
unassigned: commit_unassigned,
|
||||||
|
assigned: commit_assigned,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
total: pageResult.length,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(mappedData, 200)
|
||||||
|
})
|
||||||
|
}
|
14
apps/api/src/statistic/index.ts
Normal file
14
apps/api/src/statistic/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
import type { Variables } from '..'
|
||||||
|
import type { ContextModule } from '../utils/sentry'
|
||||||
|
import { registerStatisticGet } from './get'
|
||||||
|
|
||||||
|
export const statisticApi = new OpenAPIHono<{ Variables: Variables }>()
|
||||||
|
|
||||||
|
const module: ContextModule = {
|
||||||
|
name: 'statistic',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStatisticGet(statisticApi)
|
||||||
|
|
||||||
|
export default statisticApi
|
20
apps/app/src/hooks/useStatistic.ts
Normal file
20
apps/app/src/hooks/useStatistic.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { StatisticOutput } from '@boring.tools/schema'
|
||||||
|
import { useAuth } from '@clerk/clerk-react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import type { z } from 'zod'
|
||||||
|
import { queryFetch } from '../utils/queryFetch'
|
||||||
|
|
||||||
|
type Statistic = z.infer<typeof StatisticOutput>
|
||||||
|
|
||||||
|
export const useStatistic = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['statistic'],
|
||||||
|
queryFn: async (): Promise<Readonly<Statistic>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: 'statistic',
|
||||||
|
method: 'get',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
@ -1,13 +1,71 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
|
||||||
|
import { useUser } from '@clerk/clerk-react'
|
||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||||
|
import { PageWrapper } from '../components/PageWrapper'
|
||||||
|
import { useStatistic } from '../hooks/useStatistic'
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/')({
|
const Component = () => {
|
||||||
component: Index,
|
const { data } = useStatistic()
|
||||||
})
|
const user = useUser()
|
||||||
|
|
||||||
function Index() {
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<PageWrapper
|
||||||
<h3>Welcome Home!</h3>
|
breadcrumbs={[
|
||||||
</div>
|
{
|
||||||
|
name: 'Dashboard',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl">Welcome back, {user.user?.fullName}</h1>
|
||||||
|
<div className="grid w-full max-w-screen-md gap-10 grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Changelogs</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">
|
||||||
|
{data?.changelog.total}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Versions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">
|
||||||
|
{data?.changelog.versions.total}
|
||||||
|
<div className="text-xs text-muted-foreground tracking-normal flex gap-3">
|
||||||
|
<span>{data?.changelog.versions.published} Published</span>
|
||||||
|
<span>{data?.changelog.versions.review} Review</span>
|
||||||
|
<span>{data?.changelog.versions.draft} Draft</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Commits</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">
|
||||||
|
{data?.changelog.commits.total}
|
||||||
|
<div className="text-xs text-muted-foreground tracking-normal flex gap-3">
|
||||||
|
<span>{data?.changelog.commits.assigned} Assigned</span>
|
||||||
|
<span>{data?.changelog.commits.unassigned} Unassigned</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pages</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-bold">
|
||||||
|
{data?.page.total}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export const Route = createLazyFileRoute('/')({
|
||||||
|
component: Component,
|
||||||
|
})
|
||||||
|
@ -1 +1 @@
|
|||||||
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
|
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/AccessToken/Delete.tsx","./src/components/AccessToken/Table/Columns.tsx","./src/components/AccessToken/Table/DataTable.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useAccessToken.ts","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/access-tokens.index.lazy.tsx","./src/routes/access-tokens.new.lazy.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/cli.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
|
15
bruno/Statistic.bru
Normal file
15
bruno/Statistic.bru
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: Statistic
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{API_URL}}/v1/statistic
|
||||||
|
body: none
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: bt_7b83481e2ae0f6ab728730511adbe491f6668eb3
|
||||||
|
}
|
@ -5,3 +5,4 @@ export * from './version'
|
|||||||
export * from './page'
|
export * from './page'
|
||||||
export * from './commit'
|
export * from './commit'
|
||||||
export * from './access-token'
|
export * from './access-token'
|
||||||
|
export * from './statistic'
|
||||||
|
23
packages/schema/src/statistic/base.ts
Normal file
23
packages/schema/src/statistic/base.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export const StatisticOutput = z
|
||||||
|
.object({
|
||||||
|
changelog: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
commits: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
unassigned: z.number(),
|
||||||
|
assigned: z.number(),
|
||||||
|
}),
|
||||||
|
versions: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
published: z.number(),
|
||||||
|
review: z.number(),
|
||||||
|
draft: z.number(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
page: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.openapi('Statistic')
|
1
packages/schema/src/statistic/index.ts
Normal file
1
packages/schema/src/statistic/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './base'
|
Loading…
Reference in New Issue
Block a user