feat(dashboard): list sources with statistics
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
+1
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
+8
-18
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user