feat: revamp styling on stats page
Abstracted away CSS into stats_styling.tsx
This commit is contained in:
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -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,52 +113,59 @@ 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 (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.page}>
|
||||||
|
<div style={{ ...styles.container, ...styles.card, ...styles.headerBar }}>
|
||||||
|
<div style={styles.controls}>
|
||||||
<input
|
<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" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Word Cloud</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Most common terms across events</p>
|
||||||
|
|
||||||
|
<div style={styles.chartWrapper}>
|
||||||
<ReactWordcloud
|
<ReactWordcloud
|
||||||
words={wordFrequencyData}
|
words={wordFrequencyData}
|
||||||
options={{
|
options={{
|
||||||
@@ -166,33 +176,48 @@ const StatPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Users */}
|
||||||
{topUserData?.length > 0 && (
|
{topUserData?.length > 0 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxHeight: 450,
|
...styles.card,
|
||||||
width: "100%",
|
...styles.scrollArea,
|
||||||
overflowY: "auto",
|
gridColumn: "span 3",
|
||||||
padding: 12
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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>
|
{/* Heatmap */}
|
||||||
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Heatmap</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Activity density across time</p>
|
||||||
|
|
||||||
<div style={{ width: "100%", height: 320}}>
|
<div style={styles.heatmapWrapper}>
|
||||||
<h2>Heatmap</h2>
|
|
||||||
<ActivityHeatmap data={heatmapData} />
|
<ActivityHeatmap data={heatmapData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StatPage;
|
export default StatPage;
|
||||||
134
frontend/src/styles/stats_styling.tsx
Normal file
134
frontend/src/styles/stats_styling.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user