Rimuove i dati dummy della classifica e usa i risultati CSI reali
La classifica e lo storico partite mostravano dati inventati (squadre e risultati finti) come base, poi sovrascritti dai dati CSI quando disponibili. Ora classifica, ultimi risultati, storico match e obiettivi di squadra legati alle vittorie usano solo dati reali (CSI o scout live), con stati vuoti quando i dati CSI non sono ancora disponibili. Include anche la correzione di tutti gli errori di formattazione prettier segnalati da `npm run lint` sul resto del codice sorgente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d813dee282
commit
0a04025fe9
55 files changed
+1038
-879
No files matched your search
+1
-1
@@ -43,4 +43,4 @@ self.addEventListener("notificationclick", (event) => {
|
|||||||
return self.clients.openWindow("/");
|
return self.clients.openWindow("/");
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -14,13 +14,7 @@ import {
|
|||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
function DettaglioBadge({
|
function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
||||||
def,
|
|
||||||
stato,
|
|
||||||
}: {
|
|
||||||
def: BadgeDef;
|
|
||||||
stato?: BadgeStato;
|
|
||||||
}) {
|
|
||||||
const Icon = def.icon;
|
const Icon = def.icon;
|
||||||
const grado = stato?.grado ?? null;
|
const grado = stato?.grado ?? null;
|
||||||
const meta = grado ? gradoMeta[grado] : null;
|
const meta = grado ? gradoMeta[grado] : null;
|
||||||
@@ -94,10 +88,20 @@ function DettaglioBadge({
|
|||||||
raggiunto ? `${gm.bg} ${gm.ring}` : "bg-secondary ring-border",
|
raggiunto ? `${gm.bg} ${gm.ring}` : "bg-secondary ring-border",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className={cn("text-[10px] font-bold uppercase", raggiunto ? gm.text : "text-muted-foreground")}>
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-[10px] font-bold uppercase",
|
||||||
|
raggiunto ? gm.text : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{gm.label}
|
{gm.label}
|
||||||
</p>
|
</p>
|
||||||
<p className={cn("font-display text-xl leading-none", raggiunto ? "text-foreground" : "text-muted-foreground")}>
|
<p
|
||||||
|
className={cn(
|
||||||
|
"font-display text-xl leading-none",
|
||||||
|
raggiunto ? "text-foreground" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{def.soglie[g]}
|
{def.soglie[g]}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-muted-foreground">{def.unita}</p>
|
<p className="text-[10px] text-muted-foreground">{def.unita}</p>
|
||||||
@@ -130,7 +134,7 @@ export function BadgeDrawer({
|
|||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
<DrawerTrigger asChild>{children}</DrawerTrigger>
|
<DrawerTrigger asChild>{children}</DrawerTrigger>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader className="sr-only">
|
<DrawerHeader className="sr-only">
|
||||||
<DrawerTitle>{def.nome}</DrawerTitle>
|
<DrawerTitle>{def.nome}</DrawerTitle>
|
||||||
<DrawerDescription>{def.descrizione}</DrawerDescription>
|
<DrawerDescription>{def.descrizione}</DrawerDescription>
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ export function BottomNav() {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,4 +76,4 @@ export function CelebrazioneBadge() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
prossimoTraguardo,
|
prossimoTraguardo,
|
||||||
type BadgeStato,
|
type BadgeStato,
|
||||||
} from "@/lib/badges";
|
} from "@/lib/badges";
|
||||||
import { badgeSocialVinti, categorieSocial, type VotoSocial, type CategoriaSocial } from "@/lib/badge-social";
|
import {
|
||||||
|
badgeSocialVinti,
|
||||||
|
categorieSocial,
|
||||||
|
type VotoSocial,
|
||||||
|
type CategoriaSocial,
|
||||||
|
} from "@/lib/badge-social";
|
||||||
import { Reveal } from "@/components/motion/Reveal";
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
import { Barra } from "@/components/motion/Barra";
|
import { Barra } from "@/components/motion/Barra";
|
||||||
import { Numero } from "@/components/motion/Numero";
|
import { Numero } from "@/components/motion/Numero";
|
||||||
@@ -91,7 +96,9 @@ function SocialDrawer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
||||||
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">Vinto</p>
|
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Vinto
|
||||||
|
</p>
|
||||||
<p className="mt-1 font-display text-3xl leading-none">
|
<p className="mt-1 font-display text-3xl leading-none">
|
||||||
{conteggio}{" "}
|
{conteggio}{" "}
|
||||||
<span className="text-lg text-muted-foreground">
|
<span className="text-lg text-muted-foreground">
|
||||||
@@ -246,4 +253,4 @@ export function CollezioneBadge({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -118,33 +118,35 @@ export function EventoCard({
|
|||||||
<Barra percentuale={perc} altezza="h-1.5" trackClassName="mt-3" />
|
<Barra percentuale={perc} altezza="h-1.5" trackClassName="mt-3" />
|
||||||
|
|
||||||
{io ? (
|
{io ? (
|
||||||
<div className="mt-4 flex flex-wrap gap-1.5">
|
<div className="mt-4 flex flex-wrap gap-1.5">
|
||||||
{stati.map((s) => {
|
{stati.map((s) => {
|
||||||
const meta = statoMeta[s];
|
const meta = statoMeta[s];
|
||||||
const attivo = stato === s;
|
const attivo = stato === s;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={salva.isPending}
|
disabled={salva.isPending}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
salva.mutate({
|
salva.mutate({
|
||||||
eventoId: evento.id,
|
eventoId: evento.id,
|
||||||
giocatoreId: io.id,
|
giocatoreId: io.id,
|
||||||
stato: attivo ? null : s,
|
stato: attivo ? null : s,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full border border-border px-2.5 py-1.5 text-[11px] font-semibold transition-all active:scale-95",
|
"rounded-full border border-border px-2.5 py-1.5 text-[11px] font-semibold transition-all active:scale-95",
|
||||||
attivo ? cn(meta.className, "border-transparent shadow-card") : "bg-background text-muted-foreground",
|
attivo
|
||||||
)}
|
? cn(meta.className, "border-transparent shadow-card")
|
||||||
>
|
: "bg-background text-muted-foreground",
|
||||||
{meta.label}
|
)}
|
||||||
</button>
|
>
|
||||||
);
|
{meta.label}
|
||||||
})}
|
</button>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,9 @@ export function Pagelle({
|
|||||||
onClick={() => invia(g.id, v)}
|
onClick={() => invia(g.id, v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg py-1.5 text-[11px] font-bold tabular-nums transition-transform active:scale-90",
|
"rounded-lg py-1.5 text-[11px] font-bold tabular-nums transition-transform active:scale-90",
|
||||||
mio === v ? "bg-accent text-accent-foreground" : "bg-card text-foreground",
|
mio === v
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "bg-card text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{v}
|
{v}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { formatData } from "@/lib/crapp-data";
|
import { formatData } from "@/lib/crapp-data";
|
||||||
import {
|
import { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core";
|
||||||
eventiPalloni,
|
|
||||||
eventoPrecedente,
|
|
||||||
eventoSuccessivo,
|
|
||||||
oggiISO,
|
|
||||||
} from "@/lib/palloni-core";
|
|
||||||
import { useTurniPalloni } from "@/lib/palloni";
|
import { useTurniPalloni } from "@/lib/palloni";
|
||||||
import { useEventi } from "@/lib/eventi";
|
import { useEventi } from "@/lib/eventi";
|
||||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||||
@@ -59,4 +54,4 @@ export function PromemoriaPalloni() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@ export function RosaPresenze({ eventoId }: { eventoId: string }) {
|
|||||||
const mancanti = giocatori.filter((g) => !risposte[g.id]);
|
const mancanti = giocatori.filter((g) => !risposte[g.id]);
|
||||||
const risposteN = giocatori.length - mancanti.length;
|
const risposteN = giocatori.length - mancanti.length;
|
||||||
const perc = Math.round((risposteN / giocatori.length) * 100);
|
const perc = Math.round((risposteN / giocatori.length) * 100);
|
||||||
const daSollecitare = mancanti.length + giocatori.filter((g) => risposte[g.id] === "forse").length;
|
const daSollecitare =
|
||||||
|
mancanti.length + giocatori.filter((g) => risposte[g.id] === "forse").length;
|
||||||
|
|
||||||
async function sollecita() {
|
async function sollecita() {
|
||||||
setSollecito(true);
|
setSollecito(true);
|
||||||
|
|||||||
@@ -19,12 +19,20 @@ export function ScoutEntry({ variante = "grande" }: { variante?: "grande" | "com
|
|||||||
const occupato = !!attiva && attiva.giocatore_id !== io?.id;
|
const occupato = !!attiva && attiva.giocatore_id !== io?.id;
|
||||||
const disponibile = abilitato && pronto && !!partita && !occupato;
|
const disponibile = abilitato && pronto && !!partita && !occupato;
|
||||||
|
|
||||||
const titolo = !abilitato ? "Scout live" : !partita ? "Scout live non attivo" : occupato ? "Scout occupato" : "Scout live";
|
const titolo = !abilitato
|
||||||
const sottotitolo = !abilitato ? "Riservato ad allenatori e referenti" : !partita
|
? "Scout live"
|
||||||
? "Si attiva il giorno della partita"
|
: !partita
|
||||||
: occupato
|
? "Scout live non attivo"
|
||||||
? `In uso da ${attiva!.giocatore_nome}`
|
: occupato
|
||||||
: "Segna punti, ace e muri in tempo reale";
|
? "Scout occupato"
|
||||||
|
: "Scout live";
|
||||||
|
const sottotitolo = !abilitato
|
||||||
|
? "Riservato ad allenatori e referenti"
|
||||||
|
: !partita
|
||||||
|
? "Si attiva il giorno della partita"
|
||||||
|
: occupato
|
||||||
|
? `In uso da ${attiva!.giocatore_nome}`
|
||||||
|
: "Segna punti, ace e muri in tempo reale";
|
||||||
|
|
||||||
const contenuto = (
|
const contenuto = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid h-10 w-10 shrink-0 place-items-center rounded-2xl",
|
"grid h-10 w-10 shrink-0 place-items-center rounded-2xl",
|
||||||
attiva ? "bg-accent-grad text-accent-foreground" : "bg-secondary text-muted-foreground",
|
attiva
|
||||||
|
? "bg-accent-grad text-accent-foreground"
|
||||||
|
: "bg-secondary text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
@@ -29,7 +31,9 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
|
|||||||
<p className="text-[11px] text-muted-foreground">{s.def.descrizione}</p>
|
<p className="text-[11px] text-muted-foreground">{s.def.descrizione}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center gap-1 font-display text-2xl leading-none">
|
<span className="inline-flex items-center gap-1 font-display text-2xl leading-none">
|
||||||
<Flame className={cn("h-4 w-4", attiva ? "text-accent" : "text-muted-foreground/40")} />
|
<Flame
|
||||||
|
className={cn("h-4 w-4", attiva ? "text-accent" : "text-muted-foreground/40")}
|
||||||
|
/>
|
||||||
{s.valore}
|
{s.valore}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,4 +78,4 @@ export function SerieHome({ g }: { g: Giocatore }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,10 @@ export function SondaggioCacche({ eventoId }: { eventoId: string }) {
|
|||||||
Media squadra {media}
|
Media squadra {media}
|
||||||
</span>
|
</span>
|
||||||
{classifica.map((r, i) => (
|
{classifica.map((r, i) => (
|
||||||
<span key={r.giocatore_id} className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
|
<span
|
||||||
|
key={r.giocatore_id}
|
||||||
|
className="rounded-full bg-secondary px-2.5 py-1 font-semibold"
|
||||||
|
>
|
||||||
{i === 0 ? "🥇" : i === 1 ? "🥈" : "🥉"} {r.nome} · {r.quantita}
|
{i === 0 ? "🥇" : i === 1 ? "🥈" : "🥉"} {r.nome} · {r.quantita}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -78,4 +78,4 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,4 +97,4 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,8 +122,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold">
|
<p className="inline-flex items-center gap-1.5 text-xs font-bold">
|
||||||
<Crown className="h-3.5 w-3.5 text-oro" />
|
<Crown className="h-3.5 w-3.5 text-oro" />
|
||||||
<Icon className="h-3.5 w-3.5 text-accent" />
|
<Icon className="h-3.5 w-3.5 text-accent" />
|
||||||
{vincitore.nome} · {vincitore.voti}{" "}
|
{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
|
||||||
{vincitore.voti === 1 ? "voto" : "voti"}
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
@@ -137,4 +136,4 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ export function StatoBadge({ stato, className }: { stato: Stato; className?: str
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,4 +87,4 @@ export function StatTile({
|
|||||||
{hint ? <p className="mt-0.5 text-[11px] text-accent">{hint}</p> : null}
|
{hint ? <p className="mt-0.5 text-[11px] text-accent">{hint}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -22,4 +22,4 @@ export function Barra({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -17,4 +17,4 @@ export function Numero({
|
|||||||
{suffisso}
|
{suffisso}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -26,4 +26,4 @@ export function Reveal({
|
|||||||
{children}
|
{children}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,10 @@ type SignInOptions = {
|
|||||||
|
|
||||||
export const lovable = {
|
export const lovable = {
|
||||||
auth: {
|
auth: {
|
||||||
signInWithOAuth: async (provider: "google" | "apple" | "microsoft" | "lovable", opts?: SignInOptions) => {
|
signInWithOAuth: async (
|
||||||
|
provider: "google" | "apple" | "microsoft" | "lovable",
|
||||||
|
opts?: SignInOptions,
|
||||||
|
) => {
|
||||||
const result = await lovableAuth.signInWithOAuth(provider, {
|
const result = await lovableAuth.signInWithOAuth(provider, {
|
||||||
redirect_uri: opts?.redirect_uri ?? window.location.origin,
|
redirect_uri: opts?.redirect_uri ?? window.location.origin,
|
||||||
extraParams: {
|
extraParams: {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// This file is automatically generated. Do not edit it directly.
|
// This file is automatically generated. Do not edit it directly.
|
||||||
import { createMiddleware } from '@tanstack/react-start'
|
import { createMiddleware } from "@tanstack/react-start";
|
||||||
import { supabase } from './client'
|
import { supabase } from "./client";
|
||||||
|
|
||||||
// Must be registered as a global `functionMiddleware` in `src/start.ts`; otherwise
|
// Must be registered as a global `functionMiddleware` in `src/start.ts`; otherwise
|
||||||
// the browser never attaches the bearer token to serverFn RPCs.
|
// the browser never attaches the bearer token to serverFn RPCs.
|
||||||
export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(
|
export const attachSupabaseAuth = createMiddleware({ type: "function" }).client(
|
||||||
async ({ next }) => {
|
async ({ next }) => {
|
||||||
const { data } = await supabase.auth.getSession()
|
const { data } = await supabase.auth.getSession();
|
||||||
const token = data.session?.access_token
|
const token = data.session?.access_token;
|
||||||
return next({
|
return next({
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
// This file is automatically generated. Do not edit it directly.
|
// This file is automatically generated. Do not edit it directly.
|
||||||
import { createMiddleware } from '@tanstack/react-start'
|
import { createMiddleware } from "@tanstack/react-start";
|
||||||
import { getRequest } from '@tanstack/react-start/server'
|
import { getRequest } from "@tanstack/react-start/server";
|
||||||
import { createClient } from '@supabase/supabase-js'
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from './types'
|
import type { Database } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function isNewSupabaseApiKey(value: string): boolean {
|
function isNewSupabaseApiKey(value: string): boolean {
|
||||||
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
|
return value.startsWith("sb_publishable_") || value.startsWith("sb_secret_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
||||||
return (input, init) => {
|
return (input, init) => {
|
||||||
const headers = new Headers(
|
const headers = new Headers(
|
||||||
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
|
typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (init?.headers) {
|
if (init?.headers) {
|
||||||
@@ -21,81 +19,79 @@ function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Supabase API keys are opaque strings, not bearer JWTs.
|
// New Supabase API keys are opaque strings, not bearer JWTs.
|
||||||
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
|
if (
|
||||||
headers.delete('Authorization');
|
isNewSupabaseApiKey(supabaseKey) &&
|
||||||
|
headers.get("Authorization") === `Bearer ${supabaseKey}`
|
||||||
|
) {
|
||||||
|
headers.delete("Authorization");
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.set('apikey', supabaseKey);
|
headers.set("apikey", supabaseKey);
|
||||||
return fetch(input, { ...init, headers });
|
return fetch(input, { ...init, headers });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const requireSupabaseAuth = createMiddleware({ type: 'function' }).server(
|
export const requireSupabaseAuth = createMiddleware({ type: "function" }).server(
|
||||||
async ({ next }) => {
|
async ({ next }) => {
|
||||||
|
const SUPABASE_URL = process.env["SUPABASE_URL"];
|
||||||
const SUPABASE_URL = process.env['SUPABASE_URL'];
|
const SUPABASE_PUBLISHABLE_KEY = process.env["SUPABASE_PUBLISHABLE_KEY"];
|
||||||
const SUPABASE_PUBLISHABLE_KEY = process.env['SUPABASE_PUBLISHABLE_KEY'];
|
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
|
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
|
||||||
const missing = [
|
const missing = [
|
||||||
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
|
...(!SUPABASE_URL ? ["SUPABASE_URL"] : []),
|
||||||
...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []),
|
...(!SUPABASE_PUBLISHABLE_KEY ? ["SUPABASE_PUBLISHABLE_KEY"] : []),
|
||||||
];
|
];
|
||||||
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
|
const message = `Missing Supabase environment variable(s): ${missing.join(", ")}. Connect Supabase in Lovable Cloud.`;
|
||||||
console.error(`[Supabase] ${message}`);
|
console.error(`[Supabase] ${message}`);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = getRequest();
|
const request = getRequest();
|
||||||
|
|
||||||
if (!request?.headers) {
|
if (!request?.headers) {
|
||||||
throw new Error('Unauthorized: No request headers available');
|
throw new Error("Unauthorized: No request headers available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get("authorization");
|
||||||
|
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
throw new Error('Unauthorized: No authorization header provided');
|
throw new Error("Unauthorized: No authorization header provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authHeader.startsWith('Bearer ')) {
|
if (!authHeader.startsWith("Bearer ")) {
|
||||||
throw new Error('Unauthorized: Only Bearer tokens are supported');
|
throw new Error("Unauthorized: Only Bearer tokens are supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.replace('Bearer ', '');
|
const token = authHeader.replace("Bearer ", "");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('Unauthorized: No token provided');
|
throw new Error("Unauthorized: No token provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.split('.').length !== 3) {
|
if (token.split(".").length !== 3) {
|
||||||
throw new Error('Unauthorized: Invalid token');
|
throw new Error("Unauthorized: Invalid token");
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabase = createClient<Database>(
|
const supabase = createClient<Database>(SUPABASE_URL!, SUPABASE_PUBLISHABLE_KEY!, {
|
||||||
SUPABASE_URL!,
|
global: {
|
||||||
SUPABASE_PUBLISHABLE_KEY!,
|
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY!),
|
||||||
{
|
headers: {
|
||||||
global: {
|
Authorization: `Bearer ${token}`,
|
||||||
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY!),
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
auth: {
|
},
|
||||||
storage: undefined,
|
auth: {
|
||||||
persistSession: false,
|
storage: undefined,
|
||||||
autoRefreshToken: false,
|
persistSession: false,
|
||||||
},
|
autoRefreshToken: false,
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getClaims(token);
|
const { data, error } = await supabase.auth.getClaims(token);
|
||||||
if (error || !data?.claims) {
|
if (error || !data?.claims) {
|
||||||
throw new Error('Unauthorized: Invalid token');
|
throw new Error("Unauthorized: Invalid token");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.claims.sub) {
|
if (!data.claims.sub) {
|
||||||
throw new Error('Unauthorized: No user ID found in token');
|
throw new Error("Unauthorized: No user ID found in token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
// Server-side Supabase client with service role key - bypasses RLS.
|
// Server-side Supabase client with service role key - bypasses RLS.
|
||||||
// Use this for admin operations in server functions and server routes only.
|
// Use this for admin operations in server functions and server routes only.
|
||||||
// For user-authenticated queries (with RLS), use the auth middleware instead.
|
// For user-authenticated queries (with RLS), use the auth middleware instead.
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from './types';
|
import type { Database } from "./types";
|
||||||
|
|
||||||
function isNewSupabaseApiKey(value: string): boolean {
|
function isNewSupabaseApiKey(value: string): boolean {
|
||||||
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
|
return value.startsWith("sb_publishable_") || value.startsWith("sb_secret_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
||||||
return (input, init) => {
|
return (input, init) => {
|
||||||
const headers = new Headers(
|
const headers = new Headers(
|
||||||
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
|
typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (init?.headers) {
|
if (init?.headers) {
|
||||||
@@ -20,25 +20,28 @@ function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Supabase API keys are opaque strings, not bearer JWTs.
|
// New Supabase API keys are opaque strings, not bearer JWTs.
|
||||||
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
|
if (
|
||||||
headers.delete('Authorization');
|
isNewSupabaseApiKey(supabaseKey) &&
|
||||||
|
headers.get("Authorization") === `Bearer ${supabaseKey}`
|
||||||
|
) {
|
||||||
|
headers.delete("Authorization");
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.set('apikey', supabaseKey);
|
headers.set("apikey", supabaseKey);
|
||||||
return fetch(input, { ...init, headers });
|
return fetch(input, { ...init, headers });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSupabaseAdminClient() {
|
function createSupabaseAdminClient() {
|
||||||
const SUPABASE_URL = process.env['SUPABASE_URL'];
|
const SUPABASE_URL = process.env["SUPABASE_URL"];
|
||||||
const SUPABASE_SERVICE_ROLE_KEY = process.env['SUPABASE_SERVICE_ROLE_KEY'];
|
const SUPABASE_SERVICE_ROLE_KEY = process.env["SUPABASE_SERVICE_ROLE_KEY"];
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
||||||
const missing = [
|
const missing = [
|
||||||
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
|
...(!SUPABASE_URL ? ["SUPABASE_URL"] : []),
|
||||||
...(!SUPABASE_SERVICE_ROLE_KEY ? ['SUPABASE_SERVICE_ROLE_KEY'] : []),
|
...(!SUPABASE_SERVICE_ROLE_KEY ? ["SUPABASE_SERVICE_ROLE_KEY"] : []),
|
||||||
];
|
];
|
||||||
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
|
const message = `Missing Supabase environment variable(s): ${missing.join(", ")}. Connect Supabase in Lovable Cloud.`;
|
||||||
console.error(`[Supabase] ${message}`);
|
console.error(`[Supabase] ${message}`);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
@@ -51,7 +54,7 @@ function createSupabaseAdminClient() {
|
|||||||
storage: undefined,
|
storage: undefined,
|
||||||
persistSession: false,
|
persistSession: false,
|
||||||
autoRefreshToken: false,
|
autoRefreshToken: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// This file is automatically generated. Do not edit it directly.
|
// This file is automatically generated. Do not edit it directly.
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from './types';
|
import type { Database } from "./types";
|
||||||
|
|
||||||
function isNewSupabaseApiKey(value: string): boolean {
|
function isNewSupabaseApiKey(value: string): boolean {
|
||||||
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
|
return value.startsWith("sb_publishable_") || value.startsWith("sb_secret_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
||||||
return (input, init) => {
|
return (input, init) => {
|
||||||
const headers = new Headers(
|
const headers = new Headers(
|
||||||
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
|
typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (init?.headers) {
|
if (init?.headers) {
|
||||||
@@ -17,28 +17,31 @@ function createSupabaseFetch(supabaseKey: string): typeof fetch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Supabase API keys are opaque strings, not bearer JWTs.
|
// New Supabase API keys are opaque strings, not bearer JWTs.
|
||||||
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
|
if (
|
||||||
headers.delete('Authorization');
|
isNewSupabaseApiKey(supabaseKey) &&
|
||||||
|
headers.get("Authorization") === `Bearer ${supabaseKey}`
|
||||||
|
) {
|
||||||
|
headers.delete("Authorization");
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.set('apikey', supabaseKey);
|
headers.set("apikey", supabaseKey);
|
||||||
return fetch(input, { ...init, headers });
|
return fetch(input, { ...init, headers });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createSupabaseClient() {
|
function createSupabaseClient() {
|
||||||
// Use import.meta.env for client-side (Vite build-time replacement)
|
// Use import.meta.env for client-side (Vite build-time replacement)
|
||||||
// Fall back to process.env for SSR (server-side rendering)
|
// Fall back to process.env for SSR (server-side rendering)
|
||||||
const SUPABASE_URL = import.meta.env['VITE_SUPABASE_URL'] || process.env['SUPABASE_URL'];
|
const SUPABASE_URL = import.meta.env["VITE_SUPABASE_URL"] || process.env["SUPABASE_URL"];
|
||||||
const SUPABASE_PUBLISHABLE_KEY = import.meta.env['VITE_SUPABASE_PUBLISHABLE_KEY'] || process.env['SUPABASE_PUBLISHABLE_KEY'];
|
const SUPABASE_PUBLISHABLE_KEY =
|
||||||
|
import.meta.env["VITE_SUPABASE_PUBLISHABLE_KEY"] || process.env["SUPABASE_PUBLISHABLE_KEY"];
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
|
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
|
||||||
const missing = [
|
const missing = [
|
||||||
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
|
...(!SUPABASE_URL ? ["SUPABASE_URL"] : []),
|
||||||
...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []),
|
...(!SUPABASE_PUBLISHABLE_KEY ? ["SUPABASE_PUBLISHABLE_KEY"] : []),
|
||||||
];
|
];
|
||||||
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
|
const message = `Missing Supabase environment variable(s): ${missing.join(", ")}. Connect Supabase in Lovable Cloud.`;
|
||||||
console.error(`[Supabase] ${message}`);
|
console.error(`[Supabase] ${message}`);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
@@ -48,10 +51,10 @@ function createSupabaseClient() {
|
|||||||
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY),
|
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY),
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
storage: typeof window !== "undefined" ? localStorage : undefined,
|
||||||
persistSession: true,
|
persistSession: true,
|
||||||
autoRefreshToken: true,
|
autoRefreshToken: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,4 +68,3 @@ export const supabase = new Proxy({} as ReturnType<typeof createSupabaseClient>,
|
|||||||
return Reflect.get(_supabase, prop, receiver);
|
return Reflect.get(_supabase, prop, receiver);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
+431
-443
File diff suppressed because it is too large.
Load diff
@@ -139,8 +139,7 @@ export function mioVotoSocial(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
voti.find(
|
voti.find(
|
||||||
(v) =>
|
(v) => v.match_id === matchId && v.categoria === categoria && v.votante_id === votanteId,
|
||||||
v.match_id === matchId && v.categoria === categoria && v.votante_id === votanteId,
|
|
||||||
) ?? null
|
) ?? null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -156,4 +155,4 @@ export function badgeSocialVinti(voti: VotoSocial[], giocatoreId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
+14
-11
@@ -19,10 +19,7 @@ export type Grado = "bronzo" | "argento" | "oro";
|
|||||||
|
|
||||||
export const gradiOrdine: Grado[] = ["bronzo", "argento", "oro"];
|
export const gradiOrdine: Grado[] = ["bronzo", "argento", "oro"];
|
||||||
|
|
||||||
export const gradoMeta: Record<
|
export const gradoMeta: Record<Grado, { label: string; text: string; bg: string; ring: string }> = {
|
||||||
Grado,
|
|
||||||
{ label: string; text: string; bg: string; ring: string }
|
|
||||||
> = {
|
|
||||||
bronzo: { label: "Bronzo", text: "text-bronzo", bg: "bg-bronzo/15", ring: "ring-bronzo/40" },
|
bronzo: { label: "Bronzo", text: "text-bronzo", bg: "bg-bronzo/15", ring: "ring-bronzo/40" },
|
||||||
argento: { label: "Argento", text: "text-argento", bg: "bg-argento/20", ring: "ring-argento/50" },
|
argento: { label: "Argento", text: "text-argento", bg: "bg-argento/20", ring: "ring-argento/50" },
|
||||||
oro: { label: "Oro", text: "text-oro", bg: "bg-oro/20", ring: "ring-oro/50" },
|
oro: { label: "Oro", text: "text-oro", bg: "bg-oro/20", ring: "ring-oro/50" },
|
||||||
@@ -50,7 +47,8 @@ export const badgeDefs: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "mvp",
|
id: "mvp",
|
||||||
nome: "MVP",
|
nome: "MVP",
|
||||||
descrizione: "Riconoscimento per il miglior giocatore della partita, scelto dai compagni a fine match.",
|
descrizione:
|
||||||
|
"Riconoscimento per il miglior giocatore della partita, scelto dai compagni a fine match.",
|
||||||
unita: "MVP",
|
unita: "MVP",
|
||||||
icon: Trophy,
|
icon: Trophy,
|
||||||
soglie: { bronzo: 1, argento: 3, oro: 5 },
|
soglie: { bronzo: 1, argento: 3, oro: 5 },
|
||||||
@@ -59,7 +57,8 @@ export const badgeDefs: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "pagella",
|
id: "pagella",
|
||||||
nome: "Pagellone",
|
nome: "Pagellone",
|
||||||
descrizione: "Media dei voti che i compagni ti danno a fine partita: conta come giochi, non quanti punti fai.",
|
descrizione:
|
||||||
|
"Media dei voti che i compagni ti danno a fine partita: conta come giochi, non quanti punti fai.",
|
||||||
unita: "di media voto",
|
unita: "di media voto",
|
||||||
icon: ClipboardCheck,
|
icon: ClipboardCheck,
|
||||||
soglie: { bronzo: 6.5, argento: 7.5, oro: 8.5 },
|
soglie: { bronzo: 6.5, argento: 7.5, oro: 8.5 },
|
||||||
@@ -68,7 +67,8 @@ export const badgeDefs: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "palloni",
|
id: "palloni",
|
||||||
nome: "Sherpa dei palloni",
|
nome: "Sherpa dei palloni",
|
||||||
descrizione: "Quante volte ti sei caricato la sacca dei palloni: lavoro oscuro, badge luminoso.",
|
descrizione:
|
||||||
|
"Quante volte ti sei caricato la sacca dei palloni: lavoro oscuro, badge luminoso.",
|
||||||
unita: "turni palloni",
|
unita: "turni palloni",
|
||||||
icon: CircleDot,
|
icon: CircleDot,
|
||||||
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
||||||
@@ -86,7 +86,8 @@ export const badgeDefs: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "serie-allenamenti",
|
id: "serie-allenamenti",
|
||||||
nome: "Sempre in palestra",
|
nome: "Sempre in palestra",
|
||||||
descrizione: "Allenamenti consecutivi a cui sei stato presente: la costanza paga più del talento.",
|
descrizione:
|
||||||
|
"Allenamenti consecutivi a cui sei stato presente: la costanza paga più del talento.",
|
||||||
unita: "allenamenti di fila",
|
unita: "allenamenti di fila",
|
||||||
icon: Rocket,
|
icon: Rocket,
|
||||||
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
||||||
@@ -108,7 +109,8 @@ export const badgeSegreti: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "s-tiebreak",
|
id: "s-tiebreak",
|
||||||
nome: "Uomo tie-break",
|
nome: "Uomo tie-break",
|
||||||
descrizione: "Sbloccato da chi ha almeno 2 MVP e una media voto alta: nei momenti caldi ci sei sempre.",
|
descrizione:
|
||||||
|
"Sbloccato da chi ha almeno 2 MVP e una media voto alta: nei momenti caldi ci sei sempre.",
|
||||||
unita: "MVP con media alta",
|
unita: "MVP con media alta",
|
||||||
icon: Ghost,
|
icon: Ghost,
|
||||||
segreto: true,
|
segreto: true,
|
||||||
@@ -119,7 +121,8 @@ export const badgeSegreti: BadgeDef[] = [
|
|||||||
{
|
{
|
||||||
id: "s-mai-forfait",
|
id: "s-mai-forfait",
|
||||||
nome: "Mai un forfait",
|
nome: "Mai un forfait",
|
||||||
descrizione: "Sbloccato con 10 conferme rapide consecutive e 15 presenze: su di te la squadra può contare a occhi chiusi.",
|
descrizione:
|
||||||
|
"Sbloccato con 10 conferme rapide consecutive e 15 presenze: su di te la squadra può contare a occhi chiusi.",
|
||||||
unita: "requisito nascosto",
|
unita: "requisito nascosto",
|
||||||
icon: Anchor,
|
icon: Anchor,
|
||||||
segreto: true,
|
segreto: true,
|
||||||
@@ -268,4 +271,4 @@ export function badgeSbloccati(g: Giocatore): BadgeStato[] {
|
|||||||
|
|
||||||
export function descrizioneSoglie(def: BadgeDef) {
|
export function descrizioneSoglie(def: BadgeDef) {
|
||||||
return `${def.soglie.bronzo}/${def.soglie.argento}/${def.soglie.oro} ${def.unita}`;
|
return `${def.soglie.bronzo}/${def.soglie.argento}/${def.soglie.oro} ${def.unita}`;
|
||||||
}
|
}
|
||||||
+7
-1
@@ -63,7 +63,13 @@ function arrotonda(n: number) {
|
|||||||
export function statisticheCacche(righe: RigaCacche[]): Record<string, StatCacche> {
|
export function statisticheCacche(righe: RigaCacche[]): Record<string, StatCacche> {
|
||||||
const out: Record<string, StatCacche> = {};
|
const out: Record<string, StatCacche> = {};
|
||||||
for (const r of righe) {
|
for (const r of righe) {
|
||||||
const cur = out[r.giocatore_id] ?? { totale: 0, giornate: 0, media: 0, record: 0, giornateTop: 0 };
|
const cur = out[r.giocatore_id] ?? {
|
||||||
|
totale: 0,
|
||||||
|
giornate: 0,
|
||||||
|
media: 0,
|
||||||
|
record: 0,
|
||||||
|
giornateTop: 0,
|
||||||
|
};
|
||||||
cur.totale += r.quantita;
|
cur.totale += r.quantita;
|
||||||
cur.giornate += 1;
|
cur.giornate += 1;
|
||||||
cur.record = Math.max(cur.record, r.quantita);
|
cur.record = Math.max(cur.record, r.quantita);
|
||||||
|
|||||||
+10
-30
@@ -2,10 +2,18 @@ export type Stato = "presente" | "assente" | "forse" | "ritardo" | "infortunato"
|
|||||||
|
|
||||||
export const statoMeta: Record<Stato, { label: string; emoji: string; className: string }> = {
|
export const statoMeta: Record<Stato, { label: string; emoji: string; className: string }> = {
|
||||||
presente: { label: "Presente", emoji: "✅", className: "bg-success text-success-foreground" },
|
presente: { label: "Presente", emoji: "✅", className: "bg-success text-success-foreground" },
|
||||||
assente: { label: "Assente", emoji: "❌", className: "bg-destructive text-destructive-foreground" },
|
assente: {
|
||||||
|
label: "Assente",
|
||||||
|
emoji: "❌",
|
||||||
|
className: "bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
forse: { label: "Forse", emoji: "🤔", className: "bg-warning text-warning-foreground" },
|
forse: { label: "Forse", emoji: "🤔", className: "bg-warning text-warning-foreground" },
|
||||||
ritardo: { label: "In ritardo", emoji: "⏱️", className: "bg-info text-info-foreground" },
|
ritardo: { label: "In ritardo", emoji: "⏱️", className: "bg-info text-info-foreground" },
|
||||||
infortunato: { label: "Infortunato", emoji: "🩹", className: "bg-primary text-primary-foreground" },
|
infortunato: {
|
||||||
|
label: "Infortunato",
|
||||||
|
emoji: "🩹",
|
||||||
|
className: "bg-primary text-primary-foreground",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,24 +133,6 @@ export const giocatori: Giocatore[] = rosaCSI.map((r, i) => ({
|
|||||||
...serieDa(statsDemo[r.nome]),
|
...serieDa(statsDemo[r.nome]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type Match = {
|
|
||||||
id: string;
|
|
||||||
data: string;
|
|
||||||
avversario: string;
|
|
||||||
casa: boolean;
|
|
||||||
setNostri: number;
|
|
||||||
setLoro: number;
|
|
||||||
parziali: Array<[number, number]>;
|
|
||||||
mvp: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const storicoMatch: Match[] = [
|
|
||||||
{ id: "m1", data: "2026-07-25", avversario: "Pallavolo Sesto", casa: true, setNostri: 3, setLoro: 1, parziali: [[25, 19], [23, 25], [25, 21], [25, 18]], mvp: "Davide Grilli" },
|
|
||||||
{ id: "m2", data: "2026-07-18", avversario: "ASD Rondinella", casa: false, setNostri: 2, setLoro: 3, parziali: [[25, 22], [19, 25], [25, 23], [20, 25], [12, 15]], mvp: "Ivan Cacciari" },
|
|
||||||
{ id: "m3", data: "2026-07-11", avversario: "Virtus Cinisello", casa: true, setNostri: 3, setLoro: 0, parziali: [[25, 15], [25, 20], [25, 17]], mvp: "Laura Passabì" },
|
|
||||||
{ id: "m4", data: "2026-07-04", avversario: "Nuova Bovisa", casa: false, setNostri: 3, setLoro: 2, parziali: [[21, 25], [25, 23], [18, 25], [25, 20], [15, 11]], mvp: "Nicola Pezzoli" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export type RigaClassifica = {
|
export type RigaClassifica = {
|
||||||
pos: number;
|
pos: number;
|
||||||
squadra: string;
|
squadra: string;
|
||||||
@@ -154,16 +144,6 @@ export type RigaClassifica = {
|
|||||||
punti: number;
|
punti: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const classifica: RigaClassifica[] = [
|
|
||||||
{ pos: 1, squadra: "ASD Rondinella", giocate: 12, vinte: 10, perse: 2, setFatti: 33, setSubiti: 12, punti: 29 },
|
|
||||||
{ pos: 2, squadra: "CRAP Volley", giocate: 12, vinte: 9, perse: 3, setFatti: 31, setSubiti: 16, punti: 26 },
|
|
||||||
{ pos: 3, squadra: "Pallavolo Sesto", giocate: 12, vinte: 8, perse: 4, setFatti: 29, setSubiti: 19, punti: 24 },
|
|
||||||
{ pos: 4, squadra: "Volley Bruzzano", giocate: 12, vinte: 7, perse: 5, setFatti: 26, setSubiti: 21, punti: 21 },
|
|
||||||
{ pos: 5, squadra: "Aurora Nera", giocate: 12, vinte: 5, perse: 7, setFatti: 22, setSubiti: 25, punti: 16 },
|
|
||||||
{ pos: 6, squadra: "Virtus Cinisello", giocate: 12, vinte: 3, perse: 9, setFatti: 15, setSubiti: 30, punti: 10 },
|
|
||||||
{ pos: 7, squadra: "Nuova Bovisa", giocate: 12, vinte: 2, perse: 10, setFatti: 13, setSubiti: 32, punti: 7 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function formatData(iso: string) {
|
export function formatData(iso: string) {
|
||||||
const d = new Date(iso + "T00:00:00");
|
const d = new Date(iso + "T00:00:00");
|
||||||
return d.toLocaleDateString("it-IT", { weekday: "short", day: "2-digit", month: "long" });
|
return d.toLocaleDateString("it-IT", { weekday: "short", day: "2-digit", month: "long" });
|
||||||
|
|||||||
@@ -161,3 +161,16 @@ export function partiteDaEventi(eventi: unknown): PartitaCsi[] {
|
|||||||
export function partiteGiocate(partite: PartitaCsi[]): PartitaCsi[] {
|
export function partiteGiocate(partite: PartitaCsi[]): PartitaCsi[] {
|
||||||
return partite.filter((p) => p.setNostri !== null && p.setLoro !== null);
|
return partite.filter((p) => p.setNostri !== null && p.setLoro !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Converte una gara CSI già giocata nella forma comune usata nelle liste risultati. */
|
||||||
|
export function matchDaPartitaCsi(p: PartitaCsi) {
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
data: p.data,
|
||||||
|
avversario: p.avversario,
|
||||||
|
casa: p.casa,
|
||||||
|
setNostri: p.setNostri ?? 0,
|
||||||
|
setLoro: p.setLoro ?? 0,
|
||||||
|
parziali: p.parziali,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -57,4 +57,4 @@ export function useInfortuniERitardi(): { infortuni: ContoInfortuni; ritardi: Co
|
|||||||
() => ({ infortuni: contaInfortuni(presenze), ritardi: contaRitardi(presenze) }),
|
() => ({ infortuni: contaInfortuni(presenze), ritardi: contaRitardi(presenze) }),
|
||||||
[presenze],
|
[presenze],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -90,4 +90,4 @@ export async function coriandoli(ridotto = false) {
|
|||||||
origin: { y: 0.7 },
|
origin: { y: 0.7 },
|
||||||
disableForReducedMotion: true,
|
disableForReducedMotion: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+1
-1
@@ -83,4 +83,4 @@ export function vincitoriMvp(voti: VotoMvp[]): Record<string, string> {
|
|||||||
|
|
||||||
export function mioVoto(voti: VotoMvp[], matchId: string, votanteId: string) {
|
export function mioVoto(voti: VotoMvp[], matchId: string, votanteId: string) {
|
||||||
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
|
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import type { Giocatore } from "./crapp-data";
|
import type { Giocatore } from "./crapp-data";
|
||||||
import {
|
import { microcopyObiettivo, progressoObiettivo, type ObiettivoSquadra } from "./obiettivi";
|
||||||
microcopyObiettivo,
|
import { badgeGiocatore, badgeSegretiSbloccati, gradoMeta, prossimoTraguardo } from "./badges";
|
||||||
progressoObiettivo,
|
|
||||||
type ObiettivoSquadra,
|
|
||||||
} from "./obiettivi";
|
|
||||||
import {
|
|
||||||
badgeGiocatore,
|
|
||||||
badgeSegretiSbloccati,
|
|
||||||
gradoMeta,
|
|
||||||
prossimoTraguardo,
|
|
||||||
} from "./badges";
|
|
||||||
import { serieGiocatore } from "./serie";
|
import { serieGiocatore } from "./serie";
|
||||||
import { badgeSocialVinti, categorieSocial, type VotoSocial } from "./badge-social";
|
import { badgeSocialVinti, categorieSocial, type VotoSocial } from "./badge-social";
|
||||||
|
|
||||||
@@ -166,4 +157,4 @@ export function useNotificheSmart(g: Giocatore | null, votiSocial: VotoSocial[]
|
|||||||
const chiudi = useCallback(() => setCoda((c) => c.slice(1)), []);
|
const chiudi = useCallback(() => setCoda((c) => c.slice(1)), []);
|
||||||
|
|
||||||
return { notifica: coda[0] ?? null, restanti: Math.max(0, coda.length - 1), chiudi };
|
return { notifica: coda[0] ?? null, restanti: Math.max(0, coda.length - 1), chiudi };
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { giocatori, storicoMatch, type Giocatore } from "./crapp-data";
|
import { giocatori, type Giocatore } from "./crapp-data";
|
||||||
import type { Evento } from "./eventi";
|
import type { Evento } from "./eventi";
|
||||||
import type { MappaPresenze } from "./presenze";
|
import type { MappaPresenze } from "./presenze";
|
||||||
import { mediaSquadra, type VotoPagella } from "./pagelle";
|
import { mediaSquadra, type VotoPagella } from "./pagelle";
|
||||||
@@ -20,6 +20,8 @@ export type ContestoObiettivi = {
|
|||||||
eventi: Evento[];
|
eventi: Evento[];
|
||||||
presenze: MappaPresenze;
|
presenze: MappaPresenze;
|
||||||
pagelle: VotoPagella[];
|
pagelle: VotoPagella[];
|
||||||
|
/** Vittorie ufficiali in campionato (dato CSI). */
|
||||||
|
vittorie?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, pagelle: [] };
|
export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, pagelle: [] };
|
||||||
@@ -50,8 +52,6 @@ function percentualeRisposte(ctx: ContestoObiettivi) {
|
|||||||
return Math.round((risposte / posti) * 100);
|
return Math.round((risposte / posti) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
const vittorie = storicoMatch.filter((m) => m.setNostri > m.setLoro).length;
|
|
||||||
|
|
||||||
/** Obiettivi collaborativi: si muovono con il contributo di tutta la rosa. */
|
/** Obiettivi collaborativi: si muovono con il contributo di tutta la rosa. */
|
||||||
export function obiettiviSquadra(
|
export function obiettiviSquadra(
|
||||||
rosa: Giocatore[] = giocatori,
|
rosa: Giocatore[] = giocatori,
|
||||||
@@ -59,6 +59,7 @@ export function obiettiviSquadra(
|
|||||||
): ObiettivoSquadra[] {
|
): ObiettivoSquadra[] {
|
||||||
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
|
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
|
||||||
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
||||||
|
const vittorie = ctx.vittorie ?? 0;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "o1",
|
id: "o1",
|
||||||
|
|||||||
@@ -60,8 +60,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
|
|||||||
|
|
||||||
const out: Record<string, { presenti: number; totali: number; percentuale: number }> = {};
|
const out: Record<string, { presenti: number; totali: number; percentuale: number }> = {};
|
||||||
for (const e of rilevanti) {
|
for (const e of rilevanti) {
|
||||||
const ids =
|
const ids = e.convocati.length > 0 ? e.convocati : giocatori.map((g) => g.id);
|
||||||
e.convocati.length > 0 ? e.convocati : giocatori.map((g) => g.id);
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const rec = (out[id] ??= { presenti: 0, totali: 0, percentuale: 0 });
|
const rec = (out[id] ??= { presenti: 0, totali: 0, percentuale: 0 });
|
||||||
rec.totali += 1;
|
rec.totali += 1;
|
||||||
@@ -74,4 +73,4 @@ export function usePresenzeUltimoMeseTutti(): Record<
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}, [eventi, presenze]);
|
}, [eventi, presenze]);
|
||||||
}
|
}
|
||||||
@@ -69,4 +69,4 @@ export async function disattivaNotifiche(): Promise<void> {
|
|||||||
await sub.unsubscribe();
|
await sub.unsubscribe();
|
||||||
}
|
}
|
||||||
await reg?.unregister();
|
await reg?.unregister();
|
||||||
}
|
}
|
||||||
+7
-1
@@ -11,6 +11,8 @@ import { useGiocatoreBase } from "./user-store";
|
|||||||
import { useEventi } from "./eventi";
|
import { useEventi } from "./eventi";
|
||||||
import { useRispostePresenze } from "./presenze";
|
import { useRispostePresenze } from "./presenze";
|
||||||
import { obiettiviOrdinati } from "./obiettivi";
|
import { obiettiviOrdinati } from "./obiettivi";
|
||||||
|
import { useCsi } from "./csi";
|
||||||
|
import { partiteGiocate } from "./csi-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rosa completa con tutte le statistiche personali (presenze, MVP, media voto,
|
* Rosa completa con tutte le statistiche personali (presenze, MVP, media voto,
|
||||||
@@ -57,7 +59,11 @@ export function useObiettivi() {
|
|||||||
const { eventi } = useEventi();
|
const { eventi } = useEventi();
|
||||||
const { presenze } = useRispostePresenze();
|
const { presenze } = useRispostePresenze();
|
||||||
const { voti: pagelle } = usePagelle();
|
const { voti: pagelle } = usePagelle();
|
||||||
return obiettiviOrdinati(rosa, { eventi, presenze, pagelle });
|
const { data: csi } = useCsi();
|
||||||
|
const vittorie = csi
|
||||||
|
? partiteGiocate(csi.partite).filter((p) => (p.setNostri ?? 0) > (p.setLoro ?? 0)).length
|
||||||
|
: 0;
|
||||||
|
return obiettiviOrdinati(rosa, { eventi, presenze, pagelle, vittorie });
|
||||||
}
|
}
|
||||||
|
|
||||||
export { giocatori };
|
export { giocatori };
|
||||||
@@ -14,7 +14,14 @@ export function rigaCsv(campi: Array<string | number>) {
|
|||||||
/** Esporta la scoutizzazione di una partita in CSV (separatore ";" per Excel IT). */
|
/** Esporta la scoutizzazione di una partita in CSV (separatore ";" per Excel IT). */
|
||||||
export function csvScoutMatch(match: ScoutMatch): string {
|
export function csvScoutMatch(match: ScoutMatch): string {
|
||||||
const righe: string[] = [];
|
const righe: string[] = [];
|
||||||
righe.push(rigaCsv(["Partita", match.casa ? "CRAP Volley" : match.avversario, "vs", match.casa ? match.avversario : "CRAP Volley"]));
|
righe.push(
|
||||||
|
rigaCsv([
|
||||||
|
"Partita",
|
||||||
|
match.casa ? "CRAP Volley" : match.avversario,
|
||||||
|
"vs",
|
||||||
|
match.casa ? match.avversario : "CRAP Volley",
|
||||||
|
]),
|
||||||
|
);
|
||||||
righe.push(rigaCsv(["Data", match.data, "Set", `${match.setNostri}-${match.setLoro}`]));
|
righe.push(rigaCsv(["Data", match.data, "Set", `${match.setNostri}-${match.setLoro}`]));
|
||||||
righe.push("");
|
righe.push("");
|
||||||
righe.push(rigaCsv(["Set", "Parziale nostro", "Parziale loro"]));
|
righe.push(rigaCsv(["Set", "Parziale nostro", "Parziale loro"]));
|
||||||
|
|||||||
+44
-48
@@ -1,24 +1,54 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { giocatori, classifica, type Giocatore, type RigaClassifica } from "./crapp-data";
|
import { giocatori, type Giocatore } from "./crapp-data";
|
||||||
|
|
||||||
export type AzioneTipo =
|
export type AzioneTipo = "attacco" | "ace" | "muro" | "errore" | "punto_avv" | "errore_avv";
|
||||||
| "attacco"
|
|
||||||
| "ace"
|
|
||||||
| "muro"
|
|
||||||
| "errore"
|
|
||||||
| "punto_avv"
|
|
||||||
| "errore_avv";
|
|
||||||
|
|
||||||
export const azioniMeta: Record<
|
export const azioniMeta: Record<
|
||||||
AzioneTipo,
|
AzioneTipo,
|
||||||
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
|
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
|
||||||
> = {
|
> = {
|
||||||
attacco: { label: "Punto attacco", short: "Punto", nostro: true, richiedeGiocatore: true, className: "bg-accent text-accent-foreground" },
|
attacco: {
|
||||||
ace: { label: "Ace", short: "Ace", nostro: true, richiedeGiocatore: true, className: "bg-success text-success-foreground" },
|
label: "Punto attacco",
|
||||||
muro: { label: "Muro", short: "Muro", nostro: true, richiedeGiocatore: true, className: "bg-info text-info-foreground" },
|
short: "Punto",
|
||||||
errore: { label: "Errore nostro", short: "Errore", nostro: false, richiedeGiocatore: true, className: "bg-destructive text-destructive-foreground" },
|
nostro: true,
|
||||||
punto_avv: { label: "Punto avversario", short: "Punto avv.", nostro: false, richiedeGiocatore: false, className: "bg-muted text-muted-foreground" },
|
richiedeGiocatore: true,
|
||||||
errore_avv: { label: "Errore avversario", short: "Err. avv.", nostro: true, richiedeGiocatore: false, className: "bg-secondary text-foreground" },
|
className: "bg-accent text-accent-foreground",
|
||||||
|
},
|
||||||
|
ace: {
|
||||||
|
label: "Ace",
|
||||||
|
short: "Ace",
|
||||||
|
nostro: true,
|
||||||
|
richiedeGiocatore: true,
|
||||||
|
className: "bg-success text-success-foreground",
|
||||||
|
},
|
||||||
|
muro: {
|
||||||
|
label: "Muro",
|
||||||
|
short: "Muro",
|
||||||
|
nostro: true,
|
||||||
|
richiedeGiocatore: true,
|
||||||
|
className: "bg-info text-info-foreground",
|
||||||
|
},
|
||||||
|
errore: {
|
||||||
|
label: "Errore nostro",
|
||||||
|
short: "Errore",
|
||||||
|
nostro: false,
|
||||||
|
richiedeGiocatore: true,
|
||||||
|
className: "bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
punto_avv: {
|
||||||
|
label: "Punto avversario",
|
||||||
|
short: "Punto avv.",
|
||||||
|
nostro: false,
|
||||||
|
richiedeGiocatore: false,
|
||||||
|
className: "bg-muted text-muted-foreground",
|
||||||
|
},
|
||||||
|
errore_avv: {
|
||||||
|
label: "Errore avversario",
|
||||||
|
short: "Err. avv.",
|
||||||
|
nostro: true,
|
||||||
|
richiedeGiocatore: false,
|
||||||
|
className: "bg-secondary text-foreground",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Azione = {
|
export type Azione = {
|
||||||
@@ -143,37 +173,3 @@ export function giocatoriConScout(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Classifica demo aggiornata con i match scoutati (solo la nostra riga). */
|
|
||||||
export function classificaConScout(matches: ScoutMatch[]): RigaClassifica[] {
|
|
||||||
if (matches.length === 0) return classifica;
|
|
||||||
const agg = matches.reduce(
|
|
||||||
(s, m) => {
|
|
||||||
const vinta = m.setNostri > m.setLoro;
|
|
||||||
s.giocate += 1;
|
|
||||||
s.vinte += vinta ? 1 : 0;
|
|
||||||
s.perse += vinta ? 0 : 1;
|
|
||||||
s.setFatti += m.setNostri;
|
|
||||||
s.setSubiti += m.setLoro;
|
|
||||||
s.punti += vinta ? (m.setLoro <= 1 ? 3 : 2) : m.setLoro === 3 && m.setNostri === 2 ? 1 : 0;
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
{ giocate: 0, vinte: 0, perse: 0, setFatti: 0, setSubiti: 0, punti: 0 },
|
|
||||||
);
|
|
||||||
return classifica
|
|
||||||
.map((r) =>
|
|
||||||
r.squadra === "CRAP Volley"
|
|
||||||
? {
|
|
||||||
...r,
|
|
||||||
giocate: r.giocate + agg.giocate,
|
|
||||||
vinte: r.vinte + agg.vinte,
|
|
||||||
perse: r.perse + agg.perse,
|
|
||||||
setFatti: r.setFatti + agg.setFatti,
|
|
||||||
setSubiti: r.setSubiti + agg.setSubiti,
|
|
||||||
punti: r.punti + agg.punti,
|
|
||||||
}
|
|
||||||
: r,
|
|
||||||
)
|
|
||||||
.sort((a, b) => b.punti - a.punti || b.setFatti - b.setSubiti - (a.setFatti - a.setSubiti))
|
|
||||||
.map((r, i) => ({ ...r, pos: i + 1 }));
|
|
||||||
}
|
|
||||||
+1
-1
@@ -87,4 +87,4 @@ export function serieGiocatore(g: Giocatore): SerieStato[] {
|
|||||||
/** La serie migliore da mostrare in home. */
|
/** La serie migliore da mostrare in home. */
|
||||||
export function serieMigliore(g: Giocatore): SerieStato {
|
export function serieMigliore(g: Giocatore): SerieStato {
|
||||||
return serieGiocatore(g).sort((a, b) => b.valore - a.valore)[0]!;
|
return serieGiocatore(g).sort((a, b) => b.valore - a.valore)[0]!;
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,12 @@ async function importaChiave(publicKey: string, privateKey: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function creaVapidJwt(audience: string, subject: string, publicKey: string, privateKey: string) {
|
async function creaVapidJwt(
|
||||||
|
audience: string,
|
||||||
|
subject: string,
|
||||||
|
publicKey: string,
|
||||||
|
privateKey: string,
|
||||||
|
) {
|
||||||
const header = encodeJson({ typ: "JWT", alg: "ES256" });
|
const header = encodeJson({ typ: "JWT", alg: "ES256" });
|
||||||
const payload = encodeJson({
|
const payload = encodeJson({
|
||||||
aud: audience,
|
aud: audience,
|
||||||
@@ -74,4 +79,4 @@ export async function inviaPush(endpoint: string): Promise<number> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res.status;
|
return res.status;
|
||||||
}
|
}
|
||||||
+19
-5
@@ -88,22 +88,36 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
|
|||||||
{ title: "CrAPP — L'app del CRAP Volley" },
|
{ title: "CrAPP — L'app del CRAP Volley" },
|
||||||
{
|
{
|
||||||
name: "description",
|
name: "description",
|
||||||
content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
|
content:
|
||||||
|
"Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
|
||||||
},
|
},
|
||||||
{ name: "author", content: "CRAP Volley" },
|
{ name: "author", content: "CRAP Volley" },
|
||||||
{ name: "theme-color", content: "#111111" },
|
{ name: "theme-color", content: "#111111" },
|
||||||
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
|
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
|
||||||
{
|
{
|
||||||
property: "og:description",
|
property: "og:description",
|
||||||
content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
|
content:
|
||||||
|
"Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
|
||||||
},
|
},
|
||||||
{ property: "og:type", content: "website" },
|
{ property: "og:type", content: "website" },
|
||||||
{ name: "twitter:card", content: "summary_large_image" },
|
{ name: "twitter:card", content: "summary_large_image" },
|
||||||
{ name: "twitter:site", content: "@Lovable" },
|
{ name: "twitter:site", content: "@Lovable" },
|
||||||
{ name: "twitter:title", content: "CrAPP — L'app del CRAP Volley" },
|
{ name: "twitter:title", content: "CrAPP — L'app del CRAP Volley" },
|
||||||
{ name: "twitter:description", content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile." },
|
{
|
||||||
{ property: "og:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png" },
|
name: "twitter:description",
|
||||||
{ name: "twitter:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png" },
|
content:
|
||||||
|
"Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: "og:image",
|
||||||
|
content:
|
||||||
|
"https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "twitter:image",
|
||||||
|
content:
|
||||||
|
"https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,9 +13,15 @@ export const Route = createFileRoute("/allenamento/$id")({
|
|||||||
return {
|
return {
|
||||||
meta: [
|
meta: [
|
||||||
{ title: `${titolo} — CrAPP` },
|
{ title: `${titolo} — CrAPP` },
|
||||||
{ name: "description", content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley." },
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley.",
|
||||||
|
},
|
||||||
{ property: "og:title", content: `${titolo} — CrAPP` },
|
{ property: "og:title", content: `${titolo} — CrAPP` },
|
||||||
{ property: "og:description", content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley." },
|
{
|
||||||
|
property: "og:description",
|
||||||
|
content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley.",
|
||||||
|
},
|
||||||
{ property: "og:type", content: "website" },
|
{ property: "og:type", content: "website" },
|
||||||
{ name: "twitter:card", content: "summary" },
|
{ name: "twitter:card", content: "summary" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ export const Route = createFileRoute("/api/public/push-config")({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -38,9 +38,12 @@ export const Route = createFileRoute("/api/public/push-subscribe")({
|
|||||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||||
|
|
||||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||||
await supabaseAdmin.from("push_subscriptions").delete().eq("endpoint", parsed.data.endpoint);
|
await supabaseAdmin
|
||||||
|
.from("push_subscriptions")
|
||||||
|
.delete()
|
||||||
|
.eq("endpoint", parsed.data.endpoint);
|
||||||
return Response.json({ ok: true });
|
return Response.json({ ok: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,9 @@ function Calendario() {
|
|||||||
setDrawerAperto(true);
|
setDrawerAperto(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventiGiornoSelezionato = giornoSelezionato ? eventiPerGiorno.get(giornoSelezionato) ?? [] : [];
|
const eventiGiornoSelezionato = giornoSelezionato
|
||||||
|
? (eventiPerGiorno.get(giornoSelezionato) ?? [])
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,7 +204,8 @@ function Calendario() {
|
|||||||
.join(", ")})`
|
.join(", ")})`
|
||||||
: undefined;
|
: undefined;
|
||||||
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
|
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
|
||||||
const isOggi = !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
|
const isOggi =
|
||||||
|
!!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
|
||||||
const Cella = haEventi ? "button" : "div";
|
const Cella = haEventi ? "button" : "div";
|
||||||
return (
|
return (
|
||||||
<Cella
|
<Cella
|
||||||
@@ -224,7 +227,9 @@ function Calendario() {
|
|||||||
aria-label={haEventi ? `Eventi del ${giorno}` : undefined}
|
aria-label={haEventi ? `Eventi del ${giorno}` : undefined}
|
||||||
aria-current={isOggi ? "date" : undefined}
|
aria-current={isOggi ? "date" : undefined}
|
||||||
>
|
>
|
||||||
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">{giorno}</span>
|
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">
|
||||||
|
{giorno}
|
||||||
|
</span>
|
||||||
</Cella>
|
</Cella>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+64
-58
@@ -2,10 +2,9 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
|||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||||
import { storicoMatch } from "@/lib/crapp-data";
|
import { useScoutMatches } from "@/lib/scout-store";
|
||||||
import { classificaConScout, useScoutMatches } from "@/lib/scout-store";
|
|
||||||
import { useCsi } from "@/lib/csi";
|
import { useCsi } from "@/lib/csi";
|
||||||
import { isNostraSquadra, partiteGiocate } from "@/lib/csi-core";
|
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
||||||
|
|
||||||
export const Route = createFileRoute("/classifica")({
|
export const Route = createFileRoute("/classifica")({
|
||||||
@@ -34,25 +33,18 @@ function Classifica() {
|
|||||||
const scoutMatches = useScoutMatches();
|
const scoutMatches = useScoutMatches();
|
||||||
const { data: csi } = useCsi();
|
const { data: csi } = useCsi();
|
||||||
|
|
||||||
const classifica = csi?.classifica.length ? csi.classifica : classificaConScout(scoutMatches);
|
const classifica = csi?.classifica ?? [];
|
||||||
const risultati = csi?.partite.length
|
const risultati = csi?.partite.length
|
||||||
? partiteGiocate(csi.partite).map((p) => ({
|
? partiteGiocate(csi.partite).map(matchDaPartitaCsi)
|
||||||
id: p.id,
|
: scoutMatches.map((m) => ({
|
||||||
avversario: p.avversario,
|
id: m.id,
|
||||||
casa: p.casa,
|
data: m.data,
|
||||||
setNostri: p.setNostri ?? 0,
|
avversario: m.avversario,
|
||||||
setLoro: p.setLoro ?? 0,
|
casa: m.casa,
|
||||||
}))
|
setNostri: m.setNostri,
|
||||||
: [
|
setLoro: m.setLoro,
|
||||||
...scoutMatches.map((m) => ({
|
parziali: m.parziali,
|
||||||
id: m.id,
|
}));
|
||||||
avversario: m.avversario,
|
|
||||||
casa: m.casa,
|
|
||||||
setNostri: m.setNostri,
|
|
||||||
setLoro: m.setLoro,
|
|
||||||
})),
|
|
||||||
...storicoMatch,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -82,47 +74,61 @@ function Classifica() {
|
|||||||
<span className="text-center">Set</span>
|
<span className="text-center">Set</span>
|
||||||
<span className="text-center">Pt</span>
|
<span className="text-center">Pt</span>
|
||||||
</div>
|
</div>
|
||||||
{classifica.map((r) => {
|
{classifica.length === 0 ? (
|
||||||
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
|
<p className="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||||
return (
|
Classifica non ancora disponibile.
|
||||||
<div
|
</p>
|
||||||
key={r.pos}
|
) : (
|
||||||
className={cn(
|
classifica.map((r) => {
|
||||||
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-sm last:border-0",
|
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
|
||||||
noi && "bg-accent/10",
|
return (
|
||||||
)}
|
<div
|
||||||
>
|
key={r.pos}
|
||||||
<span className={cn("font-display text-base", noi && "text-accent")}>{r.pos}</span>
|
className={cn(
|
||||||
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>
|
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-sm last:border-0",
|
||||||
{r.squadra}
|
noi && "bg-accent/10",
|
||||||
</span>
|
)}
|
||||||
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
|
>
|
||||||
<span className="text-center text-xs tabular-nums text-muted-foreground">
|
<span className={cn("font-display text-base", noi && "text-accent")}>
|
||||||
{r.setFatti}:{r.setSubiti}
|
{r.pos}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-center font-bold tabular-nums">{r.punti}</span>
|
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>
|
||||||
</div>
|
{r.squadra}
|
||||||
);
|
</span>
|
||||||
})}
|
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
|
||||||
|
<span className="text-center text-xs tabular-nums text-muted-foreground">
|
||||||
|
{r.setFatti}:{r.setSubiti}
|
||||||
|
</span>
|
||||||
|
<span className="text-center font-bold tabular-nums">{r.punti}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section titolo="Ultimi risultati ufficiali">
|
<Section titolo="Ultimi risultati ufficiali">
|
||||||
<div className="space-y-2">
|
{risultati.length === 0 ? (
|
||||||
{risultati.map((m) => (
|
<p className="rounded-3xl bg-card p-4 text-center text-xs text-muted-foreground shadow-card">
|
||||||
<div
|
Nessun risultato disponibile.
|
||||||
key={m.id}
|
</p>
|
||||||
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-card p-3 shadow-card"
|
) : (
|
||||||
>
|
<div className="space-y-2">
|
||||||
<p className="truncate text-sm">
|
{risultati.map((m) => (
|
||||||
{m.casa ? "CRAP Volley" : m.avversario} — {m.casa ? m.avversario : "CRAP Volley"}
|
<div
|
||||||
</p>
|
key={m.id}
|
||||||
<span className="font-display text-lg tabular-nums">
|
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-card p-3 shadow-card"
|
||||||
{m.casa ? `${m.setNostri}-${m.setLoro}` : `${m.setLoro}-${m.setNostri}`}
|
>
|
||||||
</span>
|
<p className="truncate text-sm">
|
||||||
</div>
|
{m.casa ? "CRAP Volley" : m.avversario} — {m.casa ? m.avversario : "CRAP Volley"}
|
||||||
))}
|
</p>
|
||||||
</div>
|
<span className="font-display text-lg tabular-nums">
|
||||||
|
{m.casa ? `${m.setNostri}-${m.setLoro}` : `${m.setLoro}-${m.setNostri}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+10
-3
@@ -120,7 +120,9 @@ function GestioneEventi() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bozza ? (
|
{bozza ? (
|
||||||
<Section titolo={eventi.some((e) => e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}>
|
<Section
|
||||||
|
titolo={eventi.some((e) => e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}
|
||||||
|
>
|
||||||
<div className="space-y-3 rounded-3xl bg-card p-4 shadow-card">
|
<div className="space-y-3 rounded-3xl bg-card p-4 shadow-card">
|
||||||
<div className="grid grid-cols-4 gap-1 rounded-full bg-secondary p-1">
|
<div className="grid grid-cols-4 gap-1 rounded-full bg-secondary p-1">
|
||||||
{tipi.map((t) => (
|
{tipi.map((t) => (
|
||||||
@@ -241,7 +243,9 @@ function GestioneEventi() {
|
|||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
|
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
|
||||||
scelto ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
|
scelto
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "bg-secondary text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{g.nome}
|
{g.nome}
|
||||||
@@ -278,7 +282,10 @@ function GestioneEventi() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eventi.map((e) => (
|
{eventi.map((e) => (
|
||||||
<div key={e.id} className="flex items-center gap-2 rounded-3xl bg-card p-3 shadow-card">
|
<div
|
||||||
|
key={e.id}
|
||||||
|
className="flex items-center gap-2 rounded-3xl bg-card p-3 shadow-card"
|
||||||
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
|
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
|||||||
+44
-25
@@ -8,10 +8,13 @@ import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
|||||||
import { Reveal } from "@/components/motion/Reveal";
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
import { Barra } from "@/components/motion/Barra";
|
import { Barra } from "@/components/motion/Barra";
|
||||||
import { Numero } from "@/components/motion/Numero";
|
import { Numero } from "@/components/motion/Numero";
|
||||||
import { classifica, storicoMatch } from "@/lib/crapp-data";
|
|
||||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||||
import { useEventi, type Evento } from "@/lib/eventi";
|
import { useEventi, type Evento } from "@/lib/eventi";
|
||||||
import { useIo, useObiettivi } from "@/lib/rosa";
|
import { useIo, useObiettivi } from "@/lib/rosa";
|
||||||
|
import { useCsi } from "@/lib/csi";
|
||||||
|
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
|
import { useScoutMatches } from "@/lib/scout-store";
|
||||||
|
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -40,8 +43,17 @@ function Index() {
|
|||||||
const prossimi: Evento[] = eventi.filter((e) => e.data >= oggi).slice(0, 3);
|
const prossimi: Evento[] = eventi.filter((e) => e.data >= oggi).slice(0, 3);
|
||||||
const prossimo = prossimi[0] ?? null;
|
const prossimo = prossimi[0] ?? null;
|
||||||
const linkProssimo = prossimo ? linkPerEvento(prossimo) : null;
|
const linkProssimo = prossimo ? linkPerEvento(prossimo) : null;
|
||||||
const noi = classifica.find((r) => r.squadra === "CRAP Volley")!;
|
const { data: csi } = useCsi();
|
||||||
const ultima = storicoMatch[0]!;
|
const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra));
|
||||||
|
const scoutMatches = useScoutMatches();
|
||||||
|
const votiMvp = useVotiMvp();
|
||||||
|
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
|
||||||
|
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||||
|
const ultima = csiGiocate[0]
|
||||||
|
? { ...matchDaPartitaCsi(csiGiocate[0]), mvp: mvpPerMatch[csiGiocate[0].id] ?? "" }
|
||||||
|
: scoutMatches[0]
|
||||||
|
? { ...scoutMatches[0], mvp: mvpPerMatch[scoutMatches[0].id] ?? scoutMatches[0].mvp }
|
||||||
|
: null;
|
||||||
const obiettivi = useObiettivi();
|
const obiettivi = useObiettivi();
|
||||||
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
|
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
|
||||||
|
|
||||||
@@ -62,12 +74,12 @@ function Index() {
|
|||||||
|
|
||||||
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
|
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
|
||||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||||
<p className="font-display text-2xl leading-none">{noi.pos}º</p>
|
<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-[10px] uppercase text-primary-foreground/60">In classifica</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||||
<p className="font-display text-2xl leading-none">
|
<p className="font-display text-2xl leading-none">
|
||||||
{noi.vinte}-{noi.perse}
|
{noi ? `${noi.vinte}-${noi.perse}` : "—"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] uppercase text-primary-foreground/60">Bilancio</p>
|
<p className="text-[10px] uppercase text-primary-foreground/60">Bilancio</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,29 +142,36 @@ function Index() {
|
|||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="premi rounded-3xl bg-card p-4 shadow-card">
|
{ultima ? (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="premi rounded-3xl bg-card p-4 shadow-card">
|
||||||
<div className="min-w-0">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="truncate text-sm font-bold">CRAP Volley vs {ultima.avversario}</p>
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="truncate text-sm font-bold">CRAP Volley vs {ultima.avversario}</p>
|
||||||
{ultima.casa ? "In casa" : "Trasferta"} · MVP {ultima.mvp}
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{ultima.casa ? "In casa" : "Trasferta"}
|
||||||
|
{ultima.mvp ? ` · MVP ${ultima.mvp}` : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="font-display text-3xl leading-none text-accent">
|
||||||
|
{ultima.setNostri}-{ultima.setLoro}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-display text-3xl leading-none text-accent">
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||||
{ultima.setNostri}-{ultima.setLoro}
|
{ultima.parziali.map((p, i) => (
|
||||||
</p>
|
<span
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums"
|
||||||
|
>
|
||||||
|
{p[0]}-{p[1]}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
) : (
|
||||||
{ultima.parziali.map((p, i) => (
|
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
|
||||||
<span
|
Nessun risultato disponibile.
|
||||||
key={i}
|
</p>
|
||||||
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums"
|
)}
|
||||||
>
|
|
||||||
{p[0]}-{p[1]}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
|||||||
import { ArrowLeft, MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
|
import { ArrowLeft, MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||||
import { storicoMatch, formatData, giocatori } from "@/lib/crapp-data";
|
import { formatData, giocatori } from "@/lib/crapp-data";
|
||||||
import { convocatiEvento, useEvento } from "@/lib/eventi";
|
import { convocatiEvento, useEvento } from "@/lib/eventi";
|
||||||
|
import { useCsi } from "@/lib/csi";
|
||||||
|
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
import { Pagelle } from "@/components/crapp/Pagelle";
|
import { Pagelle } from "@/components/crapp/Pagelle";
|
||||||
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
|
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
|
||||||
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
||||||
@@ -22,9 +24,15 @@ export const Route = createFileRoute("/partita/$id")({
|
|||||||
return {
|
return {
|
||||||
meta: [
|
meta: [
|
||||||
{ title: `${titolo} — CrAPP` },
|
{ title: `${titolo} — CrAPP` },
|
||||||
{ name: "description", content: "Dettaglio partita, formazione e risultati del CRAP Volley." },
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
||||||
|
},
|
||||||
{ property: "og:title", content: `${titolo} — CrAPP` },
|
{ property: "og:title", content: `${titolo} — CrAPP` },
|
||||||
{ property: "og:description", content: "Dettaglio partita, formazione e risultati del CRAP Volley." },
|
{
|
||||||
|
property: "og:description",
|
||||||
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
||||||
|
},
|
||||||
{ property: "og:type", content: "website" },
|
{ property: "og:type", content: "website" },
|
||||||
{ name: "twitter:card", content: "summary" },
|
{ name: "twitter:card", content: "summary" },
|
||||||
],
|
],
|
||||||
@@ -39,6 +47,7 @@ function PartitaDetail() {
|
|||||||
const io = useGiocatoreCorrente();
|
const io = useGiocatoreCorrente();
|
||||||
const admin = useIsAdmin();
|
const admin = useIsAdmin();
|
||||||
const scoutMatches = useScoutMatches();
|
const scoutMatches = useScoutMatches();
|
||||||
|
const { data: csi } = useCsi();
|
||||||
const { risposte } = usePresenzeEvento(id);
|
const { risposte } = usePresenzeEvento(id);
|
||||||
const presentiVeri = giocatori.filter(
|
const presentiVeri = giocatori.filter(
|
||||||
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
||||||
@@ -60,6 +69,9 @@ function PartitaDetail() {
|
|||||||
|
|
||||||
const convocati = convocatiEvento(evento);
|
const convocati = convocatiEvento(evento);
|
||||||
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
|
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
|
||||||
|
const csiMatch = csi
|
||||||
|
? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
|
||||||
|
: undefined;
|
||||||
const match = scout
|
const match = scout
|
||||||
? {
|
? {
|
||||||
id: scout.id,
|
id: scout.id,
|
||||||
@@ -70,10 +82,12 @@ function PartitaDetail() {
|
|||||||
setLoro: scout.setLoro,
|
setLoro: scout.setLoro,
|
||||||
parziali: scout.parziali,
|
parziali: scout.parziali,
|
||||||
}
|
}
|
||||||
: storicoMatch.find((m) => m.data === evento.data);
|
: csiMatch
|
||||||
|
? matchDaPartitaCsi(csiMatch)
|
||||||
|
: undefined;
|
||||||
const totaliTeam = scout ? totaliSquadra([scout]) : null;
|
const totaliTeam = scout ? totaliSquadra([scout]) : null;
|
||||||
const avversario = evento.titolo.includes(" vs ")
|
const avversario = evento.titolo.includes(" vs ")
|
||||||
? evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo
|
? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo)
|
||||||
: evento.titolo;
|
: evento.titolo;
|
||||||
const casa = evento.casa;
|
const casa = evento.casa;
|
||||||
const vinta = match && match.setNostri > match.setLoro;
|
const vinta = match && match.setNostri > match.setLoro;
|
||||||
@@ -140,7 +154,9 @@ function PartitaDetail() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
||||||
vinta ? "bg-success text-success-foreground" : "bg-destructive text-destructive-foreground",
|
vinta
|
||||||
|
? "bg-success text-success-foreground"
|
||||||
|
: "bg-destructive text-destructive-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{match.setNostri} - {match.setLoro}
|
{match.setNostri} - {match.setLoro}
|
||||||
@@ -149,7 +165,10 @@ function PartitaDetail() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
{match.parziali.map(([noi, loro], i) => (
|
{match.parziali.map(([noi, loro], i) => (
|
||||||
<div key={i} className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2">
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
||||||
|
>
|
||||||
<span className="font-display text-lg">{noi}</span>
|
<span className="font-display text-lg">{noi}</span>
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Set {i + 1}</span>
|
<span className="text-xs font-semibold text-muted-foreground">Set {i + 1}</span>
|
||||||
<span className="font-display text-lg">{loro}</span>
|
<span className="font-display text-lg">{loro}</span>
|
||||||
@@ -171,7 +190,8 @@ function PartitaDetail() {
|
|||||||
) : (
|
) : (
|
||||||
<Section titolo="In programma">
|
<Section titolo="In programma">
|
||||||
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
||||||
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il risultato.
|
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
|
||||||
|
risultato.
|
||||||
</p>
|
</p>
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
@@ -229,7 +249,9 @@ function PartitaDetail() {
|
|||||||
{admin ? (
|
{admin ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => scaricaCsv(`scout-${scout.data}-${scout.avversario}.csv`, csvScoutMatch(scout))}
|
onClick={() =>
|
||||||
|
scaricaCsv(`scout-${scout.data}-${scout.avversario}.csv`, csvScoutMatch(scout))
|
||||||
|
}
|
||||||
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" /> Esporta CSV
|
<Download className="h-4 w-4" /> Esporta CSV
|
||||||
|
|||||||
+22
-12
@@ -32,7 +32,10 @@ export const Route = createFileRoute("/profilo")({
|
|||||||
content: "Foto, ruolo, statistiche personali, badge e obiettivi del giocatore CRAP Volley.",
|
content: "Foto, ruolo, statistiche personali, badge e obiettivi del giocatore CRAP Volley.",
|
||||||
},
|
},
|
||||||
{ property: "og:title", content: "Profilo giocatore — CrAPP" },
|
{ property: "og:title", content: "Profilo giocatore — CrAPP" },
|
||||||
{ property: "og:description", content: "Statistiche personali, badge e presenze della stagione." },
|
{
|
||||||
|
property: "og:description",
|
||||||
|
content: "Statistiche personali, badge e presenze della stagione.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
component: Profilo,
|
component: Profilo,
|
||||||
@@ -51,7 +54,9 @@ function Profilo() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSupportate(pushSupportato());
|
setSupportate(pushSupportato());
|
||||||
statoNotifiche().then(setNotifiche).catch(() => setNotifiche(false));
|
statoNotifiche()
|
||||||
|
.then(setNotifiche)
|
||||||
|
.catch(() => setNotifiche(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!g) return null;
|
if (!g) return null;
|
||||||
@@ -204,20 +209,25 @@ function Profilo() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid h-8 w-8 shrink-0 place-items-center rounded-xl",
|
"grid h-8 w-8 shrink-0 place-items-center rounded-xl",
|
||||||
notifiche ? "bg-accent-grad text-accent-foreground" : "bg-secondary text-muted-foreground",
|
notifiche
|
||||||
|
? "bg-accent-grad text-accent-foreground"
|
||||||
|
: "bg-secondary text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Bell className="h-4 w-4" />
|
<Bell className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{["Notifiche convocazioni", "Promemoria allenamenti", "Cambi orario", "Bacheca squadra"].map(
|
{[
|
||||||
(v) => (
|
"Notifiche convocazioni",
|
||||||
<label key={v} className="flex items-center justify-between gap-3 px-4 py-3 text-sm">
|
"Promemoria allenamenti",
|
||||||
<span className="min-w-0 truncate">{v}</span>
|
"Cambi orario",
|
||||||
<input type="checkbox" defaultChecked className="h-5 w-9 accent-[var(--accent)]" />
|
"Bacheca squadra",
|
||||||
</label>
|
].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 ? (
|
{admin ? (
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
@@ -239,4 +249,4 @@ function Profilo() {
|
|||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+20
-7
@@ -53,7 +53,17 @@ export const Route = createFileRoute("/scout")({
|
|||||||
|
|
||||||
const ordineAzioni: AzioneTipo[] = ["attacco", "ace", "muro", "errore"];
|
const ordineAzioni: AzioneTipo[] = ["attacco", "ace", "muro", "errore"];
|
||||||
|
|
||||||
function Blocco({ icona, titolo, testo, children }: { icona: React.ReactNode; titolo: string; testo: string; children?: React.ReactNode }) {
|
function Blocco({
|
||||||
|
icona,
|
||||||
|
titolo,
|
||||||
|
testo,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icona: React.ReactNode;
|
||||||
|
titolo: string;
|
||||||
|
testo: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 py-10">
|
<div className="px-5 py-10">
|
||||||
<div className="rounded-3xl bg-card p-6 text-center shadow-card">
|
<div className="rounded-3xl bg-card p-6 text-center shadow-card">
|
||||||
@@ -101,7 +111,9 @@ function Scout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
|
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
|
||||||
return <Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />;
|
return (
|
||||||
|
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!partita) {
|
if (!partita) {
|
||||||
@@ -187,7 +199,10 @@ function ScoutBoard({
|
|||||||
const base =
|
const base =
|
||||||
iniziale ??
|
iniziale ??
|
||||||
statoIniziale(
|
statoIniziale(
|
||||||
partita.titolo.replace(/CRAP Volley/gi, "").replace(/\s*vs\s*/i, "").trim() || "Avversario",
|
partita.titolo
|
||||||
|
.replace(/CRAP Volley/gi, "")
|
||||||
|
.replace(/\s*vs\s*/i, "")
|
||||||
|
.trim() || "Avversario",
|
||||||
partita.titolo.trim().toLowerCase().startsWith("crap"),
|
partita.titolo.trim().toLowerCase().startsWith("crap"),
|
||||||
);
|
);
|
||||||
const [avversario, setAvversario] = useState(base.avversario);
|
const [avversario, setAvversario] = useState(base.avversario);
|
||||||
@@ -439,9 +454,7 @@ function ScoutBoard({
|
|||||||
i === 0 && "anim-riga",
|
i === 0 && "anim-riga",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
|
||||||
{g ? `#${g.numero} ${g.nome}` : "Avversario"}
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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-[10px] font-bold uppercase",
|
||||||
@@ -505,4 +518,4 @@ function ScoutBoard({
|
|||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+59
-33
@@ -4,11 +4,13 @@ import { Cake, ChevronDown, Crown } from "lucide-react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||||
import { Avatar } from "@/components/crapp/Avatar";
|
import { Avatar } from "@/components/crapp/Avatar";
|
||||||
import { formatData, storicoMatch } from "@/lib/crapp-data";
|
import { formatData } from "@/lib/crapp-data";
|
||||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||||
import { useRosa, useObiettivi } from "@/lib/rosa";
|
import { useRosa, useObiettivi } from "@/lib/rosa";
|
||||||
import { usePresenzeUltimoMeseTutti } from "@/lib/presenze-mese";
|
import { usePresenzeUltimoMeseTutti } from "@/lib/presenze-mese";
|
||||||
import { totaliSquadra } from "@/lib/scout-store";
|
import { totaliSquadra } from "@/lib/scout-store";
|
||||||
|
import { useCsi } from "@/lib/csi";
|
||||||
|
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
import { mediaSquadra, usePagelle } from "@/lib/pagelle";
|
import { mediaSquadra, usePagelle } from "@/lib/pagelle";
|
||||||
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
||||||
import { StatTile } from "@/components/crapp/ui-bits";
|
import { StatTile } from "@/components/crapp/ui-bits";
|
||||||
@@ -43,7 +45,8 @@ export const Route = createFileRoute("/squadra")({
|
|||||||
{ title: "Squadra CRAP Volley — CrAPP" },
|
{ title: "Squadra CRAP Volley — CrAPP" },
|
||||||
{
|
{
|
||||||
name: "description",
|
name: "description",
|
||||||
content: "Rosa completa del CRAP Volley: giocatori, dati anagrafici, statistiche stagionali e badge sbloccati.",
|
content:
|
||||||
|
"Rosa completa del CRAP Volley: giocatori, dati anagrafici, statistiche stagionali e badge sbloccati.",
|
||||||
},
|
},
|
||||||
{ property: "og:title", content: "Squadra CRAP Volley — CrAPP" },
|
{ property: "og:title", content: "Squadra CRAP Volley — CrAPP" },
|
||||||
{
|
{
|
||||||
@@ -55,7 +58,6 @@ export const Route = createFileRoute("/squadra")({
|
|||||||
component: Squadra,
|
component: Squadra,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const criteri = [
|
const criteri = [
|
||||||
{ id: "presenze", label: "Presenze" },
|
{ id: "presenze", label: "Presenze" },
|
||||||
{ id: "mediaVoto", label: "Media voto" },
|
{ id: "mediaVoto", label: "Media voto" },
|
||||||
@@ -85,24 +87,31 @@ function Squadra() {
|
|||||||
const obiettivi = useObiettivi();
|
const obiettivi = useObiettivi();
|
||||||
const team = totaliSquadra(scoutMatches);
|
const team = totaliSquadra(scoutMatches);
|
||||||
const mediaPresenze = rosa.length
|
const mediaPresenze = rosa.length
|
||||||
? Math.round((rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100)
|
? Math.round(
|
||||||
|
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
||||||
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
||||||
const tuttiMatch = [
|
const { data: csi } = useCsi();
|
||||||
...scoutMatches.map((m) => ({
|
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||||
id: m.id,
|
const tuttiMatch = csiGiocate.length
|
||||||
data: m.data,
|
? csiGiocate.map((p) => ({
|
||||||
avversario: m.avversario,
|
...matchDaPartitaCsi(p),
|
||||||
casa: m.casa,
|
mvp: mvpPerMatch[p.id] ?? "",
|
||||||
setNostri: m.setNostri,
|
scout: false,
|
||||||
setLoro: m.setLoro,
|
}))
|
||||||
parziali: m.parziali,
|
: scoutMatches.map((m) => ({
|
||||||
mvp: mvpPerMatch[m.id] ?? "",
|
id: m.id,
|
||||||
scout: true,
|
data: m.data,
|
||||||
})),
|
avversario: m.avversario,
|
||||||
...storicoMatch.map((m) => ({ ...m, scout: false })),
|
casa: m.casa,
|
||||||
];
|
setNostri: m.setNostri,
|
||||||
|
setLoro: m.setLoro,
|
||||||
|
parziali: m.parziali,
|
||||||
|
mvp: mvpPerMatch[m.id] ?? "",
|
||||||
|
scout: true,
|
||||||
|
}));
|
||||||
const completati = obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
|
const completati = obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
|
||||||
const mediaObiettivi = obiettivi.length
|
const mediaObiettivi = obiettivi.length
|
||||||
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
||||||
@@ -116,7 +125,10 @@ function Squadra() {
|
|||||||
<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 = [...stati.filter((b) => b.grado !== null), ...badgeSegretiSbloccati(g)];
|
const sbloccati = [
|
||||||
|
...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">
|
||||||
@@ -126,7 +138,11 @@ function Squadra() {
|
|||||||
className="flex w-full items-center gap-3 p-3 text-left active:scale-[0.99]"
|
className="flex w-full items-center gap-3 p-3 text-left active:scale-[0.99]"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<Avatar id={g.id} fallback={g.numero ? String(g.numero) : g.iniziali} className="h-11 w-11 text-lg" />
|
<Avatar
|
||||||
|
id={g.id}
|
||||||
|
fallback={g.numero ? String(g.numero) : g.iniziali}
|
||||||
|
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">{g.nome}</span>
|
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
|
||||||
<span className="mt-1 flex items-center gap-2">
|
<span className="mt-1 flex items-center gap-2">
|
||||||
@@ -160,19 +176,20 @@ function Squadra() {
|
|||||||
<div className="border-t border-border px-4 pb-4 pt-3">
|
<div className="border-t border-border px-4 pb-4 pt-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)} {g.nascita.slice(0, 4)}
|
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
||||||
|
{g.nascita.slice(0, 4)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
{ l: "Presenze", v: `${g.presenze}/${g.totaliEventi}` },
|
{ l: "Presenze", v: `${g.presenze}/${g.totaliEventi}` },
|
||||||
{ l: "Presenze 30gg", v: `${mese[g.id]?.percentuale ?? 0}%` },
|
{ l: "Presenze 30gg", v: `${mese[g.id]?.percentuale ?? 0}%` },
|
||||||
{ l: "Presenze di fila", v: g.streak },
|
{ l: "Presenze di fila", v: g.streak },
|
||||||
{ l: "Media voto", v: g.mediaVoto || "—" },
|
{ l: "Media voto", v: g.mediaVoto || "—" },
|
||||||
{ l: "MVP", v: g.mvp },
|
{ l: "MVP", v: g.mvp },
|
||||||
{ l: "Cacche/partita 💩", v: g.cacchePartita || "—" },
|
{ l: "Cacche/partita 💩", v: g.cacchePartita || "—" },
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
|
<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="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-[10px] font-semibold uppercase text-muted-foreground">
|
||||||
@@ -201,9 +218,16 @@ function Squadra() {
|
|||||||
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
|
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
{b.valore} {b.def.unita}
|
{b.valore} {b.def.unita}
|
||||||
{b.prossimaSoglia ? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}` : ""}
|
{b.prossimaSoglia
|
||||||
|
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
||||||
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<p className={cn("mt-1.5 text-[10px] font-bold uppercase", meta.text)}>
|
<p
|
||||||
|
className={cn(
|
||||||
|
"mt-1.5 text-[10px] font-bold uppercase",
|
||||||
|
meta.text,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +247,7 @@ function Squadra() {
|
|||||||
<Section titolo="Statistiche di squadra">
|
<Section titolo="Statistiche di squadra">
|
||||||
<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={storicoMatch.length + scoutMatches.length} label="Match giocati" />
|
<StatTile valore={tuttiMatch.length} label="Match giocati" />
|
||||||
<StatTile valore={mediaSquadra(pagelle) || "—"} label="Media pagelle" />
|
<StatTile valore={mediaSquadra(pagelle) || "—"} label="Media pagelle" />
|
||||||
<StatTile valore={team.punti} label="Punti squadra" />
|
<StatTile valore={team.punti} label="Punti squadra" />
|
||||||
<StatTile valore={team.ace} label="Ace squadra" />
|
<StatTile valore={team.ace} label="Ace squadra" />
|
||||||
@@ -411,7 +435,9 @@ function Squadra() {
|
|||||||
const meta = gradoMeta[grado];
|
const meta = gradoMeta[grado];
|
||||||
const quanti = rosa.filter((g) => {
|
const quanti = rosa.filter((g) => {
|
||||||
const raggiunto = gradoRaggiunto(b, b.valore(g));
|
const raggiunto = gradoRaggiunto(b, b.valore(g));
|
||||||
return raggiunto ? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado) : false;
|
return raggiunto
|
||||||
|
? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado)
|
||||||
|
: false;
|
||||||
}).length;
|
}).length;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
+6
-1
@@ -98,7 +98,12 @@
|
|||||||
--bronzo: oklch(0.62 0.11 55);
|
--bronzo: oklch(0.62 0.11 55);
|
||||||
--argento: oklch(0.72 0.02 260);
|
--argento: oklch(0.72 0.02 260);
|
||||||
--oro: oklch(0.8 0.15 85);
|
--oro: oklch(0.8 0.15 85);
|
||||||
--gradient-hero: linear-gradient(140deg, oklch(0.18 0.012 265), oklch(0.24 0.03 20) 55%, oklch(0.45 0.19 27));
|
--gradient-hero: linear-gradient(
|
||||||
|
140deg,
|
||||||
|
oklch(0.18 0.012 265),
|
||||||
|
oklch(0.24 0.03 20) 55%,
|
||||||
|
oklch(0.45 0.19 27)
|
||||||
|
);
|
||||||
--gradient-accent: linear-gradient(120deg, oklch(0.58 0.22 27), oklch(0.68 0.19 40));
|
--gradient-accent: linear-gradient(120deg, oklch(0.58 0.22 27), oklch(0.68 0.19 40));
|
||||||
--shadow-card: 0 1px 2px oklch(0.16 0.01 260 / 6%), 0 12px 28px -18px oklch(0.16 0.01 260 / 35%);
|
--shadow-card: 0 1px 2px oklch(0.16 0.01 260 / 6%), 0 12px 28px -18px oklch(0.16 0.01 260 / 35%);
|
||||||
--shadow-pop: 0 18px 40px -20px oklch(0.58 0.22 27 / 55%);
|
--shadow-pop: 0 18px 40px -20px oklch(0.58 0.22 27 / 55%);
|
||||||
|
|||||||
Reference in new issue
Block a user