Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5281c0c62b | ||
|
|
3a892a78e6 | ||
|
|
1e4ec9113e | ||
|
|
263621456d |
No files matched your search
+4
-2
@@ -9,8 +9,10 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
|
|||||||
|
|
||||||
## Stato generale
|
## Stato generale
|
||||||
|
|
||||||
- **Versione:** 1.6.1 su `main`: risultati dello scout leggibili nella pagina
|
- **Versione:** 1.6.2 su `main`: layout corretto sugli schermi stretti (360-402 px) e
|
||||||
partita, classifica cacche completa e pagina partita a sezioni a scomparsa, senza migration. La 1.6.0
|
Ace, Attacchi, Muri e Punti dai + dello scout in Squadra e Profilo, senza migration. La 1.6.1
|
||||||
|
(tag `v1.6.1`): risultati dello scout leggibili nella pagina partita, classifica cacche
|
||||||
|
completa e pagina partita a sezioni a scomparsa. La 1.6.0
|
||||||
(tag `v1.6.0`, in produzione dal 02/11/2026): voto MVP definitivo, segreto e a
|
(tag `v1.6.0`, in produzione dal 02/11/2026): voto MVP definitivo, segreto e a
|
||||||
tempo (DD-051, DD-052) e Scout live a valutazioni − / + (DD-053). La 1.5.0: campionato CSI sulla stagione 2026/27 (nuovi `project_id` e `team_id`,
|
tempo (DD-051, DD-052) e Scout live a valutazioni − / + (DD-053). La 1.5.0: campionato CSI sulla stagione 2026/27 (nuovi `project_id` e `team_id`,
|
||||||
nessuna Coppa). La 1.4.2: ogni evento può avere un link Google Maps del luogo, mostrato come
|
nessuna Coppa). La 1.4.2: ogni evento può avere un link Google Maps del luogo, mostrato come
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ versioni `0.y.z` erano pre-release.
|
|||||||
|
|
||||||
## [Non rilasciato]
|
## [Non rilasciato]
|
||||||
|
|
||||||
|
## [1.6.2] - 2026-11-11
|
||||||
|
|
||||||
|
Layout corretto sugli schermi stretti e statistiche tecniche dai + dello scout in Squadra e
|
||||||
|
Profilo. Nessuna migration.
|
||||||
|
|
||||||
|
### Modificato
|
||||||
|
|
||||||
|
- Squadra → Stats: «Punti squadra», «Ace squadra» e «Muri squadra» sommano i + dello scout di
|
||||||
|
tutte le partite (ace = + di battuta, muri = + di muro, punti = + di attacco, battuta e muro);
|
||||||
|
le partite del metodo precedente contano con le loro azioni.
|
||||||
|
- Profilo → Season: al posto di «Turni palloni» compaiono Ace, Attacchi, Muri e Punti (totale)
|
||||||
|
del giocatore, somma dei + dello scout di tutte le partite (DD-008: solo informativi).
|
||||||
|
- Layout su schermi stretti (360-402 px, iPhone e Android): niente più pagina tagliata in
|
||||||
|
larghezza. La tabella per giocatore dello scout sta nello schermo (il nome va a capo su due righe), le
|
||||||
|
griglie di riquadri non sfondano con nomi ed etichette lunghe, la pagina non scorre in
|
||||||
|
orizzontale e i campi di testo sono a 16 px, così iOS non zooma al tocco. I nomi non si spezzano
|
||||||
|
mai a metà parola; nello scout live i riquadri dei giocatori sono a 3 colonne sotto i 400 px.
|
||||||
|
|
||||||
## [1.6.1] - 2026-11-06
|
## [1.6.1] - 2026-11-06
|
||||||
|
|
||||||
Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni
|
Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ Le statistiche **personali** in profilo e squadra devono essere **eque per tutti
|
|||||||
|
|
||||||
- Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti.
|
- Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti.
|
||||||
- Lo scout resta strumento tecnico, non gioco.
|
- Lo scout resta strumento tecnico, non gioco.
|
||||||
|
- Aggiornamento 2026-11-11: in Profilo → Season compaiono, come semplice informazione personale,
|
||||||
|
i totali di ace, attacchi, muri e punti del giocatore ricavati dai + dello scout. Non entrano
|
||||||
|
in badge, obiettivi o classifiche: la regola sulla leva competitiva resta valida.
|
||||||
|
|
||||||
**Riesame**
|
**Riesame**
|
||||||
Se la squadra chiede esplicitamente classifiche tecniche per ruolo.
|
Se la squadra chiede esplicitamente classifiche tecniche per ruolo.
|
||||||
@@ -285,8 +285,16 @@ Sezione già presente. Contiene.
|
|||||||
- Voto medio
|
- Voto medio
|
||||||
- MVP
|
- MVP
|
||||||
- Serie
|
- Serie
|
||||||
|
- **Ace, Attacchi, Muri e Punti (totale)**: somma, su tutte le partite scoutate, dei soli **+**
|
||||||
|
assegnati a quel giocatore nello scout live (ace = + di battuta, attacchi = + di attacco,
|
||||||
|
muri = + di muro; punti = ace + attacchi + muri, la ricezione non conta). Le partite scoutate
|
||||||
|
col metodo precedente, senza valutazioni, contano con le azioni registrate; se una partita ha
|
||||||
|
entrambe, valgono solo le valutazioni (`totaliTecnici()` in `scout-store.ts`). Il «Turni
|
||||||
|
palloni» non compare più qui: il turno resta nel banner palloni e nel badge dedicato.
|
||||||
- Altre statistiche disponibili
|
- Altre statistiche disponibili
|
||||||
|
|
||||||
|
Sono numeri informativi: non alimentano badge, obiettivi né classifiche (DD-008).
|
||||||
|
|
||||||
### Badge
|
### Badge
|
||||||
|
|
||||||
Sezione già presente.
|
Sezione già presente.
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ successivo non salta — 1, 1, 2, non 1, 1, 3); `dettaglioClassifica()` sceglie
|
|||||||
sottostatistica mostrare sotto il nome, coerente col criterio selezionato (es. "voti
|
sottostatistica mostrare sotto il nome, coerente col criterio selezionato (es. "voti
|
||||||
pagella" per il criterio media voto, non sempre "presenze consecutive").
|
pagella" per il criterio media voto, non sempre "presenze consecutive").
|
||||||
|
|
||||||
|
In cima alla tab, i riquadri «Punti squadra», «Ace squadra» e «Muri squadra» sommano **tutte le
|
||||||
|
partite scoutate** (`totaliSquadraValutazioni()`, che usa `totaliTecnici()`, in `scout-store.ts`), contando solo le valutazioni
|
||||||
|
**+** dello scout live: «Ace» = + di battuta, «Muri» = + di muro, «Punti» = + di attacco, battuta e
|
||||||
|
muro (la ricezione non è un punto messo a terra, come nel riquadro del giocatore). Le partite
|
||||||
|
scoutate col metodo precedente, senza valutazioni, contano con le loro azioni registrate (punti,
|
||||||
|
ace, muri); se una partita ha entrambe, valgono solo le valutazioni, così non si conta due volte.
|
||||||
|
|
||||||
Le altre tab di `/squadra` (Rosa, Obiettivi, Badge) sono viste diverse sugli stessi dati di
|
Le altre tab di `/squadra` (Rosa, Obiettivi, Badge) sono viste diverse sugli stessi dati di
|
||||||
`useRosa()`/`useObiettivi()`/`badges.ts`: non introducono altra logica di dominio, solo
|
`useRosa()`/`useObiettivi()`/`badges.ts`: non introducono altra logica di dominio, solo
|
||||||
presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squadra.md`.
|
presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squadra.md`.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.111.0",
|
"@supabase/supabase-js": "^2.111.0",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function ScoutReport({
|
|||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Periodo"
|
aria-label="Periodo"
|
||||||
className="mt-3 grid grid-flow-col auto-cols-fr gap-1 rounded-2xl bg-secondary p-1"
|
className="mt-3 grid grid-flow-col auto-cols-fr gap-0.5 rounded-2xl bg-secondary p-1"
|
||||||
>
|
>
|
||||||
{[undefined, ...sets].map((s) => (
|
{[undefined, ...sets].map((s) => (
|
||||||
<button
|
<button
|
||||||
@@ -134,7 +134,7 @@ export function ScoutReport({
|
|||||||
aria-selected={s === set}
|
aria-selected={s === set}
|
||||||
onClick={() => setSet(s)}
|
onClick={() => setSet(s)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 truncate rounded-xl px-1 py-2 text-[13px] font-bold uppercase transition-colors",
|
"min-w-0 truncate rounded-xl px-0 py-2 text-[13px] font-bold uppercase transition-colors",
|
||||||
s === set
|
s === set
|
||||||
? "bg-primary text-primary-foreground shadow-card"
|
? "bg-primary text-primary-foreground shadow-card"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
@@ -185,13 +185,13 @@ export function ScoutReport({
|
|||||||
Nessuna valutazione in questo set.
|
Nessuna valutazione in questo set.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-2 overflow-x-auto rounded-2xl bg-secondary/60">
|
<div className="mt-2 overflow-hidden rounded-2xl bg-secondary/60">
|
||||||
<table className="w-full min-w-[22rem] text-[13px]">
|
<table className="w-full table-fixed text-[13px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-[13px] uppercase text-muted-foreground">
|
<tr className="text-left text-[13px] uppercase text-muted-foreground">
|
||||||
<th className="px-3 py-2 font-bold">Giocatore</th>
|
<th className="whitespace-nowrap px-2 py-2 font-bold">Giocatore</th>
|
||||||
{fondamentali.map((f) => (
|
{fondamentali.map((f) => (
|
||||||
<th key={f.id} className="px-2 py-2 text-center font-bold">
|
<th key={f.id} className="w-11 px-0 py-2 text-center font-bold">
|
||||||
{f.label.slice(0, 3)}
|
{f.label.slice(0, 3)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -200,14 +200,14 @@ export function ScoutReport({
|
|||||||
<tbody className="divide-y divide-border">
|
<tbody className="divide-y divide-border">
|
||||||
{righe.map(({ g, conteggi }) => (
|
{righe.map(({ g, conteggi }) => (
|
||||||
<tr key={g.id}>
|
<tr key={g.id}>
|
||||||
<td className="max-w-40 truncate px-3 py-2 font-semibold">
|
<td className="px-2 py-2 font-semibold leading-tight [overflow-wrap:normal]">
|
||||||
<span className="mr-1.5 tabular-nums text-muted-foreground">
|
<span className="mr-1.5 tabular-nums text-muted-foreground">
|
||||||
#{g.numero}
|
#{g.numero}
|
||||||
</span>
|
</span>
|
||||||
{g.nome}
|
{g.nome}
|
||||||
</td>
|
</td>
|
||||||
{fondamentali.map((f) => (
|
{fondamentali.map((f) => (
|
||||||
<td key={f.id} className="px-2 py-2 text-center font-semibold tabular-nums">
|
<td key={f.id} className="px-0 py-2 text-center font-semibold tabular-nums">
|
||||||
<span className="text-success">{conteggi[f.id].piu}</span>
|
<span className="text-success">{conteggi[f.id].piu}</span>
|
||||||
<span className="text-muted-foreground">/</span>
|
<span className="text-muted-foreground">/</span>
|
||||||
<span className="text-destructive">{conteggi[f.id].meno}</span>
|
<span className="text-destructive">{conteggi[f.id].meno}</span>
|
||||||
@@ -238,7 +238,7 @@ export function ScoutReport({
|
|||||||
{ l: "Muri", v: totaliAzioni.muri },
|
{ l: "Muri", v: totaliAzioni.muri },
|
||||||
{ l: "Errori", v: totaliAzioni.errori },
|
{ l: "Errori", v: totaliAzioni.errori },
|
||||||
].map((t) => (
|
].map((t) => (
|
||||||
<div key={t.l} className="rounded-2xl bg-secondary p-2.5">
|
<div key={t.l} className="rounded-2xl bg-secondary px-1 py-2.5">
|
||||||
<p className="font-display text-xl leading-none">{t.v}</p>
|
<p className="font-display text-xl leading-none">{t.v}</p>
|
||||||
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
|
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
|
||||||
{t.l}
|
{t.l}
|
||||||
|
|||||||
@@ -339,9 +339,11 @@ export function SezioneTendina({
|
|||||||
* `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche
|
* `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche
|
||||||
* con `appearance-none`, restano un filo più alti di un input di testo a
|
* con `appearance-none`, restano un filo più alti di un input di testo a
|
||||||
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
|
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
|
||||||
|
* `text-base` (16px) non è estetica: sotto i 16px iOS Safari zooma la pagina al focus e,
|
||||||
|
* dopo il blur, la lascia ingrandita e tagliata in larghezza.
|
||||||
*/
|
*/
|
||||||
export const classiInput =
|
export const classiInput =
|
||||||
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-[15px]";
|
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-base";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
|
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
|
||||||
|
|||||||
@@ -291,6 +291,46 @@ export function totaliSquadra(matches: ScoutMatch[]) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TotaliTecnici = { punti: number; ace: number; attacchi: number; muri: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Somma, su tutte le partite scoutate, delle sole valutazioni **+** (battuta → ace, attacco →
|
||||||
|
* attacchi, muro → muri; punti = ace + attacchi + muri, non la ricezione). Con `giocatoreId`
|
||||||
|
* conta solo quel giocatore, altrimenti tutta la squadra. Una partita senza valutazioni (metodo
|
||||||
|
* precedente) conta con le sue azioni; se ha entrambe valgono solo le valutazioni, per non
|
||||||
|
* contare due volte.
|
||||||
|
*/
|
||||||
|
export function totaliTecnici(matches: ScoutMatch[], giocatoreId?: string): TotaliTecnici {
|
||||||
|
const out = { punti: 0, ace: 0, attacchi: 0, muri: 0 };
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m.valutazioni.length === 0) {
|
||||||
|
for (const [id, t] of totaliPerGiocatore(m.azioni)) {
|
||||||
|
if (giocatoreId !== undefined && id !== giocatoreId) continue;
|
||||||
|
out.ace += t.ace;
|
||||||
|
out.muri += t.muri;
|
||||||
|
out.attacchi += t.punti - t.ace - t.muri;
|
||||||
|
out.punti += t.punti;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const v of m.valutazioni) {
|
||||||
|
if (v.segno !== "+" || v.fondamentale === "ricezione") continue;
|
||||||
|
if (giocatoreId !== undefined && v.giocatoreId !== giocatoreId) continue;
|
||||||
|
if (v.fondamentale === "battuta") out.ace += 1;
|
||||||
|
if (v.fondamentale === "attacco") out.attacchi += 1;
|
||||||
|
if (v.fondamentale === "muro") out.muri += 1;
|
||||||
|
out.punti += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Punti, ace e muri di squadra per la tab Stats di `/squadra` (vedi `totaliTecnici`). */
|
||||||
|
export function totaliSquadraValutazioni(matches: ScoutMatch[]) {
|
||||||
|
const { punti, ace, muri } = totaliTecnici(matches);
|
||||||
|
return { punti, ace, muri };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore).
|
* MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore).
|
||||||
* Le presenze personali restano su `risposte_presenze`, non sullo scout.
|
* Le presenze personali restano su `risposte_presenze`, non sullo scout.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
type GiocatoreSquadra,
|
type GiocatoreSquadra,
|
||||||
} from "@/lib/giocatori-squadra";
|
} from "@/lib/giocatori-squadra";
|
||||||
import { esci } from "@/lib/auth";
|
import { esci } from "@/lib/auth";
|
||||||
|
import { totaliTecnici, useScoutMatches } from "@/lib/scout-store";
|
||||||
import { useIsAdmin } from "@/lib/ruoli";
|
import { useIsAdmin } from "@/lib/ruoli";
|
||||||
import { useEmailNotifiche } from "@/lib/preferenze-utente";
|
import { useEmailNotifiche } from "@/lib/preferenze-utente";
|
||||||
import { Reveal } from "@/components/motion/Reveal";
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
@@ -80,9 +81,12 @@ function ProfiloGiocatore() {
|
|||||||
const votiSocial = useVotiSocial();
|
const votiSocial = useVotiSocial();
|
||||||
const g = useIo();
|
const g = useIo();
|
||||||
const ultimoMese = usePresenzeUltimoMese(g?.id);
|
const ultimoMese = usePresenzeUltimoMese(g?.id);
|
||||||
|
const scoutMatches = useScoutMatches();
|
||||||
|
|
||||||
if (!g) return null;
|
if (!g) return null;
|
||||||
|
|
||||||
|
const tecnici = totaliTecnici(scoutMatches, g.id);
|
||||||
|
|
||||||
const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
|
const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,9 +126,12 @@ function ProfiloGiocatore() {
|
|||||||
}
|
}
|
||||||
label="Presenze di fila"
|
label="Presenze di fila"
|
||||||
/>
|
/>
|
||||||
<StatTile valore={g.mediaVoto || "—"} label="Media voto" />
|
<StatTile valore={tecnici.attacchi} label="Attacchi" />
|
||||||
|
<StatTile valore={tecnici.muri} label="Muri" />
|
||||||
|
<StatTile valore={tecnici.ace} label="Ace" />
|
||||||
|
<StatTile valore={tecnici.punti} label="Punti" hint="totale" />
|
||||||
<StatTile valore={g.mvp} label="MVP" />
|
<StatTile valore={g.mvp} label="MVP" />
|
||||||
<StatTile valore={g.palloni} label="Turni palloni" />
|
<StatTile valore={g.mediaVoto || "—"} label="Media voto" />
|
||||||
</div>
|
</div>
|
||||||
<SerieGriglia g={g} />
|
<SerieGriglia g={g} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ function ScoutBoard({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-3 gap-2 min-[400px]:grid-cols-4">
|
||||||
{convocati.map((g) => {
|
{convocati.map((g) => {
|
||||||
const attivo = selezionato === g.id;
|
const attivo = selezionato === g.id;
|
||||||
const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente);
|
const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente);
|
||||||
@@ -394,12 +394,14 @@ function ScoutBoard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelezionato(attivo ? null : g.id)}
|
onClick={() => setSelezionato(attivo ? null : g.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl p-2 text-center transition-all active:scale-95",
|
"rounded-2xl p-1.5 text-center transition-all active:scale-95",
|
||||||
attivo ? "bg-accent text-accent-foreground shadow-pop" : "bg-card shadow-card",
|
attivo ? "bg-accent text-accent-foreground shadow-pop" : "bg-card shadow-card",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="font-display text-2xl leading-none">{g.numero}</p>
|
<p className="font-display text-2xl leading-none">{g.numero}</p>
|
||||||
<p className="mt-1 text-[13px] font-bold leading-tight">{g.nome}</p>
|
<p className="mt-1 text-[13px] font-bold leading-tight [overflow-wrap:normal]">
|
||||||
|
{g.nome}
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-[13px] tabular-nums",
|
"text-[13px] tabular-nums",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
useGiocatoriSquadra,
|
useGiocatoriSquadra,
|
||||||
} from "@/lib/giocatori-squadra";
|
} from "@/lib/giocatori-squadra";
|
||||||
import { useSonoAllenatore } from "@/lib/user-store";
|
import { useSonoAllenatore } from "@/lib/user-store";
|
||||||
import { totaliSquadra, useScoutMatches } from "@/lib/scout-store";
|
import { totaliSquadraValutazioni, useScoutMatches } from "@/lib/scout-store";
|
||||||
import { useCsi } from "@/lib/csi";
|
import { useCsi } from "@/lib/csi";
|
||||||
import { partiteGiocate } from "@/lib/csi-core";
|
import { partiteGiocate } from "@/lib/csi-core";
|
||||||
import { mediaSquadra, usePagelle } from "@/lib/pagelle";
|
import { mediaSquadra, usePagelle } from "@/lib/pagelle";
|
||||||
@@ -110,7 +110,7 @@ function Squadra() {
|
|||||||
const [criterio, setCriterio] = useState<Criterio>("presenze");
|
const [criterio, setCriterio] = useState<Criterio>("presenze");
|
||||||
const [filtroAperto, setFiltroAperto] = useState(false);
|
const [filtroAperto, setFiltroAperto] = useState(false);
|
||||||
const obiettivi = useObiettivi();
|
const obiettivi = useObiettivi();
|
||||||
const team = totaliSquadra(scoutMatches);
|
const team = totaliSquadraValutazioni(scoutMatches);
|
||||||
const mediaPresenze = rosa.length
|
const mediaPresenze = rosa.length
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
|
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
|
||||||
|
|||||||
@@ -520,11 +520,31 @@
|
|||||||
border-color: var(--color-border);
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mai scroll orizzontale della pagina (iPhone 360-402 px). `clip` e non `hidden`:
|
||||||
|
* non crea un contenitore di scroll, quindi `position: sticky` continua a funzionare.
|
||||||
|
*/
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gli elementi di una griglia hanno `min-width: auto`: una colonna `1fr` non scende sotto
|
||||||
|
* la parola più lunga e sfonda la riga. Con `min-width: 0` la colonna resta nel suo posto
|
||||||
|
* e `overflow-wrap` spezza solo la parola che davvero non ci sta.
|
||||||
|
*/
|
||||||
|
.grid > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
overflow-wrap: break-word;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
/* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */
|
/* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Check dei requisiti di layout per schermi stretti (360 px): `bun test/unit/layout-stretto.test.ts`.
|
||||||
|
*
|
||||||
|
* Non c'è un driver browser (vedi `test/README.md`), quindi si presidiano le due regole che,
|
||||||
|
* se tolte, fanno tornare la pagina tagliata in larghezza su iPhone: i campi a 16 px (sotto,
|
||||||
|
* iOS zooma al focus e non torna indietro) e la rete di sicurezza sull'overflow orizzontale.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { classiInput } from "@/components/crapp/ui-bits";
|
||||||
|
|
||||||
|
assert.match(classiInput, /\btext-base\b/, "i campi devono essere a 16px (zoom automatico iOS)");
|
||||||
|
assert.doesNotMatch(classiInput, /text-\[1[0-5]px\]|text-sm|text-xs/);
|
||||||
|
|
||||||
|
const css = readFileSync(new URL("../../src/styles.css", import.meta.url), "utf8");
|
||||||
|
assert.match(css, /overflow-x:\s*clip/, "html/body devono impedire lo scroll orizzontale");
|
||||||
|
assert.match(
|
||||||
|
css,
|
||||||
|
/\.grid\s*>\s*\*\s*\{\s*min-width:\s*0/,
|
||||||
|
"gli elementi di griglia devono poter restringersi",
|
||||||
|
);
|
||||||
|
|
||||||
|
const scout = readFileSync(
|
||||||
|
new URL("../../src/components/crapp/ScoutReport.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
scout,
|
||||||
|
/min-w-\[\d+rem\]/,
|
||||||
|
"la tabella scout non deve imporre una larghezza minima",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("layout-stretto: ok");
|
||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
giocatoriConScout,
|
giocatoriConScout,
|
||||||
totaliPerGiocatore,
|
totaliPerGiocatore,
|
||||||
totaliSquadra,
|
totaliSquadra,
|
||||||
|
totaliSquadraValutazioni,
|
||||||
|
totaliTecnici,
|
||||||
type Azione,
|
type Azione,
|
||||||
type Valutazione,
|
type Valutazione,
|
||||||
type ScoutMatch,
|
type ScoutMatch,
|
||||||
@@ -67,6 +69,60 @@ assert.deepEqual(totaliSquadra([match("m1", 3, 0, azioni), match("m2", 3, 1, azi
|
|||||||
});
|
});
|
||||||
assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 });
|
assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 });
|
||||||
|
|
||||||
|
// --- totaliSquadraValutazioni: solo i + dello scout, fallback sulle azioni ------
|
||||||
|
const vl = (fondamentale: Valutazione["fondamentale"], segno: "+" | "-", giocatoreId = "g1") =>
|
||||||
|
({
|
||||||
|
id: Math.random().toString(),
|
||||||
|
giocatoreId,
|
||||||
|
fondamentale,
|
||||||
|
segno,
|
||||||
|
set: 1,
|
||||||
|
ts: 0,
|
||||||
|
}) as Valutazione;
|
||||||
|
const conVal = {
|
||||||
|
...match("v1", 3, 0),
|
||||||
|
valutazioni: [
|
||||||
|
vl("battuta", "+"),
|
||||||
|
vl("battuta", "+", "g2"),
|
||||||
|
vl("battuta", "-"),
|
||||||
|
vl("muro", "+"),
|
||||||
|
vl("muro", "-"),
|
||||||
|
vl("attacco", "+"),
|
||||||
|
vl("attacco", "+", "g2"),
|
||||||
|
vl("attacco", "-"),
|
||||||
|
vl("ricezione", "+"),
|
||||||
|
vl("ricezione", "+"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.deepEqual(totaliSquadraValutazioni([conVal]), { punti: 5, ace: 2, muri: 1 });
|
||||||
|
assert.deepEqual(
|
||||||
|
totaliSquadraValutazioni([conVal, conVal]),
|
||||||
|
{ punti: 10, ace: 4, muri: 2 },
|
||||||
|
"somma su più partite",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
totaliSquadraValutazioni([match("a1", 3, 0, azioni)]),
|
||||||
|
{ punti: 4, ace: 1, muri: 1 },
|
||||||
|
"senza valutazioni: contano le azioni del metodo precedente",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
totaliSquadraValutazioni([{ ...conVal, azioni }]),
|
||||||
|
{ punti: 5, ace: 2, muri: 1 },
|
||||||
|
"con entrambe valgono solo le valutazioni",
|
||||||
|
);
|
||||||
|
assert.deepEqual(totaliSquadraValutazioni([]), { punti: 0, ace: 0, muri: 0 });
|
||||||
|
|
||||||
|
// --- totaliTecnici: per giocatore, con attacchi -------------------------------
|
||||||
|
assert.deepEqual(totaliTecnici([conVal], "g1"), { punti: 3, ace: 1, attacchi: 1, muri: 1 });
|
||||||
|
assert.deepEqual(totaliTecnici([conVal], "g2"), { punti: 2, ace: 1, attacchi: 1, muri: 0 });
|
||||||
|
assert.deepEqual(totaliTecnici([conVal], "assente"), { punti: 0, ace: 0, attacchi: 0, muri: 0 });
|
||||||
|
assert.deepEqual(totaliTecnici([conVal, conVal]), { punti: 10, ace: 4, attacchi: 4, muri: 2 });
|
||||||
|
assert.deepEqual(
|
||||||
|
totaliTecnici([match("a1", 3, 0, azioni)], "g1"),
|
||||||
|
{ punti: 3, ace: 1, attacchi: 1, muri: 1 },
|
||||||
|
"metodo precedente: ace, muro e attacco dalle azioni",
|
||||||
|
);
|
||||||
|
|
||||||
// --- giocatoriConScout: solo MVP da scout, presenze separate -----------------
|
// --- giocatoriConScout: solo MVP da scout, presenze separate -----------------
|
||||||
const base = giocatori.find((g) => g.id === "g1")!;
|
const base = giocatori.find((g) => g.id === "g1")!;
|
||||||
const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome });
|
const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome });
|
||||||
|
|||||||
Reference in new issue
Block a user