Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions frontend/app/analytics/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// frontend/app/analytics/layout.tsx
export default function AnalyticsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen w-full overflow-x-hidden bg-[#0A0F1A] px-4 py-6 text-slate-100 sm:px-6 md:py-8">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
<div>
<h1 className="text-xl font-semibold text-white md:text-2xl">
Analytics
</h1>
<p className="text-sm text-slate-400">
Player engagement overview for the Mind Block platform.
</p>
</div>

{children}
</div>
</div>
);
}
12 changes: 3 additions & 9 deletions frontend/app/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import DauMauChart from "@/components/analytics/DauMauChart";

export default function AnalyticsPage() {
return (
<div className="min-h-screen w-full bg-[#0A0F1A] px-4 py-8 text-slate-100 sm:px-6">
<div className="mx-auto w-full max-w-6xl">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-white">Analytics</h1>
<p className="text-sm text-slate-400">
Player engagement overview for the Mind Block platform.
</p>
</div>

<div className="grid w-full grid-cols-1 gap-6 lg:grid-cols-2">
{/* Full-width for now; future charts can sit alongside it in the grid */}
<div className="min-w-0 lg:col-span-2">
<DauMauChart />
</div>
</div>
Expand Down
114 changes: 73 additions & 41 deletions frontend/components/analytics/DauMauChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Tooltip,
Legend,
} from "recharts";
import { TrendingUp, CalendarRange } from "lucide-react";
import { TrendingUp, CalendarRange, SlidersHorizontal, X } from "lucide-react";
import { getDauMauMetrics } from "@/lib/api/analyticsApi";

function formatDate(date: Date): string {
Expand All @@ -37,6 +37,7 @@ export default function DauMauChart() {
const defaultRange = useMemo(getDefaultRange, []);
const [startDate, setStartDate] = useState(defaultRange.startDate);
const [endDate, setEndDate] = useState(defaultRange.endDate);
const [filtersOpen, setFiltersOpen] = useState(false);

const { data, isLoading, isFetching, isError, error, refetch } = useQuery({
queryKey: ["dau-mau", startDate, endDate],
Expand All @@ -56,52 +57,78 @@ export default function DauMauChart() {
const stickinessPct =
data?.stickiness != null ? Math.round(data.stickiness * 100) : null;

const controls = (
<div className="flex flex-wrap items-center gap-2">
{PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset.days)}
className="rounded-lg border border-slate-700 px-3 py-1.5 text-xs text-slate-300 transition hover:border-blue-500/60 hover:text-white"
>
{preset.label}
</button>
))}

<div className="flex items-center gap-2 rounded-lg border border-slate-700 px-3 py-1.5">
<CalendarRange className="h-4 w-4 text-slate-400" />
<input
type="date"
value={startDate}
max={endDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-transparent text-xs text-slate-200 outline-none [color-scheme:dark]"
aria-label="Start date"
/>
<span className="text-slate-500">–</span>
<input
type="date"
value={endDate}
min={startDate}
max={formatDate(new Date())}
onChange={(e) => setEndDate(e.target.value)}
className="bg-transparent text-xs text-slate-200 outline-none [color-scheme:dark]"
aria-label="End date"
/>
</div>
</div>
);

return (
<div className="rounded-2xl border border-slate-800 bg-[#101B30] p-4 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 rounded-2xl border border-slate-800 bg-[#101B30] p-4 sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-lg font-semibold text-white">DAU / MAU</h2>
<p className="text-xs text-slate-400">
Daily vs monthly active users
</p>
</div>

<div className="flex flex-wrap items-center gap-2">
{PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset.days)}
className="rounded-lg border border-slate-700 px-3 py-1.5 text-xs text-slate-300 transition hover:border-blue-500/60 hover:text-white"
>
{preset.label}
</button>
))}

<div className="flex items-center gap-2 rounded-lg border border-slate-700 px-3 py-1.5">
<CalendarRange className="h-4 w-4 text-slate-400" />
<input
type="date"
value={startDate}
max={endDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-transparent text-xs text-slate-200 outline-none [color-scheme:dark]"
aria-label="Start date"
/>
<span className="text-slate-500">–</span>
<input
type="date"
value={endDate}
min={startDate}
max={formatDate(new Date())}
onChange={(e) => setEndDate(e.target.value)}
className="bg-transparent text-xs text-slate-200 outline-none [color-scheme:dark]"
aria-label="End date"
/>
</div>
</div>
{/* Desktop / wide tablet: controls shown inline */}
<div className="hidden md:block">{controls}</div>

{/* Narrow tablet & mobile: controls collapse behind this toggle */}
<button
type="button"
onClick={() => setFiltersOpen((open) => !open)}
className="flex items-center gap-2 rounded-lg border border-slate-700 px-3 py-1.5 text-xs text-slate-300 hover:border-blue-500/60 hover:text-white md:hidden"
>
{filtersOpen ? (
<X className="h-4 w-4" />
) : (
<SlidersHorizontal className="h-4 w-4" />
)}
Filters
</button>
</div>

{/* Collapsible controls panel, only rendered below md */}
{filtersOpen && (
<div className="mt-3 rounded-xl border border-slate-700 bg-[#0A0F1A]/60 p-3 md:hidden">
{controls}
</div>
)}

{stickinessPct !== null && hasData && (
<div className="mt-4 flex w-fit items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-2">
<TrendingUp className="h-4 w-4 text-blue-300" />
Expand All @@ -111,7 +138,7 @@ export default function DauMauChart() {
</div>
)}

<div className="mt-6 h-72 w-full">
<div className="mt-6 h-64 w-full md:h-72">
{isLoading && (
<div className="flex h-full items-center justify-center text-sm text-slate-400">
Loading engagement data...
Expand Down Expand Up @@ -144,11 +171,16 @@ export default function DauMauChart() {
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={series}
margin={{ top: 8, right: 16, left: 0, bottom: 0 }}
margin={{ top: 8, right: 8, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="date" stroke="#64748b" tick={{ fontSize: 12 }} />
<YAxis stroke="#64748b" tick={{ fontSize: 12 }} allowDecimals={false} />
<XAxis
dataKey="date"
stroke="#64748b"
tick={{ fontSize: 11 }}
interval="preserveStartEnd"
/>
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} allowDecimals={false} width={36} />
<Tooltip
contentStyle={{
background: "#0A0F1A",
Expand Down
Loading