forked from phc/website
1
0
Fork 0

Compare commits

...

76 Commits
dev ... main

Author SHA1 Message Date
Fran314 bf9709c689 updated recovery instructions for 2FA
continuous-integration/drone/push Build is passing Details
5 months ago
Luca Lombardo 4cbfc828c0 Add recovery instructions for Unipi account access without 2FA
continuous-integration/drone/push Build is passing Details
5 months ago
Luca Lombardo 8fddadb098 aggiunte domande istgeom valeria
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis 6e615d6874 mobile video fix
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis 54d95b9c59 minor drone lint fix
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis 21260f1a85 ok forse bastava solo usare alpine dall'inizio
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis 10bcde22bf yeee forse ora funge
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis 7c9d1e3fcc debugging 12
continuous-integration/drone/push Build is passing Details
8 months ago
Antonio De Lucreziis e355a29ae9 debugging 11
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 22c8357282 debugging 10
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 3308e10981 debugging 9
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 176bad6d51 debugging 8
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis f7076759e1 debugging 7
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 5eb8e3fc85 debugging 6
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 85a144b6d0 debugging 5
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis f99a7389be debugging 4
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis ff966c31a8 debugging 3
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis e5821067b1 debugging 2
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 655a4c5b22 debugging
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 40673661cf updated .drone.yml
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis 969e8bea35 updated .drone.yml
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis d409e83ccb fix drone maybe
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis abd12dd2f3 updated more packages
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis cc77ec90cc minor guide update
continuous-integration/drone/push Build is failing Details
8 months ago
Antonio De Lucreziis b8eb2eb7bf fixed build error, updated packages and astro, minor code updates 8 months ago
Francesco Minnocci 18f6df7c4d
feat: Add SSH print guide; add Meme news.
continuous-integration/drone/push Build is failing Details
8 months ago
Luca Lombardo d2a866cb0f feat: add pass/fail option for specific courses in CORSI_DISPONIBILI
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis b3251906d5 aggiunta foto e descrizione per baldo alla pagina macchinisti
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis 55d0dc8345 fixed minor style issue
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis 74ed415980 fixed half width case
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis cc12c4fc18 fixed link 11 months ago
Luca Lombardo eb667f7f04 feat: update title and heading for media calculator announcement 11 months ago
Luca Lombardo 575a37accc feat: update MediaPesataApp to cap bonus lodi based on student type and add new media calculator documentation 11 months ago
Antonio De Lucreziis bbad670baa style changes 11 months ago
Luca Lombardo 4c19ffdd9f fix: update course data comments and add new courses with passFailOnly property 11 months ago
Luca Lombardo 15c2b44fcf feat: add passFailOnly property to 'Laboratorio computazionale' course 11 months ago
Luca Lombardo 34597fe9f8 feat: implement localStorage functionality for MediaPesataApp and remove media.astro page 11 months ago
Luca Lombardo 4780133019 fix: restore 'Calcolo Media' link in header navigation 11 months ago
Luca Lombardo 55352af0f2 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.
11 months ago
Antonio De Lucreziis b595169e3b chore: final code re-organization after the css refactoring
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis d75ccecdf2 style: another minor image tweak
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis a694817c28 style: minor page card tweaks
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis a2125bdab7 major scss to css refactor, removed pageTags and simplified per page css
continuous-integration/drone/push Build is passing Details
11 months ago
Antonio De Lucreziis ddca56e868 minor latex update 11 months ago
Antonio De Lucreziis da7f526cb0 aggiunte alcune domande di teoria dei nodi
continuous-integration/drone/push Build is passing Details
12 months ago
Antonio De Lucreziis 370f8204a0 Merge pull request 'fixed order of news on the main page' (#17) from i.serdyuk/website:main into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/website#17
1 year ago
Kratacoa 2b5aea6eb8 fixed order of news on the main page 1 year ago
Francesco Minnocci 0179e7a858 minor fixes
continuous-integration/drone/push Build is passing Details
1 year ago
Francesco Minnocci 5c7342c91e ciao, ho pushato
continuous-integration/drone/push Build is passing Details
1 year ago
Francesco Minnocci 3ed6cbf57a fix
continuous-integration/drone/push Build is passing Details
1 year ago
Francesco Minnocci 1ab42ed2e2 add GTD
continuous-integration/drone/push Build is failing Details
1 year ago
Antonio De Lucreziis 419b0c6ab5 cmd: updated prettier config and reformatted content
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis f85991c409 updated readme 1 year ago
Antonio De Lucreziis d115d69b1b added phc chat icon in homepage, fixed exam question
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 18563979a2 chore: changed name of esami section
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 4b7a867a19 fix: centered text
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 517a8ee8f1 some fixes
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 0b4bf289d4 Merge branch 'feat-domande-esami'
continuous-integration/drone/push Build is failing Details
1 year ago
Antonio De Lucreziis db083e20c2 updated questions and style 1 year ago
Antonio De Lucreziis fa564754f2 simplified tagging selection 1 year ago
Antonio De Lucreziis ae70cfc5e3 last checks 1 year ago
Antonio De Lucreziis d46f62dd3f added initial batch of questions 1 year ago
Antonio De Lucreziis 4757c3e0a9 prototype mostly finished 1 year ago
Antonio De Lucreziis 7c36f87149 finished minimal working prototype 1 year ago
Antonio De Lucreziis 58d0378d49 chore: better layout for macchinisti page, founder badge
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 84caa87951 initial commit of domande esami branch 1 year ago
Luca Lombardo 23baa758cf add: Riccardo Murri to macchinisti and update history details
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 1a3948a8eb add: Antonio Spanu
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 4fca2f8b14 Aggiunto Francesco Manicastri
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 3173968f0e aggiunto francesco caporali, manca foto
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 6330a13d92 macchinisti
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis 2d0d34c4ff fix: wrong news title
continuous-integration/drone/push Build is passing Details
1 year ago
Antonio De Lucreziis af4f3d35f0 chore: support images in markdown files, reversed news list ordering
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 9684ff98ea feat: add incident report for Poisson server outage on January 2, 2025 1 year ago
Luca Lombardo 5feab1ba69 typos
continuous-integration/drone/push Build is passing Details
1 year ago
Luca Lombardo 0a2df372d5 feat: add new macchinisti entries with social links and descriptions
continuous-integration/drone/push Build is passing Details
1 year ago

@ -7,44 +7,48 @@
kind: pipeline
name: default
type: docker
steps:
- name: deploy
image: node:latest
volumes:
- name: host-website-dist
path: /mnt/website
commands:
- npm install
- npm run build
- cp -rT ./dist /mnt/website
- name: deploy
image: node:22-alpine
volumes:
- name: host-website-dist
path: /mnt/website
commands:
- uname -a
- node -v
- npm ci
- node -e 'import Sharp from "sharp"; console.log(Sharp)'
- npm run build
- cp -rT ./dist /mnt/website
volumes:
- name: host-website-dist
host: # this volume is mounted on the host machine
path: /var/www/website
- name: host-website-dist
host: # this volume is mounted on the host machine
path: /var/www/website
trigger:
branch:
- main
- main
event:
- push
- push
---
kind: pipeline
type: exec # this job is executed on the host machine
name: caddy-permissions
type: exec # this job is executed on the host machine
depends_on:
- default
- default
steps:
- name: chown
commands:
- chown -R caddy:caddy /var/www/website
- name: chown
commands:
- chown -R caddy:caddy /var/www/website
trigger:
branch:
- main
- main
event:
- push
- push

@ -1,9 +0,0 @@
{
"printWidth": 110,
"singleQuote": true,
"quoteProps": "consistent",
"tabWidth": 4,
"useTabs": false,
"semi": false,
"arrowParens": "avoid"
}

@ -0,0 +1,27 @@
/** @type {import("prettier").Config} */
export default {
printWidth: 120,
singleQuote: true,
quoteProps: 'consistent',
tabWidth: 4,
useTabs: false,
semi: false,
arrowParens: 'avoid',
plugins: ['prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
{
files: '*.{yml,yaml,json}',
excludeFiles: 'package-lock.json',
options: {
tabWidth: 2,
},
},
],
}

@ -1,3 +1,11 @@
{
"npm.packageManager": "bun"
"npm.packageManager": "bun",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

@ -17,19 +17,12 @@ bun dev
## Build
```bash
bun build
bun run build
```
## Deploy [TODO]
## Deploy
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro).
```bash
docker build -t phc-website .
docker run -p 3000:3000 phc-website
```
C'è anche un `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD.
Per ora c'è un file `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD. Al momento il sito è solo statico e non ha ancora una backend.
## Come Contribuire

@ -2,6 +2,7 @@ import { defineConfig } from 'astro/config'
import preact from '@astrojs/preact'
import mdx from '@astrojs/mdx'
import remarkMath from 'remark-math'
import yaml from '@rollup/plugin-yaml'
@ -14,10 +15,18 @@ export default defineConfig({
port: 3000,
},
markdown: {
remarkPlugins: [remarkMath],
shikiConfig: {
theme: 'github-light',
},
},
integrations: [preact(), mdx()],
integrations: [
preact({
compat: true,
}),
mdx({
remarkPlugins: [remarkMath],
}),
],
output: 'static',
})

Binary file not shown.

12480
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,47 +1,54 @@
{
"name": "website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "run-s astro:sync astro:dev",
"build": "run-s astro:build",
"astro:sync": "astro sync",
"astro:dev": "astro dev",
"astro:build": "astro check && astro build"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "9.0.0",
"@astrojs/preact": "4.0.0",
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
"@fontsource/iosevka": "^5.0.11",
"@fontsource/mononoki": "^5.0.11",
"@fontsource/open-sans": "^5.0.24",
"@fontsource/source-code-pro": "^5.0.16",
"@fontsource/source-sans-pro": "^5.0.8",
"@fontsource/space-mono": "^5.0.20",
"@phosphor-icons/core": "^2.1.1",
"@preact/signals": "^1.3.0",
"@types/jsdom": "^21.1.7",
"astro": "5.1.0",
"fuse.js": "^7.0.0",
"katex": "^0.16.9",
"lucide-static": "^0.468.0",
"preact": "^10.19.6",
"typescript": "^5.3.3"
},
"devDependencies": {
"@astrojs/mdx": "4.0.2",
"@rollup/plugin-yaml": "^4.1.2",
"@types/katex": "^0.16.7",
"jsdom": "^24.1.1",
"linkedom": "^0.18.4",
"npm-run-all": "^4.1.5",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-math": "^6.0.0",
"remark-toc": "^9.0.0",
"sass": "^1.71.1",
"tsx": "^4.7.1"
}
"name": "website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "run-s astro:sync astro:dev",
"build": "run-s astro:build",
"astro:sync": "astro sync",
"astro:dev": "astro dev",
"astro:build": "astro check && astro build"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.4.3",
"@astrojs/preact": "^4.1.1",
"@fontsource-variable/material-symbols-outlined": "^5.2.21",
"@fontsource/iosevka": "^5.2.5",
"@fontsource/mononoki": "^5.2.5",
"@fontsource/open-sans": "^5.2.6",
"@fontsource/source-code-pro": "^5.2.6",
"@fontsource/source-sans-pro": "^5.2.5",
"@fontsource/space-mono": "^5.2.8",
"@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.10",
"@preact/signals": "^1.3.2",
"@types/jsdom": "^21.1.7",
"astro": "^5.13.7",
"fuse.js": "^7.1.0",
"katex": "^0.16.22",
"lucide-static": "^0.468.0",
"marked": "^15.0.12",
"node-addon-api": "^8.5.0",
"node-gyp": "^11.4.2",
"preact": "^10.27.2",
"sharp": "^0.34.3",
"typescript": "^5.9.2"
},
"devDependencies": {
"@astrojs/mdx": "^4.3.5",
"@rollup/plugin-yaml": "^4.1.2",
"@types/katex": "^0.16.7",
"jsdom": "^24.1.3",
"linkedom": "^0.18.12",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-math": "^6.0.0",
"remark-toc": "^9.0.0",
"sass": "^1.92.1",
"tsx": "^4.20.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

@ -34,11 +34,7 @@ export const ComboBox = ({
}, [])
return (
<div
class="combobox"
ref={comboRef}
style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}
>
<div class="combobox" ref={comboRef} style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}>
<div class="selected" onClick={() => setOpen(!open)}>
<div class="content">{children[value]}</div>
{/* <span class="material-symbols-outlined">expand_more</span> */}
@ -47,7 +43,10 @@ export const ComboBox = ({
{open && (
<div
class={clsx('dropdown', cloak && 'invisible')}
ref={el => el && setItemWidth(el.offsetWidth)}
ref={el => {
if (!el) return
setItemWidth(el.offsetWidth)
}}
>
{Object.keys(children).map(key => (
<div

@ -0,0 +1,119 @@
import { useEffect, useState } from 'preact/hooks'
import { FunnelIcon } from '@phosphor-icons/react'
import { marked } from 'marked'
import extendedLatex from '@/client/lib/marked-latex'
marked.use(
extendedLatex({
lazy: false,
render: (formula: string, display: boolean) => {
return display ? '$$' + formula + '$$' : '$' + formula + '$'
},
}),
)
import type { Database } from '@/data/domande-esami.yaml'
const useRemoteValue = <T,>(url: string): T | null => {
const [value, setValue] = useState<T | null>(null)
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(value => setValue(value))
.catch(error => console.error(error))
}, [url])
return value
}
type Props = {
course: string
}
export const DomandeEsamiCourse = ({ course }: Props) => {
const database = useRemoteValue<Database>(`/domande-esami/api/${course}.json`)
if (!database) {
return <>Loading...</>
}
if ('requestIdleCallback' in window) {
// @ts-ignore
requestIdleCallback(() => window.renderMath())
} else {
// @ts-ignore
setTimeout(() => window.renderMath(), 100)
}
const courseTags = [
...new Set(
database.questions.filter(question => question.course === course).flatMap(question => question.tags),
),
]
const [selectedTag, setSelectedTag] = useState<string | null>(null)
const filteredQuestions = database.questions
.filter(question => question.course === course)
.filter(question => (selectedTag ? question.tags.includes(selectedTag) : true))
return (
<>
<div class="grid-center text-center">
<h3>
<a href="/domande-esami">Domande Orali</a>
</h3>
<h1>{database.names[course]}</h1>
</div>
{courseTags.length > 1 && (
<div class="card filter">
<div class="grid-h">
<FunnelIcon />
<strong>Filtra Tag</strong>
</div>
<div class="flex-row-wrap">
{!selectedTag
? courseTags.map(tag => (
<div class="chip clickable" onClick={() => setSelectedTag(tag)}>
{tag}
</div>
))
: courseTags.map(tag => (
<div
class={tag === selectedTag ? 'chip clickable' : 'chip clickable disabled'}
onClick={() => setSelectedTag(tag === selectedTag ? null : tag)}
>
{tag}
</div>
))}
</div>
</div>
)}
<div class="wide-card-list" id="questions">
{filteredQuestions.length === 0 ? (
<div class="grid-center">
<em>No questions found</em>
</div>
) : (
filteredQuestions.map(question => (
<div class="card">
<div
class="text"
dangerouslySetInnerHTML={{
__html: marked(question.content, { async: false }),
}}
/>
<div class="metadata">
{question.tags.map(tag => (
<div class="chip small">{tag}</div>
))}
</div>
</div>
))
)}
</div>
</>
)
}

@ -2,8 +2,8 @@ const icons = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`, {
eager: true,
})
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module])
}),
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
)
type Props = {

@ -0,0 +1,765 @@
import { useState, useEffect } from 'preact/hooks'
// 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 aggiornati dalla tabella ufficiale
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: '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: '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 },
{ 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: 'Gruppi e rappresentazioni', anno: '3', cfu: 6 },
{ nome: 'Laboratorio computazionale', anno: '3', cfu: 6, passFailOnly: true },
{ nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6, passFailOnly: true },
{ 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)
{ 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)
{ 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 },
]
export function MediaPesataApp() {
// 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<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)
// 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(() => {
const dataToSave = {
tipoStudente,
corsiSelezionati,
sezioniAperte,
mostraRisultati,
}
saveToStorage(dataToSave)
}, [tipoStudente, corsiSelezionati, sezioniAperte, mostraRisultati])
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)
setSezioniAperte({})
setMostraRisultati(false)
// Pulisce anche il localStorage
try {
localStorage.removeItem('media-pesata-data')
} catch (error) {
console.warn('Errore nella pulizia del localStorage:', error)
}
}
}
}
// 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)
// Cap del bonus lodi basato sul tipo di studente
const capBonusLodi = tipoStudente === 'triennale' ? 1.5 : 2
const bonusLodiFinal = Math.min(bonusLodi, capBonusLodi)
// 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.filter(corso => corso.anno !== 'istituzioni')
} 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['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) => {
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 class="media-pesata-app">
{/* Selezione tipo studente */}
<div class="card student-type-switcher wide">
<div class="grid-center">
<h2>Corso di Laurea</h2>
<div class="compound-button">
<button
class={tipoStudente === 'triennale' ? 'active' : ''}
onClick={() => cambiaTipoStudente('triennale')}
>
Triennale
</button>
<button
class={tipoStudente === 'magistrale' ? 'active' : ''}
onClick={() => cambiaTipoStudente('magistrale')}
>
Magistrale
</button>
</div>
</div>
</div>
{/* Counter CFU */}
<div class={`cfu-counter wide ${cfuError ? 'error' : ''}`}>
<h3>
CFU Totali: {totaleCfu}/{maxCfu}
{tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}
</h3>
{cfuError && <p class="error-text"> Hai superato il limite di CFU consentiti!</p>}
</div>
{/* Sezione selezione corsi */}
<div class="card">
<div class="title">
<h2>Seleziona Materie</h2>
</div>
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
<div key={categoria} class="course-category">
<button onClick={() => toggleSezione(categoria)}>
<div class="h-flex">
{categoria}
<div class="spacer"></div>
<span class={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}></span>
</div>
</button>
{sezioniAperte[categoria] && (
<div class="course-grid">
{corsi.map((corso, index) => (
<button
key={index}
class="course-button"
onClick={() => aggiungiCorso(corso)}
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
>
<span class="course-name">{corso.nome}</span>
<span class="course-cfu">{corso.cfu} CFU</span>
</button>
))}
</div>
)}
</div>
))}
{/* Form per materia custom */}
<div class="custom-course">
<h3>Materia Personalizzata</h3>
{!showCustomForm ? (
<button onClick={() => setShowCustomForm(true)}>+ Aggiungi Materia Personalizzata</button>
) : (
<div class="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}>Aggiungi</button>
<button onClick={() => setShowCustomForm(false)}>Annulla</button>
</div>
)}
</div>
</div>
{/* Sezione lista corsi selezionati */}
<div class="card">
<div class="h-flex">
<div class="title">
<h2>Materie Selezionate</h2>
</div>
<div class="spacer"></div>
{corsiSelezionati.length > 0 && <button onClick={resetTutto}>🗑 Cancella Tutto</button>}
</div>
{corsiSelezionati.length === 0 ? (
<p>Nessuna materia selezionata</p>
) : (
<div class="courses-list">
{corsiSelezionati.map(corso => (
<div key={corso.id} class="course-item">
<span class="course-name">{corso.nome}</span>
<span class="course-cfu">{corso.cfu} CFU</span>
{!corso.passFailOnly && (
<div class="course-grade tall">
<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 class={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
<input
class="star"
type="checkbox"
checked={corso.lode}
disabled={corso.voto !== 30}
onChange={e =>
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
}
/>
Lode
</label>
</div>
)}
<div class="actions tall">
<button
class="icon remove-btn"
onClick={() => rimuoviCorso(corso.id)}
title="Rimuovi materia"
>
×
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Pulsante Calcola */}
{corsiSelezionati.length > 0 && (
<div class="calculate-section wide">
<button onClick={calcolaMedia}>🧮 Calcola Media e Voto di Laurea</button>
</div>
)}
{/* Risultati */}
{risultati && mostraRisultati && (
<div class="results wide">
<h2>Risultati</h2>
{risultati.errore ? (
<div class="error-message">
<p> {risultati.errore}</p>
</div>
) : (
<div class="results-grid">
<div class="result-item">
<span class="label">Media Pesata:</span>
<span class="value">{risultati.mediaPesata}</span>
</div>
<div class="result-item">
<span class="label">Bonus Lodi:</span>
<span class="value">+{risultati.bonusLodi}</span>
</div>
<div class="result-item highlight">
<span class="label">Voto di Ammissione:</span>
<span class="value">{risultati.votoAmmissione}</span>
</div>
<div class="result-item highlight">
<span class="label">Massimo Voto Di Laurea Possibile:</span>
<span class="value">
{risultati.massimoVotoLaurea}
{risultati.conLode && <span class="lode-badge">(+lode)</span>}
</span>
</div>
</div>
)}
</div>
)}
{/* Nota informativa */}
<div class="card wide">
<h3>📋 Come viene calcolata la media</h3>
<div class="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 +1.5 per triennale, max +2 per magistrale)
</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

@ -68,22 +68,22 @@ export const UtentiPage = () => {
$filter.value === 'macchinisti'
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
: $filter.value === 'rappstud'
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
: $utentiData.value
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
: $utentiData.value,
)
const $fuse = useComputed(
() =>
new Fuse($filteredData.value, {
keys: ['gecos', 'uid'],
})
}),
)
const $searchText = useSignal('')
const $searchResults = useComputed(() =>
$searchText.value.trim().length > 0
? $fuse.value?.search($searchText.value).map(result => result.item) ?? []
: $filteredData.value
? ($fuse.value?.search($searchText.value).map(result => result.item) ?? [])
: $filteredData.value,
)
useEffect(() => {
@ -110,7 +110,7 @@ export const UtentiPage = () => {
<PhosphorIcon name={v.icon} />
{v.label}
</>,
])
]),
)}
</ComboBox>
<div class="search">
@ -142,17 +142,14 @@ export const UtentiPage = () => {
RAPPSTUD.includes(poissonUser.uid)
? 'bank'
: MACCHINISTI.includes(poissonUser.uid)
? 'wrench'
: 'user'
? 'wrench'
: 'user'
}
/>
</div>
<div class="text">{poissonUser.gecos}</div>
<div class="right">
<a
href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}
target="_blank"
>
<a href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`} target="_blank">
{/* <span class="material-symbols-outlined">open_in_new</span> */}
<PhosphorIcon name="arrow-square-out" />
</a>

@ -0,0 +1,27 @@
const $debugConsole = document.createElement('div')
$debugConsole.style.position = 'fixed'
$debugConsole.style.bottom = '0'
$debugConsole.style.left = '0'
$debugConsole.style.width = '100%'
$debugConsole.style.height = '25vh'
$debugConsole.style.backgroundColor = 'black'
$debugConsole.style.color = 'white'
$debugConsole.style.overflow = 'auto'
$debugConsole.style.padding = '10px'
$debugConsole.style.boxSizing = 'border-box'
$debugConsole.style.fontFamily = 'monospace'
$debugConsole.style.zIndex = '9999'
$debugConsole.style.fontSize = '15px'
$debugConsole.style.opacity = '0.8'
document.body.appendChild($debugConsole)
function logDebugConsole(...args) {
$debugConsole.innerHTML += args.join(' ') + '<br>'
}
console.error = logDebugConsole
console.warn = logDebugConsole
console.log = logDebugConsole
console.debug = logDebugConsole

@ -0,0 +1,83 @@
// took from: https://github.com/sxyazi/marked-extended-latex
// this has a peer dependency bug
const CLASS_NAME = 'latex-b172fea480b'
const extBlock = options => ({
name: 'latex-block',
level: 'block',
start(src) {
return src.match(/\$\$[^\$]/)?.index ?? -1
},
tokenizer(src, _tokens) {
const match = /^\$\$([^\$]+)\$\$/.exec(src)
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
},
renderer(token) {
if (!options.lazy) return options.render(token.formula, true)
return `<span class="${CLASS_NAME}" block>${token.formula}</span>`
},
})
const extInline = options => ({
name: 'latex',
level: 'inline',
start(src) {
return src.match(/\$[^\$]/)?.index ?? -1
},
tokenizer(src, _tokens) {
const match = /^\$([^\$]+)\$/.exec(src)
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
},
renderer(token) {
if (!options.lazy) return options.render(token.formula, false)
return `<span class="${CLASS_NAME}">${token.formula}</span>`
},
})
let observer
/* istanbul ignore next */
export default (options = {}) => {
/* istanbul ignore next */
if (options.lazy && options.env !== 'test') {
observer = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue
}
const span = entry.target
self.unobserve(span)
Promise.resolve(options.render(span.innerText, span.hasAttribute('block'))).then(html => {
span.innerHTML = html
})
span.classList.add('latex-rendered')
}
},
{ threshold: 1.0 },
)
}
return {
extensions: [extBlock(options), extInline(options)],
}
}
/* istanbul ignore next */
export const observe = () => {
if (!observer) {
return
}
observer.disconnect()
document.querySelectorAll(`span.${CLASS_NAME}:not(.latex-rendered)`).forEach(span => {
observer.observe(span)
})
}
/* istanbul ignore next */
export const disconnect = () => {
observer?.disconnect()
}

