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 }
-
+
{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 (
+
+ );
+}
+
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
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 (
+
+
+
+
+

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