From 55352af0f2cb18729ddbcf57c92d301dadac04ea Mon Sep 17 00:00:00 2001 From: Luca Lombardo Date: Wed, 18 Jun 2025 16:05:09 +0200 Subject: [PATCH 01/13] Add media pesata page and corresponding styles - Created a new page for calculating weighted averages and graduation votes (`media-pesata.astro`) with a layout and introductory text. - Implemented a script to initialize the media pesata application. - Added CSS styles for the media pesata page and its components. - Updated existing media page (`media.astro`) to include similar structure and functionality. - Introduced error handling for the application initialization script. - Established a comprehensive CSS file for the media pesata application, including styles for various UI elements and responsive design. --- src/client/MediaPesataApp.tsx | 654 ++++++++++++++++++++++++ src/components/Header.astro | 1 + src/pages/media-pesata.astro | 42 ++ src/pages/media.astro | 49 ++ src/styles/pages/media-pesata.css | 813 ++++++++++++++++++++++++++++++ 5 files changed, 1559 insertions(+) create mode 100644 src/client/MediaPesataApp.tsx create mode 100644 src/pages/media-pesata.astro create mode 100644 src/pages/media.astro create mode 100644 src/styles/pages/media-pesata.css diff --git a/src/client/MediaPesataApp.tsx b/src/client/MediaPesataApp.tsx new file mode 100644 index 0000000..2c2568f --- /dev/null +++ b/src/client/MediaPesataApp.tsx @@ -0,0 +1,654 @@ +import { useState, useEffect } from 'preact/hooks' +import { render } from 'preact' + +// Tipi per la gestione dei dati +type TipoStudente = 'triennale' | 'magistrale' + +interface Corso { + nome: string + anno: '1' | '2' | '3' | 'M' | 'istituzioni' + cfu: number + passFailOnly?: boolean // Per materie senza voto (pass/fail) +} + +interface CorsoSelezionato { + id: string + nome: string + cfu: number + voto: number | null + lode: boolean + passFailOnly?: boolean // Per materie senza voto (pass/fail) +} + +interface CorsoCustom { + nome: string + cfu: number +} + +// Dati dei corsi dal XML fornito +const CORSI_DISPONIBILI: Corso[] = [ + // Primo Anno + { nome: 'Analisi matematica 1', anno: '1', cfu: 15 }, + { nome: 'Aritmetica', anno: '1', cfu: 9 }, + { nome: 'Fisica I con laboratorio', anno: '1', cfu: 9 }, + { nome: 'Fondamenti di programmazione con laboratorio', anno: '1', cfu: 9 }, + { nome: 'Geometria 1', anno: '1', cfu: 15 }, + { nome: 'Laboratorio di introduzione alla matematica computazionale', anno: '1', cfu: 6, passFailOnly: true }, + { nome: 'Algebra 1', anno: '1', cfu: 6 }, + + // Secondo Anno + { nome: 'Algoritmi e strutture dati', anno: '2', cfu: 6 }, + { nome: 'Analisi matematica 2', anno: '2', cfu: 12 }, + { nome: 'Analisi numerica con laboratorio', anno: '2', cfu: 9 }, + { nome: 'Elementi di probabilità e statistica', anno: '2', cfu: 6 }, + { nome: 'Geometria 2', anno: '2', cfu: 12 }, + { nome: 'Algebra 2', anno: '2', cfu: 6 }, + + // Terzo Anno + { nome: 'Analisi matematica 3', anno: '3', cfu: 6 }, + { nome: 'Calcolo scientifico', anno: '3', cfu: 6 }, + { nome: 'Elementi di analisi complessa', anno: '3', cfu: 6 }, + { nome: 'Elementi di calcolo delle variazioni', anno: '3', cfu: 6 }, + { nome: 'Elementi di geometria algebrica', anno: '3', cfu: 6 }, + { nome: 'Elementi di meccanica celeste', anno: '3', cfu: 6 }, + { nome: 'Elementi di teoria degli insiemi', anno: '3', cfu: 6 }, + { nome: 'Elementi di teoria delle rappresentazioni', anno: '3', cfu: 6 }, + { nome: 'Elementi di topologia algebrica', anno: '3', cfu: 6 }, + { nome: 'Equazioni alle derivate parziali', anno: '3', cfu: 6 }, + { nome: 'Fisica II', anno: '3', cfu: 9 }, + { nome: 'Fisica III', anno: '3', cfu: 6 }, + { nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 }, + { nome: 'Laboratorio computazionale', anno: '3', cfu: 6 }, + { nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6 }, + { nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 }, + { nome: 'Logica matematica', anno: '3', cfu: 6 }, + { nome: 'Matematiche elementari da un punto di vista superiore: geometria', anno: '3', cfu: 6 }, + { nome: 'Meccanica razionale', anno: '3', cfu: 6 }, + { nome: 'Metodi numerici per equazioni differenziali ordinarie', anno: '3', cfu: 6 }, + { nome: 'Ottimizzazione non lineare', anno: '3', cfu: 6 }, + { nome: 'Probabilità', anno: '3', cfu: 6 }, + { nome: 'Ricerca operativa', anno: '3', cfu: 6 }, + { nome: 'Sistemi dinamici', anno: '3', cfu: 6 }, + { nome: 'Statistica matematica', anno: '3', cfu: 6 }, + { nome: 'Teoria algebrica dei numeri 1', anno: '3', cfu: 6 }, + { nome: 'Teoria della misura', anno: '3', cfu: 6 }, + + // Istituzioni (Magistrale) + { nome: 'Istituzioni di algebra', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di analisi matematica', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di analisi numerica', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di didattica della matematica', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di fisica matematica', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di geometria', anno: 'istituzioni', cfu: 11 }, + { nome: 'Istituzioni di probabilità', anno: 'istituzioni', cfu: 11 }, + + // Materie a scelta (Magistrale con "M" ma non istituzioni) + { nome: 'Algebra superiore A', anno: 'M', cfu: 6 }, + { nome: 'Analisi armonica', anno: 'M', cfu: 6 }, + { nome: 'Analisi dei dati', anno: 'M', cfu: 6 }, + { nome: 'Analisi superiore A', anno: 'M', cfu: 6 }, + { nome: 'Analisi superiore B', anno: 'M', cfu: 6 }, + { nome: 'Aspetti matematici nella computazione quantistica', anno: 'M', cfu: 6 }, + { nome: 'Combinatoria algebrica', anno: 'M', cfu: 6 }, + { nome: 'Complementi di analisi funzionale', anno: 'M', cfu: 6 }, + { nome: 'Complementi di meccanica razionale', anno: 'M', cfu: 6 }, + { nome: 'Didattica della matematica e nuove tecnologie', anno: 'M', cfu: 6 }, + { nome: 'Dinamica del sistema solare', anno: 'M', cfu: 6 }, + { nome: 'Dinamica iperbolica', anno: 'M', cfu: 6 }, + { nome: 'Dinamica olomorfa', anno: 'M', cfu: 6 }, + { nome: 'Elementi di calcolo in gruppi omogenei', anno: 'M', cfu: 6 }, + { nome: 'Fisica matematica', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica complessa', anno: 'M', cfu: 6 }, + { nome: 'Geometria differenziale complessa', anno: 'M', cfu: 6 }, + { nome: 'Geometria iperbolica', anno: 'M', cfu: 6 }, + { nome: 'Geometria riemanniana', anno: 'M', cfu: 6 }, + { nome: 'Gruppi di Galois e gruppi fondamentali', anno: 'M', cfu: 6 }, + { nome: 'Meccanica superiore', anno: 'M', cfu: 6 }, + { nome: 'Metodi matematici della meccanica quantistica', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per il calcolo tensoriale', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per il controllo ottimo', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per problemi inversi', anno: 'M', cfu: 6 }, + { nome: "Metodi probabilistici per l'algebra lineare numerica", anno: 'M', cfu: 6 }, + { nome: 'Modelli matematici in biomedicina e fisica matematica', anno: 'M', cfu: 6 }, + { nome: 'Sistemi dinamici aleatori', anno: 'M', cfu: 6 }, + { nome: 'Superfici di Riemann e curve algebriche', anno: 'M', cfu: 6 }, + { nome: 'Teoria dei giochi', anno: 'M', cfu: 6 }, + { nome: 'Teoria delle categorie', anno: 'M', cfu: 6 }, + { nome: 'Teoria delle rappresentazioni A', anno: 'M', cfu: 6 }, + { nome: 'Topologia algebrica A', anno: 'M', cfu: 6 }, + { nome: 'Topologia e geometria in bassa dimensione', anno: 'M', cfu: 6 }, +] + +function MediaPesataApp() { + const [tipoStudente, setTipoStudente] = useState('triennale') + const [corsiSelezionati, setCorsiSelezionati] = useState([]) + const [showCustomForm, setShowCustomForm] = useState(false) + const [customCorso, setCustomCorso] = useState({ nome: '', cfu: 0 }) + const [sezioniAperte, setSezioniAperte] = useState>({}) + const [mostraRisultati, setMostraRisultati] = useState(false) + + const toggleSezione = (nomeSezione: string) => { + setSezioniAperte(prev => ({ + ...prev, + [nomeSezione]: !prev[nomeSezione], + })) + } + + const calcolaMedia = () => { + const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly) + if (corsiConVoto.length === 0) { + alert('Inserisci almeno un voto per calcolare la media!') + return + } + setMostraRisultati(true) + } + + // Funzioni per la gestione dei corsi + const aggiungiCorso = (corso: Corso) => { + // Controlla se la materia esiste già + const materiaEsiste = corsiSelezionati.some(c => c.nome.toLowerCase() === corso.nome.toLowerCase()) + if (materiaEsiste) { + alert('Questa materia è già stata aggiunta') + return + } + + // Controllo per magistrali: massimo 3 istituzioni + if (tipoStudente === 'magistrale' && corso.anno === 'istituzioni') { + const istituzioniAttuali = corsiSelezionati.filter(c => c.nome.toLowerCase().includes('istituzioni')).length + if (istituzioniAttuali >= 3) { + alert('Puoi selezionare al massimo 3 istituzioni per la magistrale') + return + } + } + + const id = Date.now().toString() + const nuovoCorso: CorsoSelezionato = { + id, + nome: corso.nome, + cfu: corso.cfu, + voto: null, + lode: false, + passFailOnly: corso.passFailOnly, + } + setCorsiSelezionati([...corsiSelezionati, nuovoCorso]) + } + + const aggiungiCorsoCustom = () => { + // Validazioni + if (!customCorso.nome.trim()) { + alert('Il nome della materia non può essere vuoto') + return + } + if (customCorso.cfu <= 0 || customCorso.cfu > 30) { + alert('I CFU devono essere tra 1 e 30') + return + } + if (!Number.isInteger(customCorso.cfu)) { + alert('I CFU devono essere un numero intero') + return + } + + // Controlla se la materia esiste già + const materiaEsiste = corsiSelezionati.some( + corso => corso.nome.toLowerCase().trim() === customCorso.nome.toLowerCase().trim(), + ) + if (materiaEsiste) { + alert('Questa materia è già stata aggiunta') + return + } + + const id = Date.now().toString() + const nuovoCorso: CorsoSelezionato = { + id, + nome: customCorso.nome.trim(), + cfu: customCorso.cfu, + voto: null, + lode: false, + } + setCorsiSelezionati([...corsiSelezionati, nuovoCorso]) + setCustomCorso({ nome: '', cfu: 0 }) + setShowCustomForm(false) + } + + const rimuoviCorso = (id: string) => { + setCorsiSelezionati(corsiSelezionati.filter(corso => corso.id !== id)) + } + + const aggiornaVoto = (id: string, voto: number | null) => { + // Validazione voto: deve essere tra 18 e 30 e intero + if (voto !== null && (voto < 18 || voto > 30 || !Number.isInteger(voto))) { + return // Ignora valori non validi + } + + setCorsiSelezionati( + corsiSelezionati.map(corso => + corso.id === id ? { ...corso, voto, lode: voto !== 30 ? false : corso.lode } : corso, + ), + ) + } + + const aggiornaLode = (id: string, lode: boolean) => { + setCorsiSelezionati(corsiSelezionati.map(corso => (corso.id === id ? { ...corso, lode } : corso))) + } + + const resetTutto = () => { + if (corsiSelezionati.length > 0) { + if (confirm('Sei sicuro di voler cancellare tutte le materie selezionate?')) { + setCorsiSelezionati([]) + setCustomCorso({ nome: '', cfu: 0 }) + setShowCustomForm(false) + } + } + } + + // Calcoli della media pesata + const calcolaMediaPesata = () => { + // Escludi le materie pass/fail dal calcolo + const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly) + if (corsiConVoto.length === 0) { + return { + mediaPesata: 0, + votoAmmissione: 0, + massimoVotoLaurea: 0, + conLode: false, + bonusLodi: 0, + errore: 'Nessun voto inserito per il calcolo della media', + } + } + + // Calcola CFU totali con voto + const cfuTotaliConVoto = corsiConVoto.reduce((sum, corso) => sum + corso.cfu, 0) + const cfuDaEscludere = tipoStudente === 'triennale' ? 15 : 9 + + // Se i CFU sono insufficienti per l'esclusione, avvisa l'utente + if (cfuTotaliConVoto < cfuDaEscludere) { + return { + mediaPesata: 0, + votoAmmissione: 0, + massimoVotoLaurea: 0, + conLode: false, + bonusLodi: 0, + errore: `Hai inserito solo ${cfuTotaliConVoto} CFU con voto. Servono almeno ${cfuDaEscludere} CFU per applicare le regole di esclusione.`, + } + } + + // Ordina per voto crescente + const corsiOrdinati = [...corsiConVoto].sort((a, b) => a.voto! - b.voto!) + + let cfuEsclusi = 0 + const corsiValidi: CorsoSelezionato[] = [] + + for (const corso of corsiOrdinati) { + if (cfuEsclusi < cfuDaEscludere) { + const cfuRimanentiDaEscludere = cfuDaEscludere - cfuEsclusi + if (corso.cfu <= cfuRimanentiDaEscludere) { + // Escludi tutto il corso + cfuEsclusi += corso.cfu + } else { + // Escludi solo una parte del corso + const cfuValidi = corso.cfu - cfuRimanentiDaEscludere + corsiValidi.push({ ...corso, cfu: cfuValidi }) + cfuEsclusi = cfuDaEscludere + } + } else { + corsiValidi.push(corso) + } + } + + // Calcola media pesata + const sommaPesata = corsiValidi.reduce((sum, corso) => sum + corso.voto! * corso.cfu, 0) + const sommaCfu = corsiValidi.reduce((sum, corso) => sum + corso.cfu, 0) + + const mediaPesata = sommaCfu > 0 ? sommaPesata / sommaCfu : 0 + + // Calcola bonus lodi + const bonusLodi = corsiConVoto.reduce((bonus, corso) => { + if (corso.lode) { + return bonus + (corso.cfu > 6 ? 0.5 : 0.25) + } + return bonus + }, 0) + + const bonusLodiFinal = Math.min(bonusLodi, 2) + + // Voto di ammissione alla laurea (media pesata * 11/3) + const votoAmmissione = (mediaPesata * 11) / 3 + + // Voto di ammissione finale = voto ammissione + bonus lodi + const votoAmmissioneFinale = votoAmmissione + bonusLodiFinal + + // Massimo voto di laurea possibile = voto ammissione + 10 (cappato a 110) + const massimoVotoLaurea = Math.min(votoAmmissioneFinale + 10, 110) + const conLode = massimoVotoLaurea === 110 + + return { + mediaPesata: Math.round(mediaPesata * 100) / 100, + votoAmmissione: Math.round(votoAmmissioneFinale * 100) / 100, + massimoVotoLaurea: Math.round(massimoVotoLaurea * 100) / 100, + conLode, + bonusLodi: Math.round(bonusLodiFinal * 100) / 100, + errore: null, + } + } + + // Filtra corsi disponibili in base al tipo di studente + const getCorsiDisponibili = () => { + if (tipoStudente === 'triennale') { + return CORSI_DISPONIBILI + } else { + return CORSI_DISPONIBILI.filter( + corso => corso.anno === 'istituzioni' || corso.anno === '3' || corso.anno === 'M', + ) + } + } + + // Raggruppa corsi per categoria + const raggruppaCorsi = () => { + const corsi = getCorsiDisponibili() + const gruppi: Record = {} + + if (tipoStudente === 'triennale') { + gruppi['Primo Anno'] = corsi.filter(c => c.anno === '1') + gruppi['Secondo Anno'] = corsi.filter(c => c.anno === '2') + gruppi['Terzo Anno'] = corsi.filter(c => c.anno === '3') + gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni') + gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === 'M') + } else { + // Per magistrali: prima le istituzioni, poi tutto il resto come "Materie a Scelta" + gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni') + gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === '3' || c.anno === 'M') + } + + return gruppi + } + + const cambiaTipoStudente = (nuovoTipo: TipoStudente) => { + if (corsiSelezionati.length > 0 && nuovoTipo !== tipoStudente) { + if ( + !confirm( + 'Cambiando il tipo di studente, alcune materie potrebbero non essere più disponibili. Continuare?', + ) + ) { + return + } + } + setTipoStudente(nuovoTipo) + } + + const gruppiCorsi = raggruppaCorsi() + const risultati = calcolaMediaPesata() + const totaleCfu = corsiSelezionati.reduce((sum, corso) => sum + corso.cfu, 0) + const maxCfu = tipoStudente === 'triennale' ? 171 : 93 // 180 − 9 e 120 − 27 per la tesi + const cfuError = totaleCfu > maxCfu + + return ( +
+ {/* Selezione tipo studente */} +
+

