From 81baf571f9921dfb6b1088853ca54ab94621e3a7 Mon Sep 17 00:00:00 2001 From: Lars Hampe Date: Thu, 31 Oct 2024 14:58:06 +0100 Subject: [PATCH] feat: add dashboard statistics --- apps/api/src/index.ts | 2 + apps/api/src/statistic/get.ts | 113 +++++++++++++++++++++++++ apps/api/src/statistic/index.ts | 14 +++ apps/app/src/hooks/useStatistic.ts | 20 +++++ apps/app/src/routes/index.lazy.tsx | 74 ++++++++++++++-- apps/app/tsconfig.tsbuildinfo | 2 +- bruno/Statistic.bru | 15 ++++ packages/schema/src/index.ts | 1 + packages/schema/src/statistic/base.ts | 23 +++++ packages/schema/src/statistic/index.ts | 1 + 10 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/statistic/get.ts create mode 100644 apps/api/src/statistic/index.ts create mode 100644 apps/app/src/hooks/useStatistic.ts create mode 100644 bruno/Statistic.bru create mode 100644 packages/schema/src/statistic/base.ts create mode 100644 packages/schema/src/statistic/index.ts diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 188dac3..38d393d 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -9,6 +9,7 @@ import user from './user' import { accessTokenApi } from './access-token' import pageApi from './page' +import statisticApi from './statistic' import { authentication } from './utils/authentication' import { handleError, handleZodError } from './utils/errors' import { startup } from './utils/startup' @@ -37,6 +38,7 @@ app.route('/v1/user', user) app.route('/v1/changelog', changelog) app.route('/v1/page', pageApi) app.route('/v1/access-token', accessTokenApi) +app.route('/v1/statistic', statisticApi) app.doc('/openapi.json', { openapi: '3.0.0', diff --git a/apps/api/src/statistic/get.ts b/apps/api/src/statistic/get.ts new file mode 100644 index 0000000..06175b5 --- /dev/null +++ b/apps/api/src/statistic/get.ts @@ -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) + }) +} diff --git a/apps/api/src/statistic/index.ts b/apps/api/src/statistic/index.ts new file mode 100644 index 0000000..036cfbb --- /dev/null +++ b/apps/api/src/statistic/index.ts @@ -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 diff --git a/apps/app/src/hooks/useStatistic.ts b/apps/app/src/hooks/useStatistic.ts new file mode 100644 index 0000000..df14ba9 --- /dev/null +++ b/apps/app/src/hooks/useStatistic.ts @@ -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 + +export const useStatistic = () => { + const { getToken } = useAuth() + return useQuery({ + queryKey: ['statistic'], + queryFn: async (): Promise> => + await queryFetch({ + path: 'statistic', + method: 'get', + token: await getToken(), + }), + }) +} diff --git a/apps/app/src/routes/index.lazy.tsx b/apps/app/src/routes/index.lazy.tsx index f8f08fe..7597afd 100644 --- a/apps/app/src/routes/index.lazy.tsx +++ b/apps/app/src/routes/index.lazy.tsx @@ -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 { PageWrapper } from '../components/PageWrapper' +import { useStatistic } from '../hooks/useStatistic' -export const Route = createLazyFileRoute('/')({ - component: Index, -}) - -function Index() { +const Component = () => { + const { data } = useStatistic() + const user = useUser() return ( -
-

Welcome Home!

-
+ +

Welcome back, {user.user?.fullName}

+
+ + + Changelogs + + + {data?.changelog.total} + + + + + + Versions + + + {data?.changelog.versions.total} +
+ {data?.changelog.versions.published} Published + {data?.changelog.versions.review} Review + {data?.changelog.versions.draft} Draft +
+
+
+ + + + Commits + + + {data?.changelog.commits.total} +
+ {data?.changelog.commits.assigned} Assigned + {data?.changelog.commits.unassigned} Unassigned +
+
+
+ + + + Pages + + + {data?.page.total} + + +
+
) } +export const Route = createLazyFileRoute('/')({ + component: Component, +}) diff --git a/apps/app/tsconfig.tsbuildinfo b/apps/app/tsconfig.tsbuildinfo index 09a0137..05c419c 100644 --- a/apps/app/tsconfig.tsbuildinfo +++ b/apps/app/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/bruno/Statistic.bru b/bruno/Statistic.bru new file mode 100644 index 0000000..b12472d --- /dev/null +++ b/bruno/Statistic.bru @@ -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 +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index f95f5ba..0af8c75 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -5,3 +5,4 @@ export * from './version' export * from './page' export * from './commit' export * from './access-token' +export * from './statistic' diff --git a/packages/schema/src/statistic/base.ts b/packages/schema/src/statistic/base.ts new file mode 100644 index 0000000..35fe4f6 --- /dev/null +++ b/packages/schema/src/statistic/base.ts @@ -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') diff --git a/packages/schema/src/statistic/index.ts b/packages/schema/src/statistic/index.ts new file mode 100644 index 0000000..85e5652 --- /dev/null +++ b/packages/schema/src/statistic/index.ts @@ -0,0 +1 @@ +export * from './base'