Zurück
diff --git a/src/app/p/[secret]/page.tsx b/src/app/p/[secret]/page.tsx
index e78ae04..0471a87 100644
--- a/src/app/p/[secret]/page.tsx
+++ b/src/app/p/[secret]/page.tsx
@@ -3,24 +3,11 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
-import {
- Users,
- PawPrint,
- FileText,
- ShieldOff,
- Megaphone,
- Flag,
- Activity,
- CalendarDays,
- Ghost,
- Ban,
- Clock,
- BadgeCheck,
- TriangleAlert,
-} from "lucide-react";
+import { Flag, Clock, TriangleAlert } from "lucide-react";
import { TimeSeriesChart } from "@/components/admin/TimeSeriesChart";
import { GranularityToggle, type Granularity } from "@/components/admin/GranularityToggle";
import { Button } from "@/components/ui/button";
+import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent } from "@/components/ui/card";
export default function AdminDashboard() {
const trpc = useTRPC();
@@ -63,25 +50,25 @@ export default function AdminDashboard() {
return (
-
Dashboard
-
Platform overview
+
Dashboard
+
Platform overview
{canToggleMaintenance && (
-
Maintenance mode
-
+
Maintenance mode
+
{maintenance?.enabled
? "Live — all visitors see the maintenance page except the admin panel."
: "Off — the site is publicly reachable."}
@@ -100,127 +87,167 @@ export default function AdminDashboard() {
)}
{isLoading ? (
-
Loading…
+
Loading…
) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+ {/* Needs attention — the only stats that earn individual weight and
+ real color; color means "act on this", not "posts are orange". */}
+
+
+ {/* Everything else — a flat overview, not a wall of identical cards */}
+
+
Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
)}
-
-
-
-
Posts over time
-
New posts per {postsGranularity}
-
-
-
-
-
+
+
+ Posts over time
+ New posts per {postsGranularity}
+
+
+
+
+
+
+
+
-
-
-
-
Owner growth
-
Total registered owners
-
-
-
-
-
+
+
+ Owner growth
+ Total registered owners
+
+
+
+
+
+
+
+
-
-
-
-
Visits over time
-
Unique active owners per {visitsGranularity}
-
-
-
-
-
+
+
+ Visits over time
+ Unique active owners per {visitsGranularity}
+
+
+
+
+
+
+
+
-
-
-
-
Comments over time
-
New comments per {commentsGranularity}
-
-
-
-
-
+
+
+ Comments over time
+ New comments per {commentsGranularity}
+
+
+
+
+
+
+
+
-
-
-
-
Likes over time
-
New reactions per {reactionsGranularity}
-
-
-
-
-
+
+
+ Likes over time
+ New reactions per {reactionsGranularity}
+
+
+
+
+
+
+
+
);
}
-function StatCard({
+/**
+ * The only stat tiles with individual weight and color — color means
+ * "this needs action" (orange when > 0), not a decorative per-item hue.
+ */
+function AttentionStat({
icon: Icon,
label,
value,
- color = "text-zinc-100",
}: {
icon: React.ElementType;
label: string;
value: number;
- color?: string;
}) {
+ const needsAttention = value > 0;
return (
-
-
+
+
-
{value.toLocaleString()}
-
{label}
+
+ {value.toLocaleString()}
+
+
{label}
);
}
+
+/** Everything else — a plain label/value pair, no per-item card chrome. */
+function OverviewStat({ label, value }: { label: string; value: number }) {
+ return (
+
+
{label}
+ {value.toLocaleString()}
+
+ );
+}
diff --git a/src/components/admin/AdminMobileNav.tsx b/src/components/admin/AdminMobileNav.tsx
index af84685..70fcefc 100644
--- a/src/components/admin/AdminMobileNav.tsx
+++ b/src/components/admin/AdminMobileNav.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
+import { usePathname } from "next/navigation";
import { Menu, ArrowLeft } from "lucide-react";
import {
Sheet,
@@ -14,21 +15,26 @@ import {
import { Button } from "@/components/ui/button";
interface AdminMobileNavProps {
+ base: string;
nav: { href: string; label: string; icon: React.ReactNode }[];
}
/**
* Mobile topbar + slide-in nav drawer for the admin panel — the desktop
*
sidebar in layout.tsx is hidden below md: and replaced by this.
+ * Topbar shows the current page's own name (there's no permanent sidebar
+ * to read that from on mobile) instead of a static "PawFeed Admin" label.
*/
-export function AdminMobileNav({ nav }: AdminMobileNavProps) {
+export function AdminMobileNav({ base, nav }: AdminMobileNavProps) {
const [open, setOpen] = useState(false);
+ const pathname = usePathname();
+
+ const isActive = (href: string) => (href === base ? pathname === base : pathname.startsWith(href));
+ const currentLabel = nav.find((item) => isActive(item.href))?.label ?? "Admin";
return (
-
-
- PawFeed Admin
-
+
+
{currentLabel}
}>
@@ -36,12 +42,10 @@ export function AdminMobileNav({ nav }: AdminMobileNavProps) {
-
- PawFeed Admin
-
+ Admin-Navigation
{nav.map(({ href, label, icon }) => (
}
>
@@ -57,12 +66,12 @@ export function AdminMobileNav({ nav }: AdminMobileNavProps) {
{label}
))}
-
+
}
>
diff --git a/src/components/admin/AdminSidebarNav.tsx b/src/components/admin/AdminSidebarNav.tsx
new file mode 100644
index 0000000..ff15abe
--- /dev/null
+++ b/src/components/admin/AdminSidebarNav.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+interface AdminSidebarNavProps {
+ base: string;
+ nav: { href: string; label: string; icon: React.ReactNode }[];
+}
+
+/**
+ * Desktop admin sidebar nav — highlights the current page so a moderator
+ * always knows where they are. Dashboard (href === base) only lights up on
+ * an exact match; every other item lights up for itself and its sub-routes
+ * (e.g. a future /users/[id] detail page still highlights "Users").
+ */
+export function AdminSidebarNav({ base, nav }: AdminSidebarNavProps) {
+ const pathname = usePathname();
+
+ return (
+ <>
+ {nav.map(({ href, label, icon }) => {
+ const isActive = href === base ? pathname === base : pathname.startsWith(href);
+ return (
+
+ {icon}
+ {label}
+
+ );
+ })}
+ >
+ );
+}
diff --git a/src/components/admin/GranularityToggle.tsx b/src/components/admin/GranularityToggle.tsx
index 43ada08..fa25054 100644
--- a/src/components/admin/GranularityToggle.tsx
+++ b/src/components/admin/GranularityToggle.tsx
@@ -16,7 +16,7 @@ export function GranularityToggle({
onChange: (g: Granularity) => void;
}) {
return (
-
+
{OPTIONS.map((opt) => (
onChange(opt.value)}
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${
value === opt.value
- ? "bg-zinc-800 text-zinc-100"
- : "text-zinc-500 hover:text-zinc-300"
+ ? "bg-muted text-foreground"
+ : "text-muted-foreground hover:text-foreground"
}`}
>
{opt.label}
diff --git a/src/components/admin/TimeSeriesChart.tsx b/src/components/admin/TimeSeriesChart.tsx
index 1a43981..c6a86f7 100644
--- a/src/components/admin/TimeSeriesChart.tsx
+++ b/src/components/admin/TimeSeriesChart.tsx
@@ -48,7 +48,7 @@ export function TimeSeriesChart({
const [hoverIdx, setHoverIdx] = useState(null);
if (data.length === 0) {
- return No data yet
;
+ return No data yet
;
}
const maxV = Math.max(...data.map((d) => d.value), 1);