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>
143 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|