From 30b11ce66ec3c5c72291ba14051ba456058bc08c Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Feb 2026 10:28:24 +0000 Subject: [PATCH] feat: add summary cards in Stat page --- frontend/src/components/Card.tsx | 52 ++++++++++++++ frontend/src/pages/Stats.tsx | 117 ++++++++++++++++++++++++------- 2 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/Card.tsx diff --git a/frontend/src/components/Card.tsx b/frontend/src/components/Card.tsx new file mode 100644 index 0000000..cdefe2b --- /dev/null +++ b/frontend/src/components/Card.tsx @@ -0,0 +1,52 @@ +import type { CSSProperties } from "react"; + +const Card = (props: { + label: string; + value: string | number; + sublabel?: string; + rightSlot?: React.ReactNode; + style?: CSSProperties +}) => { + return ( +
+
+
+ {props.label} +
+ {props.rightSlot ?
{props.rightSlot}
: null} +
+
{props.value}
+ {props.sublabel ?
{props.sublabel}
: null} +
+ ); +} + +export default Card; \ No newline at end of file diff --git a/frontend/src/pages/Stats.tsx b/frontend/src/pages/Stats.tsx index 77f905e..08173d9 100644 --- a/frontend/src/pages/Stats.tsx +++ b/frontend/src/pages/Stats.tsx @@ -13,6 +13,7 @@ import { import ActivityHeatmap from "../stats/ActivityHeatmap"; import { ReactWordcloud } from '@cp949/react-wordcloud'; import StatsStyling from "../styles/stats_styling"; +import Card from "../components/Card"; type BackendWord = { word: string; @@ -25,8 +26,36 @@ type TopUser = { count: number; }; +type SummaryResponse = { + total_events: number; + total_posts: number; + total_comments: number; + unique_users: number; + comments_per_post: number; + lurker_ratio: number; + time_range: { + start: number; + end: number; + }; + sources: string[]; +}; + const styles = StatsStyling; +function formatDateRange(startUnix: number, endUnix: number) { + const start = new Date(startUnix * 1000); + const end = new Date(endUnix * 1000); + + const fmt = (d: Date) => + d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "2-digit", + }); + + return `${fmt(start)} → ${fmt(end)}`; +} + const StatPage = () => { const [error, setError] = useState(''); const [loading, setLoading] = useState(false); @@ -35,6 +64,8 @@ const StatPage = () => { const [heatmapData, setHeatmapData] = useState([]); const [topUserData, setTopUserData] = useState([]); const [wordFrequencyData, setWordFrequencyData] = useState([]); + const [summary, setSummary] = useState(null); + const searchInputRef = useRef(null); const beforeDateRef = useRef(null); @@ -48,8 +79,9 @@ const StatPage = () => { axios.get("http://localhost:5000/stats/time"), axios.get("http://localhost:5000/stats/user"), axios.get("http://localhost:5000/stats/content"), + axios.get(`http://localhost:5000/stats/summary`), ]) - .then(([timeRes, userRes, wordsRes]) => { + .then(([timeRes, userRes, wordsRes, summaryRes]) => { const eventsPerDay = Array.isArray(timeRes.data?.events_per_day) ? timeRes.data.events_per_day.filter((d: any) => new Date(d.date) >= new Date("2026-01-10")) : []; @@ -78,6 +110,7 @@ const StatPage = () => { value: d.count, })) ); + setSummary(summaryRes.data ?? null); }) .catch((e) => setError("Failed to load statistics: " + String(e))) .finally(() => setLoading(false)); @@ -162,7 +195,47 @@ return ( {/* main grid*/} -
+
+ + + + + 3 ? "…" : "") + : "—" + } + style={{ + gridColumn: "span 4" + }} + /> + {/* events per day */}

Events per Day

@@ -200,32 +273,26 @@ return (
{/* Top Users */} - {topUserData?.length > 0 && ( -
-

Top Users

-

Most active authors

+
+

Top Users

+

Most active authors

-
- {topUserData.map((item) => ( -
-
{item.author}
-
- {item.source} • {item.count} events -
+
+ {topUserData.map((item) => ( +
+
{item.author}
+
+ {item.source} • {item.count} events
- ))} -
+
+ ))}
- )} +
{/* Heatmap */}