feat(dashboard): list sources with statistics

This commit is contained in:
2025-11-13 11:25:07 +02:00
parent 8cc40fde67
commit 6503980cbc
24 changed files with 1016 additions and 373 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ export const sourcesRouter = createTRPCRouter({
get: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)),
getById: protectedProcedure.input(getSourceSchema).query(async ({ ctx, input }) => {
return getSourceById(ctx.db, { ...input });
return getSourceById(ctx.db, input.id);
}),
update: protectedProcedure.input(updateSourceSchema).mutation(async ({ ctx, input }) => {
+1
View File
@@ -17,6 +17,7 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "^7.66.0",
"recharts": "^3.4.1",
"superjson": "^2.2.5",
"zod": "^4.1.12",
"zustand": "^5.0.8"
@@ -1,3 +1,9 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Articles | Basango Dashboard",
};
export default function Page() {
return (
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
@@ -1,14 +1,6 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@basango/ui/components/breadcrumb";
import { Separator } from "@basango/ui/components/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@basango/ui/components/sidebar";
import { SidebarInset, SidebarProvider } from "@basango/ui/components/sidebar";
import { PageHeader } from "@/components/shell/page-header";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { HydrateClient } from "@/trpc/server";
@@ -19,23 +11,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator className="mr-2 data-[orientation=vertical]:h-4" orientation="vertical" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<PageHeader />
{children}
</SidebarInset>
</SidebarProvider>
@@ -1,12 +1,26 @@
export default function Page() {
import { Metadata } from "next";
import { PageLayout } from "@/components/shell/page-layout";
import { SourceCard } from "@/components/source-card";
import { batchPrefetch, getQueryClient, trpc } from "@/trpc/server";
export const metadata: Metadata = {
title: "Sources | Basango Dashboard",
};
export default async function Page() {
const queryClient = getQueryClient();
batchPrefetch([trpc.sources.get.queryOptions()]);
const sources = await queryClient.fetchQuery(trpc.sources.get.queryOptions());
return (
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="bg-muted/50 aspect-video rounded-xl" />
<div className="bg-muted/50 aspect-video rounded-xl" />
<div className="bg-muted/50 aspect-video rounded-xl" />
<PageLayout leading="Manage your news sources" title="Sources">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{sources.map((source) => (
<SourceCard key={source.id} {...source} />
))}
</div>
<div className="bg-muted/50 min-h-screen flex-1 rounded-xl md:min-h-min" />
</div>
</PageLayout>
);
}
@@ -0,0 +1,54 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@basango/ui/components/breadcrumb";
import { Separator } from "@basango/ui/components/separator";
//import { LanguageSelector, ThemeSelector } from "@/components/ui/shared/settings";
import { SidebarTrigger } from "@basango/ui/components/sidebar";
export function PageHeader() {
return (
<header className="flex h-16 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator className="mr-2 data-[orientation=vertical]:h-4" orientation="vertical" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
);
}
// <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">
// <div className="flex items-center gap-2 px-4">
// <SidebarTrigger className="-ml-1" />
// <Separator className="mr-2 data-[orientation=vertical]:h-4" orientation="vertical" />
// <Breadcrumb>
// <BreadcrumbList>
// <BreadcrumbItem className="hidden md:block">
// <Button className="cursor-pointer" onClick={() => navigate(-1)} variant="ghost">
// <ArrowLeftIcon />
// <span>{t("ui.shared.shell.page_header.go_back")}</span>
// </Button>
// </BreadcrumbItem>
// </BreadcrumbList>
// </Breadcrumb>
// </div>
// <div className="flex items-center gap-2 px-4">
// <LanguageSelector />
// <ThemeSelector />
// </div>
// </header>
@@ -0,0 +1,31 @@
import React from "react";
interface PageProps {
children: React.ReactNode;
title?: string | React.ReactNode;
leading?: string | React.ReactNode;
}
export const PageLayout = (props: React.PropsWithChildren<PageProps>) => {
const { title, leading, children } = props;
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="container mx-auto">
{title && (
<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>
);
};
@@ -0,0 +1,97 @@
import { Skeleton } from "@basango/ui/components/skeleton";
type Props = {
dashboard?: boolean;
details?: boolean;
};
const DashboardSkeleton = () => {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="container mx-auto space-y-10">
<div className="space-y-2 mt-4">
<Skeleton className="h-8 w-[250px]" />
<Skeleton className="h-4 w-[550px]" />
</div>
<div className="space-y-2 mt-4">
<div className="grid grid-cols-3 gap-4">
<Skeleton className="h-[400px] w-full" />
<Skeleton className="h-[400px] w-full" />
<Skeleton className="h-[400px] w-full" />
</div>
</div>
<div className="space-y-2 mt-4">
<Skeleton className="h-[500px] w-full" />
</div>
<div className="mb-8 flex items-center justify-between space-y-6"></div>
</div>
</div>
);
};
const ListSkeleton = () => {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="container mx-auto space-y-10">
<div className="space-y-2 mt-4">
<Skeleton className="h-8 w-[250px]" />
<Skeleton className="h-4 w-[550px]" />
</div>
<div className="space-y-2 mt-4">
<div className="space-y-2 mt-4 flex justify-between">
<div className="flex justify-between gap-4">
<Skeleton className="h-9 w-[380px]" />
<Skeleton className="h-9 w-[150px]" />
</div>
<Skeleton className="h-9 w-[150px]" />
</div>
<Skeleton className="h-[500px] w-full" />
</div>
<div className="mb-8 flex items-center justify-between space-y-6"></div>
</div>
</div>
);
};
const DetailsSkeleton = () => {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="container mx-auto space-y-10">
<div className="space-y-2 mt-4">
<Skeleton className="h-8 w-[250px]" />
<Skeleton className="h-4 w-[550px]" />
</div>
<div className="space-y-2 mt-4">
<div className="space-y-2 mt-4 flex justify-between">
<div className="flex justify-between gap-4">
<Skeleton className="h-9 w-[380px]" />
</div>
<div className="flex justify-between gap-2">
<Skeleton className="h-9 w-[150px]" />
<Skeleton className="h-9 w-[150px]" />
<Skeleton className="h-9 w-[150px]" />
</div>
</div>
<Skeleton className="h-1 w-full mt-4" />
<Skeleton className="h-[100px] w-[700px] my-8" />
<div className="space-y-4">
{Array.from({ length: 6 }).map((_, index) => (
<div className="grid grid-cols-3 gap-4" key={index}>
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
</div>
))}
</div>
</div>
<div className="mb-8 flex items-center justify-between space-y-6"></div>
</div>
</div>
);
};
export const PageSkeleton = (props: Props) => {
if (props.dashboard) return <DashboardSkeleton />;
if (props.details) return <DetailsSkeleton />;
return <ListSkeleton />;
};
@@ -17,7 +17,7 @@ import {
} from "@basango/ui/components/sidebar";
import { ChevronRight, type LucideIcon } from "lucide-react";
export function NavMain({
export function AppSidebarContent({
items,
}: {
items: {
@@ -0,0 +1,26 @@
"use client";
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@basango/ui/components/sidebar";
export function AppSidebarInfo() {
const version = process.env.NEXT_PUBLIC_VERSION || "0.0.0";
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<img alt="Logo" className="size-8 rounded-lg object-cover" src="/logo.svg" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Basango</span>
<span className="truncate text-xs">v{version}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
}
@@ -1,6 +1,6 @@
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@basango/ui/components/avatar";
import { Avatar, AvatarFallback } from "@basango/ui/components/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,15 +18,7 @@ import {
} from "@basango/ui/components/sidebar";
import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-react";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
export function AppSidebarUser() {
const { isMobile } = useSidebar();
return (
@@ -39,12 +31,11 @@ export function NavUser({
size="lg"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">BN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-medium">Bernard Ng</span>
<span className="truncate text-xs">bernard.ng@example.com</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@@ -58,12 +49,11 @@ export function NavUser({
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">BN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-medium">Bernard Ng</span>
<span className="truncate text-xs">bernard.ng@example.com</span>
</div>
</div>
</DropdownMenuLabel>
@@ -7,166 +7,45 @@ import {
SidebarHeader,
SidebarRail,
} from "@basango/ui/components/sidebar";
import {
AudioWaveform,
BookOpen,
Bot,
Command,
Frame,
GalleryVerticalEnd,
MapIcon,
PieChart,
Settings2,
SquareTerminal,
} from "lucide-react";
import { SquareTerminal } from "lucide-react";
import * as React from "react";
import { NavMain } from "./nav-main";
import { NavProjects } from "./nav-projects";
import { NavUser } from "./nav-user";
import { TeamSwitcher } from "./team-switcher";
import { AppSidebarContent } from "./app-sidebar-content";
import { AppSidebarInfo } from "./app-sidebar-info";
import { AppSidebarUser } from "./app-sidebar-user";
const data = {
navMain: [
main: [
{
icon: SquareTerminal,
isActive: true,
items: [
{
title: "History",
url: "#",
title: "Sources",
url: "/sources",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
title: "Articles",
url: "/articles",
},
],
title: "Playground",
url: "#",
},
{
icon: Bot,
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
title: "Models",
url: "#",
},
{
icon: BookOpen,
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
title: "Documentation",
url: "#",
},
{
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
title: "Settings",
title: "Dataset",
url: "#",
},
],
projects: [
{
icon: Frame,
name: "Design Engineering",
url: "#",
},
{
icon: PieChart,
name: "Sales & Marketing",
url: "#",
},
{
icon: MapIcon,
name: "Travel",
url: "#",
},
],
teams: [
{
logo: GalleryVerticalEnd,
name: "Acme Inc",
plan: "Enterprise",
},
{
logo: AudioWaveform,
name: "Acme Corp.",
plan: "Startup",
},
{
logo: Command,
name: "Evil Corp.",
plan: "Free",
},
],
user: {
avatar: "/avatars/shadcn.jpg",
email: "m@example.com",
name: "shadcn",
},
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
<AppSidebarInfo />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
<AppSidebarContent items={data.main} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
<AppSidebarUser />
</SidebarFooter>
<SidebarRail />
</Sidebar>
@@ -1,82 +0,0 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@basango/ui/components/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@basango/ui/components/sidebar";
import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react";
export function NavProjects({
projects,
}: {
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
align={isMobile ? "end" : "start"}
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}
@@ -1,88 +0,0 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@basango/ui/components/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@basango/ui/components/sidebar";
import { ChevronsUpDown, Plus } from "lucide-react";
import * as React from "react";
export function TeamSwitcher({
teams,
}: {
teams: {
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
if (!activeTeam) {
return null;
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
@@ -0,0 +1,191 @@
"use client";
import { Badge } from "@basango/ui/components/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@basango/ui/components/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@basango/ui/components/chart";
import { Separator } from "@basango/ui/components/separator";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
interface CredibilityMetric {
bias: string;
reliability: string;
transparency: string;
}
interface PublicationItem {
date: string;
count: number;
}
interface SourceCardProps {
id: string;
name: string;
displayName: string | null;
url: string;
description: string;
publicationGraph: {
items: PublicationItem[];
total: number;
};
credibility: CredibilityMetric;
}
const credibilityColors = {
high: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
low: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
};
function getCredibilityColor(value: string): string {
const lower = value.toLowerCase();
if (lower.includes("high") || lower.includes("strong") || lower.includes("good")) {
return credibilityColors.high;
}
if (lower.includes("medium") || lower.includes("moderate")) {
return credibilityColors.medium;
}
return credibilityColors.low;
}
const chartConfig = {
count: {
color: "var(--chart-2)",
label: "Articles",
},
views: {
label: "Articles",
},
} satisfies ChartConfig;
export function SourceCard({
id,
name,
displayName,
url,
description,
publicationGraph,
credibility,
}: SourceCardProps) {
const chartData = publicationGraph.items;
return (
<Card className="w-full max-w-6xl border-border">
<CardHeader className="border-b">
<div className="flex items-start justify-between">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2">
<CardTitle>
<a
aria-label="Visit source website"
href={url}
rel="noopener noreferrer"
target="_blank"
>
{displayName || name}
</a>
</CardTitle>
</div>
<CardDescription className="text-base">{description}</CardDescription>
<p className="text-xs text-muted-foreground">ID: {id}</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<ChartContainer className="aspect-auto h-[250px] w-full" config={chartConfig}>
<BarChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
dataKey="date"
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString("en-US", {
day: "numeric",
month: "short",
});
}}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
});
}}
nameKey="views"
/>
}
/>
<Bar dataKey="count" fill={`var(--color-2)`} />
</BarChart>
</ChartContainer>
<Separator />
<div className="space-y-2">
<h3 className="font-semibold">Credibility Metrics</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Bias</p>
<Badge className={`${getCredibilityColor(credibility.bias)} border-0`}>
{credibility.bias}
</Badge>
</div>
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Reliability</p>
<Badge className={`${getCredibilityColor(credibility.reliability)} border-0`}>
{credibility.reliability}
</Badge>
</div>
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Transparency</p>
<Badge className={`${getCredibilityColor(credibility.transparency)} border-0`}>
{credibility.transparency}
</Badge>
</div>
</div>
</div>
<Separator />
<div className="space-y-2">
<h3 className="font-semibold">Summary</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Total Publications</p>
<p className="text-2xl font-bold">{publicationGraph.total}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Timeline Entries</p>
<p className="text-2xl font-bold">{publicationGraph.items.length}</p>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
+2 -2
View File
@@ -19,10 +19,10 @@ export const trpc = createTRPCOptionsProxy<AppRouter>({
links: [
httpBatchLink({
async headers() {
const token = window.localStorage.getItem("auth_token");
//const token = window.localStorage.getItem("auth_token");
return {
Authorization: `Bearer ${token}`,
//Authorization: `Bearer ${token}`,
// "x-user-country": await getCountryCode(),
// "x-user-locale": await getLocale(),
// "x-user-timezone": await getTimezone(),