"use client"; import { useState, useEffect } from "react"; import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, } from "recharts"; import { TrendingUp, TrendingDown, Minus, Lock } from "lucide-react"; type Period = "7d" | "30d" | "90d" | "all"; type Metric = "followers" | "likes" | "videoCount"; interface Snapshot { createdAt: string; followers: number; likes: number; videoCount: number; } interface StatsChartProps { plan: "free" | "pro" | "elite" | "team"; } const PERIOD_CONFIG: { value: Period; label: string; requiredPlan: "free" | "pro" | "elite" | "team" }[] = [ { value: "7d", label: "7J", requiredPlan: "free" }, { value: "30d", label: "30J", requiredPlan: "pro" }, { value: "90d", label: "90J", requiredPlan: "pro" }, { value: "all", label: "TOUT", requiredPlan: "elite"}, ]; const PLAN_RANK: Record = { free: 0, pro: 1, elite: 2, team: 3 }; const METRICS: { value: Metric; label: string; color: string; gradientId: string }[] = [ { value: "followers", label: "Followers", color: "#c084fc", gradientId: "gradFollowers" }, { value: "likes", label: "Likes", color: "#f472b6", gradientId: "gradLikes" }, { value: "videoCount", label: "Vidéos", color: "#60a5fa", gradientId: "gradVideos" }, ]; function canAccess(plan: string, required: string) { return PLAN_RANK[plan] >= PLAN_RANK[required]; } function formatValue(value: number): string { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`; return value.toLocaleString("fr-FR"); } function formatDate(dateStr: string, period: Period): string { const d = new Date(dateStr); if (period === "7d") return d.toLocaleDateString("fr-FR", { weekday: "short", day: "numeric" }); if (period === "all") return d.toLocaleDateString("fr-FR", { month: "short", year: "2-digit" }); return d.toLocaleDateString("fr-FR", { day: "numeric", month: "short" }); } function getDelta(snapshots: Snapshot[], metric: Metric) { if (snapshots.length < 2) return null; const first = snapshots[0][metric]; const last = snapshots[snapshots.length - 1][metric]; const diff = last - first; const pct = first > 0 ? ((diff / first) * 100).toFixed(1) : null; return { diff, pct }; } function CustomTooltip({ active, payload, label, period }: any) { if (!active || !payload?.length) return null; return (
{formatDate(label, period)}
{payload.map((p: any) => (
{p.name} {formatValue(p.value)}
))}
); } export default function StatsChart({ plan }: StatsChartProps) { const [period, setPeriod] = useState("7d"); const [metric, setMetric] = useState("followers"); const [snapshots, setSnapshots] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!canAccess(plan, PERIOD_CONFIG.find(p => p.value === period)!.requiredPlan)) return; setLoading(true); setError(null); fetch(`/api/tiktok/snapshots?period=${period}`) .then(r => r.json()) .then(d => { if (d.error) throw new Error(d.error); setSnapshots(d); }) .catch(e => setError(e.message)) .finally(() => setLoading(false)); }, [period, plan]); const metricConfig = METRICS.find(m => m.value === metric)!; const delta = getDelta(snapshots, metric); const chartData = snapshots.map(s => ({ ...s, date: s.createdAt })); return (
{/* Header */}
Évolution

Statistiques

{/* Period selector */}
{PERIOD_CONFIG.map(p => { const accessible = canAccess(plan, p.requiredPlan); const active = period === p.value; return ( ); })}
{/* Metric selector */}
{METRICS.map(m => ( ))} {/* Delta badge */} {delta && !loading && (
{Number(delta.diff) > 0 ? ( ) : Number(delta.diff) < 0 ? ( ) : ( )} 0 ? "#34d399" : Number(delta.diff) < 0 ? "#f87171" : "var(--text-secondary)" }}> {Number(delta.diff) > 0 ? "+" : ""}{formatValue(delta.diff)} {delta.pct && ` (${Number(delta.diff) > 0 ? "+" : ""}${delta.pct}%)`} sur la période
)}
{loading ? (
Chargement...
) : error ? (
{error}
) : snapshots.length < 2 ? (
Pas encore assez de données Les snapshots s'accumulent toutes les heures
) : ( formatDate(d, period)} tick={{ fill: "var(--text-secondary)", fontSize: 9, fontFamily: "monospace" }} tickLine={false} axisLine={false} interval="preserveStartEnd" /> } cursor={{ stroke: "var(--border)", strokeWidth: 1 }} /> )}
{!canAccess(plan, PERIOD_CONFIG.find(p => p.value === period)!.requiredPlan) && (
Disponible en plan Pro Upgrader →
)}
); }