Aggiunge lo swipe al calendario e ripulisce le schermate

Calendario: il mese si cambiava solo con due frecce da 36 px. Ora si scorre,
e il punto d'arrivo si sceglie proiettando la velocità di rilascio invece che
dalla posizione del dito, così un colpo secco «lancia» il mese. La griglia
entra ed esce dallo stesso lato del gesto, `dragElastic` dà resistenza
progressiva al bordo al posto di uno stop netto.

Le celle con più tipi di evento generavano un gradiente a fette e ci mettevano
sopra un'ombra bianca sul numero per tenerlo leggibile: un cerotto su un
problema di contrasto. Ora fondo neutro e un puntino per tipo, con il numero
sempre su superficie piena. L'etichetta accessibile dice quanti eventi ci
sono, non solo che ce ne sono.

Profilo: le quattro switch «Notifiche convocazioni», «Promemoria allenamenti»,
«Cambi orario» e «Bacheca squadra» erano `defaultChecked` e non facevano
niente — promettevano una funzione che non esiste. Via anche la conferma
nativa prima di *cambiare* la foto: non è un'azione distruttiva, e chiedere
conferma per tutto insegna a rispondere sì senza leggere. Resta su quella che
la rimuove, che è irreversibile.

Home: la sezione «Da confermare» mostrava titolo e contenitore vuoto quando
non c'era altro da confermare, mentre tutte le altre sezioni hanno un empty
state.

Squadra: i badge in riga avevano solo `title=`, che su touch non appare mai —
aggiunto il testo per gli screen reader. I filtri per criterio erano alti
~26 px.

