feat: revamp styling on stats page

Abstracted away CSS into stats_styling.tsx
This commit is contained in:
2026-02-02 16:31:14 +00:00
parent 2a54f863f4
commit 4742193818
3 changed files with 220 additions and 93 deletions

View File

@@ -1,42 +1,10 @@
html,
body,
#root { #root {
max-width: 1280px; height: 100%;
margin: 0 auto; width: 100%;
padding: 2rem;
text-align: center;
} }
.logo { body {
height: 6em; margin: 0;
padding: 1.5em; }
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -12,6 +12,7 @@ import {
import ActivityHeatmap from "../stats/ActivityHeatmap"; import ActivityHeatmap from "../stats/ActivityHeatmap";
import { ReactWordcloud } from '@cp949/react-wordcloud'; import { ReactWordcloud } from '@cp949/react-wordcloud';
import StatsStyling from "../styles/stats_styling";
type BackendWord = { type BackendWord = {
word: string; word: string;
@@ -24,6 +25,8 @@ type TopUser = {
count: number; count: number;
}; };
const styles = StatsStyling;
const StatPage = () => { const StatPage = () => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -110,89 +113,111 @@ const StatPage = () => {
getStats(); getStats();
}, []) }, [])
if (loading) return <p className="p-6">Loading insights</p>; if (loading) return <p style={{...styles.page, minWidth: "100vh", minHeight: "100vh"}}>Loading insights</p>;
if (error) return <p className="p-6 text-red-500">{error}</p>; if (error) return <p style={{...styles.page}}>{error}</p>;
return ( return (
<div> <div style={styles.page}>
<div style={{ ...styles.container, ...styles.card, ...styles.headerBar }}>
<input <div style={styles.controls}>
<input
type="text" type="text"
id="query" id="query"
ref={inputRef} ref={inputRef}
placeholder="Search events..."
style={styles.input}
/> />
<button onClick={onSearch}>Search</button> <button onClick={onSearch} style={styles.buttonPrimary}>
<button onClick={resetFilters}>Reset</button> Search
</button>
<div <button onClick={resetFilters} style={styles.buttonSecondary}>
style={{ Reset
display: "grid", </button>
gridTemplateColumns: "2fr 2fr 2fr", </div>
gap: 12,
margin: "0 auto",
width: "100%",
height: "100%"
}}
>
<div> <div style={{ fontSize: 13, color: "#6b7280" }}>Analytics Dashboard</div>
<h2>Events per Day</h2> </div>
<ResponsiveContainer width={600} height={350}> {/* main grid*/}
<div style={{ ...styles.container, ...styles.grid }}>
{/* events per day */}
<div style={{ ...styles.card, gridColumn: "span 5" }}>
<h2 style={styles.sectionTitle}>Events per Day</h2>
<p style={styles.sectionSubtitle}>Trend of activity over time</p>
<div style={styles.chartWrapper}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={postsPerDay}> <LineChart data={postsPerDay}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" /> <XAxis dataKey="date" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Line <Line type="monotone" dataKey="count" name="Events" />
type="monotone"
dataKey="count"
name="Events"
/>
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div>
<div> {/* Word Cloud */}
<h2>Word Cloud</h2> <div style={{ ...styles.card, gridColumn: "span 4" }}>
<ReactWordcloud <h2 style={styles.sectionTitle}>Word Cloud</h2>
<p style={styles.sectionSubtitle}>Most common terms across events</p>
<div style={styles.chartWrapper}>
<ReactWordcloud
words={wordFrequencyData} words={wordFrequencyData}
options={{ options={{
rotations: 2, rotations: 2,
rotationAngles: [0, 90], rotationAngles: [0, 90],
fontSizes: [14, 60], fontSizes: [14, 60],
enableTooltip: true, enableTooltip: true,
}} }}
/> />
</div> </div>
</div>
{topUserData?.length > 0 && ( {/* Top Users */}
<div {topUserData?.length > 0 && (
style={{ <div
maxHeight: 450, style={{
width: "100%", ...styles.card,
overflowY: "auto", ...styles.scrollArea,
padding: 12 gridColumn: "span 3",
}} }}
> >
<h2>Top Users</h2> <h2 style={styles.sectionTitle}>Top Users</h2>
<p style={styles.sectionSubtitle}>Most active authors</p>
<div style={styles.topUsersList}>
{topUserData.map((item) => ( {topUserData.map((item) => (
<p key={`${item.author}-${item.source}`}> <div
{item.author} ({item.source}): {item.count} key={`${item.author}-${item.source}`}
</p> style={styles.topUserItem}
>
<div style={styles.topUserName}>{item.author}</div>
<div style={styles.topUserMeta}>
{item.source} {item.count} events
</div>
</div>
))} ))}
</div> </div>
)}
</div> </div>
)}
<div style={{ width: "100%", height: 320}}> {/* Heatmap */}
<h2>Heatmap</h2> <div style={{ ...styles.card, gridColumn: "span 12" }}>
<ActivityHeatmap data={heatmapData} /> <h2 style={styles.sectionTitle}>Heatmap</h2>
<p style={styles.sectionSubtitle}>Activity density across time</p>
<div style={styles.heatmapWrapper}>
<ActivityHeatmap data={heatmapData} />
</div>
</div> </div>
</div> </div>
); </div>
);
} }
export default StatPage; export default StatPage;

View File

@@ -0,0 +1,134 @@
import type { CSSProperties } from "react";
const StatsStyling: Record<string, CSSProperties> = {
page: {
width: "100%",
height: "100%",
minHeight: "100vh",
minWidth: "100vh",
padding: 24,
background: "#f6f7fb",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Arial, sans-serif',
color: "#111827",
},
container: {
maxWidth: 1400,
margin: "0 auto",
},
card: {
background: "white",
borderRadius: 16,
padding: 16,
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 6px 20px rgba(0,0,0,0.06)",
},
headerBar: {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
},
controls: {
display: "flex",
gap: 10,
alignItems: "center",
},
input: {
width: 320,
maxWidth: "70vw",
padding: "10px 12px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.12)",
outline: "none",
fontSize: 14,
background: "#fff",
color: "black"
},
buttonPrimary: {
padding: "10px 14px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.08)",
background: "#2563eb",
color: "white",
fontWeight: 600,
cursor: "pointer",
boxShadow: "0 6px 16px rgba(37,99,235,0.25)",
},
buttonSecondary: {
padding: "10px 14px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.12)",
background: "#fff",
color: "#111827",
fontWeight: 600,
cursor: "pointer",
},
grid: {
marginTop: 18,
display: "grid",
gridTemplateColumns: "repeat(12, 1fr)",
gap: 16,
},
sectionTitle: {
margin: 0,
fontSize: 16,
fontWeight: 700,
},
sectionSubtitle: {
margin: "6px 0 14px",
fontSize: 13,
color: "#6b7280",
},
chartWrapper: {
width: "100%",
height: 350,
},
heatmapWrapper: {
width: "100%",
height: 320,
},
topUsersList: {
display: "flex",
flexDirection: "column",
gap: 10,
},
topUserItem: {
padding: "10px 12px",
borderRadius: 12,
background: "#f9fafb",
border: "1px solid rgba(0,0,0,0.06)",
},
topUserName: {
fontWeight: 700,
fontSize: 14,
},
topUserMeta: {
fontSize: 13,
color: "#6b7280",
},
scrollArea: {
maxHeight: 450,
overflowY: "auto",
},
};
export default StatsStyling;