Riduce il lag su Android: tab memoizzate, vetro/blur/drag disattivati sui device deboli
- squadra.tsx e classifica.tsx: il contenuto di ogni tab di BarraSottosezioni è ora memoizzato con useMemo, cosi' uno stato locale o un refetch in background non ricalcolano piu' anche le sezioni nascoste. - BottomNav, BarraSottosezioni, calendario: useMotoRidotto (RAM bassa o prefers-reduced-motion) disattiva sui device deboli la catena di filtri SVG piu' pesante di .vetro, il backdrop-blur della barra tab e il drag orizzontale; su iOS e device performanti resta tutto invariato. - styles.css: rimossa l'utility .materiale, orfana da quando BottomNav e' passata a .vetro. - ui-bits.tsx: decoding="async" su TeamLogo. - todo.md: traccia le cause individuate e lo stato di applicazione di ciascuna. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { proietta } from "@/lib/molla";
|
import { proietta } from "@/lib/molla";
|
||||||
|
import { useMotoRidotto } from "@/lib/motion";
|
||||||
|
|
||||||
export type VoceSottosezione = {
|
export type VoceSottosezione = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -38,7 +39,11 @@ export function BarraSottosezioni({
|
|||||||
const [attiva, setAttiva] = useState(defaultId ?? voci[0]?.id ?? "");
|
const [attiva, setAttiva] = useState(defaultId ?? voci[0]?.id ?? "");
|
||||||
const direzione = useRef(0);
|
const direzione = useRef(0);
|
||||||
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
||||||
const ridotto = useReducedMotion();
|
// `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo
|
||||||
|
// `prefers-reduced-motion`: qui disattiva sia lo swipe orizzontale sia il
|
||||||
|
// blur della barra tab, i due costi maggiori all'ingresso in una pagina con
|
||||||
|
// sottosezioni (Squadra, Campionato, Profilo).
|
||||||
|
const ridotto = useMotoRidotto();
|
||||||
const indice = Math.max(
|
const indice = Math.max(
|
||||||
0,
|
0,
|
||||||
voci.findIndex((v) => v.id === attiva),
|
voci.findIndex((v) => v.id === attiva),
|
||||||
@@ -69,7 +74,8 @@ export function BarraSottosezioni({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 max-w-full border-b border-border bg-background",
|
"min-w-0 max-w-full border-b border-border bg-background",
|
||||||
!sottolineatura && "bg-background/80 pt-3 backdrop-blur-md",
|
!sottolineatura &&
|
||||||
|
(ridotto ? "bg-background pt-3" : "bg-background/80 pt-3 backdrop-blur-md"),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Solo la barra tab può scrollare in orizzontale: non allarga il layout pagina. */}
|
{/* Solo la barra tab può scrollare in orizzontale: non allarga il layout pagina. */}
|
||||||
@@ -136,7 +142,9 @@ export function BarraSottosezioni({
|
|||||||
key={voce.id}
|
key={voce.id}
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
aria-label={voce.label}
|
aria-label={voce.label}
|
||||||
drag="x"
|
// Sui device deboli lo swipe orizzontale (pointer listener + hit-testing
|
||||||
|
// ad ogni frame) resta disattivato: si cambia tab solo toccando la barra.
|
||||||
|
drag={ridotto ? false : "x"}
|
||||||
dragConstraints={{ left: 0, right: 0 }}
|
dragConstraints={{ left: 0, right: 0 }}
|
||||||
dragElastic={0.12}
|
dragElastic={0.12}
|
||||||
dragMomentum={false}
|
dragMomentum={false}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { CalendarDays, Home, Trophy, Users } from "lucide-react";
|
import { CalendarDays, Home, Trophy, Users } from "lucide-react";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { molla } from "@/lib/molla";
|
import { molla } from "@/lib/molla";
|
||||||
|
import { useMotoRidotto } from "@/lib/motion";
|
||||||
|
|
||||||
// Quattro voci e non cinque: il profilo sta in alto a destra
|
// Quattro voci e non cinque: il profilo sta in alto a destra
|
||||||
// nell'intestazione di ogni pagina, dove lo cerca chi arriva da iOS.
|
// nell'intestazione di ogni pagina, dove lo cerca chi arriva da iOS.
|
||||||
@@ -13,7 +14,10 @@ const items = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function BottomNav() {
|
export function BottomNav() {
|
||||||
const ridotto = useReducedMotion();
|
// `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo
|
||||||
|
// `prefers-reduced-motion`: qui disattiva sia la molla della capsula sia il
|
||||||
|
// vetro sfocato, i due costi maggiori ad ogni cambio di rotta.
|
||||||
|
const ridotto = useMotoRidotto();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
@@ -62,7 +66,13 @@ export function BottomNav() {
|
|||||||
*/}
|
*/}
|
||||||
{/* `vetro` porta con sé le proprie ombre, quindi niente `shadow-chrome`:
|
{/* `vetro` porta con sé le proprie ombre, quindi niente `shadow-chrome`:
|
||||||
sarebbero due `box-shadow` sullo stesso elemento e una vincerebbe. */}
|
sarebbero due `box-shadow` sullo stesso elemento e una vincerebbe. */}
|
||||||
<div className="vetro pointer-events-auto mx-auto grid h-[var(--altezza-nav)] max-w-md grid-cols-4 rounded-full border border-white/40 p-1.5">
|
<div
|
||||||
|
className="vetro pointer-events-auto mx-auto grid h-[var(--altezza-nav)] max-w-md grid-cols-4 rounded-full border border-white/40 p-1.5"
|
||||||
|
// Su device deboli il backdrop-filter con feTurbulence/feDisplacementMap
|
||||||
|
// (solo Blink, cioè Android) e la molla della capsula sotto sono i due
|
||||||
|
// costi maggiori ad ogni cambio di rotta: `data-leggero` li disattiva.
|
||||||
|
{...(ridotto ? { "data-leggero": "" } : {})}
|
||||||
|
>
|
||||||
{items.map(({ to, label, icon: Icon }) => (
|
{items.map(({ to, label, icon: Icon }) => (
|
||||||
<Link
|
<Link
|
||||||
key={to}
|
key={to}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function TeamLogo({
|
|||||||
alt="CRAP Volley"
|
alt="CRAP Volley"
|
||||||
width={192}
|
width={192}
|
||||||
height={192}
|
height={192}
|
||||||
|
decoding="async"
|
||||||
className={cn("shrink-0 rounded-2xl object-cover shadow-pop", className)}
|
className={cn("shrink-0 rounded-2xl object-cover shadow-pop", className)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { CalendarPlus, ChevronLeft, ChevronRight, X } from "lucide-react";
|
import { CalendarPlus, ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { molla, proietta } from "@/lib/molla";
|
import { molla, proietta } from "@/lib/molla";
|
||||||
|
import { useMotoRidotto } from "@/lib/motion";
|
||||||
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
|
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
|
||||||
import { Card, 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 { compleanniEventi, useEventi, type Evento } from "@/lib/eventi";
|
||||||
@@ -115,7 +116,9 @@ function Calendario() {
|
|||||||
const admin = useIsAdmin();
|
const admin = useIsAdmin();
|
||||||
const { eventi } = useEventi();
|
const { eventi } = useEventi();
|
||||||
const rosa = useRosa();
|
const rosa = useRosa();
|
||||||
const ridotto = useReducedMotion();
|
// `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo
|
||||||
|
// `prefers-reduced-motion`: disattiva anche lo swipe orizzontale tra mesi.
|
||||||
|
const ridotto = useMotoRidotto();
|
||||||
const { anno, mese, direzione, precedente, successivo } = useMeseNav();
|
const { anno, mese, direzione, precedente, successivo } = useMeseNav();
|
||||||
const { giorni, offsetLunedi } = giorniDelMese(anno, mese);
|
const { giorni, offsetLunedi } = giorniDelMese(anno, mese);
|
||||||
const mesePrefix = `${anno}-${pad2(mese + 1)}`;
|
const mesePrefix = `${anno}-${pad2(mese + 1)}`;
|
||||||
@@ -197,7 +200,7 @@ function Calendario() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={mesePrefix}
|
key={mesePrefix}
|
||||||
custom={direzione}
|
custom={direzione}
|
||||||
drag="x"
|
drag={ridotto ? false : "x"}
|
||||||
dragConstraints={{ left: 0, right: 0 }}
|
dragConstraints={{ left: 0, right: 0 }}
|
||||||
dragElastic={0.18}
|
dragElastic={0.18}
|
||||||
dragMomentum={false}
|
dragMomentum={false}
|
||||||
|
|||||||
+38
-31
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { ChevronRight, RefreshCw } from "lucide-react";
|
import { ChevronRight, RefreshCw } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -48,17 +49,20 @@ function Classifica() {
|
|||||||
const scoutMatches = useScoutMatches();
|
const scoutMatches = useScoutMatches();
|
||||||
const { data: csi } = useCsi();
|
const { data: csi } = useCsi();
|
||||||
const votiMvp = useVotiMvp();
|
const votiMvp = useVotiMvp();
|
||||||
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
|
|
||||||
const { eventi } = useEventi();
|
const { eventi } = useEventi();
|
||||||
|
|
||||||
const classifica = csi?.classifica ?? [];
|
const classifica = useMemo(() => csi?.classifica ?? [], [csi]);
|
||||||
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
|
||||||
const eventoIdPerData = new Map(
|
const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]);
|
||||||
eventi.filter((e) => e.tipo === "partita").map((e) => [e.data, e.id]),
|
const eventoIdPerData = useMemo(
|
||||||
|
() => new Map(eventi.filter((e) => e.tipo === "partita").map((e) => [e.data, e.id])),
|
||||||
|
[eventi],
|
||||||
);
|
);
|
||||||
// I voti MVP sono legati all'evento CrAPP, non al referto CSI né allo scout.
|
// I voti MVP sono legati all'evento CrAPP, non al referto CSI né allo scout.
|
||||||
|
const tuttiMatch = useMemo(() => {
|
||||||
const mvpPerData = (data: string) => mvpPerMatch[eventoIdPerData.get(data) ?? ""] ?? "";
|
const mvpPerData = (data: string) => mvpPerMatch[eventoIdPerData.get(data) ?? ""] ?? "";
|
||||||
const tuttiMatch = csiGiocate.length
|
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||||
|
return csiGiocate.length
|
||||||
? csiGiocate.map((p) => {
|
? csiGiocate.map((p) => {
|
||||||
const m = matchDaPartitaCsi(p);
|
const m = matchDaPartitaCsi(p);
|
||||||
return { ...m, mvp: mvpPerData(m.data), scout: false };
|
return { ...m, mvp: mvpPerData(m.data), scout: false };
|
||||||
@@ -74,23 +78,12 @@ function Classifica() {
|
|||||||
mvp: mvpPerData(m.data),
|
mvp: mvpPerData(m.data),
|
||||||
scout: true,
|
scout: true,
|
||||||
}));
|
}));
|
||||||
|
}, [csi, scoutMatches, mvpPerMatch, eventoIdPerData]);
|
||||||
|
|
||||||
return (
|
// Ogni tab è memoizzata: un refetch in background di uno solo dei dati (CSI,
|
||||||
<>
|
// scout, eventi, voti MVP) non deve ricostruire il JSX dell'altra tab.
|
||||||
<PageHeader
|
const contenutoClassifica = useMemo(
|
||||||
titolo="Campionato"
|
() => (
|
||||||
sottotitolo={csi ? `${csi.girone} · CSI Bologna` : "CSI Bologna"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BarraSottosezioni
|
|
||||||
defaultId={tab ?? "classifica"}
|
|
||||||
variante="sottolineatura"
|
|
||||||
riempiLarghezza
|
|
||||||
voci={[
|
|
||||||
{
|
|
||||||
id: "classifica",
|
|
||||||
label: "Classifica",
|
|
||||||
contenuto: (
|
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 flex items-center gap-2 rounded-2xl bg-secondary px-3 py-2 text-xs text-muted-foreground">
|
<div className="mb-3 flex items-center gap-2 rounded-2xl bg-secondary px-3 py-2 text-xs text-muted-foreground">
|
||||||
<RefreshCw className="h-3.5 w-3.5 text-accent" />
|
<RefreshCw className="h-3.5 w-3.5 text-accent" />
|
||||||
@@ -127,9 +120,7 @@ function Classifica() {
|
|||||||
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>
|
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>
|
||||||
{r.squadra}
|
{r.squadra}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-center text-xs text-muted-foreground">
|
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
|
||||||
{r.giocate}
|
|
||||||
</span>
|
|
||||||
<span className="text-center text-xs tabular-nums text-muted-foreground">
|
<span className="text-center text-xs tabular-nums text-muted-foreground">
|
||||||
{r.setFatti}:{r.setSubiti}
|
{r.setFatti}:{r.setSubiti}
|
||||||
</span>
|
</span>
|
||||||
@@ -141,11 +132,11 @@ function Classifica() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
[csi, classifica],
|
||||||
{
|
);
|
||||||
id: "storico",
|
|
||||||
label: "Storico partite",
|
const contenutoStorico = useMemo(
|
||||||
contenuto:
|
() =>
|
||||||
tuttiMatch.length === 0 ? (
|
tuttiMatch.length === 0 ? (
|
||||||
<p className="rounded-3xl bg-card p-4 text-center text-xs text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-center text-xs text-muted-foreground shadow-card">
|
||||||
Nessuna partita disponibile.
|
Nessuna partita disponibile.
|
||||||
@@ -216,7 +207,23 @@ function Classifica() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
[tuttiMatch, eventoIdPerData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
titolo="Campionato"
|
||||||
|
sottotitolo={csi ? `${csi.girone} · CSI Bologna` : "CSI Bologna"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BarraSottosezioni
|
||||||
|
defaultId={tab ?? "classifica"}
|
||||||
|
variante="sottolineatura"
|
||||||
|
riempiLarghezza
|
||||||
|
voci={[
|
||||||
|
{ id: "classifica", label: "Classifica", contenuto: contenutoClassifica },
|
||||||
|
{ id: "storico", label: "Storico partite", contenuto: contenutoStorico },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+61
-88
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Cake, ChevronDown, Crown, Trophy } from "lucide-react";
|
import { Cake, ChevronDown, Crown, Trophy } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -103,25 +103,15 @@ function Squadra() {
|
|||||||
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
// Ogni tab è memoizzata: aprire/chiudere una card giocatore (stato `aperto`)
|
||||||
<>
|
// non deve ricalcolare badge, classifica e obiettivi delle altre sezioni,
|
||||||
<PageHeader titolo="Squadra" sottotitolo={`${rosa.length} giocatori · Stagione 2026/27`} />
|
// che restano fuori schermo finché l'utente non cambia tab.
|
||||||
|
const contenutoRosa = useMemo(
|
||||||
<BarraSottosezioni
|
() => (
|
||||||
defaultId="rosa"
|
|
||||||
variante="sottolineatura"
|
|
||||||
voci={[
|
|
||||||
{
|
|
||||||
id: "rosa",
|
|
||||||
label: "Rosa",
|
|
||||||
contenuto: (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{rosa.map((g) => {
|
{rosa.map((g) => {
|
||||||
const stati = badgeGiocatore(g);
|
const stati = badgeGiocatore(g);
|
||||||
const sbloccati = [
|
const sbloccati = [...stati.filter((b) => b.grado !== null), ...badgeSegretiSbloccati(g)];
|
||||||
...stati.filter((b) => b.grado !== null),
|
|
||||||
...badgeSegretiSbloccati(g),
|
|
||||||
];
|
|
||||||
const isOpen = aperto === g.id;
|
const isOpen = aperto === g.id;
|
||||||
return (
|
return (
|
||||||
<article key={g.id} className="overflow-hidden rounded-3xl bg-card shadow-card">
|
<article key={g.id} className="overflow-hidden rounded-3xl bg-card shadow-card">
|
||||||
@@ -137,9 +127,7 @@ function Squadra() {
|
|||||||
className="h-11 w-11 text-lg"
|
className="h-11 w-11 text-lg"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-bold leading-tight">
|
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
|
||||||
{g.nome}
|
|
||||||
</span>
|
|
||||||
<span className="mt-1 flex items-center gap-2">
|
<span className="mt-1 flex items-center gap-2">
|
||||||
<RuoloBadge ruolo={g.ruolo} />
|
<RuoloBadge ruolo={g.ruolo} />
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
@@ -152,10 +140,7 @@ function Squadra() {
|
|||||||
{sbloccati.map((b) => {
|
{sbloccati.map((b) => {
|
||||||
const Icon = b.def.icon;
|
const Icon = b.def.icon;
|
||||||
return (
|
return (
|
||||||
<span
|
<span key={b.def.id} title={`${b.def.nome} — ${gradoMeta[b.grado!].label}`}>
|
||||||
key={b.def.id}
|
|
||||||
title={`${b.def.nome} — ${gradoMeta[b.grado!].label}`}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
className={cn("h-3.5 w-3.5", gradoMeta[b.grado!].text)}
|
className={cn("h-3.5 w-3.5", gradoMeta[b.grado!].text)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -204,8 +189,7 @@ function Squadra() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-4 text-xs 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}/
|
Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali}
|
||||||
{collezioneBadge(g).totali}
|
|
||||||
</p>
|
</p>
|
||||||
{sbloccati.length === 0 ? (
|
{sbloccati.length === 0 ? (
|
||||||
<p className="mt-2 rounded-2xl bg-secondary/50 p-3 text-xs text-muted-foreground">
|
<p className="mt-2 rounded-2xl bg-secondary/50 p-3 text-xs text-muted-foreground">
|
||||||
@@ -218,25 +202,16 @@ function Squadra() {
|
|||||||
const meta = gradoMeta[b.grado!];
|
const meta = gradoMeta[b.grado!];
|
||||||
return (
|
return (
|
||||||
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
||||||
<div
|
<div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}>
|
||||||
className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}
|
|
||||||
>
|
|
||||||
<Icon className={cn("h-4 w-4", meta.text)} />
|
<Icon className={cn("h-4 w-4", meta.text)} />
|
||||||
<p className="mt-1 text-xs font-bold leading-tight">
|
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
|
||||||
{b.def.nome}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{b.valore} {b.def.unita}
|
{b.valore} {b.def.unita}
|
||||||
{b.prossimaSoglia
|
{b.prossimaSoglia
|
||||||
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p className={cn("mt-1.5 text-xs font-bold uppercase", meta.text)}>
|
||||||
className={cn(
|
|
||||||
"mt-1.5 text-xs font-bold uppercase",
|
|
||||||
meta.text,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,11 +227,11 @@ function Squadra() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
[rosa, aperto, mese],
|
||||||
{
|
);
|
||||||
id: "stats",
|
|
||||||
label: "Statistiche",
|
const contenutoStats = useMemo(
|
||||||
contenuto: (
|
() => (
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<StatTile valore={`${mediaPresenze}%`} label="Media presenze" />
|
<StatTile valore={`${mediaPresenze}%`} label="Media presenze" />
|
||||||
<StatTile valore={matchGiocati} label="Match giocati" />
|
<StatTile valore={matchGiocati} label="Match giocati" />
|
||||||
@@ -266,12 +241,11 @@ function Squadra() {
|
|||||||
<StatTile valore={team.muri} label="Muri squadra" />
|
<StatTile valore={team.muri} label="Muri squadra" />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
[mediaPresenze, matchGiocati, pagelle, team],
|
||||||
{
|
);
|
||||||
id: "classifica-giocatori",
|
|
||||||
label: "Classifica",
|
const contenutoClassifica = useMemo(
|
||||||
aTuttoLarghezza: true,
|
() => (
|
||||||
contenuto: (
|
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -324,9 +298,7 @@ function Squadra() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold">
|
<p className="truncate text-sm font-bold">
|
||||||
{g.nome}
|
{g.nome}
|
||||||
{i === 0 ? (
|
{i === 0 ? <Crown className="ml-1 inline h-3.5 w-3.5 text-warning" /> : null}
|
||||||
<Crown className="ml-1 inline h-3.5 w-3.5 text-warning" />
|
|
||||||
) : null}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
|
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
|
||||||
@@ -347,11 +319,11 @@ function Squadra() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
[filtroAperto, criterio, ordinati, max],
|
||||||
{
|
);
|
||||||
id: "obiettivi",
|
|
||||||
label: "Obiettivi",
|
const contenutoObiettivi = useMemo(
|
||||||
contenuto: (
|
() => (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
|
<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 className="flex items-end justify-between gap-3">
|
||||||
@@ -367,10 +339,7 @@ function Squadra() {
|
|||||||
{completati}/{obiettivi.length} completati
|
{completati}/{obiettivi.length} completati
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Barra
|
<Barra percentuale={mediaObiettivi} trackClassName="mt-3 bg-primary-foreground/15" />
|
||||||
percentuale={mediaObiettivi}
|
|
||||||
trackClassName="mt-3 bg-primary-foreground/15"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{obiettivi.map((o, i) => {
|
{obiettivi.map((o, i) => {
|
||||||
@@ -382,11 +351,7 @@ function Squadra() {
|
|||||||
indice={i}
|
indice={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
"premi rounded-3xl bg-card p-4 shadow-card ring-1",
|
"premi rounded-3xl bg-card p-4 shadow-card ring-1",
|
||||||
fatto
|
fatto ? "ring-success/40" : pct >= 90 ? "ring-accent/40" : "ring-transparent",
|
||||||
? "ring-success/40"
|
|
||||||
: pct >= 90
|
|
||||||
? "ring-accent/40"
|
|
||||||
: "ring-transparent",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
@@ -411,9 +376,7 @@ function Squadra() {
|
|||||||
{o.valore}/{o.target} {o.unita} · {pct}%
|
{o.valore}/{o.target} {o.unita} · {pct}%
|
||||||
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
|
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-accent">
|
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p>
|
||||||
{microcopyObiettivo(o)}
|
|
||||||
</p>
|
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
|
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
@@ -421,11 +384,11 @@ function Squadra() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
[obiettivi, mediaObiettivi, completati],
|
||||||
{
|
);
|
||||||
id: "badge",
|
|
||||||
label: "Badge",
|
const contenutoBadge = useMemo(
|
||||||
contenuto: (
|
() => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{badgeDefs.map((b) => {
|
{badgeDefs.map((b) => {
|
||||||
const Icon = b.icon;
|
const Icon = b.icon;
|
||||||
@@ -435,9 +398,7 @@ function Squadra() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon className="h-5 w-5 text-accent" />
|
<Icon className="h-5 w-5 text-accent" />
|
||||||
<p className="text-sm font-bold leading-tight">{b.nome}</p>
|
<p className="text-sm font-bold leading-tight">{b.nome}</p>
|
||||||
<p className="ml-auto text-xs text-muted-foreground">
|
<p className="ml-auto text-xs text-muted-foreground">{descrizioneSoglie(b)}</p>
|
||||||
{descrizioneSoglie(b)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
||||||
{gradiOrdine.map((grado) => {
|
{gradiOrdine.map((grado) => {
|
||||||
@@ -451,18 +412,10 @@ function Squadra() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={grado}
|
key={grado}
|
||||||
className={cn(
|
className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)}
|
||||||
"rounded-2xl p-2 text-center ring-1",
|
|
||||||
meta.bg,
|
|
||||||
meta.ring,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<p className={cn("text-xs font-bold uppercase", meta.text)}>
|
<p className={cn("text-xs font-bold uppercase", meta.text)}>{meta.label}</p>
|
||||||
{meta.label}
|
<p className="font-display text-lg leading-none">{b.soglie[grado]}</p>
|
||||||
</p>
|
|
||||||
<p className="font-display text-lg leading-none">
|
|
||||||
{b.soglie[grado]}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{quanti}/{rosa.length}
|
{quanti}/{rosa.length}
|
||||||
</p>
|
</p>
|
||||||
@@ -476,7 +429,27 @@ function Squadra() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
[rosa],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader titolo="Squadra" sottotitolo={`${rosa.length} giocatori · Stagione 2026/27`} />
|
||||||
|
|
||||||
|
<BarraSottosezioni
|
||||||
|
defaultId="rosa"
|
||||||
|
variante="sottolineatura"
|
||||||
|
voci={[
|
||||||
|
{ id: "rosa", label: "Rosa", contenuto: contenutoRosa },
|
||||||
|
{ id: "stats", label: "Statistiche", contenuto: contenutoStats },
|
||||||
|
{
|
||||||
|
id: "classifica-giocatori",
|
||||||
|
label: "Classifica",
|
||||||
|
aTuttoLarghezza: true,
|
||||||
|
contenuto: contenutoClassifica,
|
||||||
},
|
},
|
||||||
|
{ id: "obiettivi", label: "Obiettivi", contenuto: contenutoObiettivi },
|
||||||
|
{ id: "badge", label: "Badge", contenuto: contenutoBadge },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+20
-58
@@ -392,55 +392,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Superfici traslucide: se l'utente chiede meno trasparenza diventano piene. */
|
|
||||||
@media (prefers-reduced-transparency: reduce) {
|
|
||||||
.materiale {
|
|
||||||
background-color: var(--color-card);
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
.materiale {
|
|
||||||
background-color: var(--color-card);
|
|
||||||
backdrop-filter: none;
|
|
||||||
border-color: var(--color-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Chrome flottante in stile Apple: la barra è un materiale traslucido sotto
|
* Vetro della barra flottante.
|
||||||
* cui il contenuto scorre, con una sfumatura al bordo al posto della linea
|
|
||||||
* netta (scroll edge effect).
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* Il materiale deve reggersi sul colore, non sul blur.
|
|
||||||
*
|
*
|
||||||
* Con un fondo poco denso (era 72%) l'aspetto dipendeva quasi tutto da
|
* Su una superficie larga quanto lo schermo un fondo all'85% di card serve a
|
||||||
* `backdrop-filter`, ed è lì che i due motori divergono: WebKit sfoca in modo
|
* tenere allineati WebKit e Blink, che sfocano in modo diverso (608f2ed); su
|
||||||
* molto più denso e "latteo" di Blink, e `saturate` amplifica lo scarto. Il
|
* una pillola piccola come questa lo scarto non si legge, quindi si scende al
|
||||||
* risultato era una barra che su Safari sembrava piena e su Chrome Android
|
* 62% e si lascia che sia il blur a fare il lavoro, condizione perché si
|
||||||
* trasparente, a parità di CSS. A 85% il colore fa il lavoro e il blur è la
|
* legga come vetro invece che come plastica bianca.
|
||||||
* rifinitura: resta la profondità, sparisce quasi tutta la differenza.
|
|
||||||
*
|
|
||||||
* `in srgb` e non `in oklab`: mescolare con `transparent` — che è nero con
|
|
||||||
* alpha 0 — dipende da come il motore premoltiplica, e in srgb è implementato
|
|
||||||
* in modo molto più uniforme.
|
|
||||||
*/
|
|
||||||
@utility materiale {
|
|
||||||
background-color: color-mix(in srgb, var(--color-card) 85%, transparent);
|
|
||||||
backdrop-filter: blur(20px) saturate(150%);
|
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Vetro della barra flottante. È `materiale` spinto: si vede attraverso.
|
|
||||||
*
|
|
||||||
* `materiale` sta all'85% di card perché è una superficie larga quanto lo
|
|
||||||
* schermo, dove il divario di blur fra WebKit e Blink si nota (608f2ed). Qui
|
|
||||||
* la superficie è una pillola piccola: si può scendere al 62% e lasciare che
|
|
||||||
* sia il blur a fare il lavoro, che è la condizione perché si legga come vetro
|
|
||||||
* invece che come plastica bianca.
|
|
||||||
*
|
*
|
||||||
* Le tre ombre interne sono la luce sul bordo, non decorazione: filo chiaro in
|
* Le tre ombre interne sono la luce sul bordo, non decorazione: filo chiaro in
|
||||||
* alto (la luce che entra), filo scuro in basso (lo spessore del vetro), alone
|
* alto (la luce che entra), filo scuro in basso (lo spessore del vetro), alone
|
||||||
@@ -475,6 +434,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `data-leggero` lo mette `BottomNav` quando `useMotoRidotto` rileva un device
|
||||||
|
* debole (RAM bassa) o `prefers-reduced-motion`: la catena di filtri SVG sopra
|
||||||
|
* (feTurbulence + feDisplacementMap, solo Blink/Android) è il costo maggiore
|
||||||
|
* della barra fissa ad ogni cambio di rotta, quindi qui si torna al semplice
|
||||||
|
* sfondo pieno. Specificità più alta di `.vetro` da sola: vince a prescindere
|
||||||
|
* dall'ordine nel file.
|
||||||
|
*/
|
||||||
|
.vetro[data-leggero] {
|
||||||
|
background-color: var(--color-card);
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Il vetro traslucido è un vezzo: se l'utente chiede meno trasparenza sparisce. */
|
/* Il vetro traslucido è un vezzo: se l'utente chiede meno trasparenza sparisce. */
|
||||||
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
||||||
.vetro {
|
.vetro {
|
||||||
@@ -490,17 +463,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Chrome su Android disattiva `backdrop-filter` quando l'accelerazione
|
|
||||||
* hardware non è disponibile: senza questo, su quei device la barra resta
|
|
||||||
* solo semitrasparente e il contenuto si legge attraverso.
|
|
||||||
*/
|
|
||||||
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
||||||
.materiale {
|
|
||||||
background-color: var(--color-card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Padding inferiore della chrome flottante. */
|
/** Padding inferiore della chrome flottante. */
|
||||||
@utility pad-sicura-fondo {
|
@utility pad-sicura-fondo {
|
||||||
padding-bottom: var(--pad-sicura-fondo);
|
padding-bottom: var(--pad-sicura-fondo);
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# TODO — Lag su Android (analisi Squadra + BarraSottosezioni)
|
||||||
|
|
||||||
|
Contesto: lag segnalato su Android nella pagina Squadra (`src/routes/squadra.tsx`) e nel
|
||||||
|
componente `BarraSottosezioni` (`src/components/crapp/BarraSottosezioni.tsx`). Elenco delle
|
||||||
|
cause individuate, in ordine di probabile impatto, con lo stato di applicazione.
|
||||||
|
|
||||||
|
## 1. [x] Ricalcolo di tutte le tab ad ogni render — APPLICATA
|
||||||
|
|
||||||
|
`voci` in `Squadra()` costruiva il JSX di tutte e 5 le sezioni (Rosa, Statistiche, Classifica,
|
||||||
|
Obiettivi, Badge) a ogni render, incluse quelle non visibili — inclusi i calcoli per giocatore
|
||||||
|
(`badgeGiocatore`, `badgeSegretiSbloccati`, `collezioneBadge`) e l'ordinamento della classifica.
|
||||||
|
Un semplice `setAperto` (aprire una card giocatore) rifaceva tutto questo lavoro per niente.
|
||||||
|
|
||||||
|
Fix: memoizzato il contenuto di ogni tab con `useMemo`, così il lavoro di ciascuna sezione si
|
||||||
|
rifà solo quando cambiano i suoi dati effettivi, non ad ogni render del componente.
|
||||||
|
|
||||||
|
Nessun trade-off: puro guadagno di performance, identico su Android e iOS.
|
||||||
|
|
||||||
|
## 2. [x] `backdrop-blur-md` sulla barra tab sticky — APPLICATA
|
||||||
|
|
||||||
|
`BarraSottosezioni.tsx` (variante "pillole", solo Profilo — Squadra/Campionato usano
|
||||||
|
"sottolineatura" e non avevano blur qui). Costoso da compositare su Android Chrome/WebView,
|
||||||
|
specialmente con la barra visibile durante lo scroll del pannello sottostante. Su iOS Safari il
|
||||||
|
costo è molto più basso (accelerazione via Core Animation).
|
||||||
|
|
||||||
|
Fix: `BarraSottosezioni` ora usa `useMotoRidotto` (stesso heuristic di RAM bassa /
|
||||||
|
`prefers-reduced-motion` già introdotto per `BottomNav`, punto 6) al posto del solo
|
||||||
|
`useReducedMotion`. Sui device deboli il blur sparisce (sfondo pieno `bg-background`); su iOS e
|
||||||
|
device performanti resta identico a prima.
|
||||||
|
|
||||||
|
Presente anche in `CelebrazioneBadge.tsx:33` (`backdrop-blur-sm`), non toccato: non è sticky e
|
||||||
|
compare solo alla celebrazione, impatto marginale.
|
||||||
|
|
||||||
|
## 3. [x] Gesture `drag="x"` di Framer Motion sul pannello swipeabile — APPLICATA
|
||||||
|
|
||||||
|
`BarraSottosezioni.tsx` e stesso pattern in `calendario.tsx` (swipe tra mesi). Ogni `pointermove`
|
||||||
|
passa dal thread JS invece di essere gestito nativamente; su Android più pesante da instradare,
|
||||||
|
specialmente con `touch-pan-y` + `drag="x"` insieme su contenuto lungo che scrolla in verticale.
|
||||||
|
|
||||||
|
Fix: `drag` è ora `ridotto ? false : "x"` in entrambi i file, stesso `useMotoRidotto` del punto
|
||||||
|
2. Sui device deboli lo swipe orizzontale (cambio tab / cambio mese) si disattiva e resta solo
|
||||||
|
il tocco diretto sulla barra/frecce; su iOS e device performanti nulla cambia.
|
||||||
|
|
||||||
|
## 4. [ ] Coriandoli via `canvas-confetti` non sempre filtrati sui device deboli
|
||||||
|
|
||||||
|
`lib/motion.ts:14-17,29-40`. Già protetti da `prefers-reduced-motion` e da
|
||||||
|
`deviceMemory < 2GB`, ma molti Android di fascia media dichiarano 3-4GB pur avendo GPU deboli:
|
||||||
|
il check attuale non li intercetta. Impatto marginale (animazione una tantum), ma coerente con
|
||||||
|
i punti 2 e 3 se si introduce un heuristic più ampio (es. anche `hardwareConcurrency`).
|
||||||
|
|
||||||
|
## 5. [ ] Nessuna virtualizzazione sulle liste lunghe (rosa, obiettivi, badge)
|
||||||
|
|
||||||
|
Non specifico di Android, ma ogni card ha due box-shadow sovrapposte (`shadow-card`, vedi
|
||||||
|
`styles.css:160-171`) e gli `obiettivi` montano un `IntersectionObserver` per elemento
|
||||||
|
(`Reveal`). Con liste lunghe il costo si somma di più su CPU/GPU Android debole.
|
||||||
|
|
||||||
|
Da valutare solo se la rosa/obiettivi crescono molto: oggi la complessità aggiunta
|
||||||
|
(virtualizzazione) non sembra giustificata dal beneficio.
|
||||||
|
|
||||||
|
## 6. [x] Lag nel cambio pagina (Home ↔ Calendario ↔ Squadra ↔ Campionato) — APPLICATA
|
||||||
|
|
||||||
|
`BottomNav` resta montata su ogni rotta ([__root.tsx:204-210](src/routes/__root.tsx#L204-L210))
|
||||||
|
e concentrava due costi proprio nel momento del cambio pagina:
|
||||||
|
|
||||||
|
- `layoutId="capsula-nav"` di Framer Motion ([BottomNav.tsx](src/components/crapp/BottomNav.tsx))
|
||||||
|
anima la pillola attiva con una misurazione di layout sincrona (FLIP) ad ogni navigazione.
|
||||||
|
- Il "vetro" della barra usa, solo su Blink/Android (`@supports (backdrop-filter: url(...))`),
|
||||||
|
una catena di filtri SVG `feTurbulence` + `feGaussianBlur` + `feDisplacementMap` molto più
|
||||||
|
pesante del semplice `blur()` che riceve iOS Safari — e ricalcola ad ogni cambio di contenuto
|
||||||
|
sotto la barra fissa.
|
||||||
|
|
||||||
|
Fix: `BottomNav` ora usa `useMotoRidotto` (già in `lib/motion.ts`, copre sia
|
||||||
|
`prefers-reduced-motion` sia RAM bassa) al posto del solo `useReducedMotion` di Framer Motion.
|
||||||
|
Quando rileva un device debole, aggiunge `data-leggero` al contenitore `.vetro`; una nuova
|
||||||
|
regola in `styles.css` disattiva backdrop-filter su quell'attributo (fallback a sfondo pieno,
|
||||||
|
stesso trattamento già riservato a `prefers-reduced-transparency`). La molla della capsula era
|
||||||
|
già condizionata alla stessa variabile `ridotto`, quindi ora si disattiva anche lei sui device
|
||||||
|
deboli, non solo con `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
Nessun impatto su iOS/device performanti: lì `ridotto` resta `false` e l'effetto vetro + la
|
||||||
|
capsula animata restano identici a prima.
|
||||||
|
|
||||||
|
## 7. [x] `classifica.tsx` (Campionato) ricalcolava entrambe le tab ad ogni render — APPLICATA
|
||||||
|
|
||||||
|
Stesso pattern di `squadra.tsx` (punto 1): `BarraSottosezioni` con le tab "Classifica" e
|
||||||
|
"Storico partite" costruiva il JSX di entrambe ad ogni render, incluso il merge tra partite CSI
|
||||||
|
e scout e il lookup MVP per data. Con `useCsi`/`useEventi`/`useVotiMvp` basati su TanStack Query,
|
||||||
|
un refetch in background di uno solo di questi dati rifaceva comunque il lavoro di tutt'e due
|
||||||
|
le tab. Meno grave del caso Squadra (qui non c'è uno stato locale tipo `aperto` che rirenderizza
|
||||||
|
il componente ad ogni tap), ma stesso spreco ad ogni mount/refetch della pagina.
|
||||||
|
|
||||||
|
Fix: stesso trattamento — `classifica`, `tuttiMatch`, `eventoIdPerData` e `mvpPerMatch` sono ora
|
||||||
|
memoizzati con `useMemo`, e il contenuto di ciascuna tab è a sua volta memoizzato sulle relative
|
||||||
|
dipendenze.
|
||||||
|
|
||||||
|
`index.tsx` (Home) non ha lo stesso problema: non usa `BarraSottosezioni`/tab multiple, quindi
|
||||||
|
non c'è contenuto "nascosto" che viene ricalcolato inutilmente — non modificata.
|
||||||
|
|
||||||
|
## 8. [x] Rifiniture minori — APPLICATA
|
||||||
|
|
||||||
|
- `TeamLogo` ([ui-bits.tsx](src/components/crapp/ui-bits.tsx)) non aveva `decoding="async"`
|
||||||
|
(a differenza di `Avatar`): aggiunto, guadagno marginale ma gratuito.
|
||||||
|
- `.materiale` in `styles.css` (backdrop-filter 20px + 3 fallback di accessibilità) non era più
|
||||||
|
referenziata da nessun componente — sostituita da `.vetro` sul `BottomNav` in un commit
|
||||||
|
precedente (c089d9e) e da allora orfana. Rimossa insieme ai suoi fallback; il commento che la
|
||||||
|
citava come confronto per `.vetro` è stato aggiornato.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- `useMotoRidotto` (`lib/motion.ts`) è ora l'heuristic condiviso di "device debole" usato in
|
||||||
|
`BottomNav.tsx`, `BarraSottosezioni.tsx` e `calendario.tsx` (punti 2, 3, 6). Se si riprende il
|
||||||
|
punto 4 (coriandoli), conviene usare lo stesso hook invece di un check separato.
|
||||||
|
- Applicati: 1, 2, 3, 6, 7. Restano da discutere/prioritizzare: 4 (coriandoli su device medi),
|
||||||
|
5 (virtualizzazione liste lunghe).
|
||||||
|
- Se il lag persistesse ancora dopo questi fix, il prossimo passo è profilare un device Android
|
||||||
|
reale (Chrome DevTools remoto o `chrome://inspect`) invece di continuare a ipotizzare: a
|
||||||
|
questo punto le cause "ovvie" lette dal codice sono coperte, e senza un trace reale si rischia
|
||||||
|
di ottimizzare a caso.
|
||||||
Reference in New Issue
Block a user