Più, in tutte le schermate: la primitiva `Card` al posto delle classi
ripetute, i bersagli sotto i 44 px portati in misura e il floor tipografico a
12 px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 12:42:45 +02:00
co-authored by Claude Opus 5
parent 5c49294ad1
commit 9b28a86f7c
9 changed files with 217 additions and 178 deletions
+3 -3
View File
@@ -90,7 +90,7 @@ function messaggioErrore(e: unknown, fallback: string): string {
function Campo({ label, children }: { label: string; children: React.ReactNode }) {
return (
<label className="block">
<span className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground">
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
{label}
</span>
<span className="mt-1 block">{children}</span>
@@ -375,7 +375,7 @@ function Documento({
onClick={scarica}
disabled={!path || inCorso}
className={cn(
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase disabled:opacity-60",
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase disabled:opacity-60",
statoClasse[stato],
)}
>
@@ -464,7 +464,7 @@ function SchedaGiocatore({
/>
<span
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase",
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
statoClasse[g.numeroTessera ? "presente" : "assente"],
)}
>
+115 -74
View File
@@ -2,9 +2,11 @@ import { useEffect, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { CalendarPlus, ChevronLeft, ChevronRight, X } from "lucide-react";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { molla, proietta } from "@/lib/molla";
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
import { PageHeader, Section } from "@/components/crapp/ui-bits";
import { Card, PageHeader, Section } from "@/components/crapp/ui-bits";
import { compleanniEventi, useEventi, type Evento } from "@/lib/eventi";
import { useRosa } from "@/lib/rosa";
import { useGiocatoreCorrente } from "@/lib/user-store";
@@ -55,8 +57,12 @@ function useMeseNav(initial?: { anno: number; mese: number }) {
const oggi = new Date();
const [anno, setAnno] = useState(initial?.anno ?? oggi.getFullYear());
const [mese, setMese] = useState(initial?.mese ?? oggi.getMonth());
// Serve a far entrare e uscire la griglia dallo stesso lato del gesto:
// se un mese esce a sinistra, il precedente deve rientrare da sinistra.
const [direzione, setDirezione] = useState(0);
const precedente = () => {
setDirezione(-1);
if (mese === 0) {
setMese(11);
setAnno((a) => a - 1);
@@ -66,6 +72,7 @@ function useMeseNav(initial?: { anno: number; mese: number }) {
};
const successivo = () => {
setDirezione(1);
if (mese === 11) {
setMese(0);
setAnno((a) => a + 1);
@@ -74,7 +81,7 @@ function useMeseNav(initial?: { anno: number; mese: number }) {
}
};
return { anno, mese, precedente, successivo };
return { anno, mese, direzione, precedente, successivo };
}
function giorniDelMese(anno: number, mese: number) {
@@ -102,7 +109,8 @@ function Calendario() {
const admin = useIsAdmin();
const { eventi } = useEventi();
const rosa = useRosa();
const { anno, mese, precedente, successivo } = useMeseNav();
const ridotto = useReducedMotion();
const { anno, mese, direzione, precedente, successivo } = useMeseNav();
const { giorni, offsetLunedi } = giorniDelMese(anno, mese);
const mesePrefix = `${anno}-${pad2(mese + 1)}`;
@@ -129,12 +137,8 @@ function Calendario() {
: [];
// Prossimi 4 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia).
const oggiIso = oggi
? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}`
: null;
const prossimiEventi = oggiIso
? eventi.filter((e) => e.data >= oggiIso).slice(0, 4)
: [];
const oggiIso = oggi ? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` : null;
const prossimiEventi = oggiIso ? eventi.filter((e) => e.data >= oggiIso).slice(0, 4) : [];
return (
<>
@@ -147,8 +151,9 @@ function Calendario() {
key={v}
type="button"
onClick={() => setVista(v)}
aria-pressed={vista === v}
className={cn(
"flex-1 rounded-full py-2 text-xs font-bold uppercase tracking-wide transition-colors",
"min-h-11 flex-1 rounded-full text-xs font-bold uppercase tracking-wide transition-colors",
vista === v ? "bg-card shadow-card text-foreground" : "text-muted-foreground",
)}
>
@@ -160,91 +165,127 @@ function Calendario() {
{vista === "mese" ? (
<Section titolo={mesiIT[mese]!}>
<div className="rounded-3xl bg-card p-4 shadow-card">
<Card>
<div className="mb-3 flex items-center justify-between">
<button
type="button"
onClick={precedente}
className="grid h-9 w-9 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
className="grid h-11 w-11 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
aria-label="Mese precedente"
>
<ChevronLeft className="h-5 w-5" />
</button>
<span className="font-display text-xl uppercase tracking-wide">
<span className="font-display-sm text-xl uppercase">
{mesiIT[mese]} {anno}
</span>
<button
type="button"
onClick={successivo}
className="grid h-9 w-9 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
className="grid h-11 w-11 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
aria-label="Mese successivo"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
<div className="grid grid-cols-7 gap-1 text-center text-[11px] font-bold text-muted-foreground">
<div className="grid grid-cols-7 gap-1 text-center text-xs font-bold text-muted-foreground">
{giorniIT.map((g, i) => (
<span key={i}>{g}</span>
))}
</div>
<div className="mt-2 grid grid-cols-7 gap-1">
{Array.from({ length: offsetLunedi }).map((_, i) => (
<span key={`v${i}`} />
))}
{Array.from({ length: giorni }).map((_, i) => {
const giorno = i + 1;
const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
const haEventi = eventiGiorno.length > 0;
const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
const coloreTipo: Record<Evento["tipo"], string> = {
partita: "var(--accent)",
allenamento: "var(--training)",
evento: "var(--warning)",
compleanno: "var(--success)",
};
const sfondo =
tipiGiorno.length > 1
? `linear-gradient(135deg, ${tipiGiorno
.map((t, idx) => {
const da = (idx / tipiGiorno.length) * 100;
const a = ((idx + 1) / tipiGiorno.length) * 100;
return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`;
})
.join(", ")})`
: undefined;
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
const isOggi =
!!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
const Cella = haEventi ? "button" : "div";
return (
<Cella
key={giorno}
type={haEventi ? "button" : undefined}
onClick={haEventi ? () => apriGiorno(giorno) : undefined}
style={sfondo ? { backgroundImage: sfondo } : undefined}
className={cn(
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
tipo === "partita" && "bg-accent text-accent-foreground",
tipo === "allenamento" && "bg-training text-training-foreground",
tipo === "evento" && "bg-warning text-warning-foreground",
tipo === "compleanno" && "bg-success text-success-foreground",
!tipo && !haEventi && "text-muted-foreground",
!tipo && haEventi && "text-foreground",
haEventi && "cursor-pointer transition-transform active:scale-90",
isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
)}
aria-label={haEventi ? `Eventi del ${giorno}` : undefined}
aria-current={isOggi ? "date" : undefined}
>
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">
{giorno}
</span>
</Cella>
);
})}
{/*
Il mese si cambia anche con lo swipe: il punto d'arrivo si
decide proiettando la velocità di rilascio (come la
decelerazione dello scroll iOS), non dalla posizione del dito.
`dragElastic` dà la resistenza progressiva al bordo invece di
uno stop netto.
*/}
<div className="relative mt-2 overflow-hidden">
<AnimatePresence initial={false} mode="popLayout" custom={direzione}>
<motion.div
key={mesePrefix}
custom={direzione}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.18}
dragMomentum={false}
onDragEnd={(_, info) => {
const arrivo = info.offset.x + proietta(info.velocity.x);
if (arrivo < -60) successivo();
else if (arrivo > 60) precedente();
}}
initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }}
animate={{ opacity: 1, x: 0 }}
exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }}
transition={ridotto ? { duration: 0.2 } : molla.foglio}
className="grid touch-pan-y grid-cols-7 gap-1"
>
{Array.from({ length: offsetLunedi }).map((_, i) => (
<span key={`v${i}`} />
))}
{Array.from({ length: giorni }).map((_, i) => {
const giorno = i + 1;
const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
const haEventi = eventiGiorno.length > 0;
const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
// Un solo tipo: cella piena, il colore è l'informazione.
// Più tipi: fondo neutro e un puntino per tipo. Prima si
// generava un gradiente a fette, che rendeva il numero
// illeggibile e costringeva a un'ombra bianca di ripiego.
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
const isOggi =
!!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
const Cella = haEventi ? "button" : "div";
return (
<Cella
key={giorno}
type={haEventi ? "button" : undefined}
onClick={haEventi ? () => apriGiorno(giorno) : undefined}
className={cn(
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
tipo === "partita" && "bg-accent text-accent-foreground",
tipo === "allenamento" && "bg-training text-training-foreground",
tipo === "evento" && "bg-warning text-warning-foreground",
tipo === "compleanno" && "bg-success text-success-foreground",
!tipo && !haEventi && "text-muted-foreground",
!tipo && haEventi && "bg-secondary text-foreground",
haEventi && "cursor-pointer transition-transform active:scale-90",
isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
)}
aria-label={
haEventi
? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}`
: undefined
}
aria-current={isOggi ? "date" : undefined}
>
<span>{giorno}</span>
{tipiGiorno.length > 1 ? (
<span aria-hidden="true" className="absolute bottom-1 flex gap-0.5">
{tipiGiorno.slice(0, 3).map((t) => (
<i
key={t}
className={cn(
"h-1 w-1 rounded-full",
t === "partita" && "bg-accent",
t === "allenamento" && "bg-training",
t === "evento" && "bg-warning",
t === "compleanno" && "bg-success",
)}
/>
))}
</span>
) : null}
</Cella>
);
})}
</motion.div>
</AnimatePresence>
</div>
<div className="mt-4 flex flex-wrap gap-3 text-[11px] font-semibold text-muted-foreground">
<p className="mt-3 text-xs text-muted-foreground">
Scorri a destra o sinistra per cambiare mese.
</p>
<div className="mt-3 flex flex-wrap gap-3 text-xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
</span>
@@ -258,7 +299,7 @@ function Calendario() {
<i className="h-2.5 w-2.5 rounded-full bg-success" /> Compleanni
</span>
</div>
</div>
</Card>
</Section>
) : null}
@@ -303,10 +344,10 @@ function Calendario() {
<Drawer open={drawerAperto} onOpenChange={setDrawerAperto}>
<DrawerContent className="rounded-t-[24px] border-border bg-background px-4 pb-6 pt-2">
<DrawerHeader className="relative px-0 pb-2 text-left">
<DrawerTitle className="font-display text-2xl uppercase tracking-wide">
<DrawerTitle className="font-display-lg text-2xl uppercase">
{giornoSelezionato ? `${giornoSelezionato} ${mesiIT[mese]}` : "Eventi"}
</DrawerTitle>
<DrawerClose className="absolute right-0 top-1 grid h-8 w-8 place-items-center rounded-full bg-secondary text-foreground transition-transform active:scale-90">
<DrawerClose className="absolute right-0 top-0 grid h-11 w-11 place-items-center rounded-full bg-secondary text-foreground transition-transform active:scale-90">
<X className="h-4 w-4" />
<span className="sr-only">Chiudi</span>
</DrawerClose>
+4 -4
View File
@@ -83,7 +83,7 @@ function Classifica() {
<Section titolo="Classifica">
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-[10px] font-bold uppercase text-muted-foreground">
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-xs font-bold uppercase text-muted-foreground">
<span>#</span>
<span>Squadra</span>
<span className="text-center">G</span>
@@ -141,7 +141,7 @@ function Classifica() {
{m.casa ? "CRAP Volley" : m.avversario} vs{" "}
{m.casa ? m.avversario : "CRAP Volley"}
</p>
<p className="text-[11px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
{formatData(m.data)} · MVP {m.mvp || "da votare"}
{m.scout ? " · scoutata" : ""}
</p>
@@ -162,7 +162,7 @@ function Classifica() {
<span
key={i}
className={cn(
"rounded-lg px-2 py-1 text-[11px] font-semibold tabular-nums",
"rounded-lg px-2 py-1 text-xs font-semibold tabular-nums",
p[0] > p[1] ? "bg-secondary" : "bg-muted text-muted-foreground",
)}
>
@@ -170,7 +170,7 @@ function Classifica() {
</span>
))}
{eventoId && !m.mvp ? (
<span className="ml-auto inline-flex items-center gap-0.5 text-[11px] font-bold uppercase text-accent">
<span className="ml-auto inline-flex items-center gap-0.5 text-xs font-bold uppercase text-accent">
Vota MVP <ChevronRight className="h-3.5 w-3.5" />
</span>
) : null}
+9 -7
View File
@@ -134,7 +134,7 @@ function GestioneEventi() {
type="button"
onClick={() => aggiorna(daCategoria(t.id))}
className={cn(
"rounded-full py-2 text-[11px] font-bold uppercase transition-colors",
"rounded-full py-2 text-xs font-bold uppercase transition-colors",
categoriaEvento(bozza) === t.id
? "bg-card shadow-card text-foreground"
: "text-muted-foreground",
@@ -281,7 +281,9 @@ function GestioneEventi() {
<Section titolo="Eventi in calendario">
{isPending ? (
<p className="text-center text-xs text-muted-foreground">Carico gli eventi</p>
<p aria-busy="true" className="text-center text-xs text-muted-foreground">
Carico gli eventi
</p>
) : (
<div className="space-y-2">
{eventi.map((e) => (
@@ -291,14 +293,14 @@ function GestioneEventi() {
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
<p className="text-[11px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
{formatData(e.data)} · {e.ora} · {e.luogo || "luogo da definire"}
</p>
</div>
<button
type="button"
onClick={() => setBozza(e)}
className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-secondary text-foreground active:scale-95"
className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-secondary text-foreground active:scale-95"
aria-label={`Modifica ${e.titolo}`}
>
<Pencil className="h-4 w-4" />
@@ -306,7 +308,7 @@ function GestioneEventi() {
<button
type="button"
onClick={() => rimuovi(e.id)}
className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-destructive/10 text-destructive active:scale-95"
className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-destructive/10 text-destructive active:scale-95"
aria-label={`Elimina ${e.titolo}`}
>
<Trash2 className="h-4 w-4" />
@@ -323,7 +325,7 @@ function GestioneEventi() {
function Campo({ label, children }: { label: string; children: React.ReactNode }) {
return (
<label className="block">
<span className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground">
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
{label}
</span>
<span className="mt-1 block">{children}</span>
@@ -345,7 +347,7 @@ function Interruttore({
type="button"
onClick={onClick}
className={cn(
"rounded-full px-3 py-1.5 text-[11px] font-bold uppercase transition-colors",
"rounded-full px-3 py-1.5 text-xs font-bold uppercase transition-colors",
attivo ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
)}
>
+22 -15
View File
@@ -2,7 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { Flame, ChevronRight } from "lucide-react";
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni";
import { Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { Card, Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
import { Reveal } from "@/components/motion/Reveal";
import { Barra } from "@/components/motion/Barra";
@@ -41,6 +41,7 @@ function Index() {
const oggi = new Date().toISOString().slice(0, 10);
const prossimi: Evento[] = eventi.filter((e) => e.data >= oggi).slice(0, 3);
const prossimo = prossimi[0] ?? null;
const daConfermare = prossimi.slice(1);
const linkProssimo = prossimo ? linkPerEvento(prossimo) : null;
const { data: csi } = useCsi();
const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra));
@@ -66,10 +67,10 @@ function Index() {
<Reveal as="section" className="bg-hero px-5 pb-10 pt-7 text-primary-foreground">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/60">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/80">
Ciao {giocatore.nome.split(" ")[0]}
</p>
<h1 className="font-display text-4xl uppercase leading-none">CrAPP</h1>
<h1 className="font-display-lg text-4xl uppercase leading-none">CrAPP</h1>
</div>
<TeamLogo className="h-12 w-12" />
</div>
@@ -77,20 +78,20 @@ function Index() {
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
<div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p>
<p className="text-[10px] uppercase text-primary-foreground/60">In classifica</p>
<p className="text-xs uppercase text-primary-foreground/80">In classifica</p>
</div>
<div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="font-display text-2xl leading-none">
{noi ? `${noi.vinte}-${noi.perse}` : "—"}
</p>
<p className="text-[10px] uppercase text-primary-foreground/60">Bilancio</p>
<p className="text-xs uppercase text-primary-foreground/80">Bilancio</p>
</div>
<div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="inline-flex items-center gap-1 font-display text-2xl leading-none">
<Flame className="h-4 w-4 text-accent" />
{giocatore.streak}
</p>
<p className="text-[10px] uppercase text-primary-foreground/60">Streak</p>
<p className="text-xs uppercase text-primary-foreground/80">Streak</p>
</div>
</div>
</Reveal>
@@ -122,10 +123,16 @@ function Index() {
<Section titolo="Da confermare" indice={3}>
<div className="space-y-3">
{prossimi.slice(1).map((e) => {
const link = linkPerEvento(e);
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
})}
{daConfermare.length > 0 ? (
daConfermare.map((e) => {
const link = linkPerEvento(e);
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
})
) : (
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
Nient'altro da confermare: sei in pari.
</p>
)}
</div>
</Section>
@@ -161,7 +168,7 @@ function Index() {
{ultima.parziali.map((p, i) => (
<span
key={i}
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums"
className="rounded-lg bg-secondary px-2 py-1 text-xs font-semibold tabular-nums"
>
{p[0]}-{p[1]}
</span>
@@ -178,7 +185,7 @@ function Index() {
{corpo}
</Link>
) : (
<div className="premi rounded-3xl bg-card p-4 shadow-card">{corpo}</div>
<Card>{corpo}</Card>
);
})()
) : (
@@ -201,7 +208,7 @@ function Index() {
}
>
{obiettivo ? (
<div className="premi rounded-3xl bg-card p-4 shadow-card">
<Card>
<div className="flex items-center gap-2 text-sm font-bold">
<span className="text-base leading-none">{obiettivo.emoji}</span> {obiettivo.titolo}
</div>
@@ -213,8 +220,8 @@ function Index() {
<p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(obiettivo)}
</p>
<p className="mt-1 text-[11px] text-muted-foreground">{obiettivo.impatto}</p>
</div>
<p className="mt-1 text-xs text-muted-foreground">{obiettivo.impatto}</p>
</Card>
) : null}
</Section>
+5 -5
View File
@@ -1,7 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowLeft, MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { PageHeader, Section } from "@/components/crapp/ui-bits";
import { Card, PageHeader, Section } from "@/components/crapp/ui-bits";
import { formatData } from "@/lib/crapp-data";
import { convocatiEvento, useEvento } from "@/lib/eventi";
import { useRosa } from "@/lib/rosa";
@@ -210,7 +210,7 @@ function PartitaDetail() {
{scout && totaliTeam ? (
<Section titolo="Report tecnico">
<div className="rounded-3xl bg-card p-4 shadow-card">
<Card>
<div className="grid grid-cols-4 gap-2 text-center">
{[
{ l: "Punti", v: totaliTeam.punti },
@@ -220,13 +220,13 @@ function PartitaDetail() {
].map((t) => (
<div key={t.l} className="rounded-2xl bg-secondary p-2.5">
<p className="font-display text-xl leading-none">{t.v}</p>
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground">
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
{t.l}
</p>
</div>
))}
</div>
<p className="mt-4 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
Dettaglio giocatori (uso interno allenatori)
</p>
<div className="mt-2 space-y-1">
@@ -262,7 +262,7 @@ function PartitaDetail() {
<Download className="h-4 w-4" /> Esporta CSV
</button>
) : null}
</div>
</Card>
</Section>
) : null}
+13 -27
View File
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
import { cn } from "@/lib/utils";
import { PageHeader, Section, SezioneTendina, StatTile } from "@/components/crapp/ui-bits";
import { Card, PageHeader, Section, SezioneTendina, StatTile } from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar";
import {
caricaAvatar,
@@ -97,15 +97,12 @@ function Profilo() {
}
}
const percPresenze = g.totaliEventi
? Math.round((g.presenze / g.totaliEventi) * 100)
: 0;
const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
async function onFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
e.target.value = "";
if (!file || !g) return;
if (!confirm("Aggiornare l'immagine profilo?")) return;
try {
await caricaAvatar(g.id, file);
setBust(Date.now());
@@ -121,7 +118,7 @@ function Profilo() {
<PageHeader titolo={g.nome} sottotitolo={`#${g.numero} · ${g.ruolo}`} />
<Reveal className="-mt-6 px-5">
<div className="premi rounded-3xl bg-card p-4 shadow-card">
<Card>
<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} />
@@ -135,7 +132,7 @@ function Profilo() {
<button
type="button"
onClick={() => inputRef.current?.click()}
className="absolute -bottom-1 -right-1 grid h-8 w-8 place-items-center rounded-full bg-accent-grad text-accent-foreground shadow-pop"
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" />
@@ -146,7 +143,7 @@ function Profilo() {
<button
type="button"
onClick={() => inputRef.current?.click()}
className="flex-1 rounded-xl bg-secondary px-3 py-2 text-xs font-bold uppercase tracking-wide"
className="premi min-h-11 flex-1 rounded-xl bg-secondary px-3 text-xs font-bold uppercase tracking-wide"
>
Cambia immagine profilo
</button>
@@ -164,14 +161,14 @@ function Profilo() {
toast.error("Non sono riuscito a rimuovere l'immagine");
}
}}
className="grid h-9 w-9 place-items-center rounded-xl bg-secondary text-muted-foreground"
className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-secondary text-muted-foreground"
aria-label="Rimuovi immagine"
>
<Trash2 className="h-4 w-4" />
</button>
) : null}
</div>
</div>
</Card>
</Reveal>
<Section titolo="Stagione" indice={1}>
@@ -213,11 +210,11 @@ function Profilo() {
type="button"
onClick={cambiaNotifiche}
disabled={!supportate || inCorso}
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
>
<span className="min-w-0">
<span className="block truncate">Notifiche turno palloni</span>
<span className="block text-[11px] text-muted-foreground">
<span className="block text-xs text-muted-foreground">
{supportate
? notifiche
? "Attive su questo dispositivo"
@@ -236,21 +233,10 @@ function Profilo() {
<Bell className="h-4 w-4" />
</span>
</button>
{[
"Notifiche convocazioni",
"Promemoria allenamenti",
"Cambi orario",
"Bacheca squadra",
].map((v) => (
<label key={v} className="flex items-center justify-between gap-3 px-4 py-3 text-sm">
<span className="min-w-0 truncate">{v}</span>
<input type="checkbox" defaultChecked className="h-5 w-9 accent-[var(--accent)]" />
</label>
))}
{admin ? (
<Link
to="/admin"
className="flex 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">Dashboard amministratore</span>
<ShieldCheck className="h-4 w-4 text-muted-foreground" />
@@ -260,7 +246,7 @@ function Profilo() {
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml"
target="_blank"
rel="noopener noreferrer"
className="flex 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>
<Bug className="h-4 w-4 text-muted-foreground" />
@@ -269,7 +255,7 @@ function Profilo() {
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml"
target="_blank"
rel="noopener noreferrer"
className="flex 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">Suggerisci una nuova funzionalità</span>
<Lightbulb className="h-4 w-4 text-muted-foreground" />
@@ -277,7 +263,7 @@ function Profilo() {
<button
type="button"
onClick={logout}
className="flex 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">Esci</span>
<LogOut className="h-4 w-4 text-muted-foreground" />
+14 -13
View File
@@ -3,6 +3,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Undo2, Save, CheckCircle2, Radio, Lock, CalendarX2, LogOut } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Card } from "@/components/crapp/ui-bits";
import { formatData } from "@/lib/crapp-data";
import type { Evento } from "@/lib/eventi";
import { useRosa } from "@/lib/rosa";
@@ -322,7 +323,7 @@ function ScoutBoard({
<>
<header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-6 text-primary-foreground">
<div className="flex items-center justify-between gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide">
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-xs font-bold uppercase tracking-wide">
<Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente}
</span>
<div className="flex items-center gap-2">
@@ -346,14 +347,14 @@ function ScoutBoard({
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center">
<div className="min-w-0">
<p className="truncate text-[11px] uppercase text-primary-foreground/60">CRAP Volley</p>
<p className="truncate text-xs uppercase text-primary-foreground/60">CRAP Volley</p>
<p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p>
</div>
<p className="font-display text-2xl leading-none text-primary-foreground/50">
{setNostri}-{setLoro}
</p>
<div className="min-w-0">
<p className="truncate text-[11px] uppercase text-primary-foreground/60">
<p className="truncate text-xs uppercase text-primary-foreground/60">
{avversario || "Avversario"}
</p>
<p className="font-display text-5xl leading-none">{puntiLoro}</p>
@@ -365,7 +366,7 @@ function ScoutBoard({
{setChiusi.map((p, i) => (
<span
key={i}
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-[11px] font-semibold tabular-nums"
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-xs font-semibold tabular-nums"
>
{p[0]}-{p[1]}
</span>
@@ -375,7 +376,7 @@ function ScoutBoard({
</header>
<section className="px-5 pt-4">
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
1. Tocca il giocatore
</p>
<div className="grid grid-cols-4 gap-2">
@@ -393,10 +394,10 @@ function ScoutBoard({
)}
>
<p className="font-display text-2xl leading-none">{g.numero}</p>
<p className="mt-1 text-[10px] font-bold leading-tight">{g.nome}</p>
<p className="mt-1 text-xs font-bold leading-tight">{g.nome}</p>
<p
className={cn(
"text-[10px] tabular-nums",
"text-xs tabular-nums",
attivo ? "text-accent-foreground/70" : "text-muted-foreground",
)}
>
@@ -409,7 +410,7 @@ function ScoutBoard({
</section>
<section className="px-5 pt-4">
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
2. Tocca l'azione
</p>
<div className="grid grid-cols-2 gap-2">
@@ -447,7 +448,7 @@ function ScoutBoard({
<section className="px-5 pt-4">
<div className="rounded-3xl bg-card p-3 shadow-card">
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
Ultime azioni
</p>
{ultime.length === 0 ? (
@@ -467,7 +468,7 @@ function ScoutBoard({
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
<span
className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase",
"shrink-0 rounded-full px-2 py-0.5 text-xs font-bold uppercase",
azioniMeta[a.tipo].className,
)}
>
@@ -482,8 +483,8 @@ function ScoutBoard({
</section>
<section className="px-5 pt-4">
<div className="rounded-3xl bg-card p-4 shadow-card">
<label className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<Card>
<label className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
Avversario
</label>
<input
@@ -507,7 +508,7 @@ function ScoutBoard({
</button>
))}
</div>
</div>
</Card>
</section>
<section className="grid grid-cols-2 gap-2 px-5 pb-4 pt-4">
+32 -30
View File
@@ -30,7 +30,7 @@ import {
function RuoloBadge({ ruolo }: { ruolo: string }) {
return (
<span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-accent-foreground">
<span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-accent-foreground">
{ruolo}
</span>
);
@@ -113,7 +113,7 @@ function Squadra() {
<button
type="button"
onClick={() => setAperto(isOpen ? null : g.id)}
className="flex w-full items-center gap-3 p-3 text-left active:scale-[0.99]"
className="flex min-h-11 w-full items-center gap-3 p-3 text-left active:scale-[0.99]"
aria-expanded={isOpen}
>
<Avatar
@@ -125,7 +125,7 @@ function Squadra() {
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
<span className="mt-1 flex items-center gap-2">
<RuoloBadge ruolo={g.ruolo} />
<span className="text-[11px] text-muted-foreground">
<span className="text-xs text-muted-foreground">
{g.numero ? `#${g.numero}` : "n° da definire"}
</span>
</span>
@@ -135,8 +135,17 @@ function Squadra() {
{sbloccati.map((b) => {
const Icon = b.def.icon;
return (
<span key={b.def.id} className="cursor-help" title={b.def.nome}>
<Icon className={cn("h-3 w-3", gradoMeta[b.grado!].text)} />
<span
key={b.def.id}
title={`${b.def.nome}${gradoMeta[b.grado!].label}`}
>
<Icon
className={cn("h-3.5 w-3.5", gradoMeta[b.grado!].text)}
aria-hidden="true"
/>
<span className="sr-only">
{b.def.nome}: {gradoMeta[b.grado!].label}
</span>
</span>
);
})}
@@ -170,14 +179,14 @@ function Squadra() {
].map((s) => (
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
<p className="font-display text-xl leading-none">{s.v}</p>
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground">
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
{s.l}
</p>
</div>
))}
</div>
<p className="mt-4 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali}
</p>
{sbloccati.length === 0 ? (
@@ -194,18 +203,13 @@ function Squadra() {
<div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}>
<Icon className={cn("h-4 w-4", meta.text)} />
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
<p className="text-[10px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
{b.valore} {b.def.unita}
{b.prossimaSoglia
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
: ""}
</p>
<p
className={cn(
"mt-1.5 text-[10px] font-bold uppercase",
meta.text,
)}
>
<p className={cn("mt-1.5 text-xs font-bold uppercase", meta.text)}>
{meta.label}
</p>
</div>
@@ -240,8 +244,9 @@ function Squadra() {
key={c.id}
type="button"
onClick={() => setCriterio(c.id)}
aria-pressed={criterio === c.id}
className={cn(
"min-w-0 flex-1 truncate rounded-full px-1.5 py-1.5 text-center text-[10px] font-bold uppercase transition-colors",
"min-h-11 min-w-0 flex-1 truncate rounded-full px-1.5 text-center text-xs font-bold uppercase transition-colors",
criterio === c.id
? "bg-accent text-accent-foreground shadow-pop"
: "bg-secondary text-muted-foreground",
@@ -264,7 +269,7 @@ function Squadra() {
{g.nome}
{i === 0 ? <Crown className="ml-1 inline h-3.5 w-3.5 text-warning" /> : null}
</p>
<p className="truncate text-[11px] text-muted-foreground">
<p className="truncate text-xs text-muted-foreground">
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
</p>
</div>
@@ -273,12 +278,11 @@ function Squadra() {
{valore(g, criterio) || "—"}
</span>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-secondary">
<div
className="h-full rounded-full bg-accent-grad"
style={{ width: `${Math.max(6, (valore(g, criterio) / max) * 100)}%` }}
/>
</div>
<Barra
percentuale={Math.max(6, (valore(g, criterio) / max) * 100)}
altezza="h-1.5"
trackClassName="mt-2"
/>
</div>
))}
</div>
@@ -290,7 +294,7 @@ function Squadra() {
<div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
<div className="flex items-end justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-wide text-primary-foreground/60">
<p className="text-xs font-semibold uppercase tracking-wide text-primary-foreground/60">
Progresso collettivo
</p>
<p className="font-display text-4xl leading-none">
@@ -326,7 +330,7 @@ function Squadra() {
</div>
<span
className={cn(
"rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide",
"rounded-full px-2 py-0.5 text-xs font-bold uppercase tracking-wide",
fatto
? "bg-success text-success-foreground"
: "bg-secondary text-muted-foreground",
@@ -341,7 +345,7 @@ function Squadra() {
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
</p>
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p>
<p className="mt-0.5 text-[11px] text-muted-foreground">{o.impatto}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
</Reveal>
);
})}
@@ -358,9 +362,7 @@ function Squadra() {
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-accent" />
<p className="text-sm font-bold leading-tight">{b.nome}</p>
<p className="ml-auto text-[10px] text-muted-foreground">
{descrizioneSoglie(b)}
</p>
<p className="ml-auto text-xs text-muted-foreground">{descrizioneSoglie(b)}</p>
</div>
<div className="mt-2 grid grid-cols-3 gap-1.5">
{gradiOrdine.map((grado) => {
@@ -376,11 +378,11 @@ function Squadra() {
key={grado}
className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)}
>
<p className={cn("text-[10px] font-bold uppercase", meta.text)}>
<p className={cn("text-xs font-bold uppercase", meta.text)}>
{meta.label}
</p>
<p className="font-display text-lg leading-none">{b.soglie[grado]}</p>
<p className="text-[10px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
{quanti}/{rosa.length}
</p>
</div>