feat(dashboard): add reports
This commit is contained in:
@@ -3,11 +3,13 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|||||||
import { createTRPCRouter } from "#api/trpc/init";
|
import { createTRPCRouter } from "#api/trpc/init";
|
||||||
import { articlesRouter } from "#api/trpc/routers/articles";
|
import { articlesRouter } from "#api/trpc/routers/articles";
|
||||||
import { authRouter } from "#api/trpc/routers/auth";
|
import { authRouter } from "#api/trpc/routers/auth";
|
||||||
|
import { reportsRouter } from "#api/trpc/routers/reports.js";
|
||||||
import { sourcesRouter } from "#api/trpc/routers/sources";
|
import { sourcesRouter } from "#api/trpc/routers/sources";
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
articles: articlesRouter,
|
articles: articlesRouter,
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
|
reports: reportsRouter,
|
||||||
sources: sourcesRouter,
|
sources: sourcesRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { getDashboardOverview } from "@basango/db/queries";
|
||||||
|
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "#api/trpc/init";
|
||||||
|
|
||||||
|
export const reportsRouter = createTRPCRouter({
|
||||||
|
getDashboardOverview: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
return getDashboardOverview(ctx.db);
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ import { DEFAULT_OPEN_GRAPH_USER_AGENT, DEFAULT_USER_AGENT } from "@basango/doma
|
|||||||
* @author Bernard Ngandu <bernard@devscast.tech>
|
* @author Bernard Ngandu <bernard@devscast.tech>
|
||||||
*/
|
*/
|
||||||
export class UserAgents {
|
export class UserAgents {
|
||||||
private static readonly USER_AGENTS: string[] = [
|
public static readonly USER_AGENTS: string[] = [
|
||||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_4_8; like Mac OS X) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.3638.271 Mobile Safari/537.5",
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_4_8; like Mac OS X) AppleWebKit/603.39 (KHTML, like Gecko) Chrome/52.0.3638.271 Mobile Safari/537.5",
|
||||||
"Mozilla/50.0 (Linux; U; Linux x86_64; en-US) Gecko/20130401 Firefox/52.7",
|
"Mozilla/50.0 (Linux; U; Linux x86_64; en-US) Gecko/20130401 Firefox/52.7",
|
||||||
"Mozilla/5.0 (Linux; U; Android 5.0; SM-P815 Build/LRX22G) AppleWebKit/600.4 (KHTML, like Gecko) Chrome/48.0.1562.260 Mobile Safari/600.0",
|
"Mozilla/5.0 (Linux; U; Android 5.0; SM-P815 Build/LRX22G) AppleWebKit/600.4 (KHTML, like Gecko) Chrome/48.0.1562.260 Mobile Safari/600.0",
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export class WordPressCrawler extends BaseCrawler {
|
|||||||
readonly source: WordPressSourceConfig;
|
readonly source: WordPressSourceConfig;
|
||||||
private categoryMap: Map<number, string> = new Map();
|
private categoryMap: Map<number, string> = new Map();
|
||||||
|
|
||||||
private static readonly POST_QUERY =
|
public static readonly POST_QUERY =
|
||||||
"_fields=date,slug,link,title.rendered,content.rendered,categories&orderby=date&order=desc";
|
"_fields=date,slug,link,title.rendered,content.rendered,categories&orderby=date&order=desc";
|
||||||
private static readonly CATEGORY_QUERY =
|
public static readonly CATEGORY_QUERY =
|
||||||
"_fields=id,slug,count&orderby=count&order=desc&per_page=100";
|
"_fields=id,slug,count&orderby=count&order=desc&per_page=100";
|
||||||
private static readonly TOTAL_PAGES_HEADER = "x-wp-totalpages";
|
public static readonly TOTAL_PAGES_HEADER = "x-wp-totalpages";
|
||||||
private static readonly TOTAL_POSTS_HEADER = "x-wp-total";
|
public static readonly TOTAL_POSTS_HEADER = "x-wp-total";
|
||||||
|
|
||||||
constructor(settings: FetchCrawlerConfig, options: { persistors?: Persistor[] } = {}) {
|
constructor(settings: FetchCrawlerConfig, options: { persistors?: Persistor[] } = {}) {
|
||||||
super(settings, options);
|
super(settings, options);
|
||||||
@@ -196,7 +196,9 @@ export class WordPressCrawler extends BaseCrawler {
|
|||||||
* @param page - Page number
|
* @param page - Page number
|
||||||
*/
|
*/
|
||||||
buildEndpointUrl(page: number): string {
|
buildEndpointUrl(page: number): string {
|
||||||
return `${this.baseUrl()}wp-json/wp/v2/posts?${WordPressCrawler.POST_QUERY}&page=${page}&per_page=100`;
|
return `${this.baseUrl()}wp-json/wp/v2/posts?${
|
||||||
|
WordPressCrawler.POST_QUERY
|
||||||
|
}&page=${page}&per_page=100`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,15 +5,23 @@
|
|||||||
"@basango/ui": "workspace:*",
|
"@basango/ui": "workspace:*",
|
||||||
"@date-fns/tz": "^1.4.1",
|
"@date-fns/tz": "^1.4.1",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.8",
|
"@tanstack/react-query": "^5.90.8",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.1",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.1",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.1",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"date-fns": "catalog:",
|
"date-fns": "catalog:",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-international": "^1.3.1",
|
"next-international": "^1.3.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ import { Metadata } from "next";
|
|||||||
|
|
||||||
import { ArticlesFeed } from "#dashboard/components/articles-feed";
|
import { ArticlesFeed } from "#dashboard/components/articles-feed";
|
||||||
import { PageLayout } from "#dashboard/components/shell/page-layout";
|
import { PageLayout } from "#dashboard/components/shell/page-layout";
|
||||||
import { HydrateClient, batchPrefetch, trpc } from "#dashboard/trpc/server";
|
import { HydrateClient, prefetch, trpc } from "#dashboard/trpc/server";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Articles | Basango Dashboard",
|
title: "Articles | Basango Dashboard",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
batchPrefetch([trpc.articles.list.infiniteQueryOptions({ limit: 12 })]);
|
prefetch(trpc.articles.list.infiniteQueryOptions({ limit: 12 }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<PageLayout leading="Track crawled content and trends" title="Articles">
|
<PageLayout title="Articles">
|
||||||
<ArticlesFeed />
|
<ArticlesFeed />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</HydrateClient>
|
</HydrateClient>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
|
|
||||||
import { PublicationGraphChart } from "#dashboard/components/charts/articles/publication-graph-chart";
|
import { PublicationGraphChart } from "#dashboard/components/charts/articles/publication-graph-chart";
|
||||||
import { SourceDistributionChart } from "#dashboard/components/charts/articles/source-distribution-chart";
|
import { SourceDistributionChart } from "#dashboard/components/charts/articles/source-distribution-chart";
|
||||||
|
import { DashboardOverviewCard } from "#dashboard/components/dashboard-overview-card";
|
||||||
import { PageLayout } from "#dashboard/components/shell/page-layout";
|
import { PageLayout } from "#dashboard/components/shell/page-layout";
|
||||||
import { HydrateClient, batchPrefetch, trpc } from "#dashboard/trpc/server";
|
import { HydrateClient, batchPrefetch, trpc } from "#dashboard/trpc/server";
|
||||||
|
|
||||||
@@ -11,15 +12,17 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
batchPrefetch([
|
batchPrefetch([
|
||||||
|
trpc.reports.getDashboardOverview.queryOptions(),
|
||||||
trpc.articles.getPublications.queryOptions({}),
|
trpc.articles.getPublications.queryOptions({}),
|
||||||
trpc.articles.getSourceDistribution.queryOptions({ limit: 8 }),
|
trpc.articles.getSourceDistribution.queryOptions({ limit: 8 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<PageLayout leading="Keep track of article volume and source coverage" title="Dashboard">
|
<PageLayout title="Dashboard">
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-4">
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3 gap-4 flex flex-col">
|
||||||
|
<DashboardOverviewCard />
|
||||||
<PublicationGraphChart />
|
<PublicationGraphChart />
|
||||||
</div>
|
</div>
|
||||||
<SourceDistributionChart />
|
<SourceDistributionChart />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SidebarInset, SidebarProvider } from "@basango/ui/components/sidebar";
|
import { SidebarInset, SidebarProvider } from "@basango/ui/components/sidebar";
|
||||||
|
|
||||||
import { PageHeader } from "#dashboard/components/shell/page-header";
|
|
||||||
import { AppSidebar } from "#dashboard/components/sidebar/app-sidebar";
|
import { AppSidebar } from "#dashboard/components/sidebar/app-sidebar";
|
||||||
import { HydrateClient } from "#dashboard/trpc/server";
|
import { HydrateClient } from "#dashboard/trpc/server";
|
||||||
|
|
||||||
@@ -10,10 +9,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
||||||
<SidebarInset>
|
<SidebarInset>{children}</SidebarInset>
|
||||||
<PageHeader />
|
|
||||||
{children}
|
|
||||||
</SidebarInset>
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</HydrateClient>
|
</HydrateClient>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<PageLayout leading={source.description ?? "No description available"} title={source.name}>
|
<PageLayout title={source.name}>
|
||||||
<Tabs className="space-y-4" defaultValue="overview">
|
<Tabs className="space-y-4" defaultValue="overview">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export default async function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<PageLayout leading="Manage your news sources" title="Sources">
|
<PageLayout title="Sources">
|
||||||
<div className="mb-6 flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Link href="?createSource=true">
|
<Link href="?createSource=true">
|
||||||
<Button type="button">
|
<Button type="button">
|
||||||
<PlusIcon className="mr-2 size-4" />
|
<PlusIcon className="mr-2 size-4" />
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted relative hidden lg:block">
|
<div className="relative hidden lg:block">
|
||||||
<img
|
<img
|
||||||
alt="verification placeholder"
|
alt="Login background"
|
||||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||||
src="https://images.pexels.com/photos/30690932/pexels-photo-30690932.jpeg"
|
src="https://images.pexels.com/photos/30690932/pexels-photo-30690932.jpeg"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@basango/ui/components/select";
|
} from "@basango/ui/components/select";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@basango/ui/components/toggle-group";
|
|
||||||
import { differenceInCalendarDays, format, subDays } from "date-fns";
|
import { differenceInCalendarDays, format, subDays } from "date-fns";
|
||||||
import { CalendarIcon, ChevronDown } from "lucide-react";
|
import { CalendarIcon, ChevronDown } from "lucide-react";
|
||||||
import { parseAsInteger, parseAsIsoDate, useQueryStates } from "nuqs";
|
import { parseAsInteger, parseAsIsoDate, useQueryStates } from "nuqs";
|
||||||
@@ -136,8 +135,10 @@ export function ChartPeriodPicker({
|
|||||||
return match ? String(match.value) : "custom";
|
return match ? String(match.value) : "custom";
|
||||||
}, [calendarRange, options]);
|
}, [calendarRange, options]);
|
||||||
|
|
||||||
const handlePresetChange = (value: string) => {
|
const presetValue = selectValue === "custom" ? undefined : selectValue;
|
||||||
if (value === "custom") {
|
|
||||||
|
const handlePresetChange = (value?: string) => {
|
||||||
|
if (!value || value === "custom") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +150,10 @@ export function ChartPeriodPicker({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePresetClick = (value: string) => {
|
||||||
|
handlePresetChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCalendarSelect = (value: DateRange | undefined) => {
|
const handleCalendarSelect = (value: DateRange | undefined) => {
|
||||||
if (value?.from && value?.to) {
|
if (value?.from && value?.to) {
|
||||||
setState({
|
setState({
|
||||||
@@ -181,29 +186,56 @@ export function ChartPeriodPicker({
|
|||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" className="w-screen space-y-4 p-4 sm:w-[520px]" sideOffset={8}>
|
<PopoverContent align="start" className="w-auto p-0" sideOffset={8}>
|
||||||
<Select onValueChange={handlePresetChange} value={selectValue}>
|
<div className="flex flex-col gap-0 sm:flex-row">
|
||||||
<SelectTrigger>
|
<div className="border-b border-border sm:hidden">
|
||||||
<SelectValue placeholder="Quick range" />
|
<div className="p-4">
|
||||||
</SelectTrigger>
|
<Select onValueChange={handlePresetChange} value={selectValue}>
|
||||||
<SelectContent>
|
<SelectTrigger>
|
||||||
{options.map((option) => (
|
<SelectValue placeholder="Quick range" />
|
||||||
<SelectItem key={option.value} value={String(option.value)}>
|
</SelectTrigger>
|
||||||
{option.label}
|
<SelectContent>
|
||||||
</SelectItem>
|
{options.map((option) => (
|
||||||
))}
|
<SelectItem key={option.value} value={String(option.value)}>
|
||||||
<SelectItem value="custom">Custom range</SelectItem>
|
{option.label}
|
||||||
</SelectContent>
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
|
<SelectItem value="custom">Custom range</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden flex-col gap-0 border-r border-border py-4 pl-4 pr-4 sm:flex">
|
||||||
|
{options.map((option) => {
|
||||||
|
const isActive = presetValue === String(option.value);
|
||||||
|
|
||||||
<Calendar
|
return (
|
||||||
mode="range"
|
<button
|
||||||
numberOfMonths={2}
|
className={`rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${
|
||||||
onSelect={handleCalendarSelect}
|
isActive
|
||||||
selected={(selectedRange ?? calendarRange) as DateRange | undefined}
|
? "bg-primary text-primary-foreground"
|
||||||
/>
|
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
|
}`}
|
||||||
<div className="flex justify-end gap-2">
|
key={option.value}
|
||||||
|
onClick={() => handlePresetClick(String(option.value))}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<Calendar
|
||||||
|
disabled={{ after: new Date() }}
|
||||||
|
mode="range"
|
||||||
|
numberOfMonths={1}
|
||||||
|
onSelect={handleCalendarSelect}
|
||||||
|
selected={(selectedRange ?? calendarRange) as DateRange | undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2 border-t px-4 py-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@basango/api/trpc/routers/_app";
|
||||||
|
import { Card } from "@basango/ui/components/card";
|
||||||
|
import { Skeleton } from "@basango/ui/components/skeleton";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { Status } from "#dashboard/components/charts/status";
|
||||||
|
import { Show } from "#dashboard/components/shell/show";
|
||||||
|
import { useTRPC } from "#dashboard/trpc/client";
|
||||||
|
import { formatNumber } from "#dashboard/utils/utils";
|
||||||
|
|
||||||
|
type DashboardOverview = RouterOutputs["reports"]["getDashboardOverview"];
|
||||||
|
type OverviewMetric = keyof DashboardOverview;
|
||||||
|
|
||||||
|
const LABELS: Record<OverviewMetric, string> = {
|
||||||
|
articles: "Articles",
|
||||||
|
sources: "Sources",
|
||||||
|
users: "Users",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MetricProps {
|
||||||
|
delta: DashboardOverview[OverviewMetric]["delta"] | undefined;
|
||||||
|
label: string;
|
||||||
|
loading: boolean;
|
||||||
|
value: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverviewMetricSkeleton() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton className="h-10 w-40" />
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Skeleton className="h-4 w-52" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverviewMetricCard({ delta, label, loading, value }: MetricProps) {
|
||||||
|
return (
|
||||||
|
<Card className="flex flex-col gap-2 border-border bg-card p-4">
|
||||||
|
<p className="text-sm text-muted-foreground">{label}</p>
|
||||||
|
<Show fallback={<OverviewMetricSkeleton />} when={!loading}>
|
||||||
|
<p className="text-4xl font-semibold">{formatNumber(value)}</p>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Status percentage value={delta} />
|
||||||
|
<span className="text-muted-foreground">vs previous 30 days</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardOverviewCard() {
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const { data, isPending } = useQuery(trpc.reports.getDashboardOverview.queryOptions());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
{(Object.keys(LABELS) as OverviewMetric[]).map((key) => (
|
||||||
|
<OverviewMetricCard
|
||||||
|
delta={data?.[key]?.delta}
|
||||||
|
key={key}
|
||||||
|
label={LABELS[key]}
|
||||||
|
loading={isPending && !data}
|
||||||
|
value={data?.[key]?.total}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,8 +12,7 @@ import { Input } from "@basango/ui/components/input";
|
|||||||
import { SubmitButton } from "@basango/ui/components/submit-button";
|
import { SubmitButton } from "@basango/ui/components/submit-button";
|
||||||
import { cn } from "@basango/ui/lib/utils";
|
import { cn } from "@basango/ui/lib/utils";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -27,11 +26,9 @@ type LoginValues = z.infer<typeof loginSchema>;
|
|||||||
|
|
||||||
export function LoginForm({ className, ...props }: React.ComponentProps<"form">) {
|
export function LoginForm({ className, ...props }: React.ComponentProps<"form">) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams<{ locale?: string }>();
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const setUser = useUserStore((state) => state.setUser);
|
const setUser = useUserStore((state) => state.setUser);
|
||||||
const locale = params?.locale ?? "en";
|
|
||||||
|
|
||||||
const form = useZodForm(loginSchema, {
|
const form = useZodForm(loginSchema, {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -53,20 +50,16 @@ export function LoginForm({ className, ...props }: React.ComponentProps<"form">)
|
|||||||
refreshTokenExpiresAt: data.refreshTokenExpiresAt,
|
refreshTokenExpiresAt: data.refreshTokenExpiresAt,
|
||||||
});
|
});
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
|
toast.success("Successfully logged in.");
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
router.push(searchParams?.get("return_to") ?? `/${locale}/dashboard`);
|
router.push(searchParams?.get("return_to") ?? `/dashboard`);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = (values: LoginValues) => mutation.mutate(values);
|
||||||
(values: LoginValues) => {
|
|
||||||
mutation.mutate(values);
|
|
||||||
},
|
|
||||||
[mutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
} from "@basango/ui/components/breadcrumb";
|
||||||
import { Separator } from "@basango/ui/components/separator";
|
import { Separator } from "@basango/ui/components/separator";
|
||||||
import { SidebarTrigger } from "@basango/ui/components/sidebar";
|
import { SidebarTrigger } from "@basango/ui/components/sidebar";
|
||||||
|
|
||||||
|
import { Show } from "#dashboard/components/shell/show";
|
||||||
import { ThemeToggle } from "#dashboard/components/theme-toggle";
|
import { ThemeToggle } from "#dashboard/components/theme-toggle";
|
||||||
|
|
||||||
export function PageHeader() {
|
type Props = {
|
||||||
|
title?: string | React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageHeader({ title }: Props) {
|
||||||
return (
|
return (
|
||||||
<header className="border-b flex h-16 shrink-0 items-center justify-between gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
<header className="flex h-16 shrink-0 items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 px-4">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator className="mr-2 data-[orientation=vertical]:h-4" orientation="vertical" />
|
|
||||||
|
<Show when={title !== undefined}>
|
||||||
|
<Separator className="mr-2 data-[orientation=vertical]:h-4" orientation="vertical" />
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem className="hidden md:block">
|
||||||
|
<BreadcrumbPage>{title}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 px-4">
|
<div className="flex items-center gap-2">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,31 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { PageHeader } from "#dashboard/components/shell/page-header";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title?: string | React.ReactNode;
|
title?: string | React.ReactNode;
|
||||||
leading?: string | React.ReactNode;
|
header?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageLayout = (props: React.PropsWithChildren<PageProps>) => {
|
export const PageLayout = (props: React.PropsWithChildren<PageProps>) => {
|
||||||
const { title, leading, children } = props;
|
const { title, header = <PageHeader title={title} />, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
<div className="container mx-auto space-y-4">
|
{header}
|
||||||
{title && (
|
{children}
|
||||||
<div className="mb-8 flex items-center justify-between space-y-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight text-balance flex items-center gap-2 justify-start">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
{leading && (
|
|
||||||
<p className="text-muted-foreground text-lg wrap-break-words">{leading}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface ShowProps<T> {
|
||||||
|
when: T | null | undefined;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
children: React.ReactNode | ((props: T) => React.ReactNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show<T>(props: ShowProps<T>): React.ReactNode {
|
||||||
|
const { when, fallback, children } = props;
|
||||||
|
let result: React.ReactNode;
|
||||||
|
|
||||||
|
if (!when) {
|
||||||
|
result = fallback;
|
||||||
|
} else {
|
||||||
|
result = typeof children === "function" ? children(when) : children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -16,21 +16,29 @@ import {
|
|||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from "@basango/ui/components/sidebar";
|
} from "@basango/ui/components/sidebar";
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
type ParentItem = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
isActive?: boolean;
|
||||||
|
items?: ChildItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChildItem = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: ParentItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppSidebarContent({ items }: Props) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
export function AppSidebarContent({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
isActive?: boolean;
|
|
||||||
items?: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||||
@@ -54,7 +62,10 @@ export function AppSidebarContent({
|
|||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{item.items?.map((subItem) => (
|
{item.items?.map((subItem) => (
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
isActive={subItem.url === pathname || pathname.includes(subItem.url)}
|
||||||
|
>
|
||||||
<a href={subItem.url}>
|
<a href={subItem.url}>
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ export function AppSidebarInfo() {
|
|||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton asChild size="lg">
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
<a href="#">
|
||||||
size="lg"
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
>
|
<span className="truncate font-medium">Basango Dashboard</span>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<span className="truncate text-xs">v{version}</span>
|
||||||
<span className="truncate font-medium">Basango Dashboard</span>
|
</div>
|
||||||
<span className="truncate text-xs">v{version}</span>
|
</a>
|
||||||
</div>
|
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
|
||||||
} from "@basango/ui/components/sidebar";
|
} from "@basango/ui/components/sidebar";
|
||||||
import { LayoutDashboard, SquareTerminal } from "lucide-react";
|
import { LayoutDashboard, SquareTerminal } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -49,7 +48,7 @@ const data = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar variant="inset" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<AppSidebarInfo />
|
<AppSidebarInfo />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
@@ -59,7 +58,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<AppSidebarUser />
|
<AppSidebarUser />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@basango/ui/*": ["../../packages/ui/src/*"],
|
||||||
"#api/*": ["../api/src/*"],
|
"#api/*": ["../api/src/*"],
|
||||||
"#dashboard/*": ["./src/*"],
|
"#dashboard/*": ["./src/*"],
|
||||||
"#db/*": ["../../packages/db/src/*"],
|
"#db/*": ["../../packages/db/src/*"],
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
"name": "basango",
|
"name": "basango",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@basango/tsconfig": "workspace:*",
|
"@basango/tsconfig": "workspace:*",
|
||||||
"@biomejs/biome": "latest",
|
"@biomejs/biome": "^2.3.6",
|
||||||
"@commitlint/cli": "latest",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"@commitlint/config-conventional": "latest",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"@manypkg/cli": "latest",
|
"@manypkg/cli": "^0.25.1",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.3.2",
|
||||||
"@types/node": "latest",
|
"@types/node": "^24.10.1",
|
||||||
"commitizen": "latest",
|
"commitizen": "^4.3.1",
|
||||||
"cz-conventional-changelog": "latest",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"husky": "latest",
|
"husky": "^9.1.7",
|
||||||
"turbo": "latest",
|
"turbo": "^2.6.1",
|
||||||
"typescript": "latest",
|
"typescript": "^5.9.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/api": {
|
"apps/api": {
|
||||||
@@ -66,15 +66,23 @@
|
|||||||
"@basango/ui": "workspace:*",
|
"@basango/ui": "workspace:*",
|
||||||
"@date-fns/tz": "^1.4.1",
|
"@date-fns/tz": "^1.4.1",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.8",
|
"@tanstack/react-query": "^5.90.8",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@trpc/client": "^11.7.1",
|
"@trpc/client": "^11.7.1",
|
||||||
"@trpc/react-query": "^11.7.1",
|
"@trpc/react-query": "^11.7.1",
|
||||||
"@trpc/server": "^11.7.1",
|
"@trpc/server": "^11.7.1",
|
||||||
"@trpc/tanstack-react-query": "^11.7.1",
|
"@trpc/tanstack-react-query": "^11.7.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"date-fns": "catalog:",
|
"date-fns": "catalog:",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-international": "^1.3.1",
|
"next-international": "^1.3.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -213,7 +221,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "catalog:",
|
"date-fns": "catalog:",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
@@ -826,7 +834,7 @@
|
|||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||||
|
|
||||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||||
|
|
||||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||||
|
|
||||||
@@ -856,7 +864,7 @@
|
|||||||
|
|
||||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
@@ -1874,7 +1882,7 @@
|
|||||||
|
|
||||||
"lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
|
"lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.553.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw=="],
|
"lucide-react": ["lucide-react@0.554.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA=="],
|
||||||
|
|
||||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||||
|
|
||||||
@@ -2714,30 +2722,100 @@
|
|||||||
|
|
||||||
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
"@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
"@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
"@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
||||||
@@ -2816,8 +2894,6 @@
|
|||||||
|
|
||||||
"chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
"chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||||
|
|
||||||
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
|
||||||
|
|
||||||
"commitizen/detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
|
"commitizen/detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
|
||||||
|
|
||||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
@@ -3152,6 +3228,38 @@
|
|||||||
|
|
||||||
"@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@turbo/workspaces/ora/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
"@turbo/workspaces/ora/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
||||||
|
|
||||||
"@turbo/workspaces/ora/log-symbols": ["log-symbols@3.0.0", "", { "dependencies": { "chalk": "^2.4.2" } }, "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ=="],
|
"@turbo/workspaces/ora/log-symbols": ["log-symbols@3.0.0", "", { "dependencies": { "chalk": "^2.4.2" } }, "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ=="],
|
||||||
|
|||||||
@@ -154,14 +154,14 @@ export async function getArticlesPublicationGraph(
|
|||||||
db: Database,
|
db: Database,
|
||||||
params: GetPublicationsParams,
|
params: GetPublicationsParams,
|
||||||
): Promise<Publications> {
|
): Promise<Publications> {
|
||||||
const [startDate, endDate] = buildDateRange(params.range);
|
const current = buildDateRange(params.range);
|
||||||
const [previousStart, previousEnd] = buildPreviousRange([startDate, endDate]);
|
const previous = buildPreviousRange(current);
|
||||||
|
|
||||||
const data = await db.execute<Publication>(sql`
|
const data = await db.execute<Publication>(sql`
|
||||||
WITH bounds AS (
|
WITH bounds AS (
|
||||||
SELECT
|
SELECT
|
||||||
${startDate}::timestamptz AS start_ts,
|
${current.start}::timestamptz AS start_ts,
|
||||||
${endDate}::timestamptz AS end_ts
|
${current.end}::timestamptz AS end_ts
|
||||||
),
|
),
|
||||||
series AS (
|
series AS (
|
||||||
SELECT (gs)::date AS d
|
SELECT (gs)::date AS d
|
||||||
@@ -189,19 +189,19 @@ export async function getArticlesPublicationGraph(
|
|||||||
ORDER BY s.d ASC
|
ORDER BY s.d ASC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const [previous] = await db
|
const [previousResult] = await db
|
||||||
.execute<{ count: number }>(
|
.execute<{ count: number }>(
|
||||||
sql`
|
sql`
|
||||||
SELECT COALESCE(COUNT(*)::int, 0) AS count
|
SELECT COALESCE(COUNT(*)::int, 0) AS count
|
||||||
FROM article a
|
FROM article a
|
||||||
WHERE a.published_at >= timezone(${DEFAULT_TIMEZONE}, ${previousStart})
|
WHERE a.published_at >= timezone(${DEFAULT_TIMEZONE}, ${previous.start})
|
||||||
AND a.published_at <= timezone(${DEFAULT_TIMEZONE}, ${previousEnd})
|
AND a.published_at <= timezone(${DEFAULT_TIMEZONE}, ${previous.end})
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.then((res) => res.rows);
|
.then((res) => res.rows);
|
||||||
|
|
||||||
const currentTotal = data.rows.reduce((acc, item) => acc + item.count, 0);
|
const currentTotal = data.rows.reduce((acc, item) => acc + item.count, 0);
|
||||||
const previousTotal = previous?.count ?? 0;
|
const previousTotal = previousResult?.count ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: data.rows,
|
items: data.rows,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./articles";
|
export * from "./articles";
|
||||||
|
export * from "./reports";
|
||||||
export * from "./sources";
|
export * from "./sources";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { DashboardOverview, DateRange } from "@basango/domain/models";
|
||||||
|
import { AnyColumn, SQL, count, sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
import { Database } from "#db/client";
|
||||||
|
import { articles, sources, users } from "#db/schema";
|
||||||
|
import { buildDateRange, buildPreviousRange, computeDelta } from "#db/utils";
|
||||||
|
|
||||||
|
const withinRange = (column: AnyColumn, range: DateRange): SQL<unknown> =>
|
||||||
|
sql`${column} >= ${range.start} AND ${column} < ${range.end}`;
|
||||||
|
|
||||||
|
const countArticles = async (db: Database, where?: SQL<unknown>) => {
|
||||||
|
const query = db.select({ count: count(articles.id) }).from(articles);
|
||||||
|
const rows = where ? query.where(where) : query;
|
||||||
|
|
||||||
|
const [result] = await rows;
|
||||||
|
return Number(result?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countUsers = async (db: Database, where?: SQL<unknown>) => {
|
||||||
|
const query = db.select({ count: count(users.id) }).from(users);
|
||||||
|
const rows = where ? query.where(where) : query;
|
||||||
|
|
||||||
|
const [result] = await rows;
|
||||||
|
return Number(result?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countSources = async (db: Database) => {
|
||||||
|
const [result] = await db.select({ count: count(sources.id) }).from(sources);
|
||||||
|
return Number(result?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countActiveSourcesInRange = async (db: Database, range: DateRange) => {
|
||||||
|
const [result] = await db
|
||||||
|
.select({
|
||||||
|
count: sql<number>`CAST(COUNT(DISTINCT ${articles.sourceId}) AS INT)`,
|
||||||
|
})
|
||||||
|
.from(articles)
|
||||||
|
.where(withinRange(articles.publishedAt, range));
|
||||||
|
|
||||||
|
return Number(result?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDashboardOverview = async (db: Database): Promise<DashboardOverview> => {
|
||||||
|
const current = buildDateRange();
|
||||||
|
const previous = buildPreviousRange(current);
|
||||||
|
const ranges = { current, previous };
|
||||||
|
|
||||||
|
const [totalArticles, totalUsers, totalSources] = await Promise.all([
|
||||||
|
countArticles(db),
|
||||||
|
countUsers(db),
|
||||||
|
countSources(db),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [articlesCurrent, articlesPrevious] = await Promise.all([
|
||||||
|
countArticles(db, withinRange(articles.publishedAt, ranges.current)),
|
||||||
|
countArticles(db, withinRange(articles.publishedAt, ranges.previous)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [usersCurrent, usersPrevious] = await Promise.all([
|
||||||
|
countUsers(db, withinRange(users.createdAt, ranges.current)),
|
||||||
|
countUsers(db, withinRange(users.createdAt, ranges.previous)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [sourcesCurrent, sourcesPrevious] = await Promise.all([
|
||||||
|
countActiveSourcesInRange(db, ranges.current),
|
||||||
|
countActiveSourcesInRange(db, ranges.previous),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
articles: {
|
||||||
|
delta: computeDelta(articlesCurrent, articlesPrevious),
|
||||||
|
total: totalArticles,
|
||||||
|
},
|
||||||
|
sources: {
|
||||||
|
delta: computeDelta(sourcesCurrent, sourcesPrevious),
|
||||||
|
total: totalSources,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
delta: computeDelta(usersCurrent, usersPrevious),
|
||||||
|
total: totalUsers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -100,13 +100,13 @@ export async function getSourcePublicationGraph(
|
|||||||
db: Database,
|
db: Database,
|
||||||
params: GetPublicationsParams,
|
params: GetPublicationsParams,
|
||||||
): Promise<Publications> {
|
): Promise<Publications> {
|
||||||
const [startDate, endDate] = buildDateRange(params.range);
|
const range = buildDateRange(params.range);
|
||||||
|
|
||||||
const data = await db.execute<Publication>(sql`
|
const data = await db.execute<Publication>(sql`
|
||||||
WITH bounds AS (
|
WITH bounds AS (
|
||||||
SELECT
|
SELECT
|
||||||
${startDate}::timestamptz AS start_ts,
|
${range.start}::timestamptz AS start_ts,
|
||||||
${endDate}::timestamptz AS end_ts
|
${range.end}::timestamptz AS end_ts
|
||||||
),
|
),
|
||||||
series AS (
|
series AS (
|
||||||
SELECT (gs)::date AS d
|
SELECT (gs)::date AS d
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/** biome-ignore-all lint/correctness/noUnusedPrivateClassMembers: false positive */
|
||||||
|
|
||||||
import { RowDataPacket } from "mysql2/promise";
|
import { RowDataPacket } from "mysql2/promise";
|
||||||
import { Pool, PoolClient } from "pg";
|
import { Pool, PoolClient } from "pg";
|
||||||
|
|
||||||
@@ -48,16 +50,16 @@ class Engine {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly sourceOptions: SourceOptions,
|
private readonly sourceOptions: SourceOptions,
|
||||||
private readonly targetOptions: TargetOptions,
|
targetOptions: TargetOptions,
|
||||||
) {
|
) {
|
||||||
this.target = new Pool({
|
this.target = new Pool({
|
||||||
allowExitOnIdle: true,
|
allowExitOnIdle: true,
|
||||||
connectionString: this.targetOptions.database,
|
connectionString: targetOptions.database,
|
||||||
max: 16,
|
max: 16,
|
||||||
});
|
});
|
||||||
this.ignore = { ...DEFAULT_IGNORE, ...(this.targetOptions.ignoreColumns ?? {}) };
|
this.ignore = { ...DEFAULT_IGNORE, ...(targetOptions.ignoreColumns ?? {}) };
|
||||||
this.pageSize = this.targetOptions.pageSize ?? 5000;
|
this.pageSize = targetOptions.pageSize ?? 5000;
|
||||||
this.batchSize = Math.max(1, this.targetOptions.batchSize ?? 500);
|
this.batchSize = Math.max(1, targetOptions.batchSize ?? 500);
|
||||||
console.log(
|
console.log(
|
||||||
`Engine initialized with pageSize=${this.pageSize} and batchSize=${this.batchSize} (resume=${this.resume})`,
|
`Engine initialized with pageSize=${this.pageSize} and batchSize=${this.batchSize} (resume=${this.resume})`,
|
||||||
);
|
);
|
||||||
@@ -70,11 +72,17 @@ class Engine {
|
|||||||
async import(table: string): Promise<number> {
|
async import(table: string): Promise<number> {
|
||||||
await this.ensureProgressTable();
|
await this.ensureProgressTable();
|
||||||
|
|
||||||
let startState: SyncProgress = { cursor: null, cursorEncoding: null, offset: 0 };
|
let startState: SyncProgress = {
|
||||||
|
cursor: null,
|
||||||
|
cursorEncoding: null,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
if (this.resume) {
|
if (this.resume) {
|
||||||
startState = await this.getProgressState(table);
|
startState = await this.getProgressState(table);
|
||||||
console.log(
|
console.log(
|
||||||
`Resuming import for ${table} from offset=${startState.offset}, cursor=${startState.cursor ?? "null"}`,
|
`Resuming import for ${table} from offset=${
|
||||||
|
startState.offset
|
||||||
|
}, cursor=${startState.cursor ?? "null"}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.reset(table);
|
await this.reset(table);
|
||||||
@@ -107,7 +115,9 @@ class Engine {
|
|||||||
let sql: string;
|
let sql: string;
|
||||||
let params: unknown[];
|
let params: unknown[];
|
||||||
if (useCursor && cursorParam != null) {
|
if (useCursor && cursorParam != null) {
|
||||||
sql = `SELECT * FROM \`${this.escapeBacktick(table)}\` WHERE \`id\` > ? ORDER BY \`id\` LIMIT ?`;
|
sql = `SELECT * FROM \`${this.escapeBacktick(
|
||||||
|
table,
|
||||||
|
)}\` WHERE \`id\` > ? ORDER BY \`id\` LIMIT ?`;
|
||||||
params = [cursorParam, size];
|
params = [cursorParam, size];
|
||||||
} else {
|
} else {
|
||||||
sql = `SELECT * FROM \`${this.escapeBacktick(table)}\` ORDER BY \`id\` LIMIT ? OFFSET ?`;
|
sql = `SELECT * FROM \`${this.escapeBacktick(table)}\` ORDER BY \`id\` LIMIT ? OFFSET ?`;
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ class Engine {
|
|||||||
private readonly pageSize: number = 1000;
|
private readonly pageSize: number = 1000;
|
||||||
private readonly batchSize: number = 50;
|
private readonly batchSize: number = 50;
|
||||||
|
|
||||||
constructor(private readonly database: string) {
|
constructor(database: string) {
|
||||||
this.db = new Pool({
|
this.db = new Pool({
|
||||||
allowExitOnIdle: true,
|
allowExitOnIdle: true,
|
||||||
connectionString: this.database,
|
connectionString: database,
|
||||||
max: 16,
|
max: 16,
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -19,30 +19,23 @@ export const buildSearchQuery = (input: string) => {
|
|||||||
.join(" & ");
|
.join(" & ");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export function buildDateRange(range?: DateRange): DateRange {
|
||||||
* Resolve a date range given an explicit range.
|
const end = endOfDay(range?.end ?? new Date());
|
||||||
* Defaults to the last 30 days when no range is provided.
|
const start = startOfDay(
|
||||||
*/
|
range?.start ?? subDays(end, Math.max(DEFAULT_PUBLICATION_GRAPH_DAYS - 1, 0)),
|
||||||
export function buildDateRange(range?: DateRange): [startDate: Date, endDate: Date] {
|
|
||||||
const endDate = endOfDay(range?.end ?? new Date());
|
|
||||||
const startDate = startOfDay(
|
|
||||||
range?.start ?? subDays(endDate, Math.max(DEFAULT_PUBLICATION_GRAPH_DAYS - 1, 0)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [startDate, endDate];
|
return { end, start };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function buildPreviousRange(range: DateRange): DateRange {
|
||||||
* Given a [start, end] date range, produce the immediately preceding range of the same length.
|
|
||||||
*/
|
|
||||||
export function buildPreviousRange([startDate, endDate]: [Date, Date]): [Date, Date] {
|
|
||||||
const days = Math.max(
|
const days = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1,
|
Math.round((range.end.getTime() - range.start.getTime()) / (1000 * 60 * 60 * 24)) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const previousRangeEnd = endOfDay(subDays(startDate, 1));
|
const end = endOfDay(subDays(range.start, 1));
|
||||||
const previousRangeStart = startOfDay(subDays(previousRangeEnd, days - 1));
|
const start = startOfDay(subDays(end, days - 1));
|
||||||
|
|
||||||
return [previousRangeStart, previousRangeEnd];
|
return { end, start };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./articles";
|
export * from "./articles";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
|
export * from "./reports";
|
||||||
export * from "./shared";
|
export * from "./shared";
|
||||||
export * from "./sources";
|
export * from "./sources";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { z } from "@hono/zod-openapi";
|
||||||
|
|
||||||
|
import { deltaSchema } from "#domain/models/shared";
|
||||||
|
|
||||||
|
export const overviewMetricSchema = z
|
||||||
|
.object({
|
||||||
|
delta: deltaSchema.openapi({
|
||||||
|
description: "Change measured over the last 30 days compared to the previous 30-day window.",
|
||||||
|
}),
|
||||||
|
total: z.number().int().nonnegative().openapi({
|
||||||
|
description: "Total count across the entire dataset.",
|
||||||
|
example: 12584,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
description: "Aggregated metric with total count and delta metadata.",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dashboardOverviewSchema = z
|
||||||
|
.object({
|
||||||
|
articles: overviewMetricSchema,
|
||||||
|
sources: overviewMetricSchema,
|
||||||
|
users: overviewMetricSchema,
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
description: "Dashboard overview metrics for key entities.",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OverviewMetric = z.infer<typeof overviewMetricSchema>;
|
||||||
|
export type DashboardOverview = z.infer<typeof dashboardOverviewSchema>;
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "catalog:",
|
"date-fns": "catalog:",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user