feat (app): add a way to mask some analytics

This commit is contained in:
Pierre Ryssen
2026-03-30 15:19:31 +02:00
parent 75fd2f5120
commit 1a5548d507

View File

@@ -1,6 +1,6 @@
"use client";
import { Music2, Link2, Unlink, Loader2, AlertTriangle } from "lucide-react";
import { Music2, Link2, Unlink, Loader2, AlertTriangle, Eye, EyeOff } from "lucide-react";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
@@ -17,12 +17,43 @@ interface TikTokStats {
plan: "free" | "pro" | "elite" | "team";
}
type CardId = "followers" | "likes" | "videos" | "views" | "ratio";
const defaultVisibleCards: Record<CardId, boolean> = {
followers: true,
likes: true,
videos: true,
views: true,
ratio: true,
};
const visibleCardsStorageKey = "tiktok.visibleCards";
function parseVisibleCards(value: string | null): Record<CardId, boolean> {
if (!value) return defaultVisibleCards;
try {
const parsed = JSON.parse(value) as Partial<Record<CardId, unknown>>;
return {
followers: typeof parsed.followers === "boolean" ? parsed.followers : defaultVisibleCards.followers,
likes: typeof parsed.likes === "boolean" ? parsed.likes : defaultVisibleCards.likes,
videos: typeof parsed.videos === "boolean" ? parsed.videos : defaultVisibleCards.videos,
views: typeof parsed.views === "boolean" ? parsed.views : defaultVisibleCards.views,
ratio: typeof parsed.ratio === "boolean" ? parsed.ratio : defaultVisibleCards.ratio,
};
} catch {
return defaultVisibleCards;
}
}
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 [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false);
const [visibleCards, setVisibleCards] = useState<Record<CardId, boolean>>(defaultVisibleCards);
const [visibleCardsHydrated, setVisibleCardsHydrated] = useState(false);
const searchParams = useSearchParams();
const router = useRouter();
@@ -48,9 +79,17 @@ export default function TikTokPage() {
}
useEffect(() => {
const persisted = parseVisibleCards(window.localStorage.getItem(visibleCardsStorageKey));
setVisibleCards(persisted);
setVisibleCardsHydrated(true);
loadStats();
}, []);
useEffect(() => {
if (!visibleCardsHydrated) return;
window.localStorage.setItem(visibleCardsStorageKey, JSON.stringify(visibleCards));
}, [visibleCards, visibleCardsHydrated]);
async function handleDisconnect() {
if (disconnecting) return;
setDisconnecting(true);
@@ -66,6 +105,49 @@ export default function TikTokPage() {
const urlError = searchParams.get("error");
const cards = stats
? [
{
id: "followers" as const,
label: "Followers",
value: stats.followers.toLocaleString("fr-FR"),
sub: "Abonnés totaux",
accent: "purple" as const,
},
{
id: "likes" as const,
label: "Likes totaux",
value: stats.likes.toLocaleString("fr-FR"),
sub: "Cumul likes",
accent: "red" as const,
},
{
id: "videos" as const,
label: "Vidéos publiées",
value: stats.videoCount.toLocaleString("fr-FR"),
sub: "Vidéos au total",
accent: "blue" as const,
},
{
id: "views" as const,
label: "Vues vidéos",
value: stats.views.toLocaleString("fr-FR"),
sub: "Vues totales des vidéos",
accent: "green" as const,
},
{
id: "ratio" as const,
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" as const,
},
]
: [];
const shownCards = cards.filter((card) => visibleCards[card.id]);
const hiddenCards = cards.filter((card) => !visibleCards[card.id]);
return (
<div>
{/* Header */}
@@ -124,38 +206,69 @@ export default function TikTokPage() {
</div>
)}
<div className="grid grid-cols-2 xl:grid-cols-5 gap-4 mb-8">
{hiddenCards.length > 0 && (
<div className="flex flex-wrap items-center gap-2 mb-3">
{hiddenCards.map((card) => (
<button
key={card.id}
type="button"
onClick={() => setVisibleCards((prev) => ({ ...prev, [card.id]: true }))}
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-mono tracking-widest uppercase border rounded-sm transition-colors hover:text-pink-300"
style={{ borderColor: "var(--border)", color: "var(--text-secondary)" }}
aria-label={`Afficher la carte ${card.label}`}
>
<Eye size={12} />
{card.label}
</button>
))}
<button
type="button"
onClick={() => setVisibleCards(defaultVisibleCards)}
className="px-2 py-1 text-[10px] font-mono tracking-widest uppercase border rounded-sm transition-colors hover:text-pink-300"
style={{ borderColor: "var(--border)", color: "var(--text-secondary)" }}
>
Tout afficher
</button>
</div>
)}
{shownCards.length > 0 ? (
<div id="tiktok-stat-cards" className="grid grid-cols-2 xl:grid-cols-5 gap-4 mb-8">
{shownCards.map((card) => (
<div key={card.id} className="relative">
<button
type="button"
onClick={() => setVisibleCards((prev) => ({ ...prev, [card.id]: false }))}
className="absolute top-2 right-2 z-10 p-1 rounded-sm border transition-colors"
style={{ borderColor: "var(--border)", color: "var(--text-secondary)", background: "var(--surface)" }}
aria-label={`Masquer la carte ${card.label}`}
>
<EyeOff size={12} />
</button>
<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="Vues vidéos"
value={stats.views.toLocaleString("fr-FR")}
sub="Vues totales des vidéos"
accent="green"
/>
<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"
label={card.label}
value={card.value}
sub={card.sub}
accent={card.accent}
/>
</div>
))}
</div>
) : (
<div className="border rounded-sm p-6 mb-8 text-center" style={{ background: "var(--surface)", borderColor: "var(--border)" }}>
<p className="font-mono text-xs tracking-widest uppercase mb-3" style={{ color: "var(--text-secondary)" }}>
Toutes les cartes sont masquées
</p>
<button
type="button"
onClick={() => setVisibleCards(defaultVisibleCards)}
className="px-3 py-2 text-[10px] font-mono tracking-widest uppercase border rounded-sm transition-colors hover:text-pink-300"
style={{ borderColor: "var(--border)", color: "var(--text-secondary)" }}
>
Afficher toutes les cartes
</button>
</div>
)}
{/* Graph */}
<StatsChart plan={stats.plan ?? "free"} />