Corso di Laurea

+
+ + +
+
+ + {/* Counter CFU */} +
+

+ CFU Totali: {totaleCfu}/{maxCfu} + {tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'} +

+ {cfuError &&

⚠️ Hai superato il limite di CFU consentiti!

} +
+ +
+ {/* Sezione selezione corsi */} +
+

Seleziona Materie

+ + {Object.entries(gruppiCorsi).map(([categoria, corsi]) => ( +
+ + + {sezioniAperte[categoria] && ( +
+ {corsi.map((corso, index) => ( + + ))} +
+ )} +
+ ))} + + {/* Form per materia custom */} +
+

Materia Personalizzata

+ {!showCustomForm ? ( + + ) : ( +
+ + setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value }) + } + /> + + setCustomCorso({ + ...customCorso, + cfu: parseInt((e.target as HTMLInputElement).value) || 0, + }) + } + /> + + +
+ )} +
+
+ + {/* Sezione lista corsi selezionati */} +
+
+

Materie Selezionate

+ {corsiSelezionati.length > 0 && ( + + )} +
+ + {corsiSelezionati.length === 0 ? ( +

Nessuna materia selezionata

+ ) : ( +
+ {corsiSelezionati.map(corso => ( +
+
+ {corso.nome} + {corso.cfu} CFU + {corso.passFailOnly && Pass/Fail} +
+ + {!corso.passFailOnly && ( +
+ + aggiornaVoto( + corso.id, + parseInt((e.target as HTMLInputElement).value) || null, + ) + } + /> + + +
+ )} + + +
+ ))} +
+ )} +
+
+ + {/* Pulsante Calcola */} + {corsiSelezionati.length > 0 && ( +
+ +
+ )} + + {/* Risultati */} + {risultati && mostraRisultati && ( +
+

Risultati

+ {risultati.errore ? ( +
+

⚠️ {risultati.errore}

+
+ ) : ( +
+
+ Media Pesata: + {risultati.mediaPesata} +
+
+ Bonus Lodi: + +{risultati.bonusLodi} +
+
+ Voto di Ammissione: + {risultati.votoAmmissione} +
+
+ Massimo Voto Di Laurea Possibile: + + {risultati.massimoVotoLaurea} + {risultati.conLode && (+lode)} + +
+
+ )} +
+ )} + + {/* Nota informativa */} +
+

