tia/src/app/admin/page.tsx
Mannu 7189fc766c refactor(ui): apply design system components across all pages
Replace raw <input>/<button>/<select> elements with Button, Card, Input,
Select, Modal, Badge, and ConfirmDialog from @/components/ui in all
non-admin and admin pages. Removes ~550 lines of inline Tailwind utility
classes from form elements while keeping all business logic intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:24:43 +05:30

219 lines
No EOL
7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Select } from "@/components/ui";
interface Stats {
overview: {
totalFamilies: number;
totalUsers: number;
totalChildren: number;
proFamilies: number;
freeFamilies: number;
mrr: number;
avgRevenuePerUser: number;
};
conversions: {
freeToPro: number;
conversionRate: number;
};
growth: {
familiesByDay: { date: string; count: number }[];
usersByDay: { date: string; count: number }[];
};
childrenByAge: { ageGroup: string; count: number }[];
}
export default function AdminDashboard() {
const [stats, setStats] = useState<Stats | null>(null);
const [loading, setLoading] = useState(true);
const [period, setPeriod] = useState("30");
useEffect(() => {
fetchStats();
}, [period]);
const fetchStats = async () => {
try {
const res = await fetch(`/api/admin/stats?period=${period}`);
const data = await res.json();
setStats(data);
} catch (err) {
console.error("Failed to fetch stats:", err);
}
setLoading(false);
};
if (loading || !stats) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-white">Loading...</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-gray-400">Platform overview and analytics</p>
</div>
<Select value={period} onChange={(e) => setPeriod(e.target.value)}>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</Select>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
label="Total Families"
value={stats.overview.totalFamilies}
icon="🏠"
color="rose"
href="/admin/families"
/>
<StatCard
label="Total Users"
value={stats.overview.totalUsers}
icon="👥"
color="blue"
href="/admin/users"
/>
<StatCard
label="Total Children"
value={stats.overview.totalChildren}
icon="👶"
color="amber"
href="/admin/children"
/>
<StatCard
label="MRR"
value={`$${stats.overview.mrr.toFixed(2)}`}
icon="💰"
color="emerald"
href="/admin/revenue"
/>
</div>
{/* Revenue & Tier Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-800 p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">Revenue Overview</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-2xl font-bold text-emerald-400">${stats.overview.mrr.toFixed(2)}</div>
<div className="text-gray-400 text-sm">Monthly Recurring Revenue</div>
</div>
<div>
<div className="text-2xl font-bold text-rose-400">{stats.overview.proFamilies}</div>
<div className="text-gray-400 text-sm">Pro Families</div>
</div>
<div>
<div className="text-2xl font-bold text-gray-400">{stats.overview.freeFamilies}</div>
<div className="text-gray-400 text-sm">Free Families</div>
</div>
<div>
<div className="text-2xl font-bold text-amber-400">${stats.overview.avgRevenuePerUser}</div>
<div className="text-gray-400 text-sm">Avg Revenue per Family</div>
</div>
</div>
</div>
<div className="bg-gray-800 p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">Conversions</h3>
<div className="flex items-center justify-center h-32">
<div className="text-center">
<div className="text-4xl font-bold text-rose-400">{stats.conversions.conversionRate}%</div>
<div className="text-gray-400">Free Pro Conversion Rate</div>
</div>
</div>
</div>
</div>
{/* Growth Charts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ChartCard
title="New Families"
data={stats.growth.familiesByDay}
icon="📈"
/>
<ChartCard
title="New Users"
data={stats.growth.usersByDay}
icon="👥"
/>
</div>
{/* Children by Age */}
{stats.childrenByAge.length > 0 && (
<div className="bg-gray-800 p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">Children by Age Group</h3>
<div className="flex flex-wrap gap-4">
{stats.childrenByAge.map((item) => (
<div key={item.ageGroup} className="bg-gray-700 px-4 py-2 rounded-lg">
<div className="text-xl font-bold">{item.count}</div>
<div className="text-xs text-gray-400">{item.ageGroup} years</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
function StatCard({ label, value, icon, color, href }: { label: string; value: number | string; icon: string; color: string; href?: string }) {
const colorClasses: Record<string, string> = {
rose: "text-rose-400",
blue: "text-blue-400",
amber: "text-amber-400",
emerald: "text-emerald-400",
};
const content = (
<div className="bg-gray-800 p-6 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-2xl">{icon}</span>
</div>
<div className={`text-3xl font-bold ${colorClasses[color]}`}>{value}</div>
<div className="text-gray-400 text-sm">{label}</div>
</div>
);
if (href) {
return <a href={href} className="cursor-pointer hover:opacity-90 transition-opacity">{content}</a>;
}
return content;
}
function ChartCard({ title, data, icon }: { title: string; data: { date: string; count: number }[]; icon: string }) {
const maxCount = Math.max(...data.map((d) => d.count), 1);
return (
<div className="bg-gray-800 p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">{icon} {title}</h3>
<div className="h-40 flex items-end gap-1">
{data.slice(-14).map((d, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div
className="w-full bg-rose-500 rounded-t"
style={{ height: `${(d.count / maxCount) * 100}%`, minHeight: d.count > 0 ? "4px" : "0" }}
/>
<div className="text-[8px] text-gray-500 truncate w-full text-center">
{d.date?.slice(5) || ""}
</div>
</div>
))}
</div>
{data.length === 0 && (
<div className="h-40 flex items-center justify-center text-gray-500">
No data available
</div>
)}
</div>
);
}