@ -3,7 +3,7 @@ import { useEffect, useState } from 'preact/hooks'
export const trottleDebounce = <T extends any[], R>(
fn: (...args: T) => R,
delay: number,
options: { leading?: boolean; trailing?: boolean } = {}
options: { leading?: boolean; trailing?: boolean } = {},
): ((...args: T) => R | undefined) => {
let lastCall = 0
let lastResult: R | undefined

@ -9,11 +9,16 @@ const ICONS_MAP: Record<string, string> = {
}
type Props = {
image: ImageMetadata
fullName: string
description: string
image: ImageMetadata
entranceDate: number
exitDate?: number
description: string
founder?: boolean
social?: {
github?: string
linkedin?: string
@ -22,13 +27,14 @@ type Props = {
}
}
const { image, fullName, entranceDate, exitDate, description, social } = Astro.props
const { fullName, description, image, entranceDate, exitDate, founder, social } = Astro.props
---
<div class="bubble">
<img src={image.src} alt={fullName.toLowerCase()} />
<div class="title">{fullName}</div>
<div class="date">{entranceDate}&mdash;{exitDate ?? 'Presente'}</div>
{founder && <div class="founder">Fondatore</div>}
<div class="description">{description}</div>
{
social && (

@ -5,6 +5,8 @@ const links = [
// { href: '/appunti', text: 'Appunti' },
{ href: '/notizie', text: 'Notizie' },
{ href: '/guide', text: 'Guide' },
{ href: '/domande-esami', text: 'Domande Orali' },
{ href: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto
{ href: '/storia', text: 'Storia' },
// { href: '/login', text: 'Login' },
]

@ -9,8 +9,8 @@ const { name } = Astro.props
const icons = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`)
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module])
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`),
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
)
if (!icons[name]) {

@ -17,7 +17,7 @@ const { href, imgSrc, style, title } = Astro.props
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
</div>
<div class="title">{title}</div>
<div class="description">
<div class="description text">
<slot />
</div>
</div>

@ -32,17 +32,17 @@ const guidesCollection = defineCollection({
})
// Per ora sono su un sito a parte ma prima o poi verranno migrati qui
const seminariettiCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
author: z.string(),
publishDate: z.date(),
eventDate: z.date(),
tags: z.array(z.string()),
}),
})
// const seminariettiCollection = defineCollection({
// type: 'content',
// schema: z.object({
// title: z.string(),
// description: z.string(),
// author: z.string(),
// publishDate: z.date(),
// eventDate: z.date(),
// tags: z.array(z.string()),
// }),
// })
const metaCollection = defineCollection({
type: 'content',
@ -53,6 +53,6 @@ const metaCollection = defineCollection({
export const collections = {
news: newsCollection,
guides: guidesCollection,
seminarietti: seminariettiCollection,
// seminarietti: seminariettiCollection,
meta: metaCollection,
}

@ -14,21 +14,21 @@ Git è un sistema di controllo di versione distribuito creato per gestire proget
### **Cos'è Git?**
- **Sistema di controllo di versione**: Gestisce le modifiche al codice sorgente nel tempo.
- **Sistema di controllo di versione**: Gestisce le modifiche al codice sorgente nel tempo.
- **Distribuito**: Ogni sviluppatore ha una copia del repository.
- **Distribuito**: Ogni sviluppatore ha una copia del repository.
- **Veloce e leggero**: Ottimizzato per la velocità e le prestazioni.
- **Veloce e leggero**: Ottimizzato per la velocità e le prestazioni.
### **Perché usare Git?**
- **Tracciabilità**: Ogni modifica è tracciata e reversibile.
- **Tracciabilità**: Ogni modifica è tracciata e reversibile.
- **Collaborazione**: Più persone possono lavorare sullo stesso progetto.
- **Collaborazione**: Più persone possono lavorare sullo stesso progetto.
- **Backup**: Repository remoto per il backup del codice.
- **Backup**: Repository remoto per il backup del codice.
- **Branching**: Lavoriamo su nuove funzionalità senza influenzare il codice principale.
- **Branching**: Lavoriamo su nuove funzionalità senza influenzare il codice principale.
---
@ -96,9 +96,9 @@ git config --list
### **Repository**
- **Repository locale**: Una cartella sul nostro computer che contiene il nostro progetto.
- **Repository locale**: Una cartella sul nostro computer che contiene il nostro progetto.
- **Repository remoto**: Una versione del progetto ospitata su un server (es. GitHub, GitLab).
- **Repository remoto**: Una versione del progetto ospitata su un server (es. GitHub, GitLab).
### **Branch**
@ -174,9 +174,9 @@ Il comando **`git commit`** è utilizzato per registrare le modifiche nel reposi
#### Cosa succede dietro le quinte:
- Git salva lo stato dei file nell'area di staging in un commit, che viene aggiunto alla cronologia del repository locale.
- Git salva lo stato dei file nell'area di staging in un commit, che viene aggiunto alla cronologia del repository locale.
- Ogni commit ha un identificatore unico (hash) che consente di risalire facilmente alle modifiche in qualsiasi momento.
- Ogni commit ha un identificatore unico (hash) che consente di risalire facilmente alle modifiche in qualsiasi momento.
---
@ -198,13 +198,13 @@ Il comando **`git commit`** è utilizzato per registrare le modifiche nel reposi
#### Cosa succede dietro le quinte:
- Git confronta il nostro branch locale con il branch remoto. Se ci sono nuovi commit nel branch remoto che non sono ancora nel nostro branch locale, ci verrà richiesto di fare un **pull** per aggiornare prima di fare il push.
- Git confronta il nostro branch locale con il branch remoto. Se ci sono nuovi commit nel branch remoto che non sono ancora nel nostro branch locale, ci verrà richiesto di fare un **pull** per aggiornare prima di fare il push.
- Il nostro repository locale viene sincronizzato con il remoto, rendendo le modifiche visibili a tutti gli altri che hanno accesso al repository remoto.
- Il nostro repository locale viene sincronizzato con il remoto, rendendo le modifiche visibili a tutti gli altri che hanno accesso al repository remoto.
#### Errori comuni:
- Se il repository remoto è stato aggiornato nel frattempo da qualcun altro (ad esempio, con un altro push), riceveremo un errore che ci avvisa che dobbiamo fare prima un `git pull` per sincronizzare il nostro lavoro.
- Se il repository remoto è stato aggiornato nel frattempo da qualcun altro (ad esempio, con un altro push), riceveremo un errore che ci avvisa che dobbiamo fare prima un `git pull` per sincronizzare il nostro lavoro.
---
@ -226,13 +226,13 @@ Il comando **`git commit`** è utilizzato per registrare le modifiche nel reposi
#### Cosa succede dietro le quinte:
- **`git fetch`** scarica tutte le modifiche dal repository remoto, ma non le integra ancora nel nostro codice.
- **`git fetch`** scarica tutte le modifiche dal repository remoto, ma non le integra ancora nel nostro codice.
- **`git merge`** unisce le modifiche scaricate al nostro branch attuale, risolvendo eventuali conflitti, se necessario.
- **`git merge`** unisce le modifiche scaricate al nostro branch attuale, risolvendo eventuali conflitti, se necessario.
#### Errori comuni:
- Se ci sono conflitti tra il nostro lavoro e quello degli altri, Git ci avviserà che dovremo risolverli manualmente. Dopo aver risolto i conflitti, dovremo aggiungere i file risolti (`git add`) e completare il merge con un commit.
- Se ci sono conflitti tra il nostro lavoro e quello degli altri, Git ci avviserà che dovremo risolverli manualmente. Dopo aver risolto i conflitti, dovremo aggiungere i file risolti (`git add`) e completare il merge con un commit.
## **7. Lavorare con branch**
@ -336,18 +336,18 @@ git branch -d <nome-branch>
## **10. Best practices**
- Scriviamo messaggi di commit chiari e descrittivi.
- Scriviamo messaggi di commit chiari e descrittivi.
- Creiamo branch per nuove funzionalità o bugfix.
- Creiamo branch per nuove funzionalità o bugfix.
- Sincronizziamo frequentemente il nostro repository locale con quello remoto.
- Sincronizziamo frequentemente il nostro repository locale con quello remoto.
---
## **11. Risorse aggiuntive**
- [Documentazione ufficiale di Git](https://git-scm.com/doc)
- [Documentazione ufficiale di Git](https://git-scm.com/doc)
- [Guida interattiva Learn Git Branching](https://learngitbranching.js.org/)
- [Guida interattiva Learn Git Branching](https://learngitbranching.js.org/)
- [GitHub Docs](https://docs.github.com/)
- [GitHub Docs](https://docs.github.com/)

@ -64,13 +64,13 @@ jobs:
Il comando `rsync` ha le seguenti opzioni:
- `-c` per controllare i file tramite checksum invece che per data e dimensione (che sono sempre diverse visto che stiamo ricosruendo il sito ogni volta con le GitHub Actions)
- `-c` per controllare i file tramite checksum invece che per data e dimensione (che sono sempre diverse visto che stiamo ricosruendo il sito ogni volta con le GitHub Actions)
- `-a` per copiare ricorsivamente i file e mantenere i permessi
- `-a` per copiare ricorsivamente i file e mantenere i permessi
- `-v` per mostrare i file copiati
- `-v` per mostrare i file copiati
- `-z` per comprimere i file durante il trasferimento
- `-z` per comprimere i file durante il trasferimento
## SSH Segreti

@ -12,11 +12,11 @@ Poisson è un server autogestito dalla comunità studentesca di matematica, da s
Se non si è mai creato un account Poisson, è necessario inviare una richiesta via email a **macchinisti@lists.dm.unipi.it** includendo:
- Nome
- Nome
- Cognome
- Cognome
- Username di ateneo (quello associato alla propria email istituzionale)
- Username di ateneo (quello associato alla propria email istituzionale)
Nella mail è sufficiente specificare che si desidera attivare un account Poisson. I "macchinisti" si occuperanno di attivare l'account il prima possibile.
@ -66,7 +66,10 @@ Vediamo un piccolo esempio di file `index.html` che possiamo creare:
style="max-width: 300px; border-radius: 10px;"
/>
<p>Ciao! Sono Sergio Steffè.</p>
<p>Email: <a href="mailto:sergio.steffe@example.com">sergio.steffe@example.com</a></p>
<p>
Email:
<a href="mailto:sergio.steffe@example.com">sergio.steffe@example.com</a>
</p>
</body>
</html>
```

@ -0,0 +1,80 @@
---
id: stampare-via-ssh
title: Stampare via SSH
description: Istruzioni per stampare in dipartimento da remoto, tramite SSH 🖨
author: Antonio De Lucreziis, Francesco Minnocci
tags: [linux, ssh, stampanti]
---
Per stampare in dipartimento non bisogna per forza usare i computer dei laboratori, possiamo che stampare direttamente da remoto tramite SSH. Vediamo come fare!
Se non l'avete mai fatto per prima cosa bisogna poter accedere da remoto ad una macchina chiamata "login", il cui indirizzo è `login.dm.unipi.it`. Per fare l'accesso possiamo usare il seguente comando con l'account di Ateneo (non quello Poisson!)
```bash shell
ssh USERNAME_ATENEO@login.dm.unipi.it
```
Una volta connessi possiamo stampare utilizzando il comando `lpr` seguito dal nome del file che vogliamo stampare. Prima però serve trasferire il file che vogliamo stampare sulla macchina "login". Per fare ciò possiamo usare il comando `scp`: per prima cosa usciamo dalla macchina "login" (premere `Ctrl+D` oppure scrivendo `exit`), andiamo nella cartella dove si trova il file che vogliamo stampare e poi eseguiamo il comando:
```bash shell
scp NOME_FILE.pdf USERNAME_ATENEO@login.dm.unipi.it:~/Documents
```
Dove `NOME_FILE.pdf` è il nome del file che vogliamo stampare e `Documents` è un esempio di cartella dove vogliamo trasferirlo. Una volta trasferito il file possiamo rifare ssh su "login" e stampare il file con il comando:
```bash shell
lpr Documents/NOME_FILE.pdf
```
Alternativamente possiamo stampare direttamente il file senza trasferirlo con il comando:
```bash shell
cat NOME_FILE.pdf | ssh USERNAME_ATENEO@login.dm.unipi.it lpr OPZIONI... -
```
Qui, `[OPZIONI...]` sono le opzioni che possiamo passare a `lpr` (vedi sotto). L'ultimo trattino "`-`" è molto importante e indica che il file da stampare è quello in standard input. Più precisamente, `cat NOME_FILE.pdf` invia il contenuto del file `NOME_FILE.pdf` allo standard output e `|` lo ridireziona a input di `ssh`, che a sua volta lo passa a `lpr` via rete.
## Opzioni di `lpr`
Il comando `lpr` accetta alcune opzioni che possono essere utili:
- `-P` seguito dal nome della stampante: permette di specificare la stampante su cui stampare, le stampanti disponibili in dipartimento sono
- `cdc4` che è la stampante di default e si trova in Aula 4
- `cdclf` che si trova al piano terra nel corridoio dopo l'Aula 4
- `cdc3` che si trova in Aula 3 (è un po' vecchia ma di solito funziona)
- `-#` seguito dal numero di copie: permette di specificare il numero di copie da stampare. In realtà questa opzione non funziona per vari motivi arcani e se uno passa `-#N` per stampare $N$ copie, la stampante stampa $N^2$ copie. (Questo ha scaturito una serie di ragionamenti sul modo ottimo di decomporre $N$ come somma di quadrati [con tanto di sito di comodo](https://shortest-sum-of-squares.netlify.app/)...)
- `-o sides=two-sided-long-edge`: permette di stampare **fronte-retro** (che dovrebbe essere già il default)
- `-o sides=two-sided-short-edge`: permette di stampare fronte-retro con "la rilegatura" delle pagine sul lato corto
- `-o sides=one-sided`: permette di stampare _solo fronte_, comodo per stampare i meme di laurea
- `-o fit-to-page`: permette di ridimensionare il documento per farlo entrare in un foglio (è buona prassi passare sempre questa opzione)
- `-o media=a4`: permette di specificare il formato del foglio, di default è A4 quindi non dovrebbere servire
## Altre comodità
Stampare da remoto porta anche altre comodità, ad esempio possiamo interrompere un file che abbiamo mandato in stampa per sbaglio con il comando (sempre tutti comandi da eseguire su "login")
```bash shell
cancel -a
```
> Attenzione, il comando sopra cancella tutta la propria coda di stampa, non solo l'ultimo lavoro inviato.
Alternativamente possiamo vedere lo stato della coda di stampa con il comando
```bash shell
lpq -a
```
e cancellare un lavoro con uno specifico ID con
```bash shell
cancel ID
```

@ -35,10 +35,7 @@ Una card semplice ha un titolo ed una descrizione.
```astro
<div class="card" style="--card-base: var(--guide-base); max-width: 25rem;">
<div class="title">Titolo</div>
<div class="text">
Descrizione lorem ipsum dolor sit amet consectetur
adipisicing elit. Aspernatur, labore?
</div>
<div class="text">Descrizione lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, labore?</div>
</div>
```
@ -51,14 +48,11 @@ Le card possono essere di dimensioni diverse. Questa è una card grande.
```astro
<div class="card large" style="--card-base: lightgreen; max-width: 25rem;">
<div class="title">Titolo</div>
<div class="text">
Descrizione lorem ipsum dolor sit amet consectetur
adipisicing elit. Aspernatur, labore?
</div>
<div class="text">Descrizione lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, labore?</div>
</div>
```
### Low Level: Mixin SCSS
### ~~Low Level: Mixin SCSS~~ Old CSS Mixin
Non dovrebbe essere mai necessario usarlo direttamente ma l'effetto di ombra delle card è ottenuto con questo mixin SCSS (che si trova in `src/styles/mixins.scss`).
@ -100,10 +94,7 @@ Se c'è poco testo, può essere inserito direttamente nella card.
```astro
<div class="card">
<div class="text">
Descrizione lorem ipsum dolor sit amet consectetur
adipisicing elit. Aspernatur, labore?
</div>
<div class="text">Descrizione lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, labore?</div>
</div>
```
@ -113,16 +104,15 @@ Altrimenti può essere inserito in un tag `<p>`.
<div class="card">
<div class="text">
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
Distinctio, vel! Veritatis est sit beatae eveniet.
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Distinctio, vel! Veritatis est sit beatae eveniet.
</p>
<p>
Error, minus, asperiores quaerat nulla cumque, nisi ipsam
assumenda consectetur accusamus tempore consequatur quae. Fugit?
Error, minus, asperiores quaerat nulla cumque, nisi ipsam assumenda consectetur accusamus tempore
consequatur quae. Fugit?
</p>
<p>
Quos sapiente amet numquam quis, libero odit eum, eius
perspiciatis repellat nesciunt cupiditate asperiores maiores?
Quos sapiente amet numquam quis, libero odit eum, eius perspiciatis repellat nesciunt cupiditate asperiores
maiores?
</p>
</div>
</div>
@ -132,19 +122,10 @@ C'è anche il modificatore `small` e `dimmed` per ridurre la grandezza del testo
```astro
<div class="card" style="max-width: 25rem;">
<div class="text">
Some normal text, this is a very long
text that should wrap on the next line
</div>
<div class="text small">
This is some small text
</div>
<div class="text dimmed">
This is some dimmed text
</div>
<div class="text small dimmed">
This is some small dimmed text
</div>
<div class="text">Some normal text, this is a very long text that should wrap on the next line</div>
<div class="text small">This is some small text</div>
<div class="text dimmed">This is some dimmed text</div>
<div class="text small dimmed">This is some small dimmed text</div>
</div>
```
@ -197,7 +178,6 @@ import { ComboBox } from '@/lib/components/ComboBox'
const [value, setValue] = useState('option-1')
```
```jsx
<ComboBox value={value} setValue={setValue}>
{{

@ -35,4 +35,3 @@ Il nuovo sito unifica varie funzionalità che prima erano sparse in diversi siti
## Conclusioni
Speriamo che il nuovo sito vi piaccia e vi sia utile. Se avete suggerimenti o richieste, non esitate a contattarci. Buona navigazione, magari iniziando dalla [pagina sulla storia del PHC](/storia)!

@ -0,0 +1,31 @@
---
title: Incidente del 2-7 Gennaio 2025
description: Riassunto dell'incidente che ha portato al guasto del server Poisson e delle azioni intraprese per ripristinare i servizi.
publishDate: 2025-01-07
---
# Incidente 2-7 Gennaio 2025
In questi giorni il server fisico principale del PHC, **Poisson**, non è stato operativo. Di conseguenza, i servizi ospitati direttamente su Poisson, come le pagine web degli utenti, l'accesso SSH e Gitea, sono rimasti offline. Al contrario, altri servizi web del PHC che non sono hostati su Poisson non hanno subito interruzioni. La buona notizia è che tutti i dati sono stati recuperati con successo e che l'infrastruttura è tornata pienamente operativa senza ulteriori complicazioni.
## Cosa è successo?
Sospettiamo che la causa principale dell'incidente sia stata una serie di brusche accensioni e spegnimenti, che hanno portato al guasto della scheda madre di Poisson. Il server non riusciva più ad avviarsi, neanche accedendo al BIOS, rendendo evidente che il problema era di natura hardware.
### Diagnosi e sostituzione
La mattina del 7 Gennaio, grazie alla disponibilità del dipartimento, abbiamo trovato una scheda madre di recupero, simile a quella originale ma meno potente. Questa è stata installata immediatamente, permettendo di riportare Poisson in funzione. Dal punto di vista software non è cambiato nulla: l'architettura della CPU è rimasta invariata, sebbene con meno core e la metà della RAM. Gli Hard Disks e una delle schede di rete sono stati riutilizzati senza problemi.
In un futuro prossimo si prevede di aggiornare la scheda madre con una più potente, per garantire le prestazioni di Poisson. Preghiamo per ora quindi di evitare di eseguire operazioni troppo pesanti sul server.
Tutto è stato rimontato nel consueto case bianco che ospita Poisson. Anche se ora è operativo, il server potrebbe essere considerato una nuova incarnazione, sollevando una riflessione interessante: **Poisson è ancora lo stesso server?** Proprio come nel paradosso della nave di Teseo, molti componenti di Poisson sono stati sostituiti nel corso degli anni. Dal 1992, pezzi come HDD, CPU, RAM e schede madri sono stati aggiornati per mantenere il server al passo con i tempi, mentre i dati e le funzionalità sono rimasti intatti.
![Visione vs Visione](/images/misc/meme-vision-vs-vision.webp)
## Ritorno alla normalità
Grazie allintervento, i servizi principali sono tornati online. Gli utenti possono nuovamente accedere alle proprie pagine web, utilizzare SSH e lavorare su Gitea senza alcuna modifica ai dati o alle configurazioni precedenti.
Per chi avesse riscontrato difficoltà nell'accesso, invitiamo a contattare i macchinisti per ricevere supporto.
> Be a System Administrator, they said. It will be fun, they said 🤦‍♂️

@ -0,0 +1,22 @@
---
title: Calcola la tua media ed il voto di laurea con il nuovissimo calcolatore del PHC!
description: È ora disponibile uno strumento per calcolare la propria media pesata e il voto di ammissione alla laurea secondo le regole del dipartimento.
publishDate: 2025-06-26
---
# Calcola la tua media ed il voto di laurea con il nuovissimo calcolatore del PHC!
È ora disponibile nella sezione "Calcolo Media" del sito uno strumento per calcolare la propria media pesata e il voto di ammissione alla laurea secondo le regole ufficiali del dipartimento.
<p align="center">
<a href="https://phc.dm.unipi.it/media-pesata/">phc.dm.unipi.it/media-pesata/</a>
</p>
Il calcolatore applica automaticamente le regole di esclusione previste dal regolamento:
- **Triennale**: vengono esclusi i 15 CFU con i voti più bassi
- **Magistrale**: vengono esclusi i 9 CFU con i voti più bassi
Il sistema calcola anche il bonus per le lodi, che vale +0.5 punti per ogni materia superiore a 6 CFU e +0.25 punti per materie da 6 CFU o meno, con un tetto massimo di +1.5 punti per la triennale e +2 punti per la magistrale.
Se dovessero esserci bug scriveteci un'email a <a href="mailto:macchinisti@lists.dm.unipi.it">macchinisti@lists.dm.unipi.it</a>!

@ -0,0 +1,46 @@
---
title: Esplora i meme dell'aula studenti online!
description: |
Gli storici meme sono stati staccati per i lavori, ma non disperare: li potrai vedere su una nuova pagina.
publishDate: 2025-06-26
---
# Esplora i meme dell'aula studenti online!
Visti gli imminenti lavori che occuperanno l'aula studenti, ad inizio Settembre tutti i meme sulle pareti sono stati staccati e riposti temporaneamente in PHC; qui sotto trovate alcuni timelapse della giornata:
<div class="grid-h-split">
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-nord.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-sud.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-termosifone.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
Per questo motivo, li abbiamo scansionati ed abbiamo creato una bacheca per poter contemplare i meme dovunque voi siate 🧳
Eccovi dunque il link alla pagina, buon divertimento:
![Screenshot Bacheca](/images/misc/screenshot-bacheca.png)
<p align="center">
<a href="https://meme.phc.dm.unipi.it">meme.phc.dm.unipi.it</a>
</p>
## Coming Soon
Prima o poi faremo anche una mappa interattiva della stanza, basata sul seguente modello 3D ricostruito con tecniche di fotogrammetria:
<video controls>
<source src="https://static.phc.dm.unipi.it/3d-scan-preview.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
> Disclaimer: Se volessi rimuovere una tua immagine da questa pagina, scrivici pure a <a href="mailto:macchinisti@lists.dm.unipi.it">macchinisti@lists.dm.unipi.it</a> e ce ne occuperemo.

@ -0,0 +1,34 @@
---
title: Non avete attivato la 2FA entro il primo dicembre? Ecco come recuperare l'accesso
description: |
Se non avete attivato l'autenticazione a due fattori entro il primo dicembre, siete bloccati fuori dall'account Unipi. La procedura di recupero passa dal supporto tecnico.
publishDate: 2025-12-05
---
# Non avete attivato la 2FA entro il primo dicembre? Ecco come recuperare l'accesso
Se siamo rimasti fuori dal nostro account Unipi perché non abbiamo attivato l'autenticazione a due fattori entro il primo dicembre, il percorso di recupero è piuttosto lineare.
Il primo passo è mandare una mail a
> <a href="mailto:help.polo2@ticket.unipi.it">help.polo2@ticket.unipi.it</a>
usando un indirizzo email personale (quello di ateneo è bloccato). Nel messaggio dobbiamo specificare chiaramente il problema e includere il nostro indirizzo email d'ateneo.
Dopo aver inviato la richiesta, tocca aspettare. Il supporto tecnico resetta manualmente lo status 2FA e ci manda una conferma. A quel punto possiamo procedere con l'attivazione seguendo le istruzioni ufficiali:
> https://it.unipi.it/configurazioni/mfa/autenticazione-a-piu-fattori-mfa-microsoft-365/
**Nota tecnica:** L'app Microsoft Authenticator non è obbligatoria! Qualsiasi autenticatore compatibile con [TOTP](https://en.wikipedia.org/wiki/Time-based_one-time_password) va bene: Google Authenticator, Bitwarden, Authy e altre alternative funzionano perfettamente. L'unica differenza è che l'app Microsoft Authenticator permette di ricevere notifiche l'accesso che consente di fare login con meno click.
Per utilizzare una di queste altre app (invece di Microsoft Authenticator), seguire questi step nella creazione del metodo di autenticazione:
1. Selezionare **"Microsoft Authenticator"**
![Selezionare Microsoft Authenticator](/images/misc/microsoft-2fa-other-app-1.webp)
2. Selezionare **"Configura un'app di autenticazione diversa"**
![Selezionare app diversa](/images/misc/microsoft-2fa-other-app-2.webp)
3. A questo punto il procedimento cambia in base all'app. In generale verrà richiesto di scannerizzare un QR code e di verificare il corretto funzionamento inserendo la TOTP (codice a 6 cifre)

File diff suppressed because it is too large Load Diff

@ -3,40 +3,93 @@
- fullName: Antonio De Lucreziis
entranceDate: 2019
description: |
Appassionato di geometria computazionale, parser, teoria dei linguaggi di programmazione, Smalltalk e Lisp.
Appassionato di geometria computazionale, parser, teoria dei linguaggi di programmazione, Smalltalk e Lisp.
social:
github: https://github.com/aziis98
website: https://poisson.phc.dm.unipi.it/~delucreziis/
github: https://github.com/aziis98
website: https://poisson.phc.dm.unipi.it/~delucreziis/
- fullName: Luca Lombardo
entranceDate: 2024
description: Appassionato di algoritmi e strutture dati, Rust e di quando matematica e informatica si incontrano.
social:
github: https://github.com/lukefleed
website: https://lukefleex.xyz
linkedin: https://www.linkedin.com/in/l-lombardo/
github: https://github.com/lukefleed
website: https://lukefleex.xyz
linkedin: https://www.linkedin.com/in/l-lombardo/
- fullName: Francesco Minnocci
entranceDate: 2022
description: Chitarrista classico ossessionato con la geometria aritmetica, linux e il rock progressivo.
social:
github: https://github.com/BachoSeven
website: https://bachoseven.com
github: https://github.com/BachoSeven
website: https://bachoseven.com
- fullName: Francesco Baldino
entranceDate: 2022
description: Bla bla Star Wars
description: |
Appassionato di Star Wars, NixOS e lunghe camminate in montagna. Pokemon preferito: Latias.
social:
github: https://github.com/Fran314
website: https://poisson.phc.dm.unipi.it/~baldino
github: https://github.com/Fran314
website: https://poisson.phc.dm.unipi.it/~baldino
- fullName: Illya Serdyuk
entranceDate: 2020
description: Bla bla Void Linux
social:
github: https://github.com/Kratacoa
github: https://github.com/Kratacoa
# Vecchi Macchinisti
- fullName: Francesco Manicastri
entranceDate: 2022
entranceDate: 2020
exitDate: 2024
description: Bla bla Keenan Crane
social:
linkedin: https://www.linkedin.com/in/gustavo-sass%C3%ACnculo-phd-92916a202/
- fullName: Cristiano Cricci
entranceDate: 2010
exitDate: 2019
social:
website: https://poisson.phc.dm.unipi.it/~cricci/
- fullName: Tommaso Biannucci
entranceDate: 2019
exitDate: 2022
social:
github: https://gitlab.com/churli
website: https://churli.gitlab.io/
- fullName: Letizia D'Achille
entranceDate: 2018
exitDate: 2022
description: Appassionata di crittografia, teoria dei codici e matematica computazionale.
social:
github: https://github.com/letizia-dachille
website: https://letizia-dachille.github.io/
linkedin: https://www.linkedin.com/in/letizia-dachille/
- fullName: Emiliano Rago
entranceDate: 2000
exitDate: 2006
description: Traviato da Linux in età troppo giovane ha difficoltà ad usare il mouse ma ama gli shortcuts con una decina di tasti.
social:
linkedin: https://www.linkedin.com/in/emiliano-rago-1a8018109/
- fullName: Francesco Caporali
entranceDate: 2018
exitDate: 2022
social:
github: https://github.com/caporali
website: https://caporali.github.io/
linkedin: https://www.linkedin.com/in/francescocaporali
- fullName: Antonio Spanu
entranceDate: 2005
exitDate: 2009
social:
linkedin: https://www.linkedin.com/in/antonio-spanu-609b3516a/
- fullName: Riccardo Murri
entranceDate: 1995
exitDate: 2000
founder: true
social:
github: https://github.com/riccardomurri

29
src/files.d.ts vendored

@ -6,12 +6,39 @@ declare module '*.yaml' {
declare module '@/data/macchinisti.yaml' {
type Macchinista = {
fullName: string
description: string
entranceDate: number
exitDate?: number
description: string
social: Record<string, string>
founder?: boolean
}
const value: Macchinista[]
export default value
}
declare module '@/data/domande-esami.yaml' {
export type Question = {
course: string
content: string
tags: string[]
}
export type Group = {
id: string
name: string
items: Array<string>
}
export type Database = {
names: Record<string, string>
groups: Group[]
questions: Question[]
}
const value: Database
export default value
}

@ -9,20 +9,17 @@ import '@fontsource/iosevka/latin.css'
import '@fontsource-variable/material-symbols-outlined/full.css'
import '../styles/main.scss'
import '@/styles/main.css'
type Props = {
title?: string
description?: string
thumbnail?: string
/** Tags for the page, used for styling */
pageTags?: string | string[]
}
import phcIcon from '../assets/icon.png'
const { title, description, thumbnail, pageTags } = Astro.props
const { title, description, thumbnail } = Astro.props
---
<!doctype html>
@ -44,7 +41,7 @@ const { title, description, thumbnail, pageTags } = Astro.props
<script>
import renderMathInElement from 'katex/contrib/auto-render'
document.addEventListener('DOMContentLoaded', function () {
const renderMath = () => {
renderMathInElement(document.body, {
delimiters: [
{ left: '$$', right: '$$', display: true },
@ -54,7 +51,12 @@ const { title, description, thumbnail, pageTags } = Astro.props
],
throwOnError: false,
})
})
}
document.addEventListener('DOMContentLoaded', () => renderMath())
// @ts-ignore
window.renderMath = renderMath
</script>
<script is:inline>
@ -70,9 +72,14 @@ const { title, description, thumbnail, pageTags } = Astro.props
src="//analytics.phc.dm.unipi.it/count.js"
data-goatcounter="https://analytics.phc.dm.unipi.it/count"></script>
<style is:inline>
/* Workaround Astro CSS loading order, this forces the layering upfront */
@layer base, typography, component, page, utility;
</style>
<title>{title ?? 'PHC'}</title>
</head>
<body class:list={typeof pageTags === 'string' ? [pageTags] : pageTags}>
<body>
<slot />
</body>
</html>

@ -1,4 +1,6 @@
---
import '@/styles/pages/appunti.css'
import PageLayout from '@layouts/PageLayout.astro'
import { AppuntiList, AppuntiCard } from '@client/Appunti'

@ -0,0 +1,27 @@
---
import '@/styles/pages/domande-esami.css'
import type { GetStaticPaths } from 'astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro'
import database from '@/data/domande-esami.yaml'
import { DomandeEsamiCourse } from '@/client/DomandeEsamiCourse'
export const getStaticPaths = (() => {
return Object.keys(database.names).map(course => ({
params: { course },
}))
}) satisfies GetStaticPaths
const { course } = Astro.params
---
<BaseLayout title="Domande Orali | PHC">
<Header />
<main>
<DomandeEsamiCourse client:only="preact" course={course} />
</main>
<Footer />
</BaseLayout>

@ -0,0 +1,11 @@
import type { APIRoute } from 'astro'
import database from '@/data/domande-esami.yaml'
export const GET: APIRoute = ({}) => {
return new Response(JSON.stringify(database), {
headers: {
'content-type': 'application/json',
},
})
}

@ -0,0 +1,24 @@
import type { APIRoute, GetStaticPaths } from 'astro'
import database from '@/data/domande-esami.yaml'
export const getStaticPaths = (() => {
return Object.keys(database.names).map(course => ({
params: { course },
}))
}) satisfies GetStaticPaths
export const GET: APIRoute = ({ params: { course } }) => {
return new Response(
JSON.stringify({
groups: [],
names: Object.fromEntries(Object.entries(database.names).filter(([key]) => key === course)),
questions: database.questions.filter(question => question.course === course),
}),
{
headers: {
'content-type': 'application/json',
},
},
)
}

@ -0,0 +1,66 @@
---
import '@/styles/pages/domande-esami.css'
import { PhosphorIcon } from '@/client/Icon'
import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
import database from '@/data/domande-esami.yaml'
const courseQuestionCounts = Object.fromEntries(
database.questions.reduce((acc, question) => {
acc.set(question.course, (acc.get(question.course) || 0) + 1)
return acc
}, new Map()),
)
---
<BaseLayout title="Domande Orali | PHC">
<Header />
<main>
<h1>Domande Orali</h1>
{
database.groups.map(group => (
<details open>
<summary>
<h2 id={group.id}>
<div class="details-closed">
<PhosphorIcon name="caret-down" />
</div>
<div class="details-openned">
<PhosphorIcon name="caret-up" />
</div>
{group.name}
</h2>
</summary>
<div class="wide-card-list">
{group.items
.filter(course => courseQuestionCounts[course] > 0)
.map(course => (
<a href={`/domande-esami/${course}`}>
<div class="card">
<h2>{database.names[course]}</h2>
<div class="text">
<p>{courseQuestionCounts[course] || 0} domande</p>
</div>
</div>
</a>
))}
</div>
</details>
))
}
<h3>Come Contribuire</h3>
<div class="card large">
<div class="text">
<p>
Se hai raccolto delle domande da un orale, puoi inviarcele per email all'indirizzo
<a href="mailto:macchinisti@lists.dm.unipi.it"> macchinisti@lists.dm.unipi.it</a>.
</p>
</div>
</div>
</main>
<Footer />
</BaseLayout>

@ -1,4 +1,6 @@
---
import '@/styles/pages/guide-item.css'
import { getCollection } from 'astro:content'
import ArticleLayout from '@/layouts/ArticleLayout.astro'
@ -16,11 +18,9 @@ const { entry } = Astro.props
const { Content } = await entry.render()
---
<ArticleLayout
{...entry.data}
title={entry.data.title + ' | Guide | PHC'}
pageTags={['guida', entry.data.id, entry.data.series && 'series']}
>
<ArticleLayout {...entry.data} title={entry.data.title + ' | Guide | PHC'}>
<!-- pageTags={['guida', entry.data.id, entry.data.series && 'series']} -->
<h1>{entry.data.title}</h1>
{entry.data.series && <div class="series">Serie: {entry.data.series}</div>}

@ -1,4 +1,6 @@
---
import '@/styles/pages/guide-list.css'
import { getCollection } from 'astro:content'
import PageLayout from '@layouts/PageLayout.astro'
@ -6,7 +8,7 @@ import PageLayout from '@layouts/PageLayout.astro'
const guides = await getCollection('guides')
---
<PageLayout title="Guide | PHC" pageTags="guide">
<PageLayout title="Guide | PHC">
<h1>
<a href="/guide">Guide</a>
</h1>

@ -1,4 +1,6 @@
---
import '@/styles/pages/guide-list.css'
import { getCollection } from 'astro:content'
import type { CollectionEntry } from 'astro:content'
import PageLayout from '@/layouts/PageLayout.astro'
@ -33,7 +35,7 @@ interface Props {
const { tag, guides } = Astro.props
---
<PageLayout title={`#${tag} | Guide | PHC`} pageTags="guide tag">
<PageLayout title={`#${tag} | Guide | PHC`}>
<h1><a href="/guide">Guide</a> > <a href={`/guide/tags/${tag}`}>#{tag}</a></h1>
<div class="card-list">
{

@ -1,4 +1,6 @@
---
import '@/styles/pages/homepage.css'
import { getCollection } from 'astro:content'
import PageLayout from '@/layouts/PageLayout.astro'
import { Content as WhatPhcContent, frontmatter as whatsPhcFrontmatter } from '@/content/meta/whats-phc.md'
@ -9,7 +11,11 @@ import Card from '@/components/Card.astro'
const news = await getCollection('news')
const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
// const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
const galleryCollage: { default: ImageMetadata }[] = Object.values(
import.meta.glob('@/assets/gallery/*.jpg', { eager: true }),
)
---
<PageLayout title="PHC" pageTags="homepage">
@ -45,21 +51,23 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
<div class="card-list">
{
news.map(newsItem => (
<Card>
<a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title}
</a>
<div class="text small dimmed">
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<div class="text">{newsItem.data.description}</div>
</Card>
))
news
.sort((s, t) => -s.id.localeCompare(t.id))
.map(newsItem => (
<Card>
<a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title}
</a>
<div class="text small dimmed">
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<div class="text">{newsItem.data.description}</div>
</Card>
))
}
</div>
@ -80,8 +88,7 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
>
<p>Gitea è un servizio di hosting per progetti software, come GitHub ma autogestito.</p>
<p>
Qui puoi trovare i progetti del PHC, e accedendo con un account di Ateneo potrai crearne
di nuovi.
Qui puoi trovare i progetti del PHC, e accedendo con un account di Ateneo potrai crearne di nuovi.
</p>
</ProjectCard>
<!-- <ProjectCard
@ -98,8 +105,8 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
imgSrc="https://lab.phc.dm.unipi.it/orario/icon.png"
>
<p>
Questo sito permette di visualizzare il proprio orario delle lezioni, con informazioni sui
docenti e le aule.
Questo sito permette di visualizzare il proprio orario delle lezioni, con informazioni sui docenti e
le aule.
</p>
</ProjectCard>
<ProjectCard
@ -118,8 +125,8 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
imgSrc="https://steffe.lb.cs.dm.unipi.it/assets/img/logo.png"
>
<p>
Cluster progettato ed assemblato durante il progetto speciale per la didattica "Calcolo
Parallelo dall'Infrastruttura alla Matematica".
Cluster progettato ed assemblato durante il progetto speciale per la didattica "Calcolo Parallelo
dall'Infrastruttura alla Matematica".
</p>
</ProjectCard>
<ProjectCard
@ -128,10 +135,7 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
style="--card-bg: #bd9fec; --masonry-height: 2;"
imgSrc="https://seminarietti.phc.dm.unipi.it/favicon.png"
>
<p>
Storico degli incontri organizzati dal PHC su argomenti di informatica, matematica e
tecnologia.
</p>
<p>Storico degli incontri organizzati dal PHC su argomenti di informatica, matematica e tecnologia.</p>
</ProjectCard>
<ProjectCard
title="Tutorato"
@ -140,8 +144,8 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
imgSrc="https://tutorato.phc.dm.unipi.it/favicon.svg"
>
<p>
Un sito con tutte le informazioni sui tutorati di Matematica, con tanto di archivio degli
anni passati.
Un sito con tutte le informazioni sui tutorati di Matematica, con tanto di archivio degli anni
passati.
</p>
</ProjectCard>
<ProjectCard
@ -151,10 +155,18 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
imgSrc="https://ggwp.phc.dm.unipi.it/ggwp-icon.png"
>
<p>
Sito per il tabellone del GGWP, utilizzato durante la gara di Novembre 2024 dagli
organizzatori per inserire le risposte alla gara in tempo reale.
Sito per il tabellone del GGWP, utilizzato durante la gara di Novembre 2024 dagli organizzatori per
inserire le risposte alla gara in tempo reale.
</p>
</ProjectCard>
<ProjectCard
title="Chat"
href="https://chat.phc.dm.unipi.it/"
style="--card-bg: #383838; --card-fg: #ddd; --masonry-height: 1;"
imgSrc="https://chat.phc.dm.unipi.it/favicon.png"
>
<p>Istanza di Open-WebUI, una chat stile ChatGPT con alcuni LLM self-hostati dal PHC.</p>
</ProjectCard>
</div>
</section>
<section class="wanna-be-macchinista">
@ -172,22 +184,18 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
supporto tecnico per installare ed usare <strong>Linux</strong> sul proprio portatile
</li>
<li>
<strong>costruiamo</strong>, <strong>smontiamo</strong> ed <strong>aggiustiamo</strong
>
<strong>costruiamo</strong>, <strong>smontiamo</strong> ed <strong>aggiustiamo</strong>
computer (antichi e moderni)
</li>
<li>
<strong>sviluppo software</strong> di backend e frontend per siti web (ma non solo)
</li>
<li>
organizzazione di <strong>seminari</strong> di divulgazione (vedi <a href="#projects"
>sopra</a
>)
organizzazione di <strong>seminari</strong> di divulgazione (vedi <a href="#projects">sopra</a>)
</li>
</ul>
<p>
Infine, il PHC è prima di tutto un luogo dove <strong>imparare</strong>, <strong
>trasmettere</strong
Infine, il PHC è prima di tutto un luogo dove <strong>imparare</strong>, <strong>trasmettere</strong
> le proprie conoscenze e <strong>condividere</strong> la passione per la tecnologia.
</p>
</div>
@ -196,14 +204,16 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
{
galleryCollage.map((module, i) => {
const src = module.default.src
const filename = src.split('/').at(-1).split('?').at(0).split('.').at(0)
const filename = src.split('/').at(-1)?.split('?').at(0)?.split('.').at(0)
if (!filename) return null
const [rows, cols] = filename.includes('@')
? filename
? (filename
.split('@')
.at(-1)
.split('x')
.map((s: string) => parseInt(s))
?.split('x')
.map((s: string) => parseInt(s)) ?? [1, 1])
: [1, 1]
return (
@ -219,13 +229,12 @@ const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
<div class="title">Vuoi diventare macchinista?</div>
<div class="text">
<p>
<strong>Macchinista non si nasce, si diventa:</strong> se sei uno studente di Matematica e
vuoi diventare un macchinista, vienici a trovare!
<strong>Macchinista non si nasce, si diventa:</strong> se sei uno studente di Matematica e vuoi diventare
un macchinista, vienici a trovare!
</p>
<p>
L'unico prerequisito è la voglia di imparare! Di solito, c'è un periodo di "apprendistato"
nel quale si apprendono le basi; una volta superato potrai diventare un macchinista a
tutti gli effetti.
L'unico prerequisito è la voglia di imparare! Di solito, c'è un periodo di "apprendistato" nel quale
si apprendono le basi; una volta superato potrai diventare un macchinista a tutti gli effetti.
</p>
</div>
</Card>

@ -1,4 +1,6 @@
---
import '@/styles/pages/macchinisti.css'
import BaseLayout from '../layouts/BaseLayout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
@ -12,8 +14,8 @@ const images = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: ImageMetadata }>('@/assets/macchinisti/*', {
eager: true,
})
).map(([path, module]) => [path.split('/').pop()!.split('.')[0], module])
}),
).map(([path, module]) => [path.split('/').pop()!.split('.')[0], module]),
)
const currentMacchinisti = macchinisti.filter(macchinista => !macchinista.exitDate)
@ -26,7 +28,7 @@ const getMacchinistaPicture = (fullName: string) => {
}
---
<BaseLayout title="Macchinisti | PHC" pageTags={'macchinisti'}>
<BaseLayout title="Macchinisti | PHC">
<Header />
<main>
<div class="card large" style={{ '--card-base': '#e1766b' }}>
@ -35,9 +37,9 @@ const getMacchinistaPicture = (fullName: string) => {
<p>
<em>> Chi sono i macchinisti?</em>
<br />
Questo è l'appellativo dato agli studenti che si occupano di gestire l'infrastuttura e i servizi
del PHC (vedi la homepage per informazioni su come diventare un macchinista). Qua sotto trovi
i macchinisti attualmente attivi in PHC.
Questo è l'appellativo dato agli studenti che si occupano di gestire l'infrastuttura e i servizi del
PHC (vedi la homepage per informazioni su come diventare un macchinista). Qua sotto trovi i macchinisti
attualmente attivi in PHC.
</p>
</div>
</div>
@ -60,8 +62,8 @@ const getMacchinistaPicture = (fullName: string) => {
<div class="title"><s>Deus</s> Ex Macchinisti</div>
<div class="text">
<p>
Qui raccogliamo qualche informazione sui macchinisti del passato, che hanno contribuito a
rendere il PHC quello che è oggi.
Qui raccogliamo qualche informazione sui macchinisti del passato, che hanno contribuito a rendere il
PHC quello che è oggi.
</p>
</div>
</div>
@ -70,11 +72,12 @@ const getMacchinistaPicture = (fullName: string) => {
{
pastMacchinisti.map(macchinista => (
<Bubble
image={getMacchinistaPicture(macchinista.fullName)}
fullName={macchinista.fullName}
description={macchinista.description}
image={getMacchinistaPicture(macchinista.fullName)}
entranceDate={macchinista.entranceDate}
exitDate={macchinista.exitDate}
description={macchinista.description}
founder={macchinista.founder}
social={macchinista.social}
/>
))

@ -0,0 +1,21 @@
---
import '@/styles/pages/media-pesata.css'
import PageLayout from '../layouts/PageLayout.astro'
import { MediaPesataApp } from '@/client/MediaPesataApp'
---
<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>
<MediaPesataApp client:load />
</div>
</PageLayout>

@ -1,4 +1,6 @@
---
import '@/styles/pages/meta-design.css'
import BaseLayout from '../../layouts/BaseLayout.astro'
import Header from '../../components/Header.astro'
@ -11,7 +13,7 @@ import { Content, getHeadings } from '../../content/meta/design.mdx'
const headings = getHeadings()
---
<BaseLayout {...Astro.props} pageTags="design">
<BaseLayout {...Astro.props}>
<Header />
<aside>
<nav>

@ -1,4 +1,6 @@
---
import '@/styles/pages/news-item.css'
import { getCollection } from 'astro:content'
import ArticleLayout from '../../layouts/ArticleLayout.astro'
@ -15,6 +17,6 @@ const { entry } = Astro.props
const { Content } = await entry.render()
---
<ArticleLayout {...entry.data} title={entry.data.title + ' | Notizie | PHC'} pageTags={['notizia']}>
<ArticleLayout {...entry.data} title={entry.data.title + ' | Notizie | PHC'}>
<Content />
</ArticleLayout>

@ -1,4 +1,6 @@
---
import '@/styles/pages/news-list.css'
import { getCollection } from 'astro:content'
import PageLayout from '@layouts/PageLayout.astro'
@ -6,25 +8,27 @@ import PageLayout from '@layouts/PageLayout.astro'
const news = await getCollection('news')
---
<PageLayout title="Notizie | PHC" pageTags="notizie">
<PageLayout title="Notizie | PHC">
<h1><a href="/notizie">Notizie</a></h1>
<div class="card-list">
{
news.map(newsItem => (
<div class="card">
<a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title}
</a>
<div class="text small dimmed">
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
news
.sort((s, t) => -s.id.localeCompare(t.id))
.map(newsItem => (
<div class="card">
<a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title}
</a>
<div class="text small dimmed">
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<div class="text">{newsItem.data.description}</div>
</div>
<div class="text">{newsItem.data.description}</div>
</div>
))
))
}
</div>
</PageLayout>

@ -1,4 +1,6 @@
---
import '@/styles/pages/storia.css'
import BaseLayout from '../layouts/BaseLayout.astro'
import Header from '../components/Header.astro'
@ -10,16 +12,15 @@ import imgCluster from '@/assets/gallery/001-cluster-fra-luca@4x3.jpg'
import WebSite from '@/assets/gallery/005-website-development@3x4.jpg'
---
<BaseLayout title="Storia | PHC" pageTags={'storia'}>
<BaseLayout title="Storia | PHC">
<Header />
<main>
<div class="card large" style={{ '--card-base': '#ffd3a0' }}>
<div class="title">Storia</div>
<div class="text">
<p>
Il PHC nasce quasi venti anni fa, nel lontano 1999 ed ha perciò una ricca storia. Qui
proveremo ad annoverare la storia del PHC, in una timeline con gli eventi più salienti del
progetto.
Il PHC nasce quasi venti anni fa, nel lontano 1999 ed ha perciò una ricca storia. Qui proveremo ad
annoverare la storia del PHC, in una timeline con gli eventi più salienti del progetto.
</p>
<img
class="small flat"
@ -36,26 +37,25 @@ import WebSite from '@/assets/gallery/005-website-development@3x4.jpg'
<div class="timeline">
<Timeline title="Un nuovo look" year="Nov 2024">
<p>
Dopo <s>mesi</s> anni di lavoro e di progettazione, il sito del PHC viene riscritto da zero
in Astro. Il progetto è stato voluto principalmente da <strong
>Antonio De Lucreziis</strong
> (in foto, in fase di sviluppo), con il supporto di <strong>Francesco Minnocci</strong>.
Dopo <s>mesi</s> anni di lavoro e di progettazione, il sito del PHC viene riscritto da zero in Astro.
Il progetto è stato voluto principalmente da <strong>Antonio De Lucreziis</strong> (in foto, in fase
di sviluppo), con il supporto di <strong>Francesco Minnocci</strong>.
</p>
<img class="fill" src={WebSite.src} alt="Sviluppo del sito" />
</Timeline>
<Timeline title="Luca e le grandi pulizie" year="Apr 2024">
<p>
Nel 2024, <strong>Luca Lombardo</strong> entra a far parte del PHC. Questo stesso anno vengono
effettuate delle grandi pulizie nella stanza del PHC, come non si faceva dal lontano 2006.
Nel 2024, <strong>Luca Lombardo</strong> entra a far parte del PHC. Questo stesso anno vengono effettuate
delle grandi pulizie nella stanza del PHC, come non si faceva dal lontano 2006.
</p>
<img class="fill" src={imgPulizie.src} alt="pulizie phc" />
</Timeline>
<Timeline title="Il cluster di Raspberry Pi" year="2023">
<p>
Il dipartimento acquista in due fasi diverse 34 (cloni) di Raspberry Pi 4, che vengono
assemblati in un cluster per il progetto "High Performance Mathematics". Il cluster è
stato assemblato, configurato e messo in funzione con la forte collaborazione macchinisti
del PHC, che ancora oggi ne curano la manutenzione.
Il dipartimento acquista in due fasi diverse 34 (cloni) di Raspberry Pi 4, che vengono assemblati in
un cluster per il progetto "High Performance Mathematics". Il cluster è stato assemblato,
configurato e messo in funzione con la forte collaborazione macchinisti del PHC, che ancora oggi ne
curano la manutenzione.
</p>
<img class="fill" src={imgCluster.src} alt="cluster di raspberry pi" />
</Timeline>
@ -82,9 +82,8 @@ import WebSite from '@/assets/gallery/005-website-development@3x4.jpg'
</Timeline>
<Timeline title="Rinnovo del sito" year="2004">
<p>
Dopo un periodo di inattività del progetto, il sito del PHC viene riscritto in PHP e
trasferito sul dominio <a
href="https://web.archive.org/web/20040823112401/http://poisson.phc.unipi.it/"
Dopo un periodo di inattività del progetto, il sito del PHC viene riscritto in PHP e trasferito sul
dominio <a href="https://web.archive.org/web/20040823112401/http://poisson.phc.unipi.it/"
>poisson.phc.unipi.it</a
>, il cui design è caratterizzato da un <a
href="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
@ -108,33 +107,28 @@ import WebSite from '@/assets/gallery/005-website-development@3x4.jpg'
</Timeline>
<Timeline title="Rete del PHC e DNS" year="1999">
<p>
Nel maggio del 1999 viene attivata la rete 131.114.10.0, con tanto di nameserver sul
dominio <a href="https://web.archive.org/web/20010410215451/http://www.phc.unipi.it/"
>phc.unipi.it</a
Nel maggio del 1999 viene attivata la rete 131.114.10.0, con tanto di nameserver sul dominio <a
href="https://web.archive.org/web/20010410215451/http://www.phc.unipi.it/">phc.unipi.it</a
>.
</p>
</Timeline>
<Timeline title="Fondazione del PHC" year="1999">
<p>
In seguito alla proposta del prof. <strong>Sergio Steffè</strong>, in data 26 febbraio
1999 il Dipartimento di Matematica approva una delibera per stanziare la stanza 106 ed
alcuni computer ad uso di un gruppo di studenti, così da avere un luogo in cui
"smanettare", dare supporto informatica agli studenti e gestire il sito Poisson.
In seguito alla proposta del prof. <strong>Sergio Steffè</strong>, in data 26 febbraio 1999 il
Dipartimento di Matematica approva una delibera per stanziare la stanza 106, una sottorete, il
dominio DNS phc.dm.unipi.it ed alcuni computer ad uso di un gruppo di studenti (Riccardo Murri e
Massimiliano Sala), così da avere un luogo in cui "smanettare", offrire spazio a progetti
interessanti, dare supporto informatico agli studenti e gestire il sito Poisson
</p>
<img
class="fill"
src="https://poisson.phc.dm.unipi.it/~steffe/sergio.jpg"
alt="Sergio Steffè"
/>
<img class="fill" src="https://poisson.phc.dm.unipi.it/~steffe/sergio.jpg" alt="Sergio Steffè" />
</Timeline>
<Timeline title="Apertura di Poisson" year="~1994">
<Timeline title="Apertura di Poisson" year="~1995">
<p>
Nell'attuale Aula 4, allora semplice Aula studenti, nasce il sito web <strong
>poisson.dm.unipi.it</strong
>
su dei computer messi a disposizione agli studenti da Vinicio Villani. Una versione del 1996
di tale sito si trova nel <a
href="https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it/"
su dei computer messi a disposizione agli studenti da Vinicio Villani. Una versione del 1996 di tale
sito si trova nel <a href="https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it/"
>Web Archive</a
>
</p>

@ -1,10 +1,12 @@
---
import '@/styles/pages/utenti.css'
import PageLayout from '../layouts/PageLayout.astro'
import { UtentiPage } from '../client/UtentiPage.tsx'
---
<PageLayout title="Utenti | PHC" pageTags="utenti">
<PageLayout title="Utenti | PHC">
<h1>Utenti</h1>
<UtentiPage client:load />
</PageLayout>

@ -75,12 +75,7 @@ function setup() {
const handle = window.setInterval(() => {
const time = new Date().getTime() - startTime.getTime()
update(
state,
g.canvas.width / window.devicePixelRatio,
g.canvas.height / window.devicePixelRatio,
time
)
update(state, g.canvas.width / window.devicePixelRatio, g.canvas.height / window.devicePixelRatio, time)
render(g, state, time)
}, 1000 / RENDERER_FPS)
@ -266,25 +261,16 @@ const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<Wi
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
['down-left']: ([x, y], grid) =>
!grid.has([x - 1, y + 1]) &&
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
!grid.has([x - 1, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
['down-right']: ([x, y], grid) =>
!grid.has([x + 1, y + 1]) &&
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
!grid.has([x + 1, y + 1]) && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
}
function pruneDirections(
grid: Grid<WireCell>,
position: Point2,
directions: WireDirection[]
): WireDirection[] {
function pruneDirections(grid: Grid<WireCell>, position: Point2, directions: WireDirection[]): WireDirection[] {
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
}
function generateWire(
grid: Grid<WireCell>,
startingPoint: Point2
): { position: Point2; direction: WireCell }[] {
function generateWire(grid: Grid<WireCell>, startingPoint: Point2): { position: Point2; direction: WireCell }[] {
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
let currentPosition = startingPoint
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
@ -292,11 +278,7 @@ function generateWire(
const steps: { position: Point2; direction: WireCell }[] = []
for (let i = 0; i < segmentLength; i++) {
const availableDirections = pruneDirections(grid, currentPosition, [
'down',
'down-left',
'down-right',
])
const availableDirections = pruneDirections(grid, currentPosition, ['down', 'down-left', 'down-right'])
if (availableDirections.length === 0) {
break
} else {

@ -1,4 +1,6 @@
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
/* This file is here for historical reasons but is not used anymore */
/* @mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
border: $size solid #222;
border-radius: $size * 2;
@ -14,4 +16,4 @@
box-shadow: $offset + 1 $offset + 1 0 0 #222;
}
}
}
} */

@ -0,0 +1,27 @@
/* This file is here for historical reasons but is not used anymore */
@layer page {
/*
.login {
background: #ddfaff;
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
max-width: 80ch;
padding: 3rem 0;
gap: 3rem;
h3 {
font-size: 28px;
font-weight: 600;
}
}
}
*/
}

@ -1,12 +1,15 @@
// $news-bg: #fffbeb;
// $news-accent-bg: #f8e8b1;
/*
$news-bg: #fffbeb;
$news-accent-bg: #f8e8b1;
*/
@import './mixins.scss';
/* @TODO: SCSS conversion - @import becomes more complex */
/* @import './mixins.scss'; */
@layer component {
//
// Components - for complex parts of the UI like search bars or compound buttons
//
/*
/* Components - for complex parts of the UI like search bars or compound buttons
*/
.phosphor-icon {
box-sizing: content-box;
@ -14,7 +17,7 @@
width: 22px;
height: 22px;
display: grid;
display: grid inline;
place-content: center;
}
@ -33,7 +36,11 @@
place-content: center;
font-size: 24px;
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24;
max-width: 32px;
}
@ -48,7 +55,13 @@
width: 100%;
height: 2.5rem;
@include neo-brutalist-card;
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 4px 4px 0 0 #222;
background: #fff;
display: grid;
grid-template-columns: 1fr auto;
@ -56,8 +69,6 @@
cursor: pointer;
background: #fff;
&:hover,
&:hover input[type='text'] {
background: #f8f8f8;
@ -79,29 +90,6 @@
}
}
.flex-column {
display: flex;
flex-direction: column;
gap: 1rem;
}
.flex-row {
display: flex;
flex-direction: row;
gap: 1rem;
}
// just to know for reference
.fake-masonry {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
grid-auto-flow: dense;
& > * {
grid-row: span var(--masonry-height);
}
}
.search-results {
width: 100%;
@ -124,7 +112,11 @@
background: #fff;
@include neo-brutalist-card;
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 4px 4px 0 0 #222;
a {
display: contents;
@ -180,7 +172,16 @@
aspect-ratio: 10 / 14;
background: #d0d0d0;
@include neo-brutalist-card($hoverable: true);
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card($hoverable: true); */
border: 3px solid #222;
border-radius: 6px;
transition: all 64ms linear;
&:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 0 #222;
}
}
& > .thumbnail + * {
@ -214,7 +215,11 @@
background: var(--card-bg, var(--project-card-bg));
color: #000e;
@include neo-brutalist-card($size: 3px, $offset: 9px);
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card($size: 3px, $offset: 9px); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 9px 9px 0 0 #222;
row-gap: 0.5rem;
padding: 1rem;
@ -238,109 +243,109 @@
display: grid;
grid-auto-flow: column;
// .news-item {
// background: $news-bg;
// color: #111;
// @include neo-brutalist-card($size: 3px, $offset: 9px);
// display: flex;
// flex-direction: column;
// width: 22rem;
// max-height: 27rem;
// overflow: hidden;
// ::-webkit-scrollbar {
// width: 10px;
// }
// ::-webkit-scrollbar-thumb {
// background-color: #c67e14;
// border: 2px solid #222;
// &:hover {
// background-color: #e69419;
// }
// }
// a {
// font-weight: 600;
// text-decoration: none;
// color: #c67e14;
// &:hover {
// text-decoration: underline solid 2px;
// }
// }
// & > .title {
// padding: 1rem;
// background: $news-accent-bg;
// line-height: 1;
// font-size: 26px;
// }
// a.title {
// color: #83530c;
// }
// & > .abstract {
// flex-grow: 1;
// padding: 1rem;
// overflow-y: auto;
// @extend .text;
// }
// & > .content {
// display: flex;
// padding: 1rem;
// flex-direction: column;
// gap: 0.5rem;
// background: #fff8da;
// & > .continue {
// padding: 1rem;
// display: grid;
// align-items: end;
// justify-content: end;
// }
// & > .description {
// font-size: 16px;
// line-height: 1.5;
// flex-grow: 1;
// }
// & > .tags {
// display: flex;
// gap: 0.5rem;
// flex-wrap: wrap;
// font-size: 14px;
// color: #555;
// }
// & > .date {
// font-size: 14px;
// font-style: italic;
// font-weight: 600;
// color: #0008;
// }
// & > .author {
// font-weight: 600;
// font-size: 15px;
// color: #555;
// }
// }
// }
/* .news-item {
background: $news-bg;
color: #111;
@include neo-brutalist-card($size: 3px, $offset: 9px);
display: flex;
flex-direction: column;
width: 22rem;
max-height: 27rem;
overflow: hidden;
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #c67e14;
border: 2px solid #222;
&:hover {
background-color: #e69419;
}
}
a {
font-weight: 600;
text-decoration: none;
color: #c67e14;
&:hover {
text-decoration: underline solid 2px;
}
}
& > .title {
padding: 1rem;
background: $news-accent-bg;
line-height: 1;
font-size: 26px;
}
a.title {
color: #83530c;
}
& > .abstract {
flex-grow: 1;
padding: 1rem;
overflow-y: auto;
@extend .text;
}
& > .content {
display: flex;
padding: 1rem;
flex-direction: column;
gap: 0.5rem;
background: #fff8da;
& > .continue {
padding: 1rem;
display: grid;
align-items: end;
justify-content: end;
}
& > .description {
font-size: 16px;
line-height: 1.5;
flex-grow: 1;
}
& > .tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 14px;
color: #555;
}
& > .date {
font-size: 14px;
font-style: italic;
font-weight: 600;
color: #0008;
}
& > .author {
font-weight: 600;
font-size: 15px;
color: #555;
}
}
} */
}
.timeline {
@ -353,7 +358,7 @@
max-width: 120ch;
grid-template-columns: 1fr var(--timeline-track-size) 1fr;
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
--timeline-track-size: 3rem;
grid-template-columns: var(--timeline-track-size) 1fr;
}
@ -367,7 +372,7 @@
grid-template-columns: auto;
padding: 2rem 1rem;
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
padding: 1rem 1rem 1rem 0;
}
@ -383,7 +388,7 @@
}
}
// timeline vertical line
/* timeline vertical line */
&::before {
content: '';
@ -405,7 +410,7 @@
bottom: 50%;
}
// timeline circle
/* timeline circle */
&::after {
content: '';
@ -425,7 +430,7 @@
grid-column: 1 / span 1;
}
@media screen and (min-width: $screen-desktop-min) {
@media screen and (min-width: 1024px) {
&:nth-child(odd)::before {
grid-column: 2 / span 1;
}
@ -444,7 +449,11 @@
}
.dropdown {
@include neo-brutalist-card;
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 3px 3px 0 0 #222;
max-width: 15rem;
position: absolute;
@ -454,8 +463,12 @@
border-top: none;
border-radius: 0 0 0.5rem 0.5rem;
@media screen and (max-width: $screen-desktop-min) {
@include neo-brutalist-card;
@media screen and (max-width: 1024px) {
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 3px 3px 0 0 #222;
position: static;
transform: none;
@ -463,7 +476,7 @@
margin-right: auto;
}
}
@media screen and (min-width: $screen-desktop-min) {
@media screen and (min-width: 1024px) {
&:nth-child(odd) {
grid-column: 1 / span 2;
grid-template-columns: 1fr var(--timeline-track-size);
@ -494,7 +507,7 @@
}
}
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
grid-column: 1 / span 2;
grid-template-columns: var(--timeline-track-size) 1fr;
@ -506,9 +519,9 @@
}
}
//
// Cards
//
/*
/* Cards
*/
.card {
display: grid;
@ -518,17 +531,25 @@
background: var(--card-base-internal);
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
@include neo-brutalist-card($size: 3px, $offset: 9px);
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card($size: 3px, $offset: 9px); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 9px 9px 0 0 #222;
row-gap: 0.5rem;
padding: 1rem;
// Variants
/* Variants */
&.large {
padding: 2rem;
@include neo-brutalist-card($size: 4px, $offset: 8px);
/* @TODO: SCSS conversion - mixin */
/* @include neo-brutalist-card($size: 4px, $offset: 8px); */
border: 4px solid #222;
border-radius: 8px;
box-shadow: 8px 8px 0 0 #222;
row-gap: 1rem;
@ -542,7 +563,7 @@
}
}
// Child Items
/* Child Items */
& > .title {
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
@ -592,7 +613,14 @@
}
}
@media screen and (max-width: $screen-desktop-min) {
.metadata {
display: grid;
grid-auto-flow: column;
justify-content: start;
gap: 0.5rem;
}
@media screen and (max-width: 1024px) {
padding: 0.9rem;
&.large {
@ -609,9 +637,41 @@
}
}
//
// Card List
//
.chip {
user-select: none;
display: grid;
place-content: center;
place-items: center;
color: #111;
background: #0004;
padding: 0 0.25rem;
border-radius: 0.25rem;
line-height: 1.5;
font-size: 16px;
font-weight: 500;
&.small {
font-size: 13px;
font-weight: 600;
}
&.disabled {
color: #0004;
background: #0002;
}
}
a:has(> .card) {
display: contents;
}
/*
/* Card List
*/
.card-list {
display: grid;
@ -643,7 +703,7 @@
gap: 1rem;
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
grid-template-columns: 1fr;
& > .search {
@ -653,44 +713,44 @@
}
/* .filter-select {
width: 100%;
height: 2.5rem;
width: 100%;
height: 2.5rem;
@include neo-brutalist-card;
@include neo-brutalist-card;
display: grid;
align-items: center;
grid-auto-flow: column;
display: grid;
align-items: center;
grid-auto-flow: column;
cursor: pointer;
cursor: pointer;
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
background: var(--filter-bg-color, #ddd);
background: var(--filter-bg-color, #ddd);
&:hover,
&:hover select {
background: var(--filter-bg-color-hover);
}
&:hover,
&:hover select {
background: var(--filter-bg-color-hover);
}
.material-symbols-outlined {
padding: 0 0.35rem;
}
.material-symbols-outlined {
padding: 0 0.35rem;
}
select {
border: none;
box-shadow: none;
outline: none;
height: 100%;
appearance: none;
select {
border: none;
box-shadow: none;
outline: none;
height: 100%;
appearance: none;
cursor: pointer;
cursor: pointer;
padding: 0;
padding: 0;
background: var(--filter-bg-color, #ddd);
}
} */
background: var(--filter-bg-color, #ddd);
}
} */
.combobox {
width: 100%;
@ -698,7 +758,10 @@
position: relative;
padding: 0 0.25rem 0 0.25rem;
@include neo-brutalist-card;
/* @TODO: SCSS conversion - mixin */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 4px 4px 0 0 #222;
cursor: pointer;
@ -733,7 +796,10 @@
top: calc(100% + 8px);
left: -3px;
@include neo-brutalist-card;
/* @TODO: SCSS conversion - mixin */
border: 3px solid #222;
border-radius: 6px;
/* box-shadow: 3px 3px 0 0 #222; */
background: #fff;
@ -750,7 +816,7 @@
}
}
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
.dropdown {
left: 3px;
right: 3px;
@ -761,10 +827,10 @@
}
.gallery-collage {
// display: flex;
// flex-wrap: wrap;
/* display: flex;
flex-wrap: wrap;
// width: 64rem;
width: 64rem; */
width: 100%;
max-width: 100%;
@ -779,9 +845,9 @@
gap: 1rem;
place-content: center;
// align-items: center;
/* align-items: center; */
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
grid-template-columns: auto;
grid-template-rows: auto;
grid-auto-rows: auto;
@ -798,7 +864,7 @@
grid-column: span var(--cols, 1);
grid-row: span var(--rows, 1);
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
max-width: 100%;
grid-column: span 1;
@ -817,7 +883,7 @@
object-fit: cover;
@media screen and (max-width: $screen-desktop-min) {
@media screen and (max-width: 1024px) {
max-height: none;
width: 25rem;
@ -832,17 +898,15 @@
width: 100%;
flex-wrap: wrap;
justify-content: center;
gap: 3rem 6rem;
gap: 3rem 0rem;
> .bubble {
display: grid;
grid-template-rows: auto auto auto auto;
grid-auto-rows: auto;
gap: 0.5rem;
text-align: center;
justify-items: center;
align-content: start;
width: 28ch;
width: 18rem;
.date {
display: grid;
@ -854,6 +918,29 @@
border-radius: 0.25rem;
}
.founder {
display: grid;
place-content: center;
font-size: 15px;
font-weight: 700;
/* gold badge */
background: #ffdb12;
color: #725306;
padding: 0 0.25rem;
border: 2px solid #b98c19;
border-radius: 0.25rem;
box-shadow: 0.125rem 0.125rem 0 0 #664b06;
}
.description {
font-size: 16px;
text-wrap: balance;
}
.social {
display: grid;
grid-auto-flow: column;
@ -872,7 +959,115 @@
object-fit: cover;
width: 100%;
aspect-ratio: 1 / 1;
width: 12rem;
}
}
}
.wide-card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(auto, 28rem));
gap: 2rem;
padding: 2rem;
width: 100%;
justify-content: center;
/* align-items: start; */
.text > * {
max-width: none;
}
.card {
display: grid;
grid-template-rows: 1fr auto;
}
@media screen and (max-width: 1024px) {
grid-template-columns: 1fr;
padding: 0;
gap: 1rem;
}
}
.filter {
min-width: 15rem;
}
.flex-column {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flex-row {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.flex-row-wrap {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
}
.grid-h {
display: grid;
grid-auto-flow: column;
justify-content: start;
align-items: center;
gap: 0.5rem;
}
.grid-v {
display: grid;
justify-items: start;
align-content: start;
grid-auto-flow: row;
gap: 0.5rem;
}
.grid-center {
display: grid;
place-content: center;
place-items: center;
gap: 0.5rem;
grid-auto-flow: row;
}
.grid-h-split {
display: grid;
place-content: center;
place-items: center;
gap: 1rem;
grid-auto-flow: column;
grid-auto-columns: 1fr;
@media screen and (max-width: 1024px) {
grid-auto-flow: row;
grid-auto-columns: auto;
}
}
.clickable {
cursor: pointer;
}
/* just to know for reference */
.fake-masonry {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
grid-auto-flow: dense;
& > * {
grid-row: span var(--masonry-height);
}
}
}

@ -0,0 +1,274 @@
/* @import './mixins.scss'; */
/*
Controls - for things like buttons, input, select
*/
@layer base {
button,
.button,
[role='button'] {
appearance: none;
background: #fff;
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 4px 4px 0 0 #222;
transition: all 64ms linear;
display: grid;
place-content: center;
&:hover {
transform: translate(-1px, -1px);
box-shadow: 5px 5px 0 0 #222;
}
&:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 0 #222;
}
& {
padding: 0.25rem 1.5rem;
text-decoration: none;
color: #222;
font-family: var(--font-secondary); /* TODO: check if this is a global variable and replace */
font-weight: 600;
cursor: pointer;
}
&.primary {
background: #1e6733;
color: #f4fef7;
&:hover {
background: #2b8b47;
}
}
&.icon {
padding: 0.25rem;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
&.flat {
background: transparent;
color: #222;
border: none;
box-shadow: none;
&:hover {
background: #0002;
}
}
}
input[type='text'],
input[type='password'] {
width: 100%;
min-height: 1.75rem;
/* @include neo-brutalist-card; */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 4px 4px 0 0 #222;
padding: 0 0.25rem;
&:hover {
background: #fdfdfd;
}
}
input[type='checkbox'] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: calc(1.5rem + 1px);
height: calc(1.5rem + 1px);
background: #fff;
border: 3px solid #222;
border-radius: 4px;
box-shadow: 3px 3px 0 0 #222;
position: relative;
cursor: pointer;
transition: all 64ms linear;
&:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 0 #222;
}
&:active {
transform: translate(1px, 1px);
box-shadow: 2px 2px 0 0 #222;
}
&:checked {
background: #1e6733;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 0.8rem;
height: 0.8rem;
background: #1e6733;
clip-path: polygon(10% 55%, 35% 75%, 85% 25%, 90% 35%, 40% 95%, 5% 60%);
}
&:hover {
background: #2b8b47;
}
}
&:disabled {
background: #eee;
border-color: #888;
box-shadow: 3px 3px 0 0 #888;
cursor: not-allowed;
&:hover {
transform: none;
box-shadow: 3px 3px 0 0 #888;
}
&:checked {
background: #aaa;
&::after {
background: #666;
}
}
}
&.star {
&:checked::after {
background: rgb(255, 197, 49);
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 91%,
50% 70%,
21% 91%,
32% 57%,
2% 35%,
39% 35%
);
}
}
}
form {
display: grid;
gap: 1rem;
padding: 2rem;
background: #38adc1;
min-width: 40ch;
/* @include neo-brutalist-card($size: 3px, $offset: 9px); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 9px 9px 0 0 #222;
button,
.button,
[role='button'] {
padding-left: 3rem;
padding-right: 3rem;
&.primary {
background: #1c7f90;
color: #f4fef7;
&:hover {
background: #4ea2b1;
}
}
}
hr {
border: none;
width: 100%;
height: 2px;
background: #0003;
margin-top: 1rem;
}
.right {
justify-self: end;
}
.left {
justify-self: start;
}
.center {
justify-self: center;
}
}
details {
width: 100%;
summary {
display: grid;
place-content: center;
place-items: center;
list-style: none;
&::-webkit-details-marker {
display: none;
}
}
.details-openned,
.details-closed {
display: contents;
}
&:not([open]) {
.details-openned {
display: none;
}
}
&[open] {
summary {
padding-bottom: 1rem;
}
.details-closed {
display: none;
}
}
@media screen and (max-width: 1024px) {
/* TODO: check if this is a global variable and replace */
summary {
place-content: stretch;
place-items: stretch;
}
}
}
}

@ -1,132 +0,0 @@
@import './mixins.scss';
//
// Controls - for things like buttons, input, select
//
@layer common {
button,
.button,
[role='button'] {
appearance: none;
background: #fff;
@include neo-brutalist-card;
transition: all 64ms linear;
display: grid;
place-content: center;
&:hover {
transform: translate(-1px, -1px);
box-shadow: 5px 5px 0 0 #222;
}
&:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 0 #222;
}
& {
padding: 0.25rem 1.5rem;
text-decoration: none;
color: #222;
font-family: var(--font-secondary);
font-weight: 600;
cursor: pointer;
}
&.primary {
background: #1e6733;
color: #f4fef7;
&:hover {
background: #2b8b47;
}
}
&.icon {
padding: 0.25rem;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
&.flat {
background: transparent;
color: #222;
border: none;
box-shadow: none;
&:hover {
background: #0002;
}
}
}
input[type='text'],
input[type='password'] {
width: 100%;
height: 2.5rem;
@include neo-brutalist-card;
padding: 0 0.25rem;
&:hover {
background: #fdfdfd;
}
}
form {
display: grid;
gap: 1rem;
padding: 2rem;
background: #38adc1;
min-width: 40ch;
@include neo-brutalist-card($size: 3px, $offset: 9px);
button,
.button,
[role='button'] {
padding-left: 3rem;
padding-right: 3rem;
&.primary {
background: #1c7f90;
color: #f4fef7;
&:hover {
background: #4ea2b1;
}
}
}
hr {
border: none;
width: 100%;
height: 2px;
background: #0003;
margin-top: 1rem;
}
.right {
justify-self: end;
}
.left {
justify-self: start;
}
.center {
justify-self: center;
}
}
}

@ -0,0 +1,371 @@
@layer base, typography, component, page, utility;
@import url(./controls.css);
@import url(./components.css);
@import url(./typography.css);
/* $screen-desktop-min: 1024px; */
/* @TODO: SCSS conversion - @import becomes more complex */
/* @import './mixins.scss'; */
/* @import './typography.scss'; */
:root {
--palette-black: #222;
--border-large: 4px solid var(--palette-black);
--header-bg: #fff;
--footer-bg: #444;
--footer-fg: #fdfdfd;
--homepage-principal-bg: #ecffe3;
--homepage-whatsphc-bg: #e4c5ff;
--homepage-news-bg: #c2a8eb;
--homepage-projects-bg: #f5f2cc;
--homepage-macchinisti-bg: #888;
--project-card-bg: #a2d4f3;
--font-primary: 'Open Sans', sans-serif;
--font-display: 'Iosevka', monospace;
--font-mono: 'Source Code Pro', monospace;
--font-secondary: 'Source Sans Pro', sans-serif;
}
:root {
--guide-base: #a2d4f3;
--guide-darkest: color-mix(in srgb, var(--guide-base), #000 75%);
--guide-darker: color-mix(in srgb, var(--guide-base), #000 50%);
--guide-dark: color-mix(in srgb, var(--guide-base), #000 25%);
--guide-light: color-mix(in srgb, var(--guide-base), #fff 25%);
--guide-lighter: color-mix(in srgb, var(--guide-base), #fff 50%);
--guide-lightest: color-mix(in srgb, var(--guide-base), #fff 75%);
--news-base: #f8e8b1;
}
@layer base {
*,
*::before,
*::after {
box-sizing: border-box;
font: inherit;
margin: 0;
}
html {
height: 100%;
}
html,
body {
min-height: 100%;
margin: 0;
font-family: var(--font-primary);
font-size: 18px;
color: #222;
}
/*
html {
scroll-snap-type: y mandatory;
scroll-padding-top: 4rem;
}
*/
img {
display: block;
}
a {
color: inherit;
text-decoration: none;
}
body {
/*
for the header spacing
padding-top: 6rem;
*/
display: grid;
grid-template-rows: auto 1fr auto;
header {
z-index: 10;
height: 6rem;
border-bottom: var(--border-large);
background: var(--header-bg);
grid-column: 1 / -1;
top: 0;
position: sticky;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
padding-left: 1rem;
img {
height: 3.5rem;
}
}
.links {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0 1.5rem;
a {
font-family: var(--font-display);
font-size: 18px;
font-weight: 500;
letter-spacing: 1px;
color: #333;
padding: 0.25rem 1.325rem;
}
@media screen and (max-width: 1024px) {
flex-direction: column;
}
}
.side-menu {
position: fixed;
/*
top: 0;
right: 0;
bottom: 0;
left: 3rem;
*/
top: 5rem;
right: 0;
bottom: 0;
left: 0;
background: #f0f0f0;
display: grid;
/*
grid-template-rows: auto 1fr;
*/
grid-template-rows: 1fr;
gap: 1.5rem;
padding: 1.5rem;
justify-content: center;
/*
& > :first-child {
justify-self: end;
}
*/
.links {
display: grid;
grid-template-columns: 1fr;
align-content: start;
width: calc(100vw - 3rem);
max-width: 100%;
gap: 1.5rem;
padding: 0;
}
}
#header-menu-toggle {
display: none;
&:not(:checked) ~ .side-menu {
display: none;
}
}
&:has(#header-menu-toggle:checked) #header-menu-toggle-menu {
display: none;
}
&:has(#header-menu-toggle:not(:checked)) #header-menu-toggle-close {
display: none;
}
@media screen and (max-width: 1024px) {
height: 5rem;
padding: 0 0.5rem;
.logo {
padding-left: 0;
img {
height: 3rem;
}
}
}
}
main {
width: 100%;
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
footer {
z-index: 10;
padding: 1rem 0;
--paragraph-margin: 0.5rem;
--zone-color: #aaa;
min-height: 6rem;
border-top: var(--border-large);
background: var(--footer-bg);
color: var(--footer-fg);
display: grid;
place-content: center;
font-family: var(--font-secondary);
font-size: 18px;
scroll-snap-align: end;
@media screen and (max-width: 1024px) {
min-height: 5rem;
font-size: 18px;
}
}
}
::-webkit-scrollbar-track:vertical {
background-color: #f0f0f0;
border-left: 2px solid #222;
border-top: 2px solid #222;
border-bottom: 2px solid #222;
}
::-webkit-scrollbar-track:horizontal {
background-color: #f0f0f0;
border-top: 2px solid #222;
border-left: 2px solid #222;
border-right: 2px solid #222;
}
::-webkit-scrollbar-thumb {
background-color: #1e6733;
border: 2px solid #222;
}
::-webkit-scrollbar-thumb:hover {
background-color: #2b8b47;
}
::-webkit-scrollbar-corner {
background-color: #f0f0f0;
/* border-left: 2px solid #222; */
/* border-top: 2px solid #222; */
}
::-webkit-scrollbar {
width: 15px;
}
::selection {
background: #0002;
}
body:has(#header-menu-toggle:checked) {
overflow: hidden;
}
}
/*
Typography
*/
/* @TODO: SCSS conversion - @import becomes more complex */
/* @import './controls.scss'; */
/* @import './components.scss'; */
/*
Custom Page Styles
*/
/* @TODO: SCSS conversion - @import becomes more complex */
/* @import './pages.scss'; */
@layer utility {
.hide {
display: none !important;
}
.invisible {
opacity: 0 !important;
}
.grid-center {
display: grid;
place-content: center;
place-items: center;
}
.h-flex {
display: flex;
gap: 0.5rem;
flex-direction: row;
}
.v-flex {
display: flex;
gap: 0.5rem;
flex-direction: column;
}
.h-flex,
.v-flex {
place-self: stretch;
align-items: center;
> * {
flex-shrink: 0;
}
> .spacer {
flex-grow: 1;
}
}
@media screen and (min-width: 1024px) {
.mobile-only {
display: none !important;
}
}
@media screen and (max-width: 1024px) {
.desktop-only {
display: none !important;
}
}
}

@ -1,309 +0,0 @@
$screen-desktop-min: 1024px;
:root {
--palette-black: #222;
--border-large: 4px solid var(--palette-black);
--header-bg: #fff;
--footer-bg: #444;
--footer-fg: #fdfdfd;
--homepage-principal-bg: #ecffe3;
--homepage-whatsphc-bg: #e4c5ff;
--homepage-news-bg: #c2a8eb;
--homepage-projects-bg: #f5f2cc;
--homepage-macchinisti-bg: #888;
--project-card-bg: #a2d4f3;
--font-primary: 'Open Sans', sans-serif;
--font-display: 'Iosevka', monospace;
--font-mono: 'Source Code Pro', monospace;
--font-secondary: 'Source Sans Pro', sans-serif;
}
@layer common, typography, component, page;
@import './mixins.scss';
@import './typography.scss';
@layer common {
*,
*::before,
*::after {
box-sizing: border-box;
font: inherit;
margin: 0;
}
html {
height: 100%;
}
html,
body {
min-height: 100%;
margin: 0;
font-family: var(--font-primary);
font-size: 18px;
color: #222;
}
// html {
// scroll-snap-type: y mandatory;
// scroll-padding-top: 4rem;
// }
img {
display: block;
}
a {
color: inherit;
text-decoration: none;
}
}
//
// Typography
//
@import './controls.scss';
@import './components.scss';
//
// Custom Page Styles
//
body {
// for the header spacing
// padding-top: 6rem;
display: grid;
grid-template-rows: auto 1fr auto;
header {
z-index: 10;
height: 6rem;
border-bottom: var(--border-large);
background: var(--header-bg);
grid-column: 1 / -1;
top: 0;
position: sticky;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
padding-left: 1rem;
img {
height: 3.5rem;
}
}
.links {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0 1.5rem;
a {
font-family: var(--font-display);
font-size: 18px;
font-weight: 500;
letter-spacing: 1px;
color: #333;
}
@media screen and (max-width: $screen-desktop-min) {
flex-direction: column;
}
}
.side-menu {
position: fixed;
// top: 0;
// right: 0;
// bottom: 0;
// left: 3rem;
top: 5rem;
right: 0;
bottom: 0;
left: 0;
background: #f0f0f0;
display: grid;
// grid-template-rows: auto 1fr;
grid-template-rows: 1fr;
gap: 1.5rem;
padding: 1.5rem;
justify-content: center;
// & > :first-child {
// justify-self: end;
// }
.links {
display: grid;
grid-template-columns: 1fr;
align-content: start;
width: calc(100vw - 3rem);
max-width: 100%;
gap: 1.5rem;
padding: 0;
}
}
#header-menu-toggle {
display: none;
&:not(:checked) ~ .side-menu {
display: none;
}
}
&:has(#header-menu-toggle:checked) #header-menu-toggle-menu {
display: none;
}
&:has(#header-menu-toggle:not(:checked)) #header-menu-toggle-close {
display: none;
}
@media screen and (max-width: $screen-desktop-min) {
height: 5rem;
padding: 0 0.5rem;
.logo {
padding-left: 0;
img {
height: 3rem;
}
}
}
}
main {
width: 100%;
@media screen and (max-width: $screen-desktop-min) {
padding: 3rem 1rem;
gap: 3rem;
}
}
footer {
z-index: 10;
padding: 1rem 0;
--paragraph-margin: 0.5rem;
--zone-color: #aaa;
min-height: 6rem;
border-top: var(--border-large);
background: var(--footer-bg);
color: var(--footer-fg);
display: grid;
place-content: center;
font-family: var(--font-secondary);
font-size: 18px;
scroll-snap-align: end;
@media screen and (max-width: $screen-desktop-min) {
min-height: 5rem;
font-size: 18px;
}
}
}
@import './pages.scss';
//
// Misc
//
::-webkit-scrollbar-track:vertical {
background-color: #f0f0f0;
border-left: 2px solid #222;
border-top: 2px solid #222;
border-bottom: 2px solid #222;
}
::-webkit-scrollbar-track:horizontal {
background-color: #f0f0f0;
border-top: 2px solid #222;
border-left: 2px solid #222;
border-right: 2px solid #222;
}
::-webkit-scrollbar-thumb {
background-color: #1e6733;
border: 2px solid #222;
}
::-webkit-scrollbar-thumb:hover {
background-color: #2b8b47;
}
::-webkit-scrollbar-corner {
background-color: #f0f0f0;
// border-left: 2px solid #222;
// border-top: 2px solid #222;
}
::-webkit-scrollbar {
width: 15px;
}
::selection {
background: #0002;
}
body:has(#header-menu-toggle:checked) {
overflow: hidden;
}
//
// Utility Classes
//
.hide {
display: none !important;
}
.invisible {
opacity: 0 !important;
}
@media screen and (min-width: $screen-desktop-min) {
.mobile-only {
display: none !important;
}
}
@media screen and (max-width: $screen-desktop-min) {
.desktop-only {
display: none !important;
}
}

@ -1,850 +0,0 @@
:root {
--guide-base: #a2d4f3;
--guide-darkest: color-mix(in srgb, var(--guide-base), #000 75%);
--guide-darker: color-mix(in srgb, var(--guide-base), #000 50%);
--guide-dark: color-mix(in srgb, var(--guide-base), #000 25%);
--guide-light: color-mix(in srgb, var(--guide-base), #fff 25%);
--guide-lighter: color-mix(in srgb, var(--guide-base), #fff 50%);
--guide-lightest: color-mix(in srgb, var(--guide-base), #fff 75%);
--news-base: #f8e8b1;
}
@layer page {
.homepage {
header:has(#header-menu-toggle:not(:checked)) .logo {
visibility: hidden;
}
section {
position: relative;
width: 100%;
min-height: calc(100vh - 6rem);
&:last-of-type {
min-height: calc(100vh - 10rem);
}
// display: flex;
// flex-direction: column;
// align-items: center;
& {
display: grid;
grid-auto-flow: row;
justify-items: center;
align-content: start;
gap: 2rem;
scroll-snap-align: start;
}
& > .title {
font-size: 48px;
padding-top: 4rem;
@media screen and (max-width: $screen-desktop-min) {
padding-top: 2rem;
}
}
}
.zig-zag {
z-index: 5;
position: absolute;
width: 100%;
display: flex;
&:first-child {
bottom: 100%;
}
}
section.principal {
z-index: 2;
min-height: calc(100vh - 7rem);
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
place-content: center;
gap: 4rem;
padding: 3rem 2rem 6rem;
background: var(--homepage-principal-bg);
position: relative;
.circuit-layer {
pointer-events: none;
z-index: 1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
canvas {
width: 100%;
height: 100%;
}
}
& > .logo {
z-index: 2;
width: 32rem;
position: relative;
user-select: none;
img {
width: 100%;
// filter: drop-shadow(6px 6px 0 var(--palette-black))
// drop-shadow(4px 0 0 var(--palette-black)) drop-shadow(0 4px 0 var(--palette-black))
// drop-shadow(-4px 0 0 var(--palette-black)) drop-shadow(0 -4px 0 var(--palette-black));
}
max-width: calc(100vw - 3rem);
@media screen and (max-width: $screen-desktop-min) {
}
}
& > .whats-phc {
z-index: 2;
background: #e4c5ff;
--zone-color: color-mix(in lab, #e4c5ff, #000 75%);
max-width: 37rem;
.title {
text-align: center;
}
a {
text-decoration: underline 2px solid;
&:hover {
--zone-color: color-mix(in lab, #e4c5ff, #000 60%);
}
}
span.highlighted {
background: color-mix(in lab, #e4c5ff, #000 10%);
cursor: default;
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
}
}
}
section.news {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
--zone-color: color-mix(in lab, var(--news-base), #000 50%);
background: var(--homepage-news-bg);
gap: 3rem;
padding-bottom: 6rem;
& > .news-list {
display: flex;
flex-direction: row;
gap: 3rem;
align-items: flex-start;
padding: 0 3rem;
justify-content: center;
flex-wrap: wrap;
}
[role='button'] {
padding: 0.5rem 2rem;
&.primary {
background: #ffdd6e;
color: #000d;
}
}
}
section.projects {
background: var(--homepage-projects-bg);
padding-bottom: 6rem;
.project-list {
// width: calc(20rem * 3 + 1.5rem * 2 + 6rem * 2);
// max-width: calc(100vw - 2rem); // HACK: capire come si propagano le width per bene
max-width: calc(20rem * 3 + 1.5rem * 2 + 1rem * 2);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-auto-flow: dense;
gap: 1.5rem;
padding: 0 1rem;
& > * {
grid-row: span var(--masonry-height);
}
.project {
width: 100%;
height: 100%;
// background: #fcddff;
// background: #ffa89c;
background: var(--card-bg, var(--project-card-bg));
color: #000e;
@include neo-brutalist-card($size: 3px, $offset: 9px);
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
gap: 0.25rem 1rem;
transition: all 128ms ease-out;
.title {
font-size: 32px;
@media screen and (max-width: $screen-desktop-min) {
font-size: 24px;
}
}
.image {
grid-row: span 2;
// place-self: center;
.box {
background: #0003;
border: 3px solid #0006;
border-radius: 6px;
width: 4rem;
height: 4rem;
}
img {
object-fit: cover;
width: 4rem;
}
// &.auto {
// img {
// width: auto;
// height: auto;
// }
// }
}
.description {
font-size: 16px;
@extend .text;
}
&:hover {
transform: translate(0, -4px);
box-shadow: 9px 13px 0 0 #222;
}
}
@media screen and (max-width: $screen-desktop-min) {
padding: 0 1rem;
grid-template-columns: 1fr;
.project {
padding: 0.8rem;
}
}
}
}
section.wanna-be-macchinista {
background: var(--homepage-macchinisti-bg);
color: #fdfdfd;
padding-bottom: 6rem;
.card {
max-width: 40rem;
}
.content {
@extend .text;
}
}
@media screen and (max-width: $screen-desktop-min) {
& > main {
padding: 0 !important;
}
section.principal {
padding: 3rem 0 6rem;
.whats-phc {
padding: 1.5rem;
margin: 0 1rem;
}
#mobile-whats-phc-read-more {
&:checked ~ .text {
max-height: 7lh;
overflow: hidden;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3lh;
background: linear-gradient(to bottom, transparent, #e4c5ff);
}
}
&:not(:checked) ~ * .button span:nth-child(1) {
display: none;
}
&:checked ~ * .button span:nth-child(2) {
display: none;
}
}
}
section.news {
& > .news-list {
padding: 0 1rem;
}
}
section.projects {
.project-list {
padding: 0 1rem;
}
}
section.wanna-be-macchinista {
.content {
padding: 0 1rem;
}
}
section {
padding: 1rem 1rem 4rem;
}
}
}
.utenti {
background: #ffffe4;
--filter-bg-color: #ffd270;
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 0;
gap: 5rem;
max-width: 80ch;
.search-result {
background: #ffeabc;
}
button {
background: #ffd270;
}
}
}
.appunti {
main {
background: #e8e8e8;
justify-self: center;
display: grid;
grid-auto-flow: row;
justify-items: center;
padding: 3rem;
gap: 3rem;
width: 100%;
position: relative;
&::after {
content: '';
width: 100%;
height: 80vh;
background: linear-gradient(to bottom, transparent, #e8e8e8);
position: absolute;
bottom: 0;
left: 0;
}
.search {
max-width: 80ch;
}
.appunti-scrollable {
justify-self: stretch;
&.center {
justify-self: center;
}
}
}
}
// .login {
// background: #ddfaff;
// main {
// justify-self: center;
// display: flex;
// flex-direction: column;
// align-items: center;
// max-width: 80ch;
// padding: 3rem 0;
// gap: 3rem;
// h3 {
// font-size: 28px;
// font-weight: 600;
// }
// }
// }
//
// Notizie
//
.notizie,
.notizia {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
--zone-color: color-mix(in lab, var(--news-base), #000 75%);
background: color-mix(in lab, var(--news-base), #fff 90%);
}
.notizie {
h1 > a {
&:hover {
text-decoration: underline 3px solid;
}
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem;
gap: 5rem;
}
}
.notizia {
main {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
max-width: calc(46rem + 2rem * 2);
justify-self: center;
padding: 3rem 2rem 2rem;
margin-top: 3rem;
margin-bottom: 6rem;
@media screen and (max-width: $screen-desktop-min) {
box-shadow: none;
border: none;
border-radius: 0;
max-width: none;
margin: 0 auto;
}
}
}
//
// Guide
//
.guide,
.guida {
--card-base: color-mix(in lab, var(--guide-base), #fff 25%);
--zone-color: color-mix(in lab, var(--guide-base), #000 75%);
background: color-mix(in lab, var(--guide-base), #fff 90%);
}
.guide {
h1 > a {
&:hover {
text-decoration: underline 3px solid;
}
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem;
gap: 5rem;
}
}
.guida {
main {
--card-base: color-mix(in lab, var(--guide-base), #fff 50%);
max-width: calc(46rem + 2rem * 2);
justify-self: center;
padding: 3rem 2rem 2rem;
margin-top: 3rem;
margin-bottom: 6rem;
position: relative;
.metadata {
position: absolute;
top: -2px;
left: calc(100% + 2rem);
width: 15rem;
display: grid;
grid-auto-flow: row;
gap: 1rem;
justify-items: start;
.metadata-item {
background: var(--card-base);
display: grid;
grid-template-rows: auto auto;
justify-items: start;
gap: 0.25rem;
padding: 0.75rem 1rem;
@include neo-brutalist-card($size: 3px);
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
}
}
@media screen and (max-width: $screen-desktop-min) {
box-shadow: none;
border: none;
border-radius: 0;
max-width: none;
margin: 0 auto;
.metadata {
position: static;
padding: 0;
width: auto;
margin: 0;
--card-base: color-mix(in lab, var(--guide-base), #fff 30%);
}
}
}
}
.tag {
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 0;
gap: 5rem;
}
}
.storia {
--card-base: #e4c5ff;
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 0;
gap: 3rem;
// background horizontal linear gradient that is black in the center
background: linear-gradient(
to right,
#ffe4c544 0%,
// #ffe4c599 25%,
#ffe4c5ff 50%,
// #ffe4c599 75%,
#ffe4c544 100%
);
}
}
.macchinisti {
main {
justify-self: center;
background: linear-gradient(to top, #d5fbff, #ffd9d5);
display: flex;
flex-direction: column;
align-items: center;
padding: 6rem;
gap: 6rem;
}
}
//
// Meta
//
.design {
grid-template-columns: minmax(15rem, 2fr) 10fr;
@media screen and (max-width: 1400px) {
grid-template-columns: 1fr;
}
aside {
margin: 1rem;
padding: 1rem;
@include neo-brutalist-card();
background: #f0f0f0;
align-self: start;
position: sticky;
top: 7rem;
height: calc(100dvh - 8rem - 6rem);
nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
li {
padding-left: calc((var(--depth) - 1) * 1rem);
display: flex;
a {
display: block;
font-size: 16px;
font-weight: 600;
transform: translate(-0.25rem, 0);
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
color: #444;
&:hover {
background: #00000018;
}
}
}
}
}
@media screen and (max-width: 1400px) {
display: none;
}
}
footer {
grid-column: 1 / -1;
}
main {
justify-self: center;
@media screen and (min-width: $screen-desktop-min) {
padding: 3rem 0;
}
}
pre[data-language='astro'] {
--code-bg: #fff7ef;
width: 100%;
max-width: 46rem;
}
.container {
margin: 2rem auto;
border: 2px dashed #ddd;
position: relative;
z-index: 1;
&.large {
min-width: calc(100% - 4rem);
}
&:not(.large) {
& > .content {
display: grid;
place-content: center;
}
}
& > .content {
padding: 2rem;
overflow: auto;
}
// label in the top left corner
&::before {
content: 'Preview';
position: absolute;
bottom: 100%;
left: 0;
padding: 0.125rem 0.5rem;
background: #eee;
color: #000;
font-family: var(--font-display);
font-size: 14px;
border-radius: 0.25rem;
z-index: -1;
transform: translate(-2px, -4px);
opacity: 0;
transition: opacity 64ms ease-in;
}
&:hover {
border-color: #bbb;
&::before {
opacity: 1;
}
}
}
.palette {
margin: 2rem auto;
display: grid;
grid-template-columns: auto auto;
gap: 1rem;
place-content: center;
& > .color {
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
border: 2px solid #000;
box-shadow: 4px 4px 0 0 #000;
overflow: hidden;
& > .region {
width: 100%;
height: 100%;
border: 2px solid #fff;
border-radius: 4px;
}
}
& > .label {
display: grid;
align-content: center;
font-family: 'JetBrains Mono', var(--font-mono);
font-size: 16px;
user-select: all;
}
}
}
}

@ -0,0 +1,55 @@
@layer page {
main {
background: #e8e8e8;
justify-self: center;
display: grid;
grid-auto-flow: row;
justify-items: center;
padding: 3rem;
gap: 3rem;
width: 100%;
position: relative;
&::after {
content: '';
width: 100%;
height: 80vh;
background: linear-gradient(to bottom, transparent, #e8e8e8);
position: absolute;
bottom: 0;
left: 0;
}
.search {
max-width: 80ch;
}
.appunti-scrollable {
justify-self: stretch;
&.center {
justify-self: center;
}
}
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
overflow: clip;
.card {
width: 100%;
}
}
}
}

@ -0,0 +1,42 @@
:root {
--card-base: hsl(180, 100%, 85%);
}
@layer page {
body {
background: hsl(180, 100%, 95%);
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 4rem 0;
gap: 2rem;
.search {
max-width: 80ch;
}
button {
background: hsl(180, 100%, 72%);
}
.card a {
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
}
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
}

@ -0,0 +1,74 @@
@layer page {
body {
--card-base: color-mix(in lab, var(--guide-base), #fff 25%);
--zone-color: color-mix(in lab, var(--guide-base), #000 75%);
background: color-mix(in lab, var(--guide-base), #fff 90%);
}
main {
--card-base: color-mix(in lab, var(--guide-base), #fff 50%);
max-width: calc(46rem + 2rem * 2);
justify-self: center;
padding: 3rem 2rem 2rem;
margin-top: 3rem;
margin-bottom: 6rem;
position: relative;
.metadata {
position: absolute;
top: -2px;
left: calc(100% + 2rem);
width: 15rem;
display: grid;
grid-auto-flow: row;
gap: 1rem;
justify-items: start;
.metadata-item {
background: var(--card-base);
display: grid;
grid-template-rows: auto auto;
justify-items: start;
gap: 0.25rem;
padding: 0.75rem 1rem;
/* @include neo-brutalist-card($size: 3px); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 3px 3px 0 0 #222;
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
}
}
@media screen and (max-width: 1024px) {
box-shadow: none;
border: none;
border-radius: 0;
max-width: none;
margin: 0 auto;
padding: 3rem 1rem 2rem;
.metadata {
position: static;
padding: 0;
width: auto;
margin: 0;
--card-base: color-mix(in lab, var(--guide-base), #fff 30%);
}
}
}
}

@ -0,0 +1,37 @@
@layer page {
body {
--card-base: color-mix(in lab, var(--guide-base), #fff 25%);
--zone-color: color-mix(in lab, var(--guide-base), #000 75%);
background: color-mix(in lab, var(--guide-base), #fff 90%);
}
h1 > a {
&:hover {
text-decoration: underline 3px solid;
}
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 2.5rem;
gap: 5rem;
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
.card {
grid-template-rows: auto 1fr auto;
}
}

@ -0,0 +1,354 @@
@layer page {
header:has(#header-menu-toggle:not(:checked)) .logo {
visibility: hidden;
}
section {
position: relative;
width: 100%;
min-height: calc(100vh - 6rem);
&:last-of-type {
min-height: calc(100vh - 10rem);
}
& {
display: grid;
grid-auto-flow: row;
justify-items: center;
align-content: start;
gap: 2rem;
scroll-snap-align: start;
}
& > .title {
font-size: 48px;
padding-top: 4rem;
@media screen and (max-width: /* $screen-desktop-min */ 1024px) {
padding-top: 2rem;
}
}
}
.zig-zag {
z-index: 5;
position: absolute;
width: 100%;
display: flex;
&:first-child {
bottom: 100%;
}
}
section.principal {
z-index: 2;
min-height: calc(100vh - 7rem);
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
place-content: center;
gap: 4rem;
padding: 3rem 2rem 6rem;
background: var(--homepage-principal-bg);
position: relative;
.circuit-layer {
pointer-events: none;
z-index: 1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
canvas {
width: 100%;
height: 100%;
}
}
& > .logo {
z-index: 2;
width: 32rem;
position: relative;
user-select: none;
img {
width: 100%;
/* filter: drop-shadow(6px 6px 0 var(--palette-black))
drop-shadow(4px 0 0 var(--palette-black)) drop-shadow(0 4px 0 var(--palette-black))
drop-shadow(-4px 0 0 var(--palette-black)) drop-shadow(0 -4px 0 var(--palette-black)); */
}
max-width: calc(100vw - 3rem);
@media screen and (max-width: /* $screen-desktop-min */ 1024px) {
}
}
& > .whats-phc {
z-index: 2;
background: #e4c5ff;
--zone-color: color-mix(in lab, #e4c5ff, #000 75%);
max-width: 37rem;
.title {
text-align: center;
}
a {
text-decoration: underline 2px solid;
&:hover {
--zone-color: color-mix(in lab, #e4c5ff, #000 60%);
}
}
span.highlighted {
background: color-mix(in lab, #e4c5ff, #000 10%);
cursor: default;
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
}
}
}
section.news {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
--zone-color: color-mix(in lab, var(--news-base), #000 50%);
background: var(--homepage-news-bg);
gap: 3rem;
padding-bottom: 6rem;
& > .news-list {
display: flex;
flex-direction: row;
gap: 3rem;
align-items: flex-start;
padding: 0 3rem;
justify-content: center;
flex-wrap: wrap;
}
[role='button'] {
padding: 0.5rem 2rem;
&.primary {
background: #ffdd6e;
color: #000d;
}
}
.card {
grid-template-rows: auto auto 1fr;
}
}
section.projects {
background: var(--homepage-projects-bg);
padding-bottom: 6rem;
.project-list {
/* width: calc(20rem * 3 + 1.5rem * 2 + 6rem * 2);
max-width: calc(100vw - 2rem); */ /* HACK: capire come si propagano le width per bene */
max-width: calc(20rem * 3 + 1.5rem * 2 + 1rem * 2);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-auto-flow: dense;
gap: 1.5rem;
padding: 0 1rem;
& > * {
grid-row: span var(--masonry-height);
}
.project {
width: 100%;
height: 100%;
/* background: #fcddff;
background: #ffa89c; */
background: var(--card-bg, var(--project-card-bg));
color: var(--card-fg, #000e);
/* @include neo-brutalist-card($size: 3px, $offset: 9px); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 9px 9px 0 0 #222;
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
gap: 0.25rem 1rem;
transition: all 128ms ease-out;
.title {
font-size: 32px;
@media screen and (max-width: /* $screen-desktop-min */ 1024px) {
font-size: 24px;
}
}
.image {
grid-row: span 2;
/* place-self: center; */
.box {
background: #0003;
border: 3px solid #0006;
border-radius: 6px;
width: 4rem;
height: 4rem;
}
img {
object-fit: cover;
width: 4rem;
}
/* &.auto {
img {
width: auto;
height: auto;
}
} */
}
.description {
font-size: 16px;
}
&:hover {
transform: translate(0, -4px);
box-shadow: 9px 13px 0 0 #222;
}
}
@media screen and (max-width: /* $screen-desktop-min */ 1024px) {
padding: 0 1rem;
grid-template-columns: 1fr;
.project {
padding: 0.8rem;
}
}
}
}
section.wanna-be-macchinista {
background: var(--homepage-macchinisti-bg);
color: #fdfdfd;
padding-bottom: 6rem;
.card {
max-width: 40rem;
}
/* .content {
/* @extend .text;
/* Placeholder: Assuming .text is a class with common text styles, you might want to define those styles directly here or in a separate CSS rule.
} */
}
@media screen and (max-width: /* $screen-desktop-min */ 1024px) {
main {
padding: 0 !important;
}
section.principal {
padding: 3rem 0 6rem;
.whats-phc {
padding: 1.5rem;
margin: 0 1rem;
}
#mobile-whats-phc-read-more {
&:checked ~ .text {
max-height: 7lh;
overflow: hidden;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3lh;
background: linear-gradient(to bottom, transparent, #e4c5ff);
}
}
&:not(:checked) ~ * .button span:nth-child(1) {
display: none;
}
&:checked ~ * .button span:nth-child(2) {
display: none;
}
}
}
section.news {
& > .news-list {
padding: 0 1rem;
}
}
section.projects {
.project-list {
padding: 0 1rem;
}
}
section.wanna-be-macchinista {
.content {
padding: 0 1rem;
}
}
section {
padding: 1rem 1rem 4rem;
}
}
}

@ -0,0 +1,22 @@
@layer page {
main {
justify-self: center;
background: linear-gradient(to top, #d5fbff, #ffd9d5);
display: flex;
flex-direction: column;
align-items: center;
padding: 4.5rem 3rem;
gap: 4.5rem;
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,190 @@
@layer page {
body {
grid-template-columns: minmax(15rem, 2fr) 10fr;
@media screen and (max-width: 1400px) {
grid-template-columns: 1fr;
}
}
aside {
margin: 1rem;
padding: 1rem;
/* @include neo-brutalist-card(); */
border: 3px solid #222;
border-radius: 6px;
box-shadow: 3px 3px 0 0 #222;
background: #f0f0f0;
align-self: start;
position: sticky;
top: 7rem;
height: calc(100dvh - 8rem - 6rem);
nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
li {
padding-left: calc((var(--depth) - 1) * 1rem);
display: flex;
a {
display: block;
font-size: 16px;
font-weight: 600;
transform: translate(-0.25rem, 0);
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
color: #444;
&:hover {
background: #00000018;
}
}
}
}
}
@media screen and (max-width: 1400px) {
display: none;
}
}
footer {
grid-column: 1 / -1;
}
main {
justify-self: center;
@media screen and (min-width: /* $screen-desktop-min */ 1024px) {
padding: 3rem 0;
}
}
pre[data-language='astro'] {
--code-bg: #fff7ef;
width: 100%;
max-width: 46rem;
}
.container {
margin: 2rem auto;
border: 2px dashed #ddd;
position: relative;
z-index: 1;
&.large {
min-width: calc(100% - 4rem);
}
&:not(.large) {
& > .content {
display: grid;
place-content: center;
}
}
& > .content {
padding: 2rem;
overflow: auto;
}
/* label in the top left corner */
&::before {
content: 'Preview';
position: absolute;
bottom: 100%;
left: 0;
padding: 0.125rem 0.5rem;
background: #eee;
color: #000;
font-family: var(--font-display);
font-size: 14px;
border-radius: 0.25rem;
z-index: -1;
transform: translate(-2px, -4px);
opacity: 0;
transition: opacity 64ms ease-in;
}
&:hover {
border-color: #bbb;
&::before {
opacity: 1;
}
}
}
.palette {
margin: 2rem auto;
display: grid;
grid-template-columns: auto auto;
gap: 1rem;
place-content: center;
& > .color {
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
border: 2px solid #000;
box-shadow: 4px 4px 0 0 #000;
overflow: hidden;
& > .region {
width: 100%;
height: 100%;
border: 2px solid #fff;
border-radius: 4px;
}
}
& > .label {
display: grid;
align-content: center;
font-family: 'JetBrains Mono', var(--font-mono);
font-size: 16px;
user-select: all;
}
}
}

@ -0,0 +1,28 @@
@layer page {
:root {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
--zone-color: color-mix(in lab, var(--news-base), #000 75%);
}
body {
background: color-mix(in lab, var(--news-base), #fff 90%);
}
main {
max-width: calc(46rem + 2rem * 2);
justify-self: center;
padding: 3rem 2rem 2rem;
margin-top: 3rem;
margin-bottom: 6rem;
@media screen and (max-width: 1024px) {
box-shadow: none;
border: none;
border-radius: 0;
max-width: none;
margin: 0 auto;
}
}
}

@ -0,0 +1,40 @@
@layer page {
:root {
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
--zone-color: color-mix(in lab, var(--news-base), #000 75%);
}
body {
background: color-mix(in lab, var(--news-base), #fff 90%);
}
h1 > a {
&:hover {
text-decoration: underline 3px solid;
}
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem;
gap: 5rem;
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
.card {
grid-template-rows: auto auto 1fr;
}
}

@ -0,0 +1,33 @@
:root {
--card-base: #e4c5ff;
}
@layer page {
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 0;
gap: 3rem;
/* background horizontal linear gradient that is black in the center */
background: linear-gradient(
to right,
#ffe4c544 0%,
/* #ffe4c599 25%, */ #ffe4c5ff 50%,
/* #ffe4c599 75%, */ #ffe4c544 100%
);
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
}

@ -0,0 +1,40 @@
:root {
--filter-bg-color: #ffd270;
}
@layer page {
body {
background: #ffffe4;
}
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 5rem 0;
gap: 5rem;
max-width: 80ch;
.search-result {
background: #ffeabc;
}
button {
background: #ffd270;
}
@media screen and (max-width: 1024px) {
padding: 3rem 1rem;
gap: 3rem;
.card {
width: 100%;
}
}
}
}

@ -1,41 +1,63 @@
@function pow($number, $exponent) {
$value: 1;
@layer typography {
:root {
--heading-base-size: 16px;
--heading-factor: 1.25;
}
@if $exponent > 0 {
@for $i from 1 through $exponent {
$value: $value * $number;
}
strong {
font-weight: 600;
}
@return $value;
}
em {
font-style: italic;
}
@mixin geometric-headings {
$base-font-size: 16px;
$heading-scale: 1.25;
.text-center {
text-align: center;
}
@for $i from 1 through 5 {
$factor: pow($heading-scale, 5 - $i);
h1 {
font-size: calc(var(--heading-base-size) * pow(var(--heading-factor), 4));
}
h#{$i} {
font-size: $base-font-size * $factor;
font-family: var(--font-display);
font-weight: 700;
margin-bottom: 0.25rem;
}
h2 {
font-size: calc(var(--heading-base-size) * pow(var(--heading-factor), 3));
}
// p + h#{$i} {
// margin-top: 0.75rem * $factor;
// }
h3 {
font-size: calc(var(--heading-base-size) * pow(var(--heading-factor), 2));
}
}
@layer typography {
@include geometric-headings;
h4 {
font-size: calc(var(--heading-base-size) * pow(var(--heading-factor), 1));
}
h5 {
font-size: calc(var(--heading-base-size) * pow(var(--heading-factor), 0));
}
h1,
h2,
h3,
h4,
h5 {
font-family: var(--font-display);
font-weight: 700;
}
.text {
// text-align: justify;
// hyphens: auto;
h1,
h2,
h3,
h4,
h5 {
font-family: var(--font-display);
font-weight: 700;
margin-bottom: 0.25rem;
}
/* text-align: justify;
hyphens: auto; */
&.center {
text-align: center;
@ -52,8 +74,8 @@
pre,
code {
background: color-mix(in lab, var(--card-base-internal, #ededed), #fff 35%) !important;
// background: color-mix(in lab, var(--zone-color), #fff 75%) !important;
// background: var(--code-bg, #00000022) !important;
/* background: color-mix(in lab, var(--zone-color), #fff 75%) !important;
background: var(--code-bg, #00000022) !important; */
font-family: var(--font-mono);
font-weight: 400;
@ -65,12 +87,16 @@
padding: 0.125rem 0.3rem;
}
p > code {
word-break: break-word;
}
pre {
margin: 2rem auto;
padding: 0.5rem 1rem;
// width: 100%;
/* width: 100%; */
max-width: 80ch;
width: fit-content;
@ -85,11 +111,6 @@
code {
padding: 0;
}
@media screen and (max-width: $screen-desktop-min) {
width: calc(100vw - 2rem);
max-width: none;
}
}
p {
@ -97,26 +118,26 @@
margin: var(--paragraph-margin, 1rem) auto;
}
// p + p {
// margin-top: 1rem;
// }
/* p + p {
margin-top: 1rem;
}
// h1 + p,
// h2 + p,
// h3 + p,
// h4 + p {
// margin-top: 1rem;
// }
h1 + p,
h2 + p,
h3 + p,
h4 + p {
margin-top: 1rem;
}
// p:has(+ h1, + h2, + h3, + h4) {
// margin-bottom: 1rem;
// }
p:has(+ h1, + h2, + h3, + h4) {
margin-bottom: 1rem;
} */
p[align='center'] {
margin: 1.5rem 0;
a {
// background: color-mix(in hsl, var(--card-base-internal, #ededed), #fff 20%);
/* background: color-mix(in hsl, var(--card-base-internal, #ededed), #fff 20%); */
background: hsl(from var(--card-base-internal, #ededed) h calc(s + 10) calc(l - 10));
padding: 0.5rem;
border-radius: 0.25rem;
@ -125,11 +146,18 @@
}
}
img {
img,
video {
display: block;
margin: 0 auto;
@include neo-brutalist-card(2px);
width: 50ch;
max-width: 100%;
border: 2px solid #333;
border-radius: 0.25rem;
box-shadow: 0.25rem 0.25rem 0 0 #333;
background: #000;
&.fill {
width: 100%;
@ -146,6 +174,10 @@
}
}
video {
margin: 1rem auto;
}
p:first-child {
margin-top: 0;
}
@ -162,7 +194,22 @@
font-style: italic;
}
@include geometric-headings;
/* @include geometric-headings; */
/* Inlined geometric-headings mixin */
/* @for $i from 1 through 5 {
$factor: pow($heading-scale, 5 - $i);
h#{$i} {
font-size: $base-font-size * $factor;
font-family: var(--font-display);
font-weight: 700;
margin-bottom: 0.25rem;
}
/* p + h#{$i} {
margin-top: 0.75rem * $factor;
}
} */
h1 {
margin-bottom: 2rem;
@ -176,7 +223,7 @@
font-weight: 700;
color: #333;
// HACK: Trick to fix anchor links with sticky header
/* HACK: Trick to fix anchor links with sticky header */
padding-top: 7rem;
margin-top: -6.5rem;
}
@ -201,6 +248,10 @@
}
}
li + li {
margin-top: 0.5rem;
}
a,
a:visited {
color: var(--zone-color, #1e6733);
@ -231,9 +282,17 @@
border-top: 2px solid #0003;
}
@media screen and (max-width: $screen-desktop-min) {
& > * {
margin: 0 0.75rem;
@media screen and (max-width: 1024px) {
max-width: calc(100vw - 2rem);
pre {
margin: 1rem 0;
width: 100%;
}
hr {
max-width: none;
margin: 3rem auto;
}
}
}

@ -1,17 +1,17 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strictNullChecks": true,
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@layouts/*": ["src/layouts/*"],
"@client/*": ["src/client/*"],
"@components/*": ["src/components/*"]
}
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strictNullChecks": true,
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@layouts/*": ["src/layouts/*"],
"@client/*": ["src/client/*"],
"@components/*": ["src/components/*"]
}
}
}

Loading…
Cancel
Save