Files
office_translator/frontend/src/app/admin/users/UserStats.tsx
2026-03-07 11:42:58 +01:00

117 lines
4.5 KiB
TypeScript

"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Users, UserCheck, Crown, Zap } from "lucide-react";
import type { AdminUser } from "./types";
import { PLAN_LABELS } from "./types";
interface UserStatsProps {
users: AdminUser[];
total: number;
isLoading?: boolean;
}
export function UserStats({ users, total, isLoading }: UserStatsProps) {
const activeUsers = users.filter((u) => u.subscription_status === "active").length;
const proUsers = users.filter((u) => u.plan === "pro" || u.plan === "business" || u.plan === "enterprise").length;
const freeUsers = users.filter((u) => u.plan === "free" || u.plan === "starter").length;
const planDistribution = users.reduce(
(acc, user) => {
acc[user.plan] = (acc[user.plan] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
if (isLoading) {
return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<Card key={i} className="animate-pulse">
<CardContent className="flex items-center gap-3 p-4">
<div className="size-9 rounded-lg bg-muted" />
<div className="flex-1 space-y-1">
<div className="h-3 w-16 rounded bg-muted" />
<div className="h-5 w-10 rounded bg-muted" />
</div>
</CardContent>
</Card>
))}
</div>
);
}
return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<Card className="py-0">
<CardContent className="flex items-center gap-3 px-4 py-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-secondary">
<Users className="size-4 text-foreground" />
</div>
<div className="flex flex-1 flex-col gap-0.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Total Users
</span>
<span className="text-lg font-semibold text-foreground">{total}</span>
</div>
</CardContent>
</Card>
<Card className="py-0">
<CardContent className="flex items-center gap-3 px-4 py-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[oklch(0.59_0.16_145/0.1)]">
<UserCheck className="size-4 text-[oklch(0.59_0.16_145)]" />
</div>
<div className="flex flex-1 flex-col gap-0.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Active This Month
</span>
<span className="text-lg font-semibold text-foreground">{activeUsers}</span>
</div>
</CardContent>
</Card>
<Card className="py-0">
<CardContent className="flex items-center gap-3 px-4 py-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[oklch(0.70_0.14_255/0.1)]">
<Crown className="size-4 text-[oklch(0.70_0.14_255)]" />
</div>
<div className="flex flex-1 flex-col gap-0.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Pro Users
</span>
<span className="text-lg font-semibold text-foreground">{proUsers}</span>
</div>
</CardContent>
</Card>
<Card className="py-0">
<CardContent className="flex items-center gap-3 px-4 py-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<Zap className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-1 flex-col gap-0.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Free Users
</span>
<span className="text-lg font-semibold text-foreground">{freeUsers}</span>
</div>
</CardContent>
</Card>
{Object.entries(planDistribution).length > 0 && (
<div className="col-span-2 flex flex-wrap items-center gap-2 md:col-span-4">
<span className="text-xs text-muted-foreground">Distribution:</span>
{Object.entries(planDistribution).map(([plan, count]) => (
<Badge key={plan} variant="outline" className="text-xs">
{PLAN_LABELS[plan as keyof typeof PLAN_LABELS] || plan}: {count}
</Badge>
))}
</div>
)}
</div>
);
}