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 */}