Files
Wyview/app/tiktok/page.tsx

185 lines
8.1 KiB
TypeScript

"use client";
import { Music2, Link2, Unlink, Loader2, AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import StatCard from "@/components/StatCard";
import StatsChart from "@/components/StatsCharts";
interface TikTokStats {
followers: number;
likes: number;
videoCount: number;
displayName: string;
avatarUrl: string;
plan: "free" | "pro" | "elite" | "team";
}
export default function TikTokPage() {
const [stats, setStats] = useState<TikTokStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [disconnecting, setDisconnecting] = useState(false);
const searchParams = useSearchParams();
const router = useRouter();
async function loadStats() {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/tiktok/stats");
if (res.status === 404) {
setStats(null);
} else if (!res.ok) {
const data = await res.json();
setError(data.error ?? "Erreur inconnue");
} else {
const data = await res.json();
setStats(data);
}
} catch {
setError("Erreur réseau");
} finally {
setLoading(false);
}
}
useEffect(() => {
loadStats();
}, []);
async function handleDisconnect() {
setDisconnecting(true);
try {
await fetch("/api/tiktok/disconnect", { method: "POST" });
setStats(null);
router.replace("/tiktok");
} finally {
setDisconnecting(false);
}
}
const urlError = searchParams.get("error");
return (
<div>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="p-2 rounded-sm bg-pink-500/10 border border-pink-500/20">
<Music2 size={18} className="text-pink-400" />
</div>
<div>
<div className="text-[9px] font-mono tracking-[0.3em] text-pink-400/70 uppercase mb-0.5">Plateforme</div>
<h1 className="text-2xl font-black tracking-widest uppercase" style={{ color: "var(--text-primary)" }}>TikTok</h1>
</div>
</div>
{stats && !loading && (
<button
onClick={handleDisconnect}
disabled={disconnecting}
className="flex items-center gap-2 px-4 py-2 text-[10px] font-mono tracking-widest uppercase border border-red-500/30 text-red-400/70 hover:text-red-400 hover:border-red-500/60 rounded-sm transition-colors disabled:opacity-50"
>
{disconnecting ? <Loader2 size={12} className="animate-spin" /> : <Unlink size={12} />}
Déconnecter
</button>
)}
</div>
{/* Erreur URL */}
{urlError && (
<div className="flex items-center gap-2 mb-6 px-4 py-3 bg-red-500/5 border border-red-500/20 rounded-sm text-red-400 text-xs font-mono">
<AlertTriangle size={14} />
{urlError === "access_denied" && "Accès refusé par TikTok."}
{urlError === "token_exchange" && "Erreur lors de l'échange du token."}
{urlError === "session_error" && "Erreur de session."}
{!["access_denied", "token_exchange", "session_error"].includes(urlError) && `Erreur : ${urlError}`}
</div>
)}
{/* Chargement */}
{loading && (
<div className="flex items-center justify-center min-h-[200px] gap-3 font-mono text-xs" style={{ color: "var(--text-secondary)" }}>
<Loader2 size={16} className="animate-spin text-pink-400/60" />
Chargement...
</div>
)}
{/* Connecté : stats */}
{!loading && stats && (
<>
{stats.displayName && (
<div className="flex items-center gap-3 mb-6 px-4 py-3 bg-pink-500/8 border border-pink-500/25 rounded-sm">
{stats.avatarUrl && (
<img src={stats.avatarUrl} alt="avatar" className="w-8 h-8 rounded-full object-cover border border-pink-500/30" />
)}
<span className="text-pink-300 font-mono text-sm font-bold">{stats.displayName}</span>
<span className="text-[9px] font-mono tracking-widest text-pink-400/50 uppercase ml-auto">Connecté</span>
</div>
)}
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4 mb-8">
<StatCard
label="Followers"
value={stats.followers.toLocaleString("fr-FR")}
sub="Abonnés totaux"
accent="purple"
/>
<StatCard
label="Likes totaux"
value={stats.likes.toLocaleString("fr-FR")}
sub="Cumul likes"
accent="red"
/>
<StatCard
label="Vidéos publiées"
value={stats.videoCount.toLocaleString("fr-FR")}
sub="Vidéos au total"
accent="blue"
/>
<StatCard
label="Ratio likes/vidéo"
value={stats.videoCount > 0 ? Math.round(stats.likes / stats.videoCount).toLocaleString("fr-FR") : "—"}
sub="Moy. likes par vidéo"
accent="gold"
/>
</div>
{/* Graph */}
<StatsChart plan={stats.plan ?? "free"} />
</>
)}
{!loading && !stats && !error && (
<div className="border rounded-sm p-12 flex flex-col items-center justify-center gap-5 min-h-[240px]"
style={{ background: "var(--surface)", borderColor: "var(--border)" }}>
<Music2 size={36} className="text-pink-400/30" />
<p className="font-mono text-xs tracking-widest text-center uppercase" style={{ color: "var(--text-secondary)" }}>
Connectez votre compte TikTok<br />pour afficher vos statistiques
</p>
<a
href="/api/tiktok/connect"
className="flex items-center gap-2 px-5 py-2.5 text-[10px] font-mono tracking-widest uppercase bg-pink-500/10 border border-pink-500/40 text-pink-300 hover:bg-pink-500/20 hover:border-pink-500/60 rounded-sm transition-colors"
>
<Link2 size={13} />
Connecter TikTok
</a>
</div>
)}
{!loading && error && (
<div className="border border-red-500/25 rounded-sm p-8 flex flex-col items-center justify-center gap-3 min-h-[200px]"
style={{ background: "var(--surface)" }}>
<AlertTriangle size={28} className="text-red-400/60" />
<p className="text-red-400/80 font-mono text-xs tracking-widest text-center uppercase">{error}</p>
<button onClick={loadStats} className="text-[10px] font-mono tracking-widest uppercase transition-colors hover:text-pink-400"
style={{ color: "var(--text-secondary)" }}>
Réessayer
</button>
</div>
)}
</div>
);
}