diff --git a/packages/client/package.json b/packages/client/package.json
index e10751f..0f2b728 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -14,18 +14,24 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
+ "@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
+ "@radix-ui/react-scroll-area": "^1.2.0",
+ "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-virtual": "^3.10.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dbtype": "workspace:dbtype",
"jotai": "^2.10.0",
+ "lucide-react": "^0.451.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-resizable-panels": "^2.1.4",
+ "recharts": "^2.12.7",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
@@ -47,6 +53,7 @@
"shadcn-ui": "^0.8.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
- "vite": "^5.4.8"
+ "vite": "^5.4.8",
+ "vitest": "^2.1.2"
}
}
diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx
index 45a6fe5..78e992f 100644
--- a/packages/client/src/App.tsx
+++ b/packages/client/src/App.tsx
@@ -15,6 +15,7 @@ import ContentInfoPage from "@/page/contentInfoPage.tsx";
import SettingPage from "@/page/settingPage.tsx";
import ComicPage from "@/page/reader/comicPage.tsx";
import DifferencePage from "./page/differencePage.tsx";
+import TagsPage from "./page/tagsPage.tsx";
const App = () => {
const { isDarkMode } = useTernaryDarkMode();
@@ -40,6 +41,7 @@ const App = () => {
+
diff --git a/packages/client/src/components/layout/nav.tsx b/packages/client/src/components/layout/nav.tsx
index 88f1c37..89da992 100644
--- a/packages/client/src/components/layout/nav.tsx
+++ b/packages/client/src/components/layout/nav.tsx
@@ -1,5 +1,5 @@
import { Link } from "wouter"
-import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons"
+import { Search, Settings, Tags, ArchiveIcon, UserIcon } from "lucide-react"
import { Button, buttonVariants } from "@/components/ui/button.tsx"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx"
import { useLogin } from "@/state/user.ts";
@@ -66,13 +66,13 @@ export function NavList() {
return
}
\ No newline at end of file
diff --git a/packages/client/src/components/ui/chart.tsx b/packages/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..b58b494
--- /dev/null
+++ b/packages/client/src/components/ui/chart.tsx
@@ -0,0 +1,368 @@
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+import {
+ NameType,
+ Payload,
+ ValueType,
+} from "recharts/types/component/DefaultTooltipContent"
+
+import { cn } from "@/lib/utils"
+
+// 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 (
+