📋 Come viene calcolata la media

+
+

Regole di esclusione CFU:

+
    +
  • + Triennale: I 15 CFU con i voti più bassi vengono esclusi dalla media +
  • +
  • + Magistrale: I 9 CFU con i voti più bassi vengono esclusi dalla media +
  • +
  • Se un corso ha più CFU di quelli da escludere, viene diviso proporzionalmente
  • +
+ +

Calcolo del voto finale:

+
    +
  • + Media pesata: Somma dei (voto × CFU) diviso per i CFU totali +
  • +
  • + Bonus lodi: +0.5 per lodi in materie > 6 CFU, +0.25 per lodi in materie + ≤ 6 CFU (max +2) +
  • +
  • + Voto di laurea: (Voto finale × 11) ÷ 3 +
  • +
+ +

Note:

+
    +
  • + Le materie Pass/Fail non contribuiscono al calcolo della media +
  • +
  • Il voto finale è limitato a 30
  • +
  • Per i magistrali: massimo 3 istituzioni selezionabili
  • +
+
+
+
+ ) +} + +// Funzione per inizializzare l'app +export function initMediaPesataApp() { + const container = document.getElementById('media-pesata-app') + if (container) { + render(, container) + } +} + +export default MediaPesataApp diff --git a/src/components/Header.astro b/src/components/Header.astro index dcb6da6..4633c91 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -6,6 +6,7 @@ const links = [ { href: '/notizie', text: 'Notizie' }, { href: '/guide', text: 'Guide' }, { href: '/domande-esami', text: 'Domande Orali' }, + // { href: '/media-pesata', text: 'Media Pesata' }, // Beta testing - solo URL diretto { href: '/storia', text: 'Storia' }, // { href: '/login', text: 'Login' }, ] diff --git a/src/pages/media-pesata.astro b/src/pages/media-pesata.astro new file mode 100644 index 0000000..b80c1d9 --- /dev/null +++ b/src/pages/media-pesata.astro @@ -0,0 +1,42 @@ +--- +import '@/styles/pages/media-pesata.css' +import PageLayout from '../layouts/PageLayout.astro' +--- + + +
+

Calcolo Media e Voto di Laurea

+

Calcola la tua media pesata e il voto con cui ti siederai alla discussione di laurea, seguendo le regole del dipartimento di Matematica.

+ +
+
+
+ + + + diff --git a/src/pages/media.astro b/src/pages/media.astro new file mode 100644 index 0000000..10b6b80 --- /dev/null +++ b/src/pages/media.astro @@ -0,0 +1,49 @@ +--- +import '@/styles/pages/media-pesata.css' +import PageLayout from '../layouts/PageLayout.astro' +--- + + +
+

Voto Laurea

+

Calcola la tua media pesata e il voto con cui ti siederai alla discussione di laurea, seguendo le regole del dipartimento di Matematica.

