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 ( +