From 6503980cbc29efc8384f3faf687a4355335b1bfb Mon Sep 17 00:00:00 2001 From: bernard-ng Date: Thu, 13 Nov 2025 11:25:07 +0200 Subject: [PATCH] feat(dashboard): list sources with statistics --- apps/api/src/trpc/routers/sources.ts | 2 +- apps/dashboard/package.json | 1 + .../(app)/(sidebar)/articles/page.tsx | 6 + .../app/[locale]/(app)/(sidebar)/layout.tsx | 30 +- .../[locale]/(app)/(sidebar)/sources/page.tsx | 30 +- .../src/components/shell/page-header.tsx | 54 +++ .../src/components/shell/page-layout.tsx | 31 ++ .../src/components/shell/page-skeleton.tsx | 97 ++++++ .../{nav-main.tsx => app-sidebar-content.tsx} | 2 +- .../components/sidebar/app-sidebar-info.tsx | 26 ++ .../{nav-user.tsx => app-sidebar-user.tsx} | 26 +- .../src/components/sidebar/app-sidebar.tsx | 147 +------- .../src/components/sidebar/nav-projects.tsx | 82 ----- .../src/components/sidebar/team-switcher.tsx | 88 ----- apps/dashboard/src/components/source-card.tsx | 191 +++++++++++ apps/dashboard/src/trpc/server.tsx | 4 +- biome.json | 5 +- bun.lock | 73 ++++ packages/db/package.json | 1 + packages/db/src/constants.ts | 13 +- packages/db/src/queries/sources.ts | 122 ++++++- packages/ui/package.json | 1 + packages/ui/src/components/badge.tsx | 38 +++ packages/ui/src/components/chart.tsx | 319 ++++++++++++++++++ 24 files changed, 1016 insertions(+), 373 deletions(-) create mode 100644 apps/dashboard/src/components/shell/page-header.tsx create mode 100644 apps/dashboard/src/components/shell/page-layout.tsx create mode 100644 apps/dashboard/src/components/shell/page-skeleton.tsx rename apps/dashboard/src/components/sidebar/{nav-main.tsx => app-sidebar-content.tsx} (98%) create mode 100644 apps/dashboard/src/components/sidebar/app-sidebar-info.tsx rename apps/dashboard/src/components/sidebar/{nav-user.tsx => app-sidebar-user.tsx} (77%) delete mode 100644 apps/dashboard/src/components/sidebar/nav-projects.tsx delete mode 100644 apps/dashboard/src/components/sidebar/team-switcher.tsx create mode 100644 apps/dashboard/src/components/source-card.tsx create mode 100644 packages/ui/src/components/badge.tsx create mode 100644 packages/ui/src/components/chart.tsx diff --git a/apps/api/src/trpc/routers/sources.ts b/apps/api/src/trpc/routers/sources.ts index 64bb8e9..1adc07f 100644 --- a/apps/api/src/trpc/routers/sources.ts +++ b/apps/api/src/trpc/routers/sources.ts @@ -11,7 +11,7 @@ export const sourcesRouter = createTRPCRouter({ get: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)), getById: protectedProcedure.input(getSourceSchema).query(async ({ ctx, input }) => { - return getSourceById(ctx.db, { ...input }); + return getSourceById(ctx.db, input.id); }), update: protectedProcedure.input(updateSourceSchema).mutation(async ({ ctx, input }) => { diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7485316..0e1550e 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -17,6 +17,7 @@ "react": "catalog:", "react-dom": "catalog:", "react-hook-form": "^7.66.0", + "recharts": "^3.4.1", "superjson": "^2.2.5", "zod": "^4.1.12", "zustand": "^5.0.8" diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx index a6b2cc9..f4725d0 100644 --- a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/articles/page.tsx @@ -1,3 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Articles | Basango Dashboard", +}; + export default function Page() { return (
diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx index 0dfe7d1..80fe938 100644 --- a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx @@ -1,14 +1,6 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@basango/ui/components/breadcrumb"; -import { Separator } from "@basango/ui/components/separator"; -import { SidebarInset, SidebarProvider, SidebarTrigger } from "@basango/ui/components/sidebar"; +import { SidebarInset, SidebarProvider } from "@basango/ui/components/sidebar"; +import { PageHeader } from "@/components/shell/page-header"; import { AppSidebar } from "@/components/sidebar/app-sidebar"; import { HydrateClient } from "@/trpc/server"; @@ -19,23 +11,7 @@ export default async function Layout({ children }: { children: React.ReactNode } -
-
- - - - - - Building Your Application - - - - Data Fetching - - - -
-
+ {children}
diff --git a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx index a6b2cc9..8d70347 100644 --- a/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx +++ b/apps/dashboard/src/app/[locale]/(app)/(sidebar)/sources/page.tsx @@ -1,12 +1,26 @@ -export default function Page() { +import { Metadata } from "next"; + +import { PageLayout } from "@/components/shell/page-layout"; +import { SourceCard } from "@/components/source-card"; +import { batchPrefetch, getQueryClient, trpc } from "@/trpc/server"; + +export const metadata: Metadata = { + title: "Sources | Basango Dashboard", +}; + +export default async function Page() { + const queryClient = getQueryClient(); + + batchPrefetch([trpc.sources.get.queryOptions()]); + const sources = await queryClient.fetchQuery(trpc.sources.get.queryOptions()); + return ( -
-
-
-
-
+ +
+ {sources.map((source) => ( + + ))}
-
-
+
); } diff --git a/apps/dashboard/src/components/shell/page-header.tsx b/apps/dashboard/src/components/shell/page-header.tsx new file mode 100644 index 0000000..1867b2d --- /dev/null +++ b/apps/dashboard/src/components/shell/page-header.tsx @@ -0,0 +1,54 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@basango/ui/components/breadcrumb"; +import { Separator } from "@basango/ui/components/separator"; +//import { LanguageSelector, ThemeSelector } from "@/components/ui/shared/settings"; +import { SidebarTrigger } from "@basango/ui/components/sidebar"; + +export function PageHeader() { + return ( +
+
+ + + + + + Building Your Application + + + + Data Fetching + + + +
+
+ ); +} + +//
+//
+// +// +// +// +// +// +// +// +// +//
+//
+// +// +//
+//
diff --git a/apps/dashboard/src/components/shell/page-layout.tsx b/apps/dashboard/src/components/shell/page-layout.tsx new file mode 100644 index 0000000..280ec66 --- /dev/null +++ b/apps/dashboard/src/components/shell/page-layout.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +interface PageProps { + children: React.ReactNode; + title?: string | React.ReactNode; + leading?: string | React.ReactNode; +} + +export const PageLayout = (props: React.PropsWithChildren) => { + const { title, leading, children } = props; + + return ( +
+
+ {title && ( +
+
+

+ {title} +

+ {leading && ( +

{leading}

+ )} +
+
+ )} + {children} +
+
+ ); +}; diff --git a/apps/dashboard/src/components/shell/page-skeleton.tsx b/apps/dashboard/src/components/shell/page-skeleton.tsx new file mode 100644 index 0000000..0b146a0 --- /dev/null +++ b/apps/dashboard/src/components/shell/page-skeleton.tsx @@ -0,0 +1,97 @@ +import { Skeleton } from "@basango/ui/components/skeleton"; + +type Props = { + dashboard?: boolean; + details?: boolean; +}; + +const DashboardSkeleton = () => { + return ( +
+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+ ); +}; + +const ListSkeleton = () => { + return ( +
+
+
+ + +
+
+
+
+ + +
+ +
+ +
+
+
+
+ ); +}; + +const DetailsSkeleton = () => { + return ( +
+
+
+ + +
+
+
+
+ +
+
+ + + +
+
+ + +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+ + + +
+ ))} +
+
+
+
+
+ ); +}; + +export const PageSkeleton = (props: Props) => { + if (props.dashboard) return ; + if (props.details) return ; + return ; +}; diff --git a/apps/dashboard/src/components/sidebar/nav-main.tsx b/apps/dashboard/src/components/sidebar/app-sidebar-content.tsx similarity index 98% rename from apps/dashboard/src/components/sidebar/nav-main.tsx rename to apps/dashboard/src/components/sidebar/app-sidebar-content.tsx index ca320b9..38cb390 100644 --- a/apps/dashboard/src/components/sidebar/nav-main.tsx +++ b/apps/dashboard/src/components/sidebar/app-sidebar-content.tsx @@ -17,7 +17,7 @@ import { } from "@basango/ui/components/sidebar"; import { ChevronRight, type LucideIcon } from "lucide-react"; -export function NavMain({ +export function AppSidebarContent({ items, }: { items: { diff --git a/apps/dashboard/src/components/sidebar/app-sidebar-info.tsx b/apps/dashboard/src/components/sidebar/app-sidebar-info.tsx new file mode 100644 index 0000000..05bf48f --- /dev/null +++ b/apps/dashboard/src/components/sidebar/app-sidebar-info.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@basango/ui/components/sidebar"; + +export function AppSidebarInfo() { + const version = process.env.NEXT_PUBLIC_VERSION || "0.0.0"; + + return ( + + + +
+ Logo +
+
+ Basango + v{version} +
+
+
+
+ ); +} diff --git a/apps/dashboard/src/components/sidebar/nav-user.tsx b/apps/dashboard/src/components/sidebar/app-sidebar-user.tsx similarity index 77% rename from apps/dashboard/src/components/sidebar/nav-user.tsx rename to apps/dashboard/src/components/sidebar/app-sidebar-user.tsx index bf11278..c29bb5b 100644 --- a/apps/dashboard/src/components/sidebar/nav-user.tsx +++ b/apps/dashboard/src/components/sidebar/app-sidebar-user.tsx @@ -1,6 +1,6 @@ "use client"; -import { Avatar, AvatarFallback, AvatarImage } from "@basango/ui/components/avatar"; +import { Avatar, AvatarFallback } from "@basango/ui/components/avatar"; import { DropdownMenu, DropdownMenuContent, @@ -18,15 +18,7 @@ import { } from "@basango/ui/components/sidebar"; import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-react"; -export function NavUser({ - user, -}: { - user: { - name: string; - email: string; - avatar: string; - }; -}) { +export function AppSidebarUser() { const { isMobile } = useSidebar(); return ( @@ -39,12 +31,11 @@ export function NavUser({ size="lg" > - - CN + BN
- {user.name} - {user.email} + Bernard Ng + bernard.ng@example.com
@@ -58,12 +49,11 @@ export function NavUser({
- - CN + BN
- {user.name} - {user.email} + Bernard Ng + bernard.ng@example.com
diff --git a/apps/dashboard/src/components/sidebar/app-sidebar.tsx b/apps/dashboard/src/components/sidebar/app-sidebar.tsx index fd40b8e..6634790 100644 --- a/apps/dashboard/src/components/sidebar/app-sidebar.tsx +++ b/apps/dashboard/src/components/sidebar/app-sidebar.tsx @@ -7,166 +7,45 @@ import { SidebarHeader, SidebarRail, } from "@basango/ui/components/sidebar"; -import { - AudioWaveform, - BookOpen, - Bot, - Command, - Frame, - GalleryVerticalEnd, - MapIcon, - PieChart, - Settings2, - SquareTerminal, -} from "lucide-react"; +import { SquareTerminal } from "lucide-react"; import * as React from "react"; -import { NavMain } from "./nav-main"; -import { NavProjects } from "./nav-projects"; -import { NavUser } from "./nav-user"; -import { TeamSwitcher } from "./team-switcher"; +import { AppSidebarContent } from "./app-sidebar-content"; +import { AppSidebarInfo } from "./app-sidebar-info"; +import { AppSidebarUser } from "./app-sidebar-user"; const data = { - navMain: [ + main: [ { icon: SquareTerminal, isActive: true, items: [ { - title: "History", - url: "#", + title: "Sources", + url: "/sources", }, { - title: "Starred", - url: "#", - }, - { - title: "Settings", - url: "#", + title: "Articles", + url: "/articles", }, ], - title: "Playground", - url: "#", - }, - { - icon: Bot, - items: [ - { - title: "Genesis", - url: "#", - }, - { - title: "Explorer", - url: "#", - }, - { - title: "Quantum", - url: "#", - }, - ], - title: "Models", - url: "#", - }, - { - icon: BookOpen, - items: [ - { - title: "Introduction", - url: "#", - }, - { - title: "Get Started", - url: "#", - }, - { - title: "Tutorials", - url: "#", - }, - { - title: "Changelog", - url: "#", - }, - ], - title: "Documentation", - url: "#", - }, - { - icon: Settings2, - items: [ - { - title: "General", - url: "#", - }, - { - title: "Team", - url: "#", - }, - { - title: "Billing", - url: "#", - }, - { - title: "Limits", - url: "#", - }, - ], - title: "Settings", + title: "Dataset", url: "#", }, ], - projects: [ - { - icon: Frame, - name: "Design Engineering", - url: "#", - }, - { - icon: PieChart, - name: "Sales & Marketing", - url: "#", - }, - { - icon: MapIcon, - name: "Travel", - url: "#", - }, - ], - teams: [ - { - logo: GalleryVerticalEnd, - name: "Acme Inc", - plan: "Enterprise", - }, - { - logo: AudioWaveform, - name: "Acme Corp.", - plan: "Startup", - }, - { - logo: Command, - name: "Evil Corp.", - plan: "Free", - }, - ], - user: { - avatar: "/avatars/shadcn.jpg", - email: "m@example.com", - name: "shadcn", - }, }; export function AppSidebar({ ...props }: React.ComponentProps) { return ( - + - - + - + diff --git a/apps/dashboard/src/components/sidebar/nav-projects.tsx b/apps/dashboard/src/components/sidebar/nav-projects.tsx deleted file mode 100644 index 85961a5..0000000 --- a/apps/dashboard/src/components/sidebar/nav-projects.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@basango/ui/components/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@basango/ui/components/sidebar"; -import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react"; - -export function NavProjects({ - projects, -}: { - projects: { - name: string; - url: string; - icon: LucideIcon; - }[]; -}) { - const { isMobile } = useSidebar(); - - return ( - - Projects - - {projects.map((item) => ( - - - - - {item.name} - - - - - - - More - - - - - - View Project - - - - Share Project - - - - - Delete Project - - - - - ))} - - - - More - - - - - ); -} diff --git a/apps/dashboard/src/components/sidebar/team-switcher.tsx b/apps/dashboard/src/components/sidebar/team-switcher.tsx deleted file mode 100644 index 4752a16..0000000 --- a/apps/dashboard/src/components/sidebar/team-switcher.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@basango/ui/components/dropdown-menu"; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@basango/ui/components/sidebar"; -import { ChevronsUpDown, Plus } from "lucide-react"; -import * as React from "react"; - -export function TeamSwitcher({ - teams, -}: { - teams: { - name: string; - logo: React.ElementType; - plan: string; - }[]; -}) { - const { isMobile } = useSidebar(); - const [activeTeam, setActiveTeam] = React.useState(teams[0]); - - if (!activeTeam) { - return null; - } - - return ( - - - - - -
- -
-
- {activeTeam.name} - {activeTeam.plan} -
- -
-
- - Teams - {teams.map((team, index) => ( - setActiveTeam(team)} - > -
- -
- {team.name} - ⌘{index + 1} -
- ))} - - -
- -
-
Add team
-
-
-
-
-
- ); -} diff --git a/apps/dashboard/src/components/source-card.tsx b/apps/dashboard/src/components/source-card.tsx new file mode 100644 index 0000000..ab47534 --- /dev/null +++ b/apps/dashboard/src/components/source-card.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { Badge } from "@basango/ui/components/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@basango/ui/components/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@basango/ui/components/chart"; +import { Separator } from "@basango/ui/components/separator"; +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; + +interface CredibilityMetric { + bias: string; + reliability: string; + transparency: string; +} + +interface PublicationItem { + date: string; + count: number; +} + +interface SourceCardProps { + id: string; + name: string; + displayName: string | null; + url: string; + description: string; + publicationGraph: { + items: PublicationItem[]; + total: number; + }; + credibility: CredibilityMetric; +} + +const credibilityColors = { + high: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", + low: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", + medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", +}; + +function getCredibilityColor(value: string): string { + const lower = value.toLowerCase(); + if (lower.includes("high") || lower.includes("strong") || lower.includes("good")) { + return credibilityColors.high; + } + if (lower.includes("medium") || lower.includes("moderate")) { + return credibilityColors.medium; + } + return credibilityColors.low; +} + +const chartConfig = { + count: { + color: "var(--chart-2)", + label: "Articles", + }, + views: { + label: "Articles", + }, +} satisfies ChartConfig; + +export function SourceCard({ + id, + name, + displayName, + url, + description, + publicationGraph, + credibility, +}: SourceCardProps) { + const chartData = publicationGraph.items; + + return ( + + +
+
+ + {description} +

ID: {id}

+
+
+
+ + + + + + { + const date = new Date(value); + return date.toLocaleDateString("en-US", { + day: "numeric", + month: "short", + }); + }} + tickLine={false} + tickMargin={8} + /> + { + return new Date(value).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }); + }} + nameKey="views" + /> + } + /> + + + + + +
+

Credibility Metrics

+
+
+

Bias

+ + {credibility.bias} + +
+
+

Reliability

+ + {credibility.reliability} + +
+
+

Transparency

+ + {credibility.transparency} + +
+
+
+ + +
+

Summary

+
+
+

Total Publications

+

{publicationGraph.total}

+
+
+

Timeline Entries

+

{publicationGraph.items.length}

+
+
+
+
+
+ ); +} diff --git a/apps/dashboard/src/trpc/server.tsx b/apps/dashboard/src/trpc/server.tsx index 48ad845..9da7731 100644 --- a/apps/dashboard/src/trpc/server.tsx +++ b/apps/dashboard/src/trpc/server.tsx @@ -19,10 +19,10 @@ export const trpc = createTRPCOptionsProxy({ links: [ httpBatchLink({ async headers() { - const token = window.localStorage.getItem("auth_token"); + //const token = window.localStorage.getItem("auth_token"); return { - Authorization: `Bearer ${token}`, + //Authorization: `Bearer ${token}`, // "x-user-country": await getCountryCode(), // "x-user-locale": await getLocale(), // "x-user-timezone": await getTimezone(), diff --git a/biome.json b/biome.json index 516d0ac..a84e84b 100644 --- a/biome.json +++ b/biome.json @@ -58,7 +58,7 @@ "rules": { "a11y": { "noSvgWithoutTitle": "off", - "useFocusableInteractive": "warn", + "useFocusableInteractive": "off", "useSemanticElements": "off", "useValidAnchor": "off" }, @@ -67,6 +67,9 @@ "useImportExtensions": "off" }, "recommended": true, + "security": { + "noDangerouslySetInnerHtml": "off" + }, "style": { "noNonNullAssertion": "off", "useImportType": "off" diff --git a/bun.lock b/bun.lock index 22bc87c..1521cf0 100644 --- a/bun.lock +++ b/bun.lock @@ -79,6 +79,7 @@ "react": "catalog:", "react-dom": "catalog:", "react-hook-form": "^7.66.0", + "recharts": "^3.4.1", "superjson": "^2.2.5", "zod": "^4.1.12", "zustand": "^5.0.8", @@ -133,6 +134,7 @@ "@basango/encryption": "workspace:*", "@basango/logger": "workspace:*", "@date-fns/utc": "^2.1.1", + "date-fns": "^4.1.0", "drizzle-orm": "^0.44.7", "mysql2": "^3.15.3", "pg": "^8.16.3", @@ -186,6 +188,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", @@ -896,6 +899,8 @@ "@react-navigation/routers": ["@react-navigation/routers@7.5.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.10.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.1", "", { "os": "android", "cpu": "arm" }, "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.1", "", { "os": "android", "cpu": "arm64" }, "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g=="], @@ -1032,6 +1037,24 @@ "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -1068,6 +1091,8 @@ "@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/yargs": ["@types/yargs@17.0.34", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A=="], "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], @@ -1332,6 +1357,28 @@ "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], @@ -1342,6 +1389,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], @@ -1422,6 +1471,8 @@ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-toolkit": ["es-toolkit@1.41.0", "", {}, "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -1446,6 +1497,8 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], @@ -1636,6 +1689,8 @@ "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -1656,6 +1711,8 @@ "international-types": ["international-types@0.8.1", "", {}, "sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], @@ -2166,6 +2223,8 @@ "react-native-worklets": ["react-native-worklets@0.5.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w=="], + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], @@ -2178,10 +2237,16 @@ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "recharts": ["recharts@3.4.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], @@ -2204,6 +2269,8 @@ "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], @@ -2402,6 +2469,8 @@ "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -2512,6 +2581,8 @@ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="], "vitest": ["vitest@4.0.8", "", { "dependencies": { "@vitest/expect": "4.0.8", "@vitest/mocker": "4.0.8", "@vitest/pretty-format": "4.0.8", "@vitest/runner": "4.0.8", "@vitest/snapshot": "4.0.8", "@vitest/spy": "4.0.8", "@vitest/utils": "4.0.8", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.8", "@vitest/browser-preview": "4.0.8", "@vitest/browser-webdriverio": "4.0.8", "@vitest/ui": "4.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg=="], @@ -2604,6 +2675,8 @@ "@basango/dashboard/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "@basango/db/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], diff --git a/packages/db/package.json b/packages/db/package.json index bbcd8bf..58d7bc5 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -3,6 +3,7 @@ "@basango/encryption": "workspace:*", "@basango/logger": "workspace:*", "@date-fns/utc": "^2.1.1", + "date-fns": "^4.1.0", "drizzle-orm": "^0.44.7", "mysql2": "^3.15.3", "pg": "^8.16.3", diff --git a/packages/db/src/constants.ts b/packages/db/src/constants.ts index cd91bf8..bcb79ee 100644 --- a/packages/db/src/constants.ts +++ b/packages/db/src/constants.ts @@ -8,7 +8,18 @@ export const SOURCE_IMAGE_BASE = "https://devscast.org/images/sources/"; * Number of days to include in the publication graph for sources. * This defines the time range for which publication data is aggregated and displayed. */ -export const PUBLICATION_GRAPH_DAYS = 180; +export const PUBLICATION_GRAPH_DAYS = 60; + +/** + * Maximum number of category shares to return for a source. + * This limits the number of categories displayed in the category share breakdown. + */ +export const CATEGORY_SHARES_LIMIT = 10; + +/** + * The default timezone + */ +export const TIMEZONE = "Africa/Lubumbashi"; /** * Default pagination settings. diff --git a/packages/db/src/queries/sources.ts b/packages/db/src/queries/sources.ts index ed1e2cd..5f4eb0e 100644 --- a/packages/db/src/queries/sources.ts +++ b/packages/db/src/queries/sources.ts @@ -1,12 +1,24 @@ -import { eq } from "drizzle-orm"; +import { endOfDay, startOfDay, subDays } from "date-fns"; +import { eq, sql } from "drizzle-orm"; import { v7 as uuidV7 } from "uuid"; import { Database } from "@/client"; +import { CATEGORY_SHARES_LIMIT, PUBLICATION_GRAPH_DAYS, TIMEZONE } from "@/constants"; import { NotFoundError } from "@/errors"; -import { Credibility, source } from "@/schema"; +import { Credibility, article, source } from "@/schema"; export async function getSources(db: Database) { - return db.query.source.findMany(); + const rows = await db.query.source.findMany(); + + const data = await Promise.all( + rows.map(async (it) => ({ + ...it, + categoryShares: await getCategoryShares(db, it.id), + publicationGraph: await getPublicationGraph(db, it.id), + })), + ); + + return data; } export type CreateSourceParams = { @@ -69,10 +81,20 @@ export async function getSourceByName(db: Database, name: string) { }); } -export async function getById(db: Database, id: string) { - return db.query.source.findFirst({ +export async function getSourceById(db: Database, id: string) { + const item = db.query.source.findFirst({ where: eq(source.id, id), }); + + if (item === undefined) { + throw new NotFoundError("Source not found"); + } + + return { + ...item, + categoryShares: await getCategoryShares(db, id), + publicationGraph: await getPublicationGraph(db, id), + }; } export async function getSourceIdByName(db: Database, name: string): Promise { @@ -84,7 +106,7 @@ export async function getSourceIdByName(db: Database, name: string): Promise { + const endDate = endOfDay(new Date()); + const startDate = startOfDay(subDays(endDate, days - 1)); + + const data = await db.execute<{ date: string; count: number }>(sql` + WITH bounds AS ( + SELECT + ${startDate}::timestamptz AS start_ts, + ${endDate}::timestamptz AS end_ts + ), + series AS ( + SELECT (gs)::date AS d + FROM bounds b, + LATERAL generate_series( + date_trunc('day', timezone(${TIMEZONE}, b.start_ts)), + date_trunc('day', timezone(${TIMEZONE}, b.end_ts)), + INTERVAL '1 day' + ) AS gs + ), + counts AS ( + SELECT + a.published_at::date AS d, + COUNT(*)::int AS c + FROM article a, bounds b + WHERE a.source_id = ${id}::uuid + AND a.published_at >= timezone(${TIMEZONE}, b.start_ts) + AND a.published_at <= timezone(${TIMEZONE}, b.end_ts) + GROUP BY 1 + ) + SELECT + to_char(s.d, 'YYYY-MM-DD') AS date, + COALESCE(c.c, 0) AS count + FROM series s + LEFT JOIN counts c USING (d) + ORDER BY s.d ASC + `); + + return { items: data.rows, total: data.rows.length }; +} + +async function getCategoryShares(db: Database, id: string): Promise { + const data = await db.execute(sql` + SELECT + cat AS category, + COUNT(*)::int AS count, + ROUND((COUNT(*)::numeric / SUM(COUNT(*)) OVER ()) * 100, 2) AS percentage + FROM ( + SELECT NULLIF(BTRIM(c), '') AS cat + FROM ${article} + CROSS JOIN LATERAL UNNEST(COALESCE(${article.categories}, ARRAY[]::text[])) AS c + WHERE ${article.sourceId} = ${id} + ) t + WHERE cat IS NOT NULL + GROUP BY cat + ORDER BY count DESC + LIMIT ${CATEGORY_SHARES_LIMIT} + `); + + return { items: data.rows, total: data.rowCount ?? 0 }; } diff --git a/packages/ui/package.json b/packages/ui/package.json index 4a66fee..53ded91 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -24,6 +24,7 @@ "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", + "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", diff --git a/packages/ui/src/components/badge.tsx b/packages/ui/src/components/badge.tsx new file mode 100644 index 0000000..cceb5bd --- /dev/null +++ b/packages/ui/src/components/badge.tsx @@ -0,0 +1,38 @@ +import { cn } from "@basango/ui/lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + defaultVariants: { + variant: "default", + }, + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + }, + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/packages/ui/src/components/chart.tsx b/packages/ui/src/components/chart.tsx new file mode 100644 index 0000000..5a993d3 --- /dev/null +++ b/packages/ui/src/components/chart.tsx @@ -0,0 +1,319 @@ +import { cn } from "@basango/ui/lib/utils"; +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { dark: ".dark", light: "" } 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; +} + +function ChartContainer({ + id, + className, + children, + config, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps["children"]; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + {children} +
+
+ ); +} + +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 ( +