Aggiunge sottosezioni a swipe nel profilo, con stagione e serie unite.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Cacciari
2026-09-05 22:07:58 +02:00
co-authored by Cursor
parent f4398fce25
commit e9755d364c
2 changed files with 202 additions and 175 deletions
+81 -67
View File
@@ -265,9 +265,12 @@ export function CampiProfilo({
export function ProfiloAmministrativo({ export function ProfiloAmministrativo({
giocatoreId, giocatoreId,
indice = 0, indice = 0,
/** Se false, mostra solo il contenuto (es. dentro `BarraSottosezioni`). */
conTendina = true,
}: { }: {
giocatoreId: string; giocatoreId: string;
indice?: number; indice?: number;
conTendina?: boolean;
}) { }) {
const { profili } = useProfili(); const { profili } = useProfili();
const salva = useSalvaProfilo(); const salva = useSalvaProfilo();
@@ -301,79 +304,90 @@ export function ProfiloAmministrativo({
const caricato = (campo: keyof Profilo) => async (path: string) => const caricato = (campo: keyof Profilo) => async (path: string) =>
scrivi({ ...corrente, [campo]: path }); scrivi({ ...corrente, [campo]: path });
const corpo = (
<div className="space-y-3 rounded-3xl bg-card p-4 shadow-card">
<div className="flex items-center justify-between gap-2">
<div className="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-secondary">
<div
className="h-full rounded-full bg-accent-grad transition-all"
style={{ width: `${perc}%` }}
/>
</div>
<span className="shrink-0 text-xs font-bold tabular-nums text-muted-foreground">
{perc}%
</span>
</div>
<p className="text-xs text-muted-foreground">
Servono agli amministratori per il tesseramento CSI. Li vedi solo tu e loro.
</p>
<CampiProfilo
corrente={corrente}
aggiorna={aggiorna}
sezioni={sezioni}
fileDocumento={
<div className="divide-y divide-border">
<CampoFile
label="Foto fronte"
path={corrente.documentoFrontePath}
sezione="documento-fronte"
giocatoreId={giocatoreId}
onCaricato={caricato("documentoFrontePath")}
/>
<CampoFile
label="Foto retro"
path={corrente.documentoRetroPath}
sezione="documento-retro"
giocatoreId={giocatoreId}
onCaricato={caricato("documentoRetroPath")}
/>
</div>
}
fileCertificato={
<CampoFile
label="Certificato medico"
path={corrente.certificatoPath}
sezione="certificato"
giocatoreId={giocatoreId}
onCaricato={caricato("certificatoPath")}
/>
}
fileFoto={
<>
<Intestazione titolo="Foto tessera" completa={sezioni.foto} />
<CampoFile
label="Foto tessera"
path={corrente.fotoPath}
sezione="foto"
giocatoreId={giocatoreId}
onCaricato={caricato("fotoPath")}
/>
</>
}
/>
<button
type="button"
onClick={salvaBozza}
disabled={!sporco || salva.isPending}
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
>
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
{sporco ? "Salva" : "Salvato"}
</button>
</div>
);
if (!conTendina) return corpo;
return ( return (
<SezioneTendina <SezioneTendina
titolo="Dati per il tesseramento" titolo="Dati per il tesseramento"
indice={indice} indice={indice}
azione={<span className="text-xs font-bold tabular-nums text-muted-foreground">{perc}%</span>} azione={<span className="text-xs font-bold tabular-nums text-muted-foreground">{perc}%</span>}
> >
<div className="space-y-3 rounded-3xl bg-card p-4 shadow-card"> {corpo}
<div className="h-1.5 overflow-hidden rounded-full bg-secondary">
<div
className="h-full rounded-full bg-accent-grad transition-all"
style={{ width: `${perc}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
Servono agli amministratori per il tesseramento CSI. Li vedi solo tu e loro.
</p>
<CampiProfilo
corrente={corrente}
aggiorna={aggiorna}
sezioni={sezioni}
fileDocumento={
<div className="divide-y divide-border">
<CampoFile
label="Foto fronte"
path={corrente.documentoFrontePath}
sezione="documento-fronte"
giocatoreId={giocatoreId}
onCaricato={caricato("documentoFrontePath")}
/>
<CampoFile
label="Foto retro"
path={corrente.documentoRetroPath}
sezione="documento-retro"
giocatoreId={giocatoreId}
onCaricato={caricato("documentoRetroPath")}
/>
</div>
}
fileCertificato={
<CampoFile
label="Certificato medico"
path={corrente.certificatoPath}
sezione="certificato"
giocatoreId={giocatoreId}
onCaricato={caricato("certificatoPath")}
/>
}
fileFoto={
<>
<Intestazione titolo="Foto tessera" completa={sezioni.foto} />
<CampoFile
label="Foto tessera"
path={corrente.fotoPath}
sezione="foto"
giocatoreId={giocatoreId}
onCaricato={caricato("fotoPath")}
/>
</>
}
/>
<button
type="button"
onClick={salvaBozza}
disabled={!sporco || salva.isPending}
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
>
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
{sporco ? "Salva" : "Salvato"}
</button>
</div>
</SezioneTendina> </SezioneTendina>
); );
} }
+121 -108
View File
@@ -3,14 +3,8 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react"; import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
Card, import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
PageHeader,
Section,
SezioneTendina,
StatTile,
TeamLogo,
} from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar"; import { Avatar } from "@/components/crapp/Avatar";
import { import {
caricaAvatar, caricaAvatar,
@@ -126,7 +120,7 @@ function Profilo() {
<PageHeader <PageHeader
titolo={g.nome} titolo={g.nome}
sottotitolo={`#${g.numero} · ${g.ruolo}`} sottotitolo={`#${g.numero} · ${g.ruolo}`}
azione={<TeamLogo src="/logo-nerorosso.svg" className="h-11 w-11" />} azione={<TeamLogo src="/logo-nerorosso.svg" className="h-14 w-14" />}
/> />
<Reveal className="-mt-6 px-5"> <Reveal className="-mt-6 px-5">
@@ -183,105 +177,124 @@ function Profilo() {
</Card> </Card>
</Reveal> </Reveal>
<Section titolo="Stagione" indice={1}> <BarraSottosezioni
<div className="grid grid-cols-3 gap-2"> defaultId="stagione"
<StatTile valore={g.presenze} label="Presenze" hint={`${percPresenze}% del totale`} /> voci={[
<StatTile {
valore={`${ultimoMese.percentuale}%`} id: "stagione",
label="Presenze 30gg" label: "Stagione",
hint={`${ultimoMese.presenti}/${ultimoMese.totali} eventi`} contenuto: (
/> <div className="space-y-4">
<StatTile <div className="grid grid-cols-3 gap-2">
valore={ <StatTile
<span className="inline-flex items-center gap-1"> valore={g.presenze}
<Flame className="h-5 w-5 text-accent" /> label="Presenze"
{g.streak} hint={`${percPresenze}% del totale`}
</span> />
} <StatTile
label="Presenze di fila" valore={`${ultimoMese.percentuale}%`}
/> label="Presenze 30gg"
<StatTile valore={g.mediaVoto || "—"} label="Media voto" /> hint={`${ultimoMese.presenti}/${ultimoMese.totali} eventi`}
<StatTile valore={g.mvp} label="MVP" /> />
<StatTile valore={g.palloni} label="Turni palloni" /> <StatTile
</div> valore={
</Section> <span className="inline-flex items-center gap-1">
<Flame className="h-5 w-5 text-accent" />
<Section titolo="Serie di presenze" indice={2}> {g.streak}
<SerieGriglia g={g} /> </span>
</Section> }
label="Presenze di fila"
<SezioneTendina titolo="Collezione badge" indice={3}> />
<CollezioneBadge g={g} votiSocial={votiSocial.data ?? []} /> <StatTile valore={g.mediaVoto || "—"} label="Media voto" />
</SezioneTendina> <StatTile valore={g.mvp} label="MVP" />
<StatTile valore={g.palloni} label="Turni palloni" />
<ProfiloAmministrativo giocatoreId={g.id} indice={4} /> </div>
<SerieGriglia g={g} />
<Section titolo="Impostazioni"> </div>
<div className="divide-y divide-border overflow-hidden rounded-3xl bg-card shadow-card"> ),
<button },
type="button" {
onClick={cambiaNotifiche} id: "badge",
disabled={!supportate || inCorso} label: "Badge",
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60" contenuto: <CollezioneBadge g={g} votiSocial={votiSocial.data ?? []} />,
> },
<span className="min-w-0"> {
<span className="block truncate">Notifiche turno palloni</span> id: "tesseramento",
<span className="block text-xs text-muted-foreground"> label: "Tesseramento",
{supportate contenuto: <ProfiloAmministrativo giocatoreId={g.id} conTendina={false} />,
? notifiche },
? "Attive su questo dispositivo" {
: "Ricevi l'avviso il giorno stesso e la volta dopo" id: "impostazioni",
: "Non supportate su questo dispositivo"} label: "Impostazioni",
</span> contenuto: (
</span> <div className="divide-y divide-border overflow-hidden rounded-3xl bg-card shadow-card">
<span <button
className={cn( type="button"
"grid h-8 w-8 shrink-0 place-items-center rounded-xl", onClick={cambiaNotifiche}
notifiche disabled={!supportate || inCorso}
? "bg-accent-grad text-accent-foreground" className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
: "bg-secondary text-muted-foreground", >
)} <span className="min-w-0">
> <span className="block truncate">Notifiche turno palloni</span>
<Bell className="h-4 w-4" /> <span className="block text-xs text-muted-foreground">
</span> {supportate
</button> ? notifiche
{admin ? ( ? "Attive su questo dispositivo"
<Link : "Ricevi l'avviso il giorno stesso e la volta dopo"
to="/admin" : "Non supportate su questo dispositivo"}
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5" </span>
> </span>
<span className="min-w-0 truncate">Dashboard amministratore</span> <span
<ShieldCheck className="h-4 w-4 text-muted-foreground" /> className={cn(
</Link> "grid h-8 w-8 shrink-0 place-items-center rounded-xl",
) : null} notifiche
<a ? "bg-accent-grad text-accent-foreground"
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml" : "bg-secondary text-muted-foreground",
target="_blank" )}
rel="noopener noreferrer" >
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5" <Bell className="h-4 w-4" />
> </span>
<span className="min-w-0 truncate">Segnala un bug</span> </button>
<Bug className="h-4 w-4 text-muted-foreground" /> {admin ? (
</a> <Link
<a to="/admin"
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml" className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
target="_blank" >
rel="noopener noreferrer" <span className="min-w-0 truncate">Dashboard amministratore</span>
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5" <ShieldCheck className="h-4 w-4 text-muted-foreground" />
> </Link>
<span className="min-w-0 truncate">Suggerisci una nuova funzionalità</span> ) : null}
<Lightbulb className="h-4 w-4 text-muted-foreground" /> <a
</a> href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml"
<button target="_blank"
type="button" rel="noopener noreferrer"
onClick={logout} className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5" >
> <span className="min-w-0 truncate">Segnala un bug</span>
<span className="min-w-0 truncate">Esci</span> <Bug className="h-4 w-4 text-muted-foreground" />
<LogOut className="h-4 w-4 text-muted-foreground" /> </a>
</button> <a
</div> href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml"
</Section> target="_blank"
rel="noopener noreferrer"
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
>
<span className="min-w-0 truncate">Suggerisci una nuova funzionalità</span>
<Lightbulb className="h-4 w-4 text-muted-foreground" />
</a>
<button
type="button"
onClick={logout}
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
>
<span className="min-w-0 truncate">Esci</span>
<LogOut className="h-4 w-4 text-muted-foreground" />
</button>
</div>
),
},
]}
/>
</> </>
); );
} }