fix(app): remove legacy and scoped namespace alias

This commit is contained in:
2025-11-14 11:56:34 +02:00
parent 085851527e
commit 4ec2a608b1
681 changed files with 655 additions and 36825 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"aliases": {
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"components": "#dashboard/components",
"hooks": "#dashboard/hooks",
"lib": "#dashboard/lib",
"ui": "@basango/ui/components",
"utils": "@basango/ui/lib/utils"
},
+5 -1
View File
@@ -4,10 +4,13 @@
"@basango/ui": "workspace:*",
"@date-fns/tz": "^1.4.1",
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "^5.90.7",
"@tanstack/react-query": "^5.90.8",
"@tanstack/react-table": "^8.21.3",
"@trpc/client": "^11.7.1",
"@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1",
"client-only": "^0.0.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.553.0",
"next": "catalog:",
@@ -18,6 +21,7 @@
"react-dom": "catalog:",
"react-hook-form": "^7.66.0",
"recharts": "^3.4.1",
"server-only": "^0.0.1",
"superjson": "^2.2.5",
"zod": "^4.1.12",
"zustand": "^5.0.8"
@@ -1,18 +1,22 @@
import { Metadata } from "next";
import { PageLayout } from "#dashboard/components/shell/page-layout";
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">
<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 articles" title="Articles">
<div className="flex flex-1 flex-col gap-4">
<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" />
</div>
<div className="bg-muted/50 min-h-screen flex-1 rounded-xl md:min-h-min" />
</div>
<div className="bg-muted/50 min-h-screen flex-1 rounded-xl md:min-h-min" />
</div>
</PageLayout>
);
}
@@ -1,8 +1,8 @@
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";
import { PageHeader } from "#dashboard/components/shell/page-header";
import { AppSidebar } from "#dashboard/components/sidebar/app-sidebar";
import { HydrateClient } from "#dashboard/trpc/server";
export default async function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -1,26 +1,31 @@
import { RouterOutputs } from "@basango/api/trpc/routers/_app";
import { Metadata } from "next";
import { PageLayout } from "@/components/shell/page-layout";
import { SourceCard } from "@/components/source-card";
import { batchPrefetch, getQueryClient, trpc } from "@/trpc/server";
import { PageLayout } from "#dashboard/components/shell/page-layout";
import { SourceCard } from "#dashboard/components/source-card";
import { HydrateClient, getQueryClient, prefetch, trpc } from "#dashboard/trpc/server";
export const metadata: Metadata = {
title: "Sources | Basango Dashboard",
};
type SourceDetails = RouterOutputs["sources"]["get"][number];
export default async function Page() {
const queryClient = getQueryClient();
batchPrefetch([trpc.sources.get.queryOptions()]);
prefetch(trpc.sources.get.queryOptions());
const sources = await queryClient.fetchQuery(trpc.sources.get.queryOptions());
return (
<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>
</PageLayout>
<HydrateClient>
<PageLayout leading="Manage your news sources" title="Sources">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{sources.map((source: SourceDetails) => (
<SourceCard key={source.id} source={source} />
))}
</div>
</PageLayout>
</HydrateClient>
);
}
@@ -1,6 +1,6 @@
import { GalleryVerticalEnd } from "lucide-react";
import { LoginForm } from "@/components/forms/login-form";
import { LoginForm } from "#dashboard/components/forms/login-form";
export default function LoginPage() {
return (
@@ -3,8 +3,8 @@
import { ThemeProvider } from "next-themes";
import type { ReactNode } from "react";
import { I18nProviderClient } from "@/locales/client";
import { TRPCReactProvider } from "@/trpc/client";
import { I18nProviderClient } from "#dashboard/locales/client";
import { TRPCReactProvider } from "#dashboard/trpc/client";
type ProviderProps = {
locale: string;
+37 -139
View File
@@ -1,10 +1,10 @@
"use client";
import { Badge } from "@basango/ui/components/badge";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@basango/ui/components/card";
@@ -14,49 +14,9 @@ import {
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;
}
import { formatNumber } from "#dashboard/utils/utils";
const chartConfig = {
count: {
@@ -68,50 +28,34 @@ const chartConfig = {
},
} satisfies ChartConfig;
export function SourceCard({
id,
name,
displayName,
url,
description,
publicationGraph,
credibility,
}: SourceCardProps) {
const chartData = publicationGraph.items;
type SourceDetails = {
id: string;
name: string;
displayName: string | null;
url: string;
description: string;
publicationGraph: {
items: { date: string; count: number }[];
total: number;
};
credibility: {
bias: string;
reliability: string;
transparency: string;
};
articles: number;
};
export function SourceCard({ source }: { source: SourceDetails }) {
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>
<Card>
<CardHeader>
<CardTitle>{source.name}</CardTitle>
<CardDescription>{source.id}</CardDescription>
</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,
}}
>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={source.publicationGraph.items}>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
@@ -127,65 +71,19 @@ export function SourceCard({
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)`} />
<ChartTooltip content={<ChartTooltipContent indicator="dashed" />} cursor={false} />
<Bar dataKey="count" fill="var(--color-count)" radius={4} />
</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>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 leading-none font-medium">
{formatNumber(source.articles)} articles crawled
</div>
<div className="text-muted-foreground leading-none">
Showing last {source.publicationGraph.total} days
</div>
</CardFooter>
</Card>
);
}
+11 -11
View File
@@ -2,8 +2,8 @@
import type { AppRouter } from "@basango/api/trpc/routers/_app";
import type { QueryClient } from "@tanstack/react-query";
import { QueryClientProvider, isServer } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client";
import { QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCContext } from "@trpc/tanstack-react-query";
import { useState } from "react";
import superjson from "superjson";
@@ -15,7 +15,7 @@ export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
let browserQueryClient: QueryClient;
function getQueryClient() {
if (isServer) {
if (typeof window === "undefined") {
// Server: always make a new query client
return makeQueryClient();
}
@@ -34,25 +34,25 @@ export function TRPCReactProvider(
children: React.ReactNode;
}>,
) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
async headers() {
const token = window.localStorage.getItem("auth_token");
headers: async () => {
//const token = window.localStorage.getItem("auth_token");
return {
Authorization: `Bearer ${token}`,
//Authorization: `Bearer ${token}`,
};
},
transformer: superjson,
url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`,
}),
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
],
}),
);
+4 -28
View File
@@ -1,7 +1,7 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: needed for tRPC type inference */
import "server-only";
import type { AppRouter } from "@basango/api/trpc/routers/_app";
//import { getCountryCode, getLocale, getTimezone } from "@basango/location";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client";
import { type TRPCQueryOptions, createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
@@ -43,38 +43,14 @@ export const trpc = createTRPCOptionsProxy<AppRouter>({
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return <HydrationBoundary state={dehydrate(queryClient)}>{props.children}</HydrationBoundary>;
}
export function prefetch<T extends ReturnType<TRPCQueryOptions<AppRouter>>>(queryOptions: T) {
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(queryOptions: T) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === "infinite") {
void queryClient.prefetchInfiniteQuery(
queryOptions as unknown as Parameters<typeof queryClient.prefetchInfiniteQuery>[0],
);
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(
queryOptions as unknown as Parameters<typeof queryClient.prefetchQuery>[0],
);
}
}
export function batchPrefetch<T extends ReturnType<TRPCQueryOptions<AppRouter>>>(
queryOptionsArray: T[],
) {
const queryClient = getQueryClient();
for (const queryOptions of queryOptionsArray) {
if (queryOptions.queryKey[1]?.type === "infinite") {
void queryClient.prefetchInfiniteQuery(
queryOptions as unknown as Parameters<typeof queryClient.prefetchInfiniteQuery>[0],
);
} else {
void queryClient.prefetchQuery(
queryOptions as unknown as Parameters<typeof queryClient.prefetchQuery>[0],
);
}
void queryClient.prefetchQuery(queryOptions);
}
}
+110
View File
@@ -0,0 +1,110 @@
import type { TZDate } from "@date-fns/tz";
import { format, isSameYear } from "date-fns";
export function formatSize(bytes: number): string {
const units = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte"];
const unitIndex = Math.max(
0,
Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1),
);
return Intl.NumberFormat("en-US", {
style: "unit",
unit: units[unitIndex],
}).format(+Math.round(bytes / 1024 ** unitIndex));
}
export function secondsToHoursAndMinutes(seconds: number) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours && minutes) {
return `${hours}h ${minutes}m`;
}
if (hours) {
return `${hours}h`;
}
if (minutes) {
return `${minutes}m`;
}
return "0m";
}
export function formatDate(date: string, dateFormat?: string | null, checkYear = true) {
if (checkYear && isSameYear(new Date(), new Date(date))) {
return format(new Date(date), "MMM d");
}
return format(new Date(date), dateFormat ?? "P");
}
export function formatNumber(value: number): string {
return Intl.NumberFormat("en-US").format(value);
}
export function getInitials(value: string) {
const formatted = value.toUpperCase().replace(/[\s.-]/g, "");
if (formatted.split(" ").length > 1) {
return `${formatted.charAt(0)}${formatted.charAt(1)}`;
}
if (value.length > 1) {
return formatted.charAt(0) + formatted.charAt(1);
}
return formatted.charAt(0);
}
export function formatDateRange(dates: TZDate[]): string {
if (!dates.length) return "";
const formatFullDate = (date: TZDate) => format(date, "MMM d");
const formatDay = (date: TZDate) => format(date, "d");
const startDate = dates[0];
const endDate = dates[1];
if (!startDate) return "";
if (dates.length === 1 || !endDate || startDate.getTime() === endDate.getTime()) {
return formatFullDate(startDate);
}
if (startDate.getMonth() === endDate.getMonth()) {
// Same month
return `${format(startDate, "MMM")} ${formatDay(startDate)} - ${formatDay(endDate)}`;
}
// Different months
return `${formatFullDate(startDate)} - ${formatFullDate(endDate)}`;
}
export function formatRelativeTime(date: Date): string {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return "just now";
}
const intervals = [
{ label: "y", seconds: 31536000 },
{ label: "mo", seconds: 2592000 },
{ label: "d", seconds: 86400 },
{ label: "h", seconds: 3600 },
{ label: "m", seconds: 60 },
] as const;
for (const interval of intervals) {
const count = Math.floor(diffInSeconds / interval.seconds);
if (count > 0) {
return `${count}${interval.label} ago`;
}
}
return "just now";
}
+8 -9
View File
@@ -2,8 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@basango/ui/*": ["../../packages/ui/src/*"]
"@basango/api/*": ["../api/src/*"],
"@basango/ui/*": ["../../packages/ui/src/*"],
"#dashboard/*": ["./src/*"]
},
"plugins": [
{
@@ -13,12 +14,10 @@
},
"exclude": ["node_modules"],
"extends": "@basango/tsconfig/nextjs.json",
"include": [
"next-env.d.ts",
"next.config.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/ui/src/hooks/use-mobile.ts"
"include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"references": [
{
"path": "../api"
}
]
}