Ripulisce profilo e tab, e mette Apri evento sull'ultima riga delle presenze.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Cacciari
2026-09-07 08:48:21 +02:00
co-authored by Cursor
parent 6bc5e8f006
commit ca1eee5425
4 changed files with 61 additions and 71 deletions
+6 -9
View File
@@ -7,8 +7,8 @@ export type VoceSottosezione = {
id: string; id: string;
label: string; label: string;
contenuto: ReactNode; contenuto: ReactNode;
/** Se true, non ripete il titolo della tab sopra il contenuto (es. Classifica già nella barra). */ /** Se true, il pannello è a tutta larghezza (niente padding), es. barra filtri Classifica. */
nascondiTitolo?: boolean; aTuttoLarghezza?: boolean;
}; };
/** Tween breve: evita molle + exit che tengono due pannelli in DOM insieme. */ /** Tween breve: evita molle + exit che tengono due pannelli in DOM insieme. */
@@ -19,9 +19,9 @@ const transizioneTab = { type: "tween" as const, duration: 0.16, ease: [0.25, 0.
* pannello che mostra una sola sezione alla volta, cambiabile anche con * pannello che mostra una sola sezione alla volta, cambiabile anche con
* swipe sul contenuto. * swipe sul contenuto.
* *
* `variante="sottolineatura"`: tab a testo con sottolineatura rossa e senza titolo * `variante="sottolineatura"`: tab a testo con sottolineatura rossa (Squadra e Classifica
* ripetuto sotto la barra (Squadra e Classifica CSI). Default `pillole`: restano le * CSI). Default `pillole`: restano le pill usate dal Profilo. Il titolo ripetuto sotto la
* pill e i titoli usati dal Profilo. * barra non viene mai mostrato: letichetta è già nella tab.
*/ */
export function BarraSottosezioni({ export function BarraSottosezioni({
voci, voci,
@@ -135,11 +135,8 @@ export function BarraSottosezioni({
initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione.current * 20 }} initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione.current * 20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={ridotto ? { duration: 0.1 } : transizioneTab} transition={ridotto ? { duration: 0.1 } : transizioneTab}
className={cn("touch-pan-y", voce.nascondiTitolo ? "px-0 pt-0 pb-4" : "px-5 py-4")} className={cn("touch-pan-y", voce.aTuttoLarghezza ? "px-0 pt-0 pb-4" : "px-5 py-4")}
> >
{voce.nascondiTitolo || sottolineatura ? null : (
<h2 className="mb-3 font-display-sm text-lg uppercase">{voce.label}</h2>
)}
{voce.contenuto} {voce.contenuto}
</motion.div> </motion.div>
</div> </div>
+44 -41
View File
@@ -54,6 +54,36 @@ export function EventoCard({
const isCompleanno = evento.tipo === "compleanno"; const isCompleanno = evento.tipo === "compleanno";
const passato = evento.data < dataOggi(); const passato = evento.data < dataOggi();
function ChipPresenza({ s }: { s: Stato }) {
const meta = statoMeta[s];
const attivo = stato === s;
return (
<button
type="button"
disabled={!io || salva.isPending || passato}
onClick={() => {
if (!io) return;
salva.mutate({
eventoId: evento.id,
giocatoreId: io.id,
stato: attivo ? null : s,
});
}}
aria-pressed={attivo}
className={cn(
// min-h-11: sono i controlli più toccati dell'app, sotto i
// 44px si sbaglia bersaglio.
"min-h-11 rounded-full border border-border px-3 text-xs font-semibold transition-all active:scale-95 disabled:opacity-50",
attivo
? cn(meta.className, "border-transparent shadow-card")
: "bg-background text-muted-foreground",
)}
>
{meta.label}
</button>
);
}
if (isCompleanno) { if (isCompleanno) {
return ( return (
<Card as="article" className="flex items-center gap-3"> <Card as="article" className="flex items-center gap-3">
@@ -96,18 +126,6 @@ export function EventoCard({
</div> </div>
</div> </div>
{linkTo && (
<div className="mt-2 flex justify-end">
<Link
to={linkTo.to}
params={linkTo.params}
className="inline-flex min-h-11 items-center gap-1 rounded-full bg-primary px-4 text-xs font-bold uppercase tracking-wide text-primary-foreground transition-transform active:scale-95"
>
{linkTo.label} <ArrowRight className="h-3 w-3" />
</Link>
</div>
)}
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground"> <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<Clock className="h-3.5 w-3.5" /> {evento.ora} <Clock className="h-3.5 w-3.5" /> {evento.ora}
@@ -123,37 +141,22 @@ export function EventoCard({
<Barra percentuale={perc} altezza="h-1.5" trackClassName="mt-3" /> <Barra percentuale={perc} altezza="h-1.5" trackClassName="mt-3" />
{io ? ( {io || linkTo ? (
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
{stati.map((s) => { {io ? stati.slice(0, -1).map((s) => <ChipPresenza key={s} s={s} />) : null}
const meta = statoMeta[s]; {/* Ultima riga: ultimo chip + Apri insieme, così non nasce una terza riga. */}
const attivo = stato === s; <div className="flex w-full basis-full items-center gap-2">
return ( {io ? <ChipPresenza s={stati[stati.length - 1]!} /> : null}
<button {linkTo ? (
key={s} <Link
type="button" to={linkTo.to}
disabled={salva.isPending || passato} params={linkTo.params}
onClick={() => className="ml-auto inline-flex min-h-11 items-center gap-1 rounded-full bg-primary px-4 text-xs font-bold uppercase tracking-wide text-primary-foreground transition-transform active:scale-95"
salva.mutate({
eventoId: evento.id,
giocatoreId: io.id,
stato: attivo ? null : s,
})
}
aria-pressed={attivo}
className={cn(
// min-h-11: sono i controlli più toccati dell'app, sotto i
// 44px si sbaglia bersaglio.
"min-h-11 rounded-full border border-border px-3 text-xs font-semibold transition-all active:scale-95 disabled:opacity-50",
attivo
? cn(meta.className, "border-transparent shadow-card")
: "bg-background text-muted-foreground",
)}
> >
{meta.label} {linkTo.label} <ArrowRight className="h-3 w-3" />
</button> </Link>
); ) : null}
})} </div>
</div> </div>
) : null} ) : null}
</Card> </Card>
+10 -20
View File
@@ -1,7 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react"; 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, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits"; import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni"; import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
@@ -140,24 +140,14 @@ function Profilo() {
<Reveal className="-mt-6 px-5"> <Reveal className="-mt-6 px-5">
<Card> <Card>
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<div className="relative"> <Avatar id={g.id} fallback={g.iniziali} className="h-20 w-20 text-2xl" bust={bust} />
<Avatar id={g.id} fallback={g.iniziali} className="h-20 w-20 text-2xl" bust={bust} /> <input
<input ref={inputRef}
ref={inputRef} type="file"
type="file" accept="image/*"
accept="image/*" className="hidden"
className="hidden" onChange={onFile}
onChange={onFile} />
/>
<button
type="button"
onClick={() => inputRef.current?.click()}
className="absolute -bottom-1 -right-1 grid h-11 w-11 place-items-center rounded-full bg-accent-grad text-accent-foreground shadow-pop"
aria-label="Cambia foto"
>
<Camera className="h-4 w-4" />
</button>
</div>
</div> </div>
<div className="mt-4 flex gap-2"> <div className="mt-4 flex gap-2">
<button <button
@@ -181,7 +171,7 @@ function Profilo() {
toast.error("Non sono riuscito a rimuovere l'immagine"); toast.error("Non sono riuscito a rimuovere l'immagine");
} }
}} }}
className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-secondary text-muted-foreground" className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-destructive/15 text-destructive"
aria-label="Rimuovi immagine" aria-label="Rimuovi immagine"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
+1 -1
View File
@@ -270,7 +270,7 @@ function Squadra() {
{ {
id: "classifica-giocatori", id: "classifica-giocatori",
label: "Classifica", label: "Classifica",
nascondiTitolo: true, aTuttoLarghezza: true,
contenuto: ( contenuto: (
<> <>
<div className="flex min-w-0 items-center gap-2 border-b border-border bg-card px-5 py-2.5"> <div className="flex min-w-0 items-center gap-2 border-b border-border bg-card px-5 py-2.5">