Files
CRAPP/src/components/crapp/VotoSocial.tsx
T
davideandClaude Sonnet 5 3258a24f20 Estende il fix di useIo/useRosa a EventoCard e altri 13 usi non statistici
EventoCard monta un hook per ogni card mostrata: usava useGiocatoreCorrente
(= useIo = useRosa, le 6 statistiche pesanti) solo per leggere il proprio id.
Il Calendario ne rende diverse insieme (prossimi eventi, compleanni, drawer del
giorno), quindi ogni apertura moltiplicava il ricalcolo dell'intera rosa.

Audit di tutti gli altri usi di useGiocatoreCorrente: 12 su 13 leggevano solo
id/nome/verita', mai una statistica. Corretti allo stesso modo (-> useGiocatoreBase):
benvenuto.tsx, VotazioneMvp, SondaggioCacche, VotoSocial, eventi.tsx,
PromemoriaPalloni (Home), TurnoPalloni, ScoutEntry, Pagelle.

partita.$id.tsx: `io` era dichiarato e mai piu' usato, rimosso.

Stesso pattern trovato anche su useRosa (non solo useGiocatoreCorrente) in
scout.tsx e RosaPresenze.tsx (montata su partita e allenamento): entrambi
passati al nuovo useAnagraficaRosa, esteso con ruolo e numero.

Unica eccezione: CelebrazioneBadge.tsx usa davvero le statistiche complete
(badge, MVP, serie) ed e' montato globalmente in __root.tsx - non downgradabile,
resta il costo di base piu' alto rimasto in giro.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:00:52 +02:00

143 lines
5.4 KiB
TypeScript

import { useState } from "react";
import { Check, Crown, Sparkles } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
import { useGiocatoreBase } from "@/lib/user-store";
import {
categorieSocial,
conteggioCategoria,
mioVotoSocial,
useVotaSocial,
useVotiSocial,
vincitoreCategoria,
} from "@/lib/badge-social";
/** Voto social post-partita: un compagno per categoria, veloce da mobile. */
export function VotoSocial({ matchId }: { matchId: string }) {
// Solo `.id` serve qui: `useGiocatoreBase` basta, niente statistiche.
const io = useGiocatoreBase();
const voti = useVotiSocial();
const vota = useVotaSocial();
const { righe: squadra } = useGiocatoriSquadra();
const rosa = squadra.filter((g) => g.attivo);
const [aperta, setAperta] = useState<string | null>(null);
const tutti = voti.data ?? [];
const fatti = io
? categorieSocial.filter((c) => mioVotoSocial(tutti, matchId, c.id, io.id)).length
: 0;
async function invia(categoria: string, id: string, nome: string) {
if (!io) return;
if (id === io.id) {
toast.error("Non puoi votare te stesso");
return;
}
try {
await vota.mutateAsync({
match_id: matchId,
categoria,
votante_id: io.id,
votato_id: id,
votato_nome: nome,
});
setAperta(null);
toast.success(`Voto registrato: ${nome}`);
} catch {
toast.error("Voto non riuscito, riprova");
}
}
return (
<div className="space-y-2">
<div className="rounded-2xl bg-secondary/60 px-3 py-2 text-xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<Sparkles className="h-3.5 w-3.5 text-accent" />
Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria
</span>
</div>
{categorieSocial.map((cat) => {
const Icon = cat.icon;
const mio = io ? mioVotoSocial(tutti, matchId, cat.id, io.id) : null;
const vincitore = vincitoreCategoria(tutti, matchId, cat.id);
const conteggio = conteggioCategoria(tutti, matchId, cat.id);
const totale = conteggio.reduce((s, c) => s + c.voti, 0);
const isOpen = aperta === cat.id;
return (
<div key={cat.id} className="overflow-hidden rounded-3xl bg-card shadow-card">
<button
type="button"
onClick={() => setAperta(isOpen ? null : cat.id)}
disabled={!io}
className="flex w-full items-center gap-3 p-3 text-left active:scale-[0.99] disabled:opacity-60"
aria-expanded={isOpen}
>
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-2xl bg-secondary text-lg">
{cat.emoji}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-bold leading-tight">{cat.nome}</span>
<span className="block truncate text-xs text-muted-foreground">
{mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione}
</span>
</span>
<span
className={cn(
"shrink-0 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
mio ? "bg-success text-success-foreground" : "bg-accent text-accent-foreground",
)}
>
{mio ? <Check className="h-3 w-3" /> : "Vota"}
</span>
</button>
{isOpen ? (
<div className="grid max-h-56 grid-cols-2 gap-1.5 overflow-y-auto border-t border-border p-3">
{rosa
.filter((g) => g.id !== io?.id)
.map((g) => (
<button
key={g.id}
type="button"
disabled={vota.isPending}
onClick={() => invia(cat.id, g.id, nomeCompleto(g))}
className={cn(
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
mio?.votato_id === g.id
? "bg-accent text-accent-foreground"
: "bg-secondary text-foreground",
)}
>
{nomeCompleto(g)}
</button>
))}
</div>
) : (
<div className="border-t border-border px-3 py-2">
{totale === 0 ? (
<p className="text-xs text-muted-foreground">
Nessun voto ancora: apri e scegli un compagno.
</p>
) : vincitore ? (
<p className="inline-flex items-center gap-1.5 text-xs font-bold">
<Crown className="h-3.5 w-3.5 text-oro" />
<Icon className="h-3.5 w-3.5 text-accent" />
{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
</p>
) : (
<p className="text-xs text-muted-foreground">
Parità con {totale} voti: servono altri voti per assegnare il badge.
</p>
)}
</div>
)}
</div>
);
})}
</div>
);
}