+ +
+
+
+ + + + diff --git a/src/styles/pages/media-pesata.css b/src/styles/pages/media-pesata.css new file mode 100644 index 0000000..63ef853 --- /dev/null +++ b/src/styles/pages/media-pesata.css @@ -0,0 +1,813 @@ +@layer page { + .media-pesata-app { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; + background: var(--homepage-whatsphc-bg); + } + + .media-pesata-title { + text-align: center; + color: var(--palette-black); + margin-bottom: 2rem; + font-family: var(--font-display); + font-weight: 700; + font-size: 2.5rem; + } + + /* Selezione tipo studente */ + .student-type-selector { + background: #fff; + border: var(--border-large); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 1.5rem; + margin-bottom: 2rem; + } + + .student-type-selector h2 { + margin-bottom: 1rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.5rem; + text-align: center; + } + + .radio-group { + display: flex; + gap: 1.5rem; + justify-content: center; + } + + .radio-group label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.1rem; + cursor: pointer; + padding: 0.75rem 1.5rem; + border: 3px solid var(--palette-black); + border-radius: 6px; + background: #fff; + font-family: var(--font-secondary); + font-weight: 600; + transition: all 64ms linear; + box-shadow: 4px 4px 0 0 var(--palette-black); + } + + .radio-group label:hover { + transform: translate(-1px, -1px); + box-shadow: 5px 5px 0 0 var(--palette-black); + } + + .radio-group input[type='radio']:checked + span { + background: var(--homepage-projects-bg); + } + + .radio-group input[type='radio'] { + width: 18px; + height: 18px; + margin: 0; + } + + /* Counter CFU */ + .cfu-counter { + background: var(--project-card-bg); + border: var(--border-large); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 1rem; + margin-bottom: 2rem; + text-align: center; + } + + .cfu-counter.error { + background: #fee; + border-color: #dc3545; + } + + .cfu-counter h3 { + margin: 0; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.3rem; + } + + .error-message { + color: #d63031; + font-weight: bold; + margin: 0; + font-family: var(--font-secondary); + } + + .error-message { + background: #ff6b7a; + border: 2px solid var(--palette-black); + border-radius: 6px; + padding: 1rem; + color: var(--palette-black); + font-weight: 600; + } + + .error-message p { + margin: 0; + text-align: center; + } + + /* Layout principale */ + .main-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; + } + + @media (max-width: 768px) { + .main-content { + grid-template-columns: 1fr; + } + } + + /* Sezione selezione corsi */ + .course-selection { + background: #fff; + border: var(--border-large); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 1.5rem; + } + + .course-selection h2 { + margin-bottom: 1.5rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.5rem; + } + + .course-category { + margin-bottom: 2rem; + } + + /* Rimozione del vecchio stile per h3 che causava il rettangolo blu */ + + .course-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 0.5rem; + margin-top: 1rem; + padding-top: 0.5rem; + } + + .course-button { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0.75rem; + border: 2px solid var(--palette-black); + border-radius: 4px; + background: #f8f9fa; + cursor: pointer; + transition: all 64ms linear; + text-align: left; + font-family: var(--font-secondary); + } + + .course-button:hover:not(:disabled) { + background: var(--homepage-projects-bg); + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .course-button:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #e0e0e0; + } + + .course-button.selected { + background: var(--guide-base); + box-shadow: 2px 2px 0 0 var(--palette-black); + transform: translate(1px, 1px); + } + + .course-name { + font-weight: 600; + font-size: 0.9rem; + line-height: 1.3; + margin-bottom: 0.25rem; + color: var(--palette-black); + } + + .course-cfu { + font-size: 0.8rem; + color: #666; + font-weight: bold; + font-family: var(--font-mono); + } + + /* Materia personalizzata */ + .custom-course { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 3px solid var(--palette-black); + } + + .custom-course h3 { + margin-bottom: 1rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.2rem; + } + + .add-custom-btn { + background: #1e6733; + color: #f4fef7; + border: 3px solid var(--palette-black); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 0.75rem 1.5rem; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + font-family: var(--font-secondary); + transition: all 64ms linear; + } + + .add-custom-btn:hover { + background: #2b8b47; + transform: translate(-1px, -1px); + box-shadow: 5px 5px 0 0 var(--palette-black); + } + + .custom-form { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-top: 1rem; + } + + .custom-form input { + padding: 0.5rem; + border: 2px solid var(--palette-black); + border-radius: 4px; + font-size: 0.9rem; + background: #fff; + font-family: var(--font-primary); + } + + .custom-form input[type='text'] { + flex: 2; + min-width: 200px; + } + + .custom-form input[type='number'] { + flex: 0 0 80px; + } + + .confirm-btn, + .cancel-btn { + padding: 0.5rem 1rem; + border: 2px solid var(--palette-black); + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + font-family: var(--font-secondary); + transition: all 64ms linear; + } + + .confirm-btn { + background: #28a745; + color: white; + } + + .confirm-btn:hover { + background: #218838; + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .cancel-btn { + background: #6c757d; + color: white; + } + + .cancel-btn:hover { + background: #5a6268; + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + /* Sezione corsi selezionati */ + .selected-courses { + background: #fff; + border: var(--border-large); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 1.5rem; + } + + .selected-courses h2 { + margin-bottom: 1.5rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.5rem; + } + + .no-courses { + text-align: center; + color: #666; + font-style: italic; + padding: 2rem; + font-family: var(--font-secondary); + } + + .courses-list { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .course-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: var(--guide-base); + border: 2px solid var(--palette-black); + border-radius: 4px; + transition: all 64ms linear; + } + + .course-item:hover { + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .course-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .course-info .course-name { + font-weight: 600; + font-size: 0.9rem; + color: var(--palette-black); + font-family: var(--font-secondary); + } + + .course-info .course-cfu { + font-size: 0.8rem; + color: #666; + font-family: var(--font-mono); + } + + .course-grade { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .course-grade input[type='number'] { + width: 90px; + padding: 0.5rem; + border: 2px solid var(--palette-black); + border-radius: 4px; + font-size: 0.9rem; + text-align: center; + background: #fff; + font-weight: 600; + font-family: var(--font-primary); + } + + .lode-checkbox { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.8rem; + cursor: pointer; + font-weight: 600; + font-family: var(--font-secondary); + } + + .lode-checkbox.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .lode-checkbox input[type='checkbox'] { + width: 16px; + height: 16px; + margin: 0; + } + + .remove-btn { + background: #d63031; + color: white; + border: 2px solid var(--palette-black); + width: 30px; + height: 30px; + border-radius: 50%; + aspect-ratio: 1; + cursor: pointer; + font-size: 1.2rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 64ms linear; + flex-shrink: 0; + } + + .remove-btn:hover { + background: #b71c1c; + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + /* Calcolo e risultati */ + .calculation-section { + background: var(--homepage-principal-bg); + border: var(--border-large); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 2rem; + text-align: center; + margin-top: 2rem; + } + + .calculate-button { + background: #1e6733; + color: #f4fef7; + border: 3px solid var(--palette-black); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 1rem 2rem; + font-size: 1.2rem; + font-weight: 600; + font-family: var(--font-secondary); + cursor: pointer; + transition: all 64ms linear; + margin-bottom: 2rem; + } + + .calculate-button:hover { + background: #2b8b47; + transform: translate(-1px, -1px); + box-shadow: 5px 5px 0 0 var(--palette-black); + } + + .calculate-button:active { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .results { + background: #fff; + border: 3px solid var(--palette-black); + border-radius: 6px; + box-shadow: 4px 4px 0 0 var(--palette-black); + padding: 2rem; + margin-top: 1rem; + } + + .results h2 { + margin-bottom: 1.5rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.6rem; + text-align: center; + } + + .results-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; + } + + /* Ordine degli elementi: il massimo voto di laurea al centro */ + .result-item:nth-child(4) { + order: -1; + grid-column: 1 / -1; + justify-self: center; + max-width: 500px; + width: 100%; + } + + .result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: var(--guide-base); + border: 2px solid var(--palette-black); + border-radius: 4px; + font-family: var(--font-secondary); + font-weight: 600; + font-size: 1rem; + } + + .result-item.highlight { + background: var(--homepage-projects-bg); + font-weight: bold; + font-size: 1.2rem; + } + + .result-item .label { + font-weight: 600; + color: var(--palette-black); + font-size: 0.9rem; + line-height: 1.2; + } + + .result-item .value { + font-weight: bold; + font-size: 1.1rem; + font-family: var(--font-mono); + color: var(--palette-black); + } + + .pass-fail-badge { + background: var(--color-accent); + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .lode-badge { + color: var(--palette-black); + font-weight: bold; + margin-left: 0.5rem; + } + + /* Responsive */ + @media (max-width: 768px) { + .media-pesata-app { + padding: 1rem; + } + + .course-grid { + grid-template-columns: 1fr; + } + + .course-item { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + } + + .course-info { + flex: 1 1 100%; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + } + + .course-grade { + flex: 1 1 auto; + justify-content: flex-start; + gap: 0.5rem; + } + + .course-grade input[type='number'] { + width: 70px; + } + + .remove-btn { + width: 28px; + height: 28px; + font-size: 1rem; + flex-shrink: 0; + } + + .custom-form { + flex-direction: column; + } + + .results-grid { + grid-template-columns: 1fr; + } + + /* Mantieni il massimo voto centrato anche su mobile */ + .result-item:nth-child(4) { + order: -1; + grid-column: 1; + justify-self: center; + max-width: 100%; + } + } + + /* Layout ultra-compatto per schermi molto piccoli */ + @media (max-width: 480px) { + .course-item { + padding: 0.5rem; + gap: 0.25rem; + } + + .course-info { + flex: 1 1 100%; + margin-bottom: 0.25rem; + } + + .course-info .course-name { + font-size: 0.85rem; + } + + .course-info .course-cfu { + font-size: 0.75rem; + } + + .course-grade { + flex: 1 1 auto; + gap: 0.25rem; + } + + .course-grade input[type='number'] { + width: 65px; + padding: 0.375rem; + font-size: 0.85rem; + } + + .lode-checkbox { + font-size: 0.75rem; + } + + .remove-btn { + width: 26px; + height: 26px; + font-size: 0.9rem; + } + } + + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + + .reset-btn { + background: #d63031; + color: white; + border: 2px solid var(--palette-black); + padding: 0.5rem 1rem; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 64ms linear; + } + + .reset-btn:hover { + background: #b71c1c; + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .cfu-info { + font-size: 0.9rem; + color: #666; + margin: 0.25rem 0 0 0; + font-weight: normal; + } + + .error-text { + color: #d63031; + font-weight: 600; + margin: 0.5rem 0 0 0; + } + + /* Category headers collapsible */ + .category-header { + width: 100%; + background: #e4c5ff; + border: 2px solid var(--palette-black); + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 64ms linear; + margin-bottom: 0.5rem; + min-height: 36px; + } + + .category-header:hover { + background: #d9b3ff; + transform: translate(-1px, -1px); + box-shadow: 1px 1px 0 0 var(--palette-black); + } + + .category-header h3 { + margin: 0; + font-family: var(--font-display); + font-weight: 600; + color: var(--palette-black); + font-size: 1rem; + } + + .toggle-icon { + font-size: 1rem; + color: var(--palette-black); + transition: transform 150ms ease; + user-select: none; + } + + .toggle-icon.expanded { + transform: rotate(90deg); + } + + /* Calculate button */ + .calculate-section { + text-align: center; + margin: 2rem 0; + } + + .calculate-btn { + background: #1e6733; + color: #f4fef7; + border: var(--border-large); + border-radius: 6px; + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 64ms linear; + font-family: var(--font-primary); + display: inline-block; + } + + .calculate-btn:hover { + background: #2b8b47; + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0 0 var(--palette-black); + } + + /* Info section */ + .info-section { + background: var(--color-accent-light); + border: var(--border-large); + border-radius: 8px; + padding: 1.5rem; + margin: 2rem 0; + } + + .info-section h3 { + margin-top: 0; + margin-bottom: 1rem; + color: var(--palette-black); + font-family: var(--font-display); + font-weight: 600; + } + + .info-content h4 { + color: var(--palette-black); + font-weight: 600; + margin-top: 1rem; + margin-bottom: 0.5rem; + } + + .info-content ul { + margin-bottom: 1rem; + padding-left: 1.5rem; + } + + .info-content li { + margin-bottom: 0.5rem; + line-height: 1.4; + } + + /* Gestione testi lunghi su schermi più piccoli */ + @media (max-width: 480px) { + .result-item { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .result-item .label { + font-size: 0.85rem; + } + + .result-item .value { + align-self: flex-end; + font-size: 1.2rem; + } + } +} From 4780133019603f1775da635c343588732d2f73e6 Mon Sep 17 00:00:00 2001 From: Luca Lombardo Date: Wed, 18 Jun 2025 16:05:57 +0200 Subject: [PATCH 02/13] fix: restore 'Calcolo Media' link in header navigation --- src/components/Header.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Header.astro b/src/components/Header.astro index 4633c91..db587a4 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -6,7 +6,7 @@ const links = [ { href: '/notizie', text: 'Notizie' }, { href: '/guide', text: 'Guide' }, { href: '/domande-esami', text: 'Domande Orali' }, - // { href: '/media-pesata', text: 'Media Pesata' }, // Beta testing - solo URL diretto + { href: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto { href: '/storia', text: 'Storia' }, // { href: '/login', text: 'Login' }, ] From 34597fe9f848c020526aa6e5a27963e12ec06a56 Mon Sep 17 00:00:00 2001 From: Luca Lombardo Date: Wed, 18 Jun 2025 16:29:48 +0200 Subject: [PATCH 03/13] feat: implement localStorage functionality for MediaPesataApp and remove media.astro page --- src/client/MediaPesataApp.tsx | 61 ++++++++++++++++++++++++-- src/pages/media.astro | 49 --------------------- src/styles/pages/media-pesata.css | 72 +++++++++++++++++++++++++------ 3 files changed, 115 insertions(+), 67 deletions(-) delete mode 100644 src/pages/media.astro diff --git a/src/client/MediaPesataApp.tsx b/src/client/MediaPesataApp.tsx index 2c2568f..50fbd1e 100644 --- a/src/client/MediaPesataApp.tsx +++ b/src/client/MediaPesataApp.tsx @@ -120,12 +120,57 @@ const CORSI_DISPONIBILI: Corso[] = [ ] function MediaPesataApp() { - const [tipoStudente, setTipoStudente] = useState('triennale') - const [corsiSelezionati, setCorsiSelezionati] = useState([]) + // Funzioni per localStorage + const loadFromStorage = () => { + try { + const savedData = localStorage.getItem('media-pesata-data') + if (savedData) { + const parsed = JSON.parse(savedData) + return { + tipoStudente: parsed.tipoStudente || 'triennale', + corsiSelezionati: parsed.corsiSelezionati || [], + sezioniAperte: parsed.sezioniAperte || {}, + mostraRisultati: parsed.mostraRisultati || false, + } + } + } catch (error) { + console.warn('Errore nel caricamento dei dati salvati:', error) + } + return { + tipoStudente: 'triennale' as TipoStudente, + corsiSelezionati: [], + sezioniAperte: {}, + mostraRisultati: false, + } + } + + const saveToStorage = (data: any) => { + try { + localStorage.setItem('media-pesata-data', JSON.stringify(data)) + } catch (error) { + console.warn('Errore nel salvataggio dei dati:', error) + } + } + + // Inizializzazione con dati salvati + const initialData = loadFromStorage() + const [tipoStudente, setTipoStudente] = useState(initialData.tipoStudente) + const [corsiSelezionati, setCorsiSelezionati] = useState(initialData.corsiSelezionati) const [showCustomForm, setShowCustomForm] = useState(false) const [customCorso, setCustomCorso] = useState({ nome: '', cfu: 0 }) - const [sezioniAperte, setSezioniAperte] = useState>({}) - const [mostraRisultati, setMostraRisultati] = useState(false) + const [sezioniAperte, setSezioniAperte] = useState>(initialData.sezioniAperte) + const [mostraRisultati, setMostraRisultati] = useState(initialData.mostraRisultati) + + // Salva automaticamente quando cambiano i dati importanti + useEffect(() => { + const dataToSave = { + tipoStudente, + corsiSelezionati, + sezioniAperte, + mostraRisultati, + } + saveToStorage(dataToSave) + }, [tipoStudente, corsiSelezionati, sezioniAperte, mostraRisultati]) const toggleSezione = (nomeSezione: string) => { setSezioniAperte(prev => ({ @@ -237,6 +282,14 @@ function MediaPesataApp() { setCorsiSelezionati([]) setCustomCorso({ nome: '', cfu: 0 }) setShowCustomForm(false) + setSezioniAperte({}) + setMostraRisultati(false) + // Pulisce anche il localStorage + try { + localStorage.removeItem('media-pesata-data') + } catch (error) { + console.warn('Errore nella pulizia del localStorage:', error) + } } } } diff --git a/src/pages/media.astro b/src/pages/media.astro deleted file mode 100644 index 10b6b80..0000000 --- a/src/pages/media.astro +++ /dev/null @@ -1,49 +0,0 @@ ---- -import '@/styles/pages/media-pesata.css' -import PageLayout from '../layouts/PageLayout.astro' ---- - - -
-

Voto Laurea

-

Calcola la tua media pesata e il voto con cui ti siederai alla discussione di laurea, seguendo le regole del dipartimento di Matematica.

- -
-
-
- - - - diff --git a/src/styles/pages/media-pesata.css b/src/styles/pages/media-pesata.css index 63ef853..0c16d19 100644 --- a/src/styles/pages/media-pesata.css +++ b/src/styles/pages/media-pesata.css @@ -47,29 +47,31 @@ gap: 0.5rem; font-size: 1.1rem; cursor: pointer; - padding: 0.75rem 1.5rem; - border: 3px solid var(--palette-black); + padding: 0.5rem 1rem; + border: 2px solid var(--palette-black); border-radius: 6px; background: #fff; font-family: var(--font-secondary); font-weight: 600; transition: all 64ms linear; - box-shadow: 4px 4px 0 0 var(--palette-black); + box-shadow: 2px 2px 0 0 var(--palette-black); } .radio-group label:hover { transform: translate(-1px, -1px); - box-shadow: 5px 5px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); } - .radio-group input[type='radio']:checked + span { - background: var(--homepage-projects-bg); + .radio-group label:active { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } .radio-group input[type='radio'] { width: 18px; height: 18px; margin: 0; + accent-color: #007bff; } /* Counter CFU */ @@ -174,12 +176,18 @@ transition: all 64ms linear; text-align: left; font-family: var(--font-secondary); + box-shadow: 2px 2px 0 0 var(--palette-black); } .course-button:hover:not(:disabled) { background: var(--homepage-projects-bg); transform: translate(-1px, -1px); - box-shadow: 2px 2px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); + } + + .course-button:active:not(:disabled) { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } .course-button:disabled { @@ -190,8 +198,8 @@ .course-button.selected { background: var(--guide-base); - box-shadow: 2px 2px 0 0 var(--palette-black); transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } .course-name { @@ -284,23 +292,35 @@ .confirm-btn { background: #28a745; color: white; + box-shadow: 2px 2px 0 0 var(--palette-black); } .confirm-btn:hover { background: #218838; transform: translate(-1px, -1px); - box-shadow: 2px 2px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); + } + + .confirm-btn:active { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } .cancel-btn { background: #6c757d; color: white; + box-shadow: 2px 2px 0 0 var(--palette-black); } .cancel-btn:hover { background: #5a6268; transform: translate(-1px, -1px); - box-shadow: 2px 2px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); + } + + .cancel-btn:active { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } /* Sezione corsi selezionati */ @@ -424,12 +444,18 @@ justify-content: center; transition: all 64ms linear; flex-shrink: 0; + box-shadow: 2px 2px 0 0 var(--palette-black); } .remove-btn:hover { background: #b71c1c; transform: translate(-1px, -1px); - box-shadow: 2px 2px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); + } + + .remove-btn:active { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } /* Calcolo e risultati */ @@ -671,12 +697,18 @@ font-weight: 600; cursor: pointer; transition: all 64ms linear; + box-shadow: 2px 2px 0 0 var(--palette-black); } .reset-btn:hover { background: #b71c1c; transform: translate(-1px, -1px); - box-shadow: 2px 2px 0 0 var(--palette-black); + box-shadow: 3px 3px 0 0 var(--palette-black); + } + + .reset-btn:active { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 0 var(--palette-black); } .cfu-info { @@ -706,11 +738,17 @@ transition: all 64ms linear; margin-bottom: 0.5rem; min-height: 36px; + box-shadow: 1px 1px 0 0 var(--palette-black); } .category-header:hover { background: #d9b3ff; transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 0 var(--palette-black); + } + + .category-header:active { + transform: translate(0px, 0px); box-shadow: 1px 1px 0 0 var(--palette-black); } @@ -751,12 +789,18 @@ transition: all 64ms linear; font-family: var(--font-primary); display: inline-block; + box-shadow: 4px 4px 0 0 var(--palette-black); } .calculate-btn:hover { background: #2b8b47; - transform: translate(-2px, -2px); - box-shadow: 4px 4px 0 0 var(--palette-black); + transform: translate(-1px, -1px); + box-shadow: 5px 5px 0 0 var(--palette-black); + } + + .calculate-btn:active { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0 0 var(--palette-black); } /* Info section */ From 15c2b44fcf371fc5ace7131b73cef15f0e757079 Mon Sep 17 00:00:00 2001 From: Luca Lombardo Date: Wed, 18 Jun 2025 18:45:23 +0200 Subject: [PATCH 04/13] feat: add passFailOnly property to 'Laboratorio computazionale' course --- src/client/MediaPesataApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/MediaPesataApp.tsx b/src/client/MediaPesataApp.tsx index 50fbd1e..06df3c5 100644 --- a/src/client/MediaPesataApp.tsx +++ b/src/client/MediaPesataApp.tsx @@ -58,7 +58,7 @@ const CORSI_DISPONIBILI: Corso[] = [ { nome: 'Fisica II', anno: '3', cfu: 9 }, { nome: 'Fisica III', anno: '3', cfu: 6 }, { nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 }, - { nome: 'Laboratorio computazionale', anno: '3', cfu: 6 }, + { nome: 'Laboratorio computazionale', anno: '3', cfu: 6, passFailOnly: true }, { nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6 }, { nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 }, { nome: 'Logica matematica', anno: '3', cfu: 6 }, From 4c19ffdd9fd242d46513d92f04c5d27d664b69b0 Mon Sep 17 00:00:00 2001 From: Luca Lombardo Date: Wed, 18 Jun 2025 19:06:33 +0200 Subject: [PATCH 05/13] fix: update course data comments and add new courses with passFailOnly property --- src/client/MediaPesataApp.tsx | 82 +++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/src/client/MediaPesataApp.tsx b/src/client/MediaPesataApp.tsx index 06df3c5..a9991b3 100644 --- a/src/client/MediaPesataApp.tsx +++ b/src/client/MediaPesataApp.tsx @@ -25,7 +25,7 @@ interface CorsoCustom { cfu: number } -// Dati dei corsi dal XML fornito +// Dati dei corsi aggiornati dalla tabella ufficiale const CORSI_DISPONIBILI: Corso[] = [ // Primo Anno { nome: 'Analisi matematica 1', anno: '1', cfu: 15 }, @@ -34,18 +34,22 @@ const CORSI_DISPONIBILI: Corso[] = [ { nome: 'Fondamenti di programmazione con laboratorio', anno: '1', cfu: 9 }, { nome: 'Geometria 1', anno: '1', cfu: 15 }, { nome: 'Laboratorio di introduzione alla matematica computazionale', anno: '1', cfu: 6, passFailOnly: true }, - { nome: 'Algebra 1', anno: '1', cfu: 6 }, + { nome: 'Laboratorio di comunicazione mediante calcolatore', anno: '1', cfu: 3, passFailOnly: true }, // Secondo Anno + { nome: 'Algebra 1', anno: '2', cfu: 6 }, { nome: 'Algoritmi e strutture dati', anno: '2', cfu: 6 }, { nome: 'Analisi matematica 2', anno: '2', cfu: 12 }, { nome: 'Analisi numerica con laboratorio', anno: '2', cfu: 9 }, { nome: 'Elementi di probabilità e statistica', anno: '2', cfu: 6 }, { nome: 'Geometria 2', anno: '2', cfu: 12 }, - { nome: 'Algebra 2', anno: '2', cfu: 6 }, + { nome: 'Inglese scientifico', anno: '2', cfu: 6, passFailOnly: true }, + { nome: 'Laboratorio didattico di matematica computazionale', anno: '2', cfu: 3, passFailOnly: true }, // Terzo Anno + { nome: 'Algebra 2', anno: '3', cfu: 6 }, { nome: 'Analisi matematica 3', anno: '3', cfu: 6 }, + { nome: 'Analisi reale', anno: '3', cfu: 6 }, { nome: 'Calcolo scientifico', anno: '3', cfu: 6 }, { nome: 'Elementi di analisi complessa', anno: '3', cfu: 6 }, { nome: 'Elementi di calcolo delle variazioni', anno: '3', cfu: 6 }, @@ -58,19 +62,25 @@ const CORSI_DISPONIBILI: Corso[] = [ { nome: 'Fisica II', anno: '3', cfu: 9 }, { nome: 'Fisica III', anno: '3', cfu: 6 }, { nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 }, - { nome: 'Laboratorio computazionale', anno: '3', cfu: 6, passFailOnly: true }, - { nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6 }, + { nome: 'Gruppi e rappresentazioni', anno: '3', cfu: 6 }, + { nome: 'Laboratorio computazionale', anno: '3', cfu: 6 }, { nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 }, { nome: 'Logica matematica', anno: '3', cfu: 6 }, + { nome: 'Matematiche elementari da un punto di vista superiore: aritmetica', anno: '3', cfu: 6 }, { nome: 'Matematiche elementari da un punto di vista superiore: geometria', anno: '3', cfu: 6 }, { nome: 'Meccanica razionale', anno: '3', cfu: 6 }, { nome: 'Metodi numerici per equazioni differenziali ordinarie', anno: '3', cfu: 6 }, + { nome: 'Metodi topologici in analisi globale', anno: '3', cfu: 6 }, { nome: 'Ottimizzazione non lineare', anno: '3', cfu: 6 }, { nome: 'Probabilità', anno: '3', cfu: 6 }, { nome: 'Ricerca operativa', anno: '3', cfu: 6 }, { nome: 'Sistemi dinamici', anno: '3', cfu: 6 }, + { nome: 'Spazi di Sobolev', anno: '3', cfu: 6 }, { nome: 'Statistica matematica', anno: '3', cfu: 6 }, + { nome: 'Storia della matematica', anno: '3', cfu: 6 }, { nome: 'Teoria algebrica dei numeri 1', anno: '3', cfu: 6 }, + { nome: 'Teoria dei campi e teoria di Galois', anno: '3', cfu: 6 }, + { nome: 'Teoria dei numeri elementare', anno: '3', cfu: 6 }, { nome: 'Teoria della misura', anno: '3', cfu: 6 }, // Istituzioni (Magistrale) @@ -82,41 +92,98 @@ const CORSI_DISPONIBILI: Corso[] = [ { nome: 'Istituzioni di geometria', anno: 'istituzioni', cfu: 11 }, { nome: 'Istituzioni di probabilità', anno: 'istituzioni', cfu: 11 }, - // Materie a scelta (Magistrale con "M" ma non istituzioni) + // Materie a scelta (Magistrale) + { nome: '4-varietà', anno: 'M', cfu: 6 }, { nome: 'Algebra superiore A', anno: 'M', cfu: 6 }, + { nome: 'Algebre e gruppi di Lie', anno: 'M', cfu: 6 }, { nome: 'Analisi armonica', anno: 'M', cfu: 6 }, + { nome: 'Analisi complessa A', anno: 'M', cfu: 6 }, + { nome: 'Analisi complessa B', anno: 'M', cfu: 6 }, + { nome: 'Analisi convessa', anno: 'M', cfu: 6 }, { nome: 'Analisi dei dati', anno: 'M', cfu: 6 }, + { nome: 'Analisi non standard', anno: 'M', cfu: 6 }, + { nome: 'Analisi reale', anno: 'M', cfu: 6 }, + { nome: 'Analisi su spazi gaussiani', anno: 'M', cfu: 6 }, + { nome: 'Analisi superiore', anno: 'M', cfu: 6 }, { nome: 'Analisi superiore A', anno: 'M', cfu: 6 }, { nome: 'Analisi superiore B', anno: 'M', cfu: 6 }, { nome: 'Aspetti matematici nella computazione quantistica', anno: 'M', cfu: 6 }, + { nome: 'Calcolo delle variazioni B', anno: 'M', cfu: 6 }, + { nome: 'Calcolo della variazioni A', anno: 'M', cfu: 6 }, // Variante nome { nome: 'Combinatoria algebrica', anno: 'M', cfu: 6 }, { nome: 'Complementi di analisi funzionale', anno: 'M', cfu: 6 }, { nome: 'Complementi di meccanica razionale', anno: 'M', cfu: 6 }, + { nome: 'Crittografia post-quantistica', anno: 'M', cfu: 6 }, + { nome: 'Curve ellittiche', anno: 'M', cfu: 6 }, + { nome: 'Determinazione orbitale', anno: 'M', cfu: 6 }, { nome: 'Didattica della matematica e nuove tecnologie', anno: 'M', cfu: 6 }, { nome: 'Dinamica del sistema solare', anno: 'M', cfu: 6 }, { nome: 'Dinamica iperbolica', anno: 'M', cfu: 6 }, { nome: 'Dinamica olomorfa', anno: 'M', cfu: 6 }, { nome: 'Elementi di calcolo in gruppi omogenei', anno: 'M', cfu: 6 }, + { nome: 'Equazioni della fluidodinamica', anno: 'M', cfu: 6 }, + { nome: 'Equazioni differenziali stocastiche e applicazioni', anno: 'M', cfu: 6 }, + { nome: 'Equazioni ellittiche', anno: 'M', cfu: 6 }, + { nome: 'Finanza matematica', anno: 'M', cfu: 6 }, { nome: 'Fisica matematica', anno: 'M', cfu: 6 }, + { nome: 'Forme modulari', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica B', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica C', anno: 'M', cfu: 6 }, { nome: 'Geometria algebrica complessa', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica D', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica E', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica F', anno: 'M', cfu: 6 }, + { nome: 'Geometria algebrica G', anno: 'M', cfu: 6 }, + { nome: 'Geometria e analisi complessa', anno: 'M', cfu: 6 }, { nome: 'Geometria differenziale complessa', anno: 'M', cfu: 6 }, { nome: 'Geometria iperbolica', anno: 'M', cfu: 6 }, { nome: 'Geometria riemanniana', anno: 'M', cfu: 6 }, + { nome: 'Gruppi algebrici lineari', anno: 'M', cfu: 6 }, + { nome: 'Gruppi di Coxeter', anno: 'M', cfu: 6 }, { nome: 'Gruppi di Galois e gruppi fondamentali', anno: 'M', cfu: 6 }, + { nome: 'Meccanica celeste', anno: 'M', cfu: 6 }, + { nome: 'Meccanica spaziale', anno: 'M', cfu: 6 }, { nome: 'Meccanica superiore', anno: 'M', cfu: 6 }, + { nome: 'Metodi di analisi armonica in analisi non lineare', anno: 'M', cfu: 6 }, + { nome: 'Metodi di approssimazione', anno: 'M', cfu: 6 }, + { nome: 'Metodi matematici della crittografia', anno: 'M', cfu: 6 }, { nome: 'Metodi matematici della meccanica quantistica', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per catene di Markov', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per equazioni alle derivate parziali', anno: 'M', cfu: 6 }, { nome: 'Metodi numerici per il calcolo tensoriale', anno: 'M', cfu: 6 }, { nome: 'Metodi numerici per il controllo ottimo', anno: 'M', cfu: 6 }, + { nome: 'Metodi numerici per la grafica', anno: 'M', cfu: 6 }, { nome: 'Metodi numerici per problemi inversi', anno: 'M', cfu: 6 }, { nome: "Metodi probabilistici per l'algebra lineare numerica", anno: 'M', cfu: 6 }, { nome: 'Modelli matematici in biomedicina e fisica matematica', anno: 'M', cfu: 6 }, + { nome: 'Origini e sviluppo delle matematiche moderne', anno: 'M', cfu: 6 }, + { nome: 'Probabilità superiore', anno: 'M', cfu: 6 }, + { nome: 'Problemi e metodi della ricerca in didattica della matematica', anno: 'M', cfu: 6 }, + { nome: 'Problemi e metodi in storia della matematica', anno: 'M', cfu: 6 }, { nome: 'Sistemi dinamici aleatori', anno: 'M', cfu: 6 }, + { nome: 'Statistica superiore', anno: 'M', cfu: 6 }, + { nome: 'Storia della matematica antica e della sua tradizione', anno: 'M', cfu: 6 }, { nome: 'Superfici di Riemann e curve algebriche', anno: 'M', cfu: 6 }, + { nome: 'Tecnologie per la didattica', anno: 'M', cfu: 6 }, + { nome: 'Teoria algebrica dei numeri 2', anno: 'M', cfu: 6 }, + { nome: 'Teoria analitica dei numeri A', anno: 'M', cfu: 6 }, { nome: 'Teoria dei giochi', anno: 'M', cfu: 6 }, + { nome: 'Teoria dei modelli', anno: 'M', cfu: 6 }, + { nome: 'Teoria dei nodi A', anno: 'M', cfu: 6 }, + { nome: 'Teoria degli insiemi', anno: 'M', cfu: 6 }, + { nome: 'Teoria degli insiemi A', anno: 'M', cfu: 6 }, + { nome: 'Teoria degli insiemi B', anno: 'M', cfu: 6 }, { nome: 'Teoria delle categorie', anno: 'M', cfu: 6 }, { nome: 'Teoria delle rappresentazioni A', anno: 'M', cfu: 6 }, + { nome: "Teoria e metodi dell'ottimizzazione", anno: 'M', cfu: 6 }, + { nome: 'Teoria ergodica', anno: 'M', cfu: 6 }, + { nome: 'Teoria geometrica della misura', anno: 'M', cfu: 6 }, + { nome: 'Topologia algebrica', anno: 'M', cfu: 6 }, { nome: 'Topologia algebrica A', anno: 'M', cfu: 6 }, + { nome: 'Topologia algebrica B', anno: 'M', cfu: 6 }, + { nome: 'Topologia differenziale', anno: 'M', cfu: 6 }, { nome: 'Topologia e geometria in bassa dimensione', anno: 'M', cfu: 6 }, + { nome: 'Ultrafiltri e metodi non-standard', anno: 'M', cfu: 6 }, ] function MediaPesataApp() { @@ -387,7 +454,7 @@ function MediaPesataApp() { // Filtra corsi disponibili in base al tipo di studente const getCorsiDisponibili = () => { if (tipoStudente === 'triennale') { - return CORSI_DISPONIBILI + return CORSI_DISPONIBILI.filter(corso => corso.anno !== 'istituzioni') } else { return CORSI_DISPONIBILI.filter( corso => corso.anno === 'istituzioni' || corso.anno === '3' || corso.anno === 'M', @@ -404,7 +471,6 @@ function MediaPesataApp() { gruppi['Primo Anno'] = corsi.filter(c => c.anno === '1') gruppi['Secondo Anno'] = corsi.filter(c => c.anno === '2') gruppi['Terzo Anno'] = corsi.filter(c => c.anno === '3') - gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni') gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === 'M') } else { // Per magistrali: prima le istituzioni, poi tutto il resto come "Materie a Scelta" From bbad670baac8b2c5a8b4a47c78e66d0f3b609985 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Wed, 25 Jun 2025 18:23:21 +0200 Subject: [PATCH 06/13] style changes --- src/client/MediaPesataApp.tsx | 360 +++++++------- src/pages/media-pesata.astro | 43 +- src/styles/controls.css | 91 +++- src/styles/main.css | 32 ++ src/styles/pages.scss | 0 src/styles/pages/media-pesata.css | 755 ++++++++++++++++++++---------- 6 files changed, 804 insertions(+), 477 deletions(-) create mode 100644 src/styles/pages.scss diff --git a/src/client/MediaPesataApp.tsx b/src/client/MediaPesataApp.tsx index a9991b3..03f7be3 100644 --- a/src/client/MediaPesataApp.tsx +++ b/src/client/MediaPesataApp.tsx @@ -186,7 +186,7 @@ const CORSI_DISPONIBILI: Corso[] = [ { nome: 'Ultrafiltri e metodi non-standard', anno: 'M', cfu: 6 }, ] -function MediaPesataApp() { +export function MediaPesataApp() { // Funzioni per localStorage const loadFromStorage = () => { try { @@ -220,13 +220,22 @@ function MediaPesataApp() { } // Inizializzazione con dati salvati - const initialData = loadFromStorage() - const [tipoStudente, setTipoStudente] = useState(initialData.tipoStudente) - const [corsiSelezionati, setCorsiSelezionati] = useState(initialData.corsiSelezionati) + // const initialData = loadFromStorage() + const [tipoStudente, setTipoStudente] = useState('triennale') + const [corsiSelezionati, setCorsiSelezionati] = useState([]) const [showCustomForm, setShowCustomForm] = useState(false) const [customCorso, setCustomCorso] = useState({ nome: '', cfu: 0 }) - const [sezioniAperte, setSezioniAperte] = useState>(initialData.sezioniAperte) - const [mostraRisultati, setMostraRisultati] = useState(initialData.mostraRisultati) + const [sezioniAperte, setSezioniAperte] = useState>({}) + const [mostraRisultati, setMostraRisultati] = useState(false) + + // Load data from localStorage on mount + useEffect(() => { + const initialData = loadFromStorage() + setTipoStudente(initialData.tipoStudente) + setCorsiSelezionati(initialData.corsiSelezionati) + setSezioniAperte(initialData.sezioniAperte) + setMostraRisultati(initialData.mostraRisultati) + }, []) // Salva automaticamente quando cambiano i dati importanti useEffect(() => { @@ -482,15 +491,6 @@ function MediaPesataApp() { } const cambiaTipoStudente = (nuovoTipo: TipoStudente) => { - if (corsiSelezionati.length > 0 && nuovoTipo !== tipoStudente) { - if ( - !confirm( - 'Cambiando il tipo di studente, alcune materie potrebbero non essere più disponibili. Continuare?', - ) - ) { - return - } - } setTipoStudente(nuovoTipo) } @@ -501,217 +501,207 @@ function MediaPesataApp() { const cfuError = totaleCfu > maxCfu return ( -
+
{/* Selezione tipo studente */} -
-

Corso di Laurea

-
- - +
+
+

Corso di Laurea

+
+ + +
{/* Counter CFU */} -
+

CFU Totali: {totaleCfu}/{maxCfu} {tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}

- {cfuError &&

⚠️ Hai superato il limite di CFU consentiti!

} + {cfuError &&

⚠️ Hai superato il limite di CFU consentiti!

}
-
- {/* Sezione selezione corsi */} -
+ {/* Sezione selezione corsi */} +
+

Seleziona Materie

+
- {Object.entries(gruppiCorsi).map(([categoria, corsi]) => ( -
- - - {sezioniAperte[categoria] && ( -
- {corsi.map((corso, index) => ( - - ))} -
- )} -
- ))} - - {/* Form per materia custom */} -
-

Materia Personalizzata

- {!showCustomForm ? ( - - ) : ( -
- - setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value }) - } - /> - - setCustomCorso({ - ...customCorso, - cfu: parseInt((e.target as HTMLInputElement).value) || 0, - }) - } - /> - - + {Object.entries(gruppiCorsi).map(([categoria, corsi]) => ( +
+ + + {sezioniAperte[categoria] && ( +
+ {corsi.map((corso, index) => ( + + ))}
)}
+ ))} + + {/* Form per materia custom */} +
+

