diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 1c9b665..1005891 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -8,6 +8,7 @@ import { FolderClosed, LayoutDashboard, LayoutGrid, + Link2, LogOut, MessageSquare, Users, @@ -22,6 +23,7 @@ const NAV = [ { href: "/users", label: "Users", icon: Users }, { href: "/posts", label: "Posts", icon: LayoutGrid }, { href: "/collections", label: "Collections", icon: FolderClosed }, + { href: "/short-urls", label: "Short URLs", icon: Link2 }, { href: "/feedback", label: "Feedback", icon: MessageSquare }, { href: "/reports", label: "Reports", icon: Flag }, ]; diff --git a/app/(dashboard)/short-urls/page.tsx b/app/(dashboard)/short-urls/page.tsx new file mode 100644 index 0000000..f4eafc7 --- /dev/null +++ b/app/(dashboard)/short-urls/page.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { api, type AdminShortUrl, type ShortUrlStats } from "@/lib/api"; +import { usePagedList } from "@/lib/use-paged-list"; +import { ListShell, EmptyRow } from "@/components/list-shell"; +import { Table, THead, TR, TH, TD } from "@/components/ui/table"; +import { fmtDate } from "@/lib/utils"; + +export default function ShortUrlsPage() { + // ── Stats ──────────────────────────────────────────────────────────────── + const [stats, setStats] = useState(null); + const [from, setFrom] = useState(""); + const [to, setTo] = useState(""); + + useEffect(() => { + // Fetch once both custom dates are set, or once both are cleared (fixed + // windows only). A half-filled range waits for the second date. + const ready = (!!from && !!to) || (!from && !to); + if (!ready) return; + let cancelled = false; + api + .shortUrlStats(from || undefined, to || undefined) + .then((s) => { + if (!cancelled) setStats(s); + }) + .catch(() => { + /* leave the previous stats in place on error */ + }); + return () => { + cancelled = true; + }; + }, [from, to]); + + // ── Paginated list ───────────────────────────────────────────────────────── + const [q, setQ] = useState(""); + const fetcher = useCallback( + (limit: number, offset: number) => api.shortUrls(q, limit, offset), + [q], + ); + const list = usePagedList(fetcher, [q]); + + return ( +
+
+
+

+ Short URL stats +

+ {/* Custom date range (inclusive). Clearing both returns to the fixed + windows below. */} +
+ + + {(from || to) && ( + + )} +
+
+ +
+ + + + + {stats?.range && ( + + )} +
+
+ + + + + + + + + + + + + + + {list.items.length === 0 && !list.loading ? ( + + ) : ( + list.items.map((s) => ( + + + + + + + + + )) + )} + +
CodeTargetTypeClicksStatusCreated
+ {s.code} + + + {s.long_url} + + + {s.content_type || "—"} + + + {s.click_count.toLocaleString()} + + + ({s.human_click_count.toLocaleString()} human) + + + + {s.disabled ? "Disabled" : "Active"} + + {fmtDate(s.created_at)}
+
+
+ ); +} + +function StatCard({ + label, + created, + clicks, + highlight, +}: { + label: string; + created?: number; + clicks?: number; + highlight?: boolean; +}) { + return ( +
+
+ {label} +
+
+
+
+ {(created ?? 0).toLocaleString()} +
+
created
+
+
+
+ {(clicks ?? 0).toLocaleString()} +
+
clicks
+
+
+
+ ); +} diff --git a/lib/api.ts b/lib/api.ts index a4010ab..97b8e27 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -92,6 +92,28 @@ export type Stats = { feedback_by_category: { category: string; count: number }[]; }; +export type AdminShortUrl = { + id: number; + code: string; + long_url: string; + content_type: string; + click_count: number; + human_click_count: number; + disabled: boolean; + created_at: string; +}; + +export type ShortUrlRangeStat = { created: number; clicks: number }; + +export type ShortUrlStats = { + today: ShortUrlRangeStat; + week: ShortUrlRangeStat; + month: ShortUrlRangeStat; + total_urls: number; + total_clicks: number; + range?: ShortUrlRangeStat; // present only when a custom from/to is requested +}; + export type AdminUser = { id: string; email: string | null; @@ -289,6 +311,15 @@ export const api = { collections(q: string, limit: number, offset: number) { return listed(`/admin/collections${qs({ q, limit, offset })}`); }, + shortUrls(q: string, limit: number, offset: number) { + return listed(`/admin/short-urls${qs({ q, limit, offset })}`); + }, + async shortUrlStats(from?: string, to?: string): Promise { + const { data } = await call( + `/admin/short-urls/stats${qs({ from, to })}`, + ); + return data; + }, feedback(status: string, limit: number, offset: number) { return listed(`/admin/feedback${qs({ status, limit, offset })}`); },