From facfa05e6a74fbcbf3a9acbf87ca94cacdf9f72d Mon Sep 17 00:00:00 2001 From: Ray-D-Song Date: Tue, 29 Oct 2024 17:34:24 +0800 Subject: [PATCH 1/7] feat: pie chart --- .github/workflows/preview-service.yml | 1 - package.json | 1 + packages/server/src/api/data.ts | 14 + packages/server/src/api/pages.ts | 7 +- packages/server/src/model/data.ts | 29 ++ packages/server/src/model/page.ts | 19 + packages/server/src/server.ts | 2 + packages/shared/components/chart.tsx | 365 ++++++++++++++++++ packages/shared/global.css | 22 +- packages/web/src/components/card-view.tsx | 9 +- .../web/src/components/total-pie-card.tsx | 115 ++++++ packages/web/src/data/data.ts | 20 + packages/web/src/data/page.ts | 7 + packages/web/src/pages/(layout)/index.tsx | 14 +- pnpm-lock.yaml | 266 +++++++++++++ 15 files changed, 875 insertions(+), 16 deletions(-) create mode 100644 packages/server/src/api/data.ts create mode 100644 packages/server/src/model/data.ts create mode 100644 packages/shared/components/chart.tsx create mode 100644 packages/web/src/components/total-pie-card.tsx create mode 100644 packages/web/src/data/data.ts diff --git a/.github/workflows/preview-service.yml b/.github/workflows/preview-service.yml index a40a755..8d2ee3d 100644 --- a/.github/workflows/preview-service.yml +++ b/.github/workflows/preview-service.yml @@ -6,7 +6,6 @@ on: - dev - main - hotfix - - new_card jobs: deploy: diff --git a/package.json b/package.json index b601347..19e9177 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-error-boundary": "^4.0.13", "react-hook-form": "^7.53.1", "react-hot-toast": "^2.4.1", + "recharts": "^2.13.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" diff --git a/packages/server/src/api/data.ts b/packages/server/src/api/data.ts new file mode 100644 index 0000000..d8f3e45 --- /dev/null +++ b/packages/server/src/api/data.ts @@ -0,0 +1,14 @@ +import { Hono } from 'hono' +import type { HonoTypeUserInformation } from '~/constants/binding' +import { getHomeChartData } from '~/model/data' +import result from '~/utils/result' + +const app = new Hono() + +app.get('/home_chart', async (c) => { + const data = await getHomeChartData(c.env.DB) + + return c.json(result.success(data)) +}) + +export default app diff --git a/packages/server/src/api/pages.ts b/packages/server/src/api/pages.ts index 47a51ec..835fac8 100644 --- a/packages/server/src/api/pages.ts +++ b/packages/server/src/api/pages.ts @@ -3,7 +3,7 @@ import { validator } from 'hono/validator' import { isNil, isNotNil, isNumberString } from '@web-archive/shared/utils' import type { HonoTypeUserInformation } from '~/constants/binding' import result from '~/utils/result' -import { clearDeletedPage, deletePageById, getPageById, insertPage, queryDeletedPage, queryPage, restorePage, selectPageTotalCount } from '~/model/page' +import { clearDeletedPage, deletePageById, getPageById, insertPage, queryDeletedPage, queryPage, queryRecentSavePage, restorePage, selectPageTotalCount } from '~/model/page' import { getFolderById, restoreFolder } from '~/model/folder' import { getFileFromBucket, saveFileToBucket } from '~/utils/file' import type { Page } from '~/sql/types' @@ -103,6 +103,11 @@ app.post( }, ) +app.get('/recent_save', async (c) => { + const pages = await queryRecentSavePage(c.env.DB) + return c.json(result.success(pages)) +}) + app.get( '/detail', validator('query', (value, c) => { diff --git a/packages/server/src/model/data.ts b/packages/server/src/model/data.ts new file mode 100644 index 0000000..ded6faa --- /dev/null +++ b/packages/server/src/model/data.ts @@ -0,0 +1,29 @@ +import type { D1Database } from '@cloudflare/workers-types/experimental' +import { selectAllPageCount, selectPageTotalCount } from './page' +import { selectAllFolders } from './folder' + +async function getHomeChartData(DB: D1Database) { + const folderList = await selectAllFolders(DB) + const folderPageCountList = await Promise.all(folderList.map(async (folder) => { + const pageCount = await selectPageTotalCount(DB, { folderId: folder.id }) + return { + id: folder.id, + name: folder.name, + pageCount: pageCount as number, + } + })) + + const sortedFolderPageCountList = folderPageCountList + .sort((a, b) => b.pageCount - a.pageCount) + .slice(0, 5) + + const allPageCount = await selectAllPageCount(DB) + return { + folders: sortedFolderPageCountList, + all: allPageCount, + } +} + +export { + getHomeChartData, +} diff --git a/packages/server/src/model/page.ts b/packages/server/src/model/page.ts index 9108785..37663bd 100644 --- a/packages/server/src/model/page.ts +++ b/packages/server/src/model/page.ts @@ -16,6 +16,15 @@ async function selectPageTotalCount(DB: D1Database, options: { folderId: number, return result.count } +async function selectAllPageCount(DB: D1Database) { + const sql = ` + SELECT COUNT(*) as count FROM pages + WHERE isDeleted = 0 + ` + const result = await DB.prepare(sql).first() + return result.count +} + async function queryPage(DB: D1Database, options: { folderId: number, pageNumber?: number, pageSize?: number, keyword?: string }) { const { folderId, pageNumber, pageSize, keyword } = options let sql = ` @@ -151,6 +160,14 @@ async function clearDeletedPage(DB: D1Database) { return result.success } +async function queryRecentSavePage(DB: D1Database) { + const sql = ` + SELECT * FROM pages WHERE isDeleted = 0 ORDER BY createdAt DESC LIMIT 20 + ` + const result = await DB.prepare(sql).all() + return result.results +} + export { selectPageTotalCount, queryPage, @@ -161,4 +178,6 @@ export { getPageById, insertPage, clearDeletedPage, + queryRecentSavePage, + selectAllPageCount, } diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index cf92c39..31db48f 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono' import type { Bindings, HonoTypeUserInformation } from './constants/binding' import tokenMiddleware from './middleware/token' +import data from './api/data' import showcase from '~/api/showcase' import pages from '~/api/pages' import auth from '~/api/auth' @@ -35,6 +36,7 @@ api.use(tokenMiddleware) api.route('/pages', pages) api.route('/auth', auth) api.route('/folders', folders) +api.route('/data', data) app.route('/api', api) export default app diff --git a/packages/shared/components/chart.tsx b/packages/shared/components/chart.tsx new file mode 100644 index 0000000..94803ae --- /dev/null +++ b/packages/shared/components/chart.tsx @@ -0,0 +1,365 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "../utils/helper" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +