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.
media-pesata
Luca Lombardo 11 months ago
parent b595169e3b
commit 55352af0f2

@ -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<TipoStudente>('triennale')
const [corsiSelezionati, setCorsiSelezionati] = useState<CorsoSelezionato[]>([])
const [showCustomForm, setShowCustomForm] = useState(false)
const [customCorso, setCustomCorso] = useState<CorsoCustom>({ nome: '', cfu: 0 })
const [sezioniAperte, setSezioniAperte] = useState<Record<string, boolean>>({})
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<string, Corso[]> = {}
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 (
<div className="media-pesata-app">
{/* Selezione tipo studente */}
<div className="student-type-selector">
<h2>Corso di Laurea</h2>
<div className="radio-group">
<label>
<input
type="radio"
value="triennale"
checked={tipoStudente === 'triennale'}
onChange={e => cambiaTipoStudente((e.target as HTMLInputElement).value as TipoStudente)}
/>
Triennale
</label>
<label>
<input
type="radio"
value="magistrale"
checked={tipoStudente === 'magistrale'}
onChange={e => cambiaTipoStudente((e.target as HTMLInputElement).value as TipoStudente)}
/>
Magistrale
</label>
</div>
</div>
{/* Counter CFU */}
<div className={`cfu-counter ${cfuError ? 'error' : ''}`}>
<h3>
CFU Totali: {totaleCfu}/{maxCfu}
{tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}
</h3>
{cfuError && <p className="error-text"> Hai superato il limite di CFU consentiti!</p>}
</div>
<div className="main-content">
{/* Sezione selezione corsi */}
<div className="course-selection">
<h2>Seleziona Materie</h2>
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
<div key={categoria} className="course-category">
<button className="category-header" onClick={() => toggleSezione(categoria)}>
<h3>{categoria}</h3>
<span className={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}></span>
</button>
{sezioniAperte[categoria] && (
<div className="course-grid">
{corsi.map((corso, index) => (
<button
key={index}
className="course-button"
onClick={() => aggiungiCorso(corso)}
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
>
<span className="course-name">{corso.nome}</span>
<span className="course-cfu">{corso.cfu} CFU</span>
</button>
))}
</div>
)}
</div>
))}
{/* Form per materia custom */}
<div className="custom-course">
<h3>Materia Personalizzata</h3>
{!showCustomForm ? (
<button onClick={() => setShowCustomForm(true)} className="add-custom-btn">
+ Aggiungi Materia Personalizzata
</button>
) : (
<div className="custom-form">
<input
type="text"
placeholder="Nome materia"
value={customCorso.nome}
onChange={e =>
setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value })
}
/>
<input
type="number"
placeholder="CFU"
min="1"
max="30"
step="1"
value={customCorso.cfu || ''}
onChange={e =>
setCustomCorso({
...customCorso,
cfu: parseInt((e.target as HTMLInputElement).value) || 0,
})
}
/>
<button onClick={aggiungiCorsoCustom} className="confirm-btn">
Aggiungi
</button>
<button onClick={() => setShowCustomForm(false)} className="cancel-btn">
Annulla
</button>
</div>
)}
</div>
</div>
{/* Sezione lista corsi selezionati */}
<div className="selected-courses">
<div className="section-header">
<h2>Materie Selezionate</h2>
{corsiSelezionati.length > 0 && (
<button onClick={resetTutto} className="reset-btn">
🗑 Cancella Tutto
</button>
)}
</div>
{corsiSelezionati.length === 0 ? (
<p className="no-courses">Nessuna materia selezionata</p>
) : (
<div className="courses-list">
{corsiSelezionati.map(corso => (
<div key={corso.id} className="course-item">
<div className="course-info">
<span className="course-name">{corso.nome}</span>
<span className="course-cfu">{corso.cfu} CFU</span>
{corso.passFailOnly && <span className="pass-fail-badge">Pass/Fail</span>}
</div>
{!corso.passFailOnly && (
<div className="course-grade">
<input
type="number"
placeholder="Voto"
min="18"
max="30"
step="1"
value={corso.voto || ''}
onChange={e =>
aggiornaVoto(
corso.id,
parseInt((e.target as HTMLInputElement).value) || null,
)
}
/>
<label className={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
<input
type="checkbox"
checked={corso.lode}
disabled={corso.voto !== 30}
onChange={e =>
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
}
/>
Lode
</label>
</div>
)}
<button
onClick={() => rimuoviCorso(corso.id)}
className="remove-btn"
title="Rimuovi materia"
>
×
</button>
</div>
))}
</div>
)}
</div>
</div>
{/* Pulsante Calcola */}
{corsiSelezionati.length > 0 && (
<div className="calculate-section">
<button onClick={calcolaMedia} className="calculate-btn">
🧮 Calcola Media e Voto di Laurea
</button>
</div>
)}
{/* Risultati */}
{risultati && mostraRisultati && (
<div className="results">
<h2>Risultati</h2>
{risultati.errore ? (
<div className="error-message">
<p> {risultati.errore}</p>
</div>
) : (
<div className="results-grid">
<div className="result-item">
<span className="label">Media Pesata:</span>
<span className="value">{risultati.mediaPesata}</span>
</div>
<div className="result-item">
<span className="label">Bonus Lodi:</span>
<span className="value">+{risultati.bonusLodi}</span>
</div>
<div className="result-item highlight">
<span className="label">Voto di Ammissione:</span>
<span className="value">{risultati.votoAmmissione}</span>
</div>
<div className="result-item highlight">
<span className="label">Massimo Voto Di Laurea Possibile:</span>
<span className="value">
{risultati.massimoVotoLaurea}
{risultati.conLode && <span className="lode-badge">(+lode)</span>}
</span>
</div>
</div>
)}
</div>
)}
{/* Nota informativa */}
<div className="info-section">
<h3>📋 Come viene calcolata la media</h3>
<div className="info-content">
<h4>Regole di esclusione CFU:</h4>
<ul>
<li>
<strong>Triennale:</strong> I 15 CFU con i voti più bassi vengono esclusi dalla media
</li>
<li>
<strong>Magistrale:</strong> I 9 CFU con i voti più bassi vengono esclusi dalla media
</li>
<li>Se un corso ha più CFU di quelli da escludere, viene diviso proporzionalmente</li>
</ul>
<h4>Calcolo del voto finale:</h4>
<ul>
<li>
<strong>Media pesata:</strong> Somma dei (voto × CFU) diviso per i CFU totali
</li>
<li>
<strong>Bonus lodi:</strong> +0.5 per lodi in materie &gt; 6 CFU, +0.25 per lodi in materie
6 CFU (max +2)
</li>
<li>
<strong>Voto di laurea:</strong> (Voto finale × 11) ÷ 3
</li>
</ul>
<h4>Note:</h4>
<ul>
<li>
Le materie <strong>Pass/Fail</strong> non contribuiscono al calcolo della media
</li>
<li>Il voto finale è limitato a 30</li>
<li>Per i magistrali: massimo 3 istituzioni selezionabili</li>
</ul>
</div>
</div>
</div>
)
}
// Funzione per inizializzare l'app
export function initMediaPesataApp() {
const container = document.getElementById('media-pesata-app')
if (container) {
render(<MediaPesataApp />, container)
}
}
export default MediaPesataApp

@ -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' },
]

@ -0,0 +1,42 @@
---
import '@/styles/pages/media-pesata.css'
import PageLayout from '../layouts/PageLayout.astro'
---
<PageLayout title="Voto Laurea" description="Calcola la tua media pesata e il voto di laurea seguendo le regole del dipartimento">
<div class="media-pesata-container">
<h1>Calcolo Media e Voto di Laurea</h1>
<p>Calcola la tua media pesata e il voto con cui ti siederai alla discussione di laurea, seguendo le regole del dipartimento di Matematica.</p>
<div id="media-pesata-app"></div>
</div>
</PageLayout>
<script>
// Importiamo e inizializziamo l'applicazione
import('../client/MediaPesataApp')
.then(module => {
const { initMediaPesataApp } = module
initMediaPesataApp()
})
</script>
<style>
.media-pesata-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
h1 {
text-align: center;
margin-bottom: 1rem;
color: var(--color-primary);
}
p {
text-align: center;
margin-bottom: 2rem;
color: var(--color-text-secondary);
}
</style>

@ -0,0 +1,49 @@
---
import '@/styles/pages/media-pesata.css'
import PageLayout from '../layouts/PageLayout.astro'
---
<PageLayout title="Voto Laurea" description="Calcola la tua media pesata e il voto di laurea seguendo le regole del dipartimento">
<div class="media-pesata-container">
<h1>Voto Laurea</h1>
<p>Calcola la tua media pesata e il voto con cui ti siederai alla discussione di laurea, seguendo le regole del dipartimento di Matematica.</p>
<div id="media-pesata-app"></div>
</div>
</PageLayout>
<script>
// Importiamo e inizializziamo l'applicazione
import('../client/MediaPesataApp')
.then(module => {
const { initMediaPesataApp } = module
initMediaPesataApp()
})
.catch(error => {
console.error('Errore nel caricamento dell\'applicazione:', error)
})
</script>
<style>
.media-pesata-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.media-pesata-container h1 {
text-align: center;
color: var(--palette-black);
margin-bottom: 1rem;
font-family: var(--font-display);
font-weight: 700;
font-size: 2.5rem;
}
.media-pesata-container p {
text-align: center;
color: #666;
margin-bottom: 2rem;
font-size: 1.1rem;
}
</style>

@ -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;
}
}
}
Loading…
Cancel
Save