Materia Personalizzata

+ {!showCustomForm ? ( + + ) : ( +
+ + setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value }) + } + /> + + setCustomCorso({ + ...customCorso, + cfu: parseInt((e.target as HTMLInputElement).value) || 0, + }) + } + /> + + +
+ )}
+
- {/* Sezione lista corsi selezionati */} -
-
+ {/* Sezione lista corsi selezionati */} +
+
+

Materie Selezionate

- {corsiSelezionati.length > 0 && ( - - )}
+
+ {corsiSelezionati.length > 0 && } +
- {corsiSelezionati.length === 0 ? ( -

Nessuna materia selezionata

- ) : ( -
- {corsiSelezionati.map(corso => ( -
-
- {corso.nome} - {corso.cfu} CFU - {corso.passFailOnly && Pass/Fail} -
- - {!corso.passFailOnly && ( -
+ {corsiSelezionati.length === 0 ? ( +

Nessuna materia selezionata

+ ) : ( +
+ {corsiSelezionati.map(corso => ( +
+ {corso.nome} + {corso.cfu} CFU + + {!corso.passFailOnly && ( +
+ + aggiornaVoto( + corso.id, + parseInt((e.target as HTMLInputElement).value) || null, + ) + } + /> + + +
+ )} - -
- )} - +
- ))} -
- )} -
+
+ ))} +
+ )}
{/* Pulsante Calcola */} {corsiSelezionati.length > 0 && ( -
- +
+
)} {/* Risultati */} {risultati && mostraRisultati && ( -
+

Risultati

{risultati.errore ? ( -
+

⚠️ {risultati.errore}

) : ( -
-
- Media Pesata: - {risultati.mediaPesata} +
+
+ Media Pesata: + {risultati.mediaPesata}
-
- Bonus Lodi: - +{risultati.bonusLodi} +
+ Bonus Lodi: + +{risultati.bonusLodi}
-
- Voto di Ammissione: - {risultati.votoAmmissione} +
+ Voto di Ammissione: + {risultati.votoAmmissione}
-
- Massimo Voto Di Laurea Possibile: - +
+ Massimo Voto Di Laurea Possibile: + {risultati.massimoVotoLaurea} - {risultati.conLode && (+lode)} + {risultati.conLode && (+lode)}
@@ -720,9 +710,9 @@ function MediaPesataApp() { )} {/* Nota informativa */} -
+

📋 Come viene calcolata la media

-
+

Regole di esclusione CFU: