Compare commits

...

No commits in common. 'main' and 'next' have entirely different histories.
main ... next

30
.gitignore vendored

@ -1,26 +1,10 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# local data
*.local*
# environment variables
.env .env
.env.production *.local*
bin/
# macOS-specific files
.DS_Store
# Drizzle build output .out/
out/ out/
dist/
node_modules/
.vscode/

@ -0,0 +1,2 @@
# Needed by pnpm to work with "@preact/preset-vite"
shamefully-hoist=true

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

@ -1,3 +0,0 @@
{
"npm.packageManager": "bun"
}

@ -0,0 +1,31 @@
# Architettura
Questo è un progetto _fullstack_ con una backend scritta in [Go](https://go.dev/) ed una frontend in JS, più precisamente utilizziamo NodeJS con [Vite](https://vitejs.dev/) per build-are la frontend.
La cosa più interessante è l'integrazione tra **Vite** e la backend in **Go** per siti con più pagine (il nostro caso). Di base Vite supporta siti con più file html ma ha bisogno che gli venga detto quali sono tutti gli _entrypoint_ HTML. Vedremo che questo progetto usa una tecnica che ci permette di indicare una volta sola le cose nel codice in Go senza stare a tenere sincronizzato il codice in Go e la configurazione di Vite.
## Usage
Ci sono vari modi per lanciare la nostra applicazione in base all'_environment_, in particolare sono tutti isolati sotto forma di eseguibili in Go:
- Quando saremo in produzione l'unico server sarà quello di Go lanciato attraverso l'_entry-point_ [./cmd/server/main.go](./cmd/server/main.go)
In particolare prima di poter lanciare questo server bisogna aver eseguito [./cmd/build/main.go](./cmd/build/main.go) che esegue solo il codice relativo al router della nostra applicazione e genera un file `out/routes.json`. Poi bisogna eseguire `npm run build` che chiama Vite e genera il codice per tutte le route dentro la cartella `out/frontend/`.
- Quando siamo in development usiamo solo [./cmd/devserver/main.go](./cmd/devserver/main.go) che lancia in background il server di Vite (chiama `npm run dev` che a sua volta è un alias per `node server.js`) quindi possiamo vedere tutto in tempo reale da `localhost:3000`.
Più precisamente il server di Vite all'avvio richiede al server in Go tutte le route da montare utilizzando la route speciale `/api/development/routes` (in particolare Fiber ed ExpressJS hanno la stessa sintassi per definire le route quindi questa cosa è facile da fare).
Poi quando si prova ad andare su una pagina ci sono due casi
- Se la route era **statica** allora leggiamo il file html, lo facciamo processare a Vite e poi lo rimandiamo all'utente.
- Se invece la route era di tipo **dinamico** allora leggiamo sempre il file e lo processiamo con Vite però ora utilizziamo l'altra route speciale che esiste solo in fase di sviluppo `/api/development/render` che renderizza la pagina applicando il _templating del server_ e poi una volta finito inviamo la pagina al client.
Invece quando saremo in produzione tutte le pagina saranno già state renderizzate da Vite quindi saremo nel caso standard di _http server_ con views da renderizzare con il _template engine_ del caso prima di mandare la pagina al client.
- L'ultimo _entrypoint_ è [./cmd/build/main.go](./cmd/build/main.go) e lancia la nostra applicazione in una modalità "finta" senza server http ma vengono comunque registrate tutte le route utilizzando sempre il modulo `dev`. Questo ci permette di costruire l'albero delle route (statiche e dinamiche) che poi servirà a Vite quando faremo `npm run build`.
Ciò serve perché così ci basta definire tutte le route una volta sola nel Go e poi in automatico funzioneranno anche nel server di Vite senza dover ripetere due volte il codice. (questa è la parte più magica di _meta-programming_ di tutto il progetto)
![architecture-1](./docs/architecture-1.svg)

@ -0,0 +1,10 @@
.PHONY: test
test:
PROJECT_DIR="$(shell pwd)" go test -v ./...
.PHONY: build
build:
go run -v ./cmd/build
pnpm run build
go build -o ./out/bin/server ./cmd/server

@ -1,38 +1,25 @@
# PHC Website # Website 2
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web. Repo per il nuovo sito del PHC
## Installazione ## Docs
```bash - [./ARCHITECTURE.md](./ARCHITECTURE.md)
bun install
```
## Sviluppo Alcune note sull'architettura di questo progetto.
```bash ## Usage
bun dev
```
## Build ## Development
```bash ```bash shell
bun build # Starts the backend on port :4000 and the frontend development server on port :3000
$ go run -v ./cmd/devserver
``` ```
## Deploy [TODO] ## Production
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro). ```bash shell
# Generates "routes.json", builds all frontend artifacts and finally the main binary
```bash $ make build
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.
## Come Contribuire
Cose da fare
- Aggiungere guide in [src/content/guides/](./src/content/guides/)

@ -1,31 +0,0 @@
import { defineConfig } from 'astro/config'
import preact from '@astrojs/preact'
import node from '@astrojs/node'
import mdx from '@astrojs/mdx'
import remarkToc from 'remark-toc'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
// https://astro.build/config
export default defineConfig({
server: {
port: 3000,
},
markdown: {
shikiConfig: {
theme: 'github-light',
},
},
integrations: [preact(), mdx()],
// adapter: node({
// mode: 'standalone',
// }),
output: 'hybrid',
outDir: './out/astro',
build: {
client: './out/astro/client',
server: './out/astro/server',
},
})

Binary file not shown.

@ -0,0 +1,44 @@
package main
import (
"encoding/json"
"log"
"os"
"git.phc.dm.unipi.it/phc/website/services/config"
"git.phc.dm.unipi.it/phc/website/services/database"
"git.phc.dm.unipi.it/phc/website/services/server"
"git.phc.dm.unipi.it/phc/website/services/server/dev"
"git.phc.dm.unipi.it/phc/website/sl"
)
func main() {
l := sl.New()
// sl.Inject[config.Interface](l, &config.EnvConfig{})
sl.InjectValue(l, config.Slot, &config.Config{
Mode: "production",
})
sl.InjectValue(l, database.Slot, database.Database(
&database.Memory{}),
)
if _, err := server.Configure(l); err != nil {
log.Fatal(err)
}
f, err := os.Create("out/routes.json")
if err != nil {
log.Fatal(err)
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(dev.UseRoutesMetadata(l)); err != nil {
log.Fatal(err)
}
log.Printf(`generated "out/routes.json"`)
}

@ -0,0 +1,69 @@
package main
import (
"bufio"
"io"
"log"
"os/exec"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/services/config"
"git.phc.dm.unipi.it/phc/website/services/database"
"git.phc.dm.unipi.it/phc/website/services/server"
"git.phc.dm.unipi.it/phc/website/sl"
)
func init() {
log.SetFlags(0)
}
func main() {
l := sl.New()
cfg := sl.InjectValue(l, config.Slot, &config.Config{
Mode: "development",
Host: ":4000",
})
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
Users: []model.User{
{
Id: "claire",
FullName: "Claire Doe",
Nickname: "claire-doe",
AuthSources: map[string]model.AuthSource{},
},
{
Id: "john",
FullName: "John Smith",
Nickname: "john-smith",
AuthSources: map[string]model.AuthSource{},
},
},
})
srv, err := server.Configure(l)
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(srv.Router.Listen(cfg.Host))
}()
r, w := io.Pipe()
cmd := exec.Command("npm", "run", "dev")
cmd.Stdout = w
go func() {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
}
}()
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,44 @@
package main
import (
"log"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/services/config"
"git.phc.dm.unipi.it/phc/website/services/database"
"git.phc.dm.unipi.it/phc/website/services/server"
"git.phc.dm.unipi.it/phc/website/sl"
)
func main() {
l := sl.New()
cfg := sl.InjectValue(l, config.Slot, &config.Config{
Mode: "production",
Host: ":4000",
})
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
Users: []model.User{
{
Id: "claire",
FullName: "Claire Doe",
Nickname: "claire-doe",
AuthSources: map[string]model.AuthSource{},
},
{
Id: "john",
FullName: "John Smith",
Nickname: "john-smith",
AuthSources: map[string]model.AuthSource{},
},
},
})
srv, err := server.Configure(l)
if err != nil {
log.Fatal(err)
}
log.Fatal(srv.Router.Listen(cfg.Host))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 251 KiB

@ -1,7 +0,0 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "src/db/schema.ts",
driver: "better-sqlite",
out: "out/drizzle",
});

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Homepage</title>
</head>
<body>
Homepage
</body>
</html>

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ .Title }} &bull; Articoli &bull; PHC</title>
<link rel="stylesheet" href="/styles/main.scss" />
<link rel="stylesheet" href="./typography.scss" />
</head>
<body>
<h1>Articolo "{{ .Example }}"</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Neque, quasi...</p>
</body>
</html>

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Articoli &bull; PHC</title>
<link rel="stylesheet" href="/styles/main.scss" />
</head>
<body>
<h1>Articoli</h1>
{{ .Example }}
</body>
</html>

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ListaUtenti</title>
</head>
<body>
<main class="page-lista-utenti"></main>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>

@ -0,0 +1,13 @@
import { render } from 'preact'
import './lista-utenti.scss'
render(
<>
<h1>Lista Utenti</h1>
<div class="list">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Odit, illo molestiae. Sapiente cumque saepe maxime, temporibus ad
nulla id officiis impedit ut dolorem asperiores voluptate illo, molestiae facilis inventore. Ea.
</div>
</>,
document.querySelector('main')
)

@ -0,0 +1,5 @@
*,
*::before,
*::after {
box-sizing: border-box;
}

@ -0,0 +1,22 @@
module git.phc.dm.unipi.it/phc/website
go 1.19
require (
github.com/alecthomas/repr v0.2.0
github.com/gofiber/fiber/v2 v2.41.0
github.com/joho/godotenv v1.4.0
github.com/valyala/fasthttp v1.43.0
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.1.0 // indirect
)

@ -0,0 +1,40 @@
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.41.0 h1:YhNoUS/OTjEz+/WLYuQ01xI7RXgKEFnGBKMagAu5f0M=
github.com/gofiber/fiber/v2 v2.41.0/go.mod h1:RdebcCuCRFp4W6hr3968/XxwJVg0K+jr9/Ae0PFzZ0Q=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

@ -0,0 +1,18 @@
import fetch from 'node-fetch'
import { readFile } from 'fs/promises'
export async function getBuildRoutesMetadata(file) {
console.log('Loading routes from disk...')
const routesRaw = await readFile(file, 'utf8')
return JSON.parse(routesRaw)
}
export async function getDevRoutesMetadata(url) {
console.log('Loading routes from go server...')
const routesReq = await fetch(url)
const routes = await routesReq.json()
return routes
}

@ -0,0 +1,15 @@
package model
type User struct {
Id string
FullName string
Nickname string
AuthSources map[string]AuthSource
}
type AuthSource struct {
Provider string
AuthToken string
}

12049
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,49 +1,21 @@
{ {
"name": "website", "name": "website",
"version": "1.0.0",
"type": "module", "type": "module",
"version": "0.0.1",
"scripts": { "scripts": {
"dev": "run-s astro:sync drizzle:* astro:dev", "dev": "node server.js",
"build": "run-s drizzle:generate astro:build", "build": "vite build --emptyOutDir"
"astro:sync": "astro sync",
"astro:dev": "astro dev",
"astro:build": "astro check && astro build",
"drizzle:generate": "drizzle-kit generate:sqlite",
"drizzle:migrate": "tsx src/db/migrate.ts"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^8.3.4",
"@astrojs/preact": "^3.5.3",
"@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",
"@preact/signals": "^1.3.0",
"astro": "^4.15.11",
"better-sqlite3": "^9.4.3",
"drizzle-orm": "^0.29.4",
"fuse.js": "^7.0.0",
"katex": "^0.16.9",
"preact": "^10.19.6",
"typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/mdx": "^3.1.7", "@preact/preset-vite": "^2.5.0",
"@types/better-sqlite3": "^7.6.9", "axios": "^1.2.6",
"@types/katex": "^0.16.7", "express": "^4.18.2",
"drizzle-kit": "^0.20.14", "morgan": "^1.10.0",
"jsdom": "^24.1.1", "node-fetch": "^3.3.0",
"linkedom": "^0.18.4", "sass": "^1.57.1",
"npm-run-all": "^4.1.5", "vite": "^4.0.4"
"rehype-autolink-headings": "^7.1.0", },
"rehype-slug": "^6.0.0", "dependencies": {
"remark-math": "^6.0.0", "preact": "^10.11.3"
"remark-toc": "^9.0.0",
"sass": "^1.71.1",
"tsx": "^4.7.1"
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

@ -1,87 +0,0 @@
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="80" y="190" width="60" height="120" fill="#1E6733"/>
<rect x="160" y="50" width="150" height="60" fill="#1E6733"/>
<rect x="140" y="90" width="10" height="50" fill="#ECC333"/>
<rect x="140" y="200" width="10" height="50" fill="#ECC333"/>
<rect x="140" y="410" width="10" height="20" fill="#ECC333"/>
<rect x="140" y="350" width="10" height="50" fill="#ECC333"/>
<rect x="240" y="110" width="70" height="10" fill="#ECC333"/>
<rect x="250" y="130" width="60" height="130" fill="#1E6733"/>
<rect x="340" y="50" width="60" height="120" fill="#1E6733"/>
<rect x="340" y="190" width="60" height="120" fill="#1E6733"/>
<rect x="590" y="190" width="60" height="120" fill="#1E6733"/>
<rect x="690" y="180" width="60" height="120" fill="#1E6733"/>
<rect x="690" y="310" width="60" height="140" fill="#1E6733"/>
<rect x="690" y="50" width="60" height="120" fill="#1E6733"/>
<rect x="590" y="320" width="60" height="130" fill="#1E6733"/>
<rect x="590" y="50" width="60" height="130" fill="#1E6733"/>
<rect x="420" y="240" width="150" height="60" fill="#1E6733"/>
<rect x="340" y="320" width="60" height="130" fill="#1E6733"/>
<rect x="240" y="140" width="10" height="50" fill="#ECC333"/>
<rect x="350" y="170" width="40" height="10" fill="#ECC333"/>
<rect x="330" y="330" width="10" height="50" fill="#ECC333"/>
<rect x="160" y="200" width="80" height="60" fill="#1E6733"/>
<rect x="650" y="200" width="10" height="50" fill="#ECC333"/>
<rect x="750" y="330" width="10" height="60" fill="#ECC333"/>
<rect x="800" y="450" width="40" height="10" fill="#ECC333"/>
<rect x="850" y="450" width="30" height="10" fill="#ECC333"/>
<rect x="750" y="90" width="10" height="50" fill="#ECC333"/>
<rect x="810" y="110" width="60" height="10" fill="#ECC333"/>
<rect x="580" y="330" width="10" height="50" fill="#ECC333"/>
<rect x="580" y="60" width="10" height="50" fill="#ECC333"/>
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4"/>
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4"/>
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4"/>
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4"/>
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4"/>
<rect x="350" y="430" width="40" height="10" fill="#303030"/>
<rect x="350" y="410" width="40" height="10" fill="#303030"/>
<rect x="350" y="390" width="40" height="10" fill="#303030"/>
<rect x="700" y="70" width="20" height="40" fill="#303030"/>
<rect x="700" y="120" width="20" height="40" fill="#303030"/>
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4"/>
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4"/>
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4"/>
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4"/>
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4"/>
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4"/>
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4"/>
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4"/>
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4"/>
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4"/>
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4"/>
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4"/>
<rect x="370" y="70" width="20" height="30" fill="#303030"/>
<rect x="370" y="110" width="20" height="30" fill="#303030"/>
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030"/>
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030"/>
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030"/>
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030"/>
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030"/>
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030"/>
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030"/>
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z" fill="#1E6733"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z" fill="#1E6733"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z" fill="#1E6733"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z" fill="#1E6733"/>
<rect x="750" y="190" width="10" height="40" fill="#ECC333"/>
<rect x="750" y="240" width="10" height="20" fill="#ECC333"/>
<rect x="400" y="200" width="10" height="40" fill="#ECC333"/>
<rect x="400" y="250" width="10" height="20" fill="#ECC333"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

@ -1,8 +0,0 @@
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
</pattern>
</defs>
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
</svg>

Before

Width:  |  Height:  |  Size: 344 B

@ -0,0 +1,93 @@
import express from 'express'
import { createServer as createViteServer } from 'vite'
import { getDevRoutesMetadata } from './meta/routes.js'
import fetch from 'node-fetch'
import { readFile } from 'fs/promises'
import { dirname, resolve } from 'path'
import morgan from 'morgan'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
async function main() {
const routes = await getDevRoutesMetadata('http://127.0.0.1:4000/api/development/routes')
console.log('Found static routes:')
for (const [route, file] of Object.entries(routes.static)) {
console.log(`- ${route} -> "${file}"`)
}
console.log('Found dynamic routes:')
for (const [route, file] of Object.entries(routes.dynamic)) {
console.log(`- ${route} -> "${file}"`)
}
const app = express()
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'))
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
app.use(vite.middlewares)
for (const [route, file] of Object.entries(routes.static)) {
app.get(route, async (req, res) => {
console.log(`Requested static route "${route}":`)
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
// Replace "./" with the absolute path of the html page
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
console.log(`- applying vite transformations for "${file}"`)
const html = await vite.transformIndexHtml(file, htmlPage)
console.log(`- sending resulting page for "${route}"`)
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html)
})
}
for (const [route, file] of Object.entries(routes.dynamic)) {
app.get(route, async (req, res) => {
console.log(`Requested dynamic route "${route}":`)
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
// Replace "./" with the absolute path of the html page
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
console.log(`- applying vite transformations for "${file}"`)
const html = await vite.transformIndexHtml(file, htmlPage)
console.log(`- applying server transformations for "${file}"`)
const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
route,
page: html,
request: {
params: req.params,
query: req.query,
},
}),
})
const renderedHtml = await templateHtmlReq.json()
console.log(`- sending resulting page for "${route}"`)
res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml)
})
}
app.listen(3000, () => {
console.log(`Listening on port 3000...`)
})
}
main()

@ -0,0 +1,35 @@
package config
import (
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/joho/godotenv"
)
var Slot = sl.NewSlot[*Config]()
type Config struct {
Mode string
Host string
}
func Load(l *sl.ServiceLocator) (*Config, error) {
m, err := godotenv.Read(".env")
if err != nil {
return nil, err
}
mode := "production"
if v, ok := m["MODE"]; ok {
mode = v
}
host := ":4000"
if v, ok := m["HOST"]; ok {
host = v
}
return &Config{
mode,
host,
}, nil
}

@ -0,0 +1,63 @@
package database
import (
"fmt"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/sl"
)
var Slot = sl.NewSlot[Database]()
type Database interface {
CreateUser(user model.User) error
ReadUser(id string) (model.User, error)
ReadUsers() ([]model.User, error)
UpdateUser(id string, user model.User) error
DeleteUser(id string) error
}
type Memory struct {
Users []model.User
}
func (m *Memory) CreateUser(user model.User) error {
m.Users = append(m.Users, user)
return nil
}
func (m *Memory) ReadUser(id string) (model.User, error) {
for _, u := range m.Users {
if u.Id == id {
return u, nil
}
}
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
}
func (m *Memory) ReadUsers() ([]model.User, error) {
return m.Users, nil
}
func (m *Memory) UpdateUser(id string, user model.User) error {
for i, u := range m.Users {
if u.Id == id {
m.Users[i] = user
return nil
}
}
return fmt.Errorf(`no user with id "%s"`, id)
}
func (m *Memory) DeleteUser(id string) error {
for i, u := range m.Users {
if u.Id == id {
m.Users = append(m.Users[:i], m.Users[i+1:]...)
return nil
}
}
return fmt.Errorf(`no user with id "%s"`, id)
}

@ -0,0 +1,58 @@
package articles
import (
"html/template"
"git.phc.dm.unipi.it/phc/website/services/server/dev"
"git.phc.dm.unipi.it/phc/website/services/server/router"
"git.phc.dm.unipi.it/phc/website/sl"
)
func Configure(l *sl.ServiceLocator) error {
router.UseRouteTemplatedPage(l, "/articles",
"pages/articles/index.html",
func(w dev.ResponseWriter, r dev.Request) error {
tmpl := template.New("")
tmpl, err := tmpl.Parse(string(r.Page()))
if err != nil {
return err
}
ctx := map[string]any{
"Example": "Bla bla",
}
if err := tmpl.Execute(w, ctx); err != nil {
return err
}
return nil
},
)
router.UseRouteTemplatedPage(l, "/articles/:slug",
"pages/articles/article.html",
func(w dev.ResponseWriter, r dev.Request) error {
tmpl := template.New("")
tmpl, err := tmpl.Parse(string(r.Page()))
if err != nil {
return err
}
ctx := map[string]any{
"Title": r.Param("slug"),
"Example": "Bla bla " + r.Param("slug"),
}
if err := tmpl.Execute(w, ctx); err != nil {
return err
}
return nil
},
)
return nil
}

@ -0,0 +1,169 @@
package dev
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path"
"git.phc.dm.unipi.it/phc/website/services/config"
"git.phc.dm.unipi.it/phc/website/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
)
// Logger is the debug logger, in the future this will be disabled and discard by default.
var Logger *log.Logger = log.New(os.Stderr, "[services/server/dev] ", log.Lmsgprefix)
// slot represents a private "write only" service
var slot = sl.NewSlot[*devService]()
// InjectInto a [*sl.ServiceLocator] an instance of the dev service
func InjectInto(l *sl.ServiceLocator) {
sl.InjectLazy(l, slot, Configure)
}
func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any {
dev, err := sl.Use(l, slot)
if err != nil {
Logger.Fatal(err)
}
return map[string]any{
"static": dev.staticRoutes,
"dynamic": dev.dynamicRoutes,
}
}
type Request interface {
Page() []byte
Param(key string) string
Query(key string) string
}
type ResponseWriter interface {
io.Writer
}
// devServerRequest is used when handling request from the dev server where params and queries are parsed by express
type devServerRequest struct {
page []byte
params map[string]string
query map[string]string
}
func (r devServerRequest) Page() []byte {
return r.page
}
func (r devServerRequest) Param(key string) string {
return r.params[key]
}
func (r devServerRequest) Query(key string) string {
return r.query[key]
}
// Handler is a custom routes handler
type Handler func(ResponseWriter, Request) error
type devService struct {
staticRoutes map[string]string
dynamicRoutes map[string]string
dynamicRoutesHandlers map[string]Handler
}
func Configure(l *sl.ServiceLocator) (*devService, error) {
d := &devService{
map[string]string{},
map[string]string{},
map[string]Handler{},
}
r, err := sl.Use(l, routes.Root)
if err != nil {
return nil, err
}
config, _ := sl.Use(l, config.Slot)
if config.Mode != "development" {
return d, nil
}
r.Get("/api/development/routes", func(c *fiber.Ctx) error {
return c.JSON(map[string]any{
"static": d.staticRoutes,
"dynamic": d.dynamicRoutes,
})
})
r.Post("/api/development/render", func(c *fiber.Ctx) error {
var data struct {
Route string `json:"route"`
HtmlPage string `json:"page"`
Request struct {
ParamsMap map[string]string `json:"params"`
QueryMap map[string]string `json:"query"`
} `json:"request"`
}
if err := c.BodyParser(&data); err != nil {
return err
}
Logger.Printf(`server rendering route "%s"`, data.Route)
Logger.Printf(`- params: %s`, repr.String(data.Request.ParamsMap))
Logger.Printf(`- query: %s`, repr.String(data.Request.QueryMap))
handler, ok := d.dynamicRoutesHandlers[data.Route]
if !ok {
return fmt.Errorf(`no handler for "%s"`, data.Route)
}
var buf bytes.Buffer
if err := handler(&buf, devServerRequest{
[]byte(data.HtmlPage),
data.Request.ParamsMap,
data.Request.QueryMap,
}); err != nil {
return err
}
return c.JSON(buf.String())
})
return d, nil
}
// RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page
func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) {
dev, err := sl.Use(l, slot)
if err != nil {
Logger.Fatal(err)
}
dev.staticRoutes[mountPoint] = frontendFile
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
}
func RegisterDynamicRoute(l *sl.ServiceLocator, mountPoint, frontendFile string, handler Handler) {
dev, err := sl.Use(l, slot)
if err != nil {
Logger.Fatal(err)
}
dev.dynamicRoutes[mountPoint] = frontendFile
dev.dynamicRoutesHandlers[mountPoint] = handler
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
}
func GetArtifactPath(frontendFile string) string {
return path.Join("./out/frontend/", frontendFile)
}

@ -0,0 +1,35 @@
package listautenti
import (
"git.phc.dm.unipi.it/phc/website/services/database"
"git.phc.dm.unipi.it/phc/website/services/server/router"
"git.phc.dm.unipi.it/phc/website/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/gofiber/fiber/v2"
)
func Configure(l *sl.ServiceLocator) error {
router.UseRoutePage(l, "/utenti", "pages/lista-utenti/index.html")
db, err := sl.Use(l, database.Slot)
if err != nil {
return err
}
r, err := sl.Use(l, routes.Root)
if err != nil {
return err
}
r.Get("/api/lista-utenti", func(c *fiber.Ctx) error {
users, err := db.ReadUsers()
if err != nil {
return err
}
return c.JSON(users)
})
return nil
}

@ -0,0 +1,78 @@
package listautenti_test
import (
"context"
"fmt"
"io"
"net"
"net/http"
"testing"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/services/database"
"git.phc.dm.unipi.it/phc/website/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
)
func Test1(t *testing.T) {
l := sl.New()
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
Users: []model.User{
{
Id: "claire",
FullName: "Claire Doe",
Nickname: "claire-doe",
AuthSources: map[string]model.AuthSource{},
},
{
Id: "john",
FullName: "John Smith",
Nickname: "john-smith",
AuthSources: map[string]model.AuthSource{},
},
},
})
r := fiber.New()
sl.InjectValue(l, routes.Root, fiber.Router(r))
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
if err != nil {
t.Error(err)
}
ln := fasthttputil.NewInmemoryListener()
defer ln.Close()
go func() {
err := fasthttp.Serve(ln, r.Handler())
if err != nil {
panic(fmt.Errorf("failed to serve: %v", err))
}
}()
client := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return ln.Dial()
},
},
}
res, err := client.Do(req)
if err != nil {
t.Error(err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
t.Log(string(body))
}

@ -0,0 +1,73 @@
package router
import (
"bytes"
"log"
"os"
"git.phc.dm.unipi.it/phc/website/services/server/dev"
"git.phc.dm.unipi.it/phc/website/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/gofiber/fiber/v2"
)
// assert type of [ServerRequest] is [dev.Request]
var _ dev.Request = ServerRequest{}
// ServerRequest is used when the request is directly for the Go server
type ServerRequest struct {
page []byte
fiberContext *fiber.Ctx
}
func (r ServerRequest) Page() []byte {
return r.page
}
func (ctx ServerRequest) Param(key string) string {
return ctx.fiberContext.Params(key)
}
func (ctx ServerRequest) Query(key string) string {
return ctx.fiberContext.Query(key)
}
func UseRoutePage(l *sl.ServiceLocator, route, frontendFile string) {
root, err := sl.Use(l, routes.Root)
if err != nil {
log.Fatal(err)
}
dev.RegisterRoute(l, route, frontendFile)
root.Get(route, func(c *fiber.Ctx) error {
return c.SendFile(dev.GetArtifactPath(frontendFile))
})
}
func UseRouteTemplatedPage(l *sl.ServiceLocator, route, frontendFile string, handler dev.Handler) {
r, err := sl.Use(l, routes.Root)
if err != nil {
log.Fatal(err)
}
dev.RegisterDynamicRoute(l, route, frontendFile, handler)
r.Get(route, func(c *fiber.Ctx) error {
rawPage, err := os.ReadFile(dev.GetArtifactPath(frontendFile))
if err != nil {
return err
}
var buf bytes.Buffer
if err := handler(&buf, ServerRequest{
rawPage,
c,
}); err != nil {
return err
}
return c.Type(".html").Send(buf.Bytes())
})
}

@ -0,0 +1,9 @@
package routes
import (
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/gofiber/fiber/v2"
)
var Root = sl.NewSlot[fiber.Router]()

@ -0,0 +1,30 @@
package server
import (
"git.phc.dm.unipi.it/phc/website/services/server/articles"
"git.phc.dm.unipi.it/phc/website/services/server/dev"
"git.phc.dm.unipi.it/phc/website/services/server/listautenti"
"git.phc.dm.unipi.it/phc/website/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl"
"github.com/gofiber/fiber/v2"
)
type Server struct{ Router *fiber.App }
func Configure(l *sl.ServiceLocator) (*Server, error) {
r := fiber.New(fiber.Config{})
r.Static("/assets", "./out/frontend/assets")
sl.InjectValue(l, routes.Root, fiber.Router(r))
dev.InjectInto(l)
if err := listautenti.Configure(l); err != nil {
return nil, err
}
if err := articles.Configure(l); err != nil {
return nil, err
}
return &Server{r}, nil
}

@ -0,0 +1,127 @@
// The [sl] package has two main concepts, the [ServiceLocator] itself is the main object that one should pass around through the application. A [ServiceLocator] has a list of slots that can be filled with [InjectLazy] and [InjectValue] and retrieved with [Use]. As slots should be unique they can only be created with the [NewSlot] function.
//
// The usual way to use this module is to make slots for go interfaces and then pass implementations using the [InjectValue] and [InjectLazy] functions.
//
// Services can be of various types:
// - a service with no dependencies can be directly injected inside a ServiceLocator using [InjectValue].
// - a service with dependencies on other service should use [InjectLazy]. This lets the service to initialize itself when required and makes the developer not think the topological sort to put onto the DAG of service dependencies.
// - a service can also be private, in this case the slot for a service should be a private field in the service package. This kind of services should also provide a way to inject them into a ServiceLocator.
// - a package also just provide a slot. This is useful for using the ServiceLocator to easily pass around values, effectively threating slots just as dynamically scoped variables.
package sl
import (
"fmt"
"log"
"os"
)
// Logger is the debug logger, in the future this will be disabled and discard by default.
//
// As this is the service locator module it was meaning less to pass this through the ServiceLocator itself (without making the whole module more complex)
var Logger *log.Logger = log.New(os.Stderr, "[sl] ", log.Lmsgprefix)
// slot is just a "typed" unique "symbol".
type slot[T any] *struct{}
// NewSlot is the only way to create instances of the slot type. Each instance is unique.
//
// This then lets you attach a service instance of type "T" to a [ServiceLocator] object.
func NewSlot[T any]() slot[T] {
return slot[T](new(struct{}))
}
// slotEntry represents a service that can lazily initialized (using "createFunc"). Once initialized the instance is kept in the "value" field. The field "typeName" just for debugging purposes.
type slotEntry struct {
createFunc func(*ServiceLocator) (any, error)
created bool
value any
typeName string
}
func (s *slotEntry) checkInitialized(l *ServiceLocator) error {
if !s.created {
v, err := s.createFunc(l)
if err != nil {
return err
}
Logger.Printf(`initialized lazy value of type %T for slot of type %s`, v, s.typeName)
s.created = true
s.value = v
}
return nil
}
// ServiceLocator is the main context passed around to retrive service instances, the interface uses generics so to inject and retrive service instances you should use the functions [InjectValue], [InjectLazy] and [Use].
type ServiceLocator struct {
providers map[any]*slotEntry
}
// New creates a new [ServiceLocator] context to pass around in the application.
func New() *ServiceLocator {
return &ServiceLocator{
providers: map[any]*slotEntry{},
}
}
// InjectValue will inject a concrete instance inside the ServiceLocator "l" for the given "slotKey". This should be used for injecting "static" services, for instances whose construction depend on other services you should use the [InjectLazy] function.
//
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
func InjectValue[T any](l *ServiceLocator, slotKey slot[T], value T) T {
Logger.Printf(`injected value of type %T for slot of type %s`, value, getTypeName[T]())
l.providers[slotKey] = &slotEntry{
nil,
true,
value,
getTypeName[T](),
}
return value
}
// InjectLazy will inject an instance inside the given ServiceLocator and "slotKey" that is created only when requested with a call to the [Use] function.
//
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
func InjectLazy[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
Logger.Printf(`injected lazy for slot of type %s`, getTypeName[T]())
l.providers[slotKey] = &slotEntry{
createFunc: func(l *ServiceLocator) (any, error) {
return createFunc(l)
},
created: false,
value: nil,
typeName: getTypeName[T](),
}
}
// Use retrieves the value of type T associated with the given slot key from the provided ServiceLocator instance.
//
// If the ServiceLocator does not have a value for the slot key, or if the value wasn't correctly initialized (in the case of a lazy slot), an error is returned.
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
var zero T
slot, ok := l.providers[slotKey]
if !ok {
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
}
err := slot.checkInitialized(l)
if err != nil {
return zero, err
}
v := slot.value.(T)
Logger.Printf(`using slot of type %s with value of type %T`, getTypeName[T](), v)
return v, nil
}
// getTypeName is a trick to get the name of a type (even if it is an interface type)
func getTypeName[T any]() string {
var zero T
return fmt.Sprintf(`%T`, &zero)[1:]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

@ -1,30 +0,0 @@
/**
* @typedef {{
* image?: string,
* course?: string,
* title?: string,
* author: string,
* courseYear: string
* }} AppuntiCardProps
*/
/**
*
* @param {AppuntiCardProps} param0
* @returns
*/
export const AppuntiCard = ({ image, course, title, author, courseYear }) => {
return (
<div class="appunti-item">
<div class="thumbnail"></div>
{title && <div class="title">{title}</div>}
{course && <div class="course">{course}</div>}
<div class="author">@{author}</div>
<div class="course-year">{courseYear}</div>
</div>
)
}
export const AppuntiList = ({ children }) => {
return <div class="appunti-list">{children}</div>
}

@ -1,65 +0,0 @@
import { type ComponentChildren } from 'preact'
import { useState, useRef, useEffect } from 'preact/hooks'
import { clsx, isMobile } from './lib/util'
export const ComboBox = ({
value,
setValue,
children,
}: {
value: string
setValue: (s: string) => void
children: Record<string, ComponentChildren>
}) => {
const [cloak, setCloak] = useState(true)
const [open, setOpen] = useState(true)
const comboRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
const [itemWidth, setItemWidth] = useState<number>(200)
useEffect(() => {
setOpen(false)
setCloak(false)
}, [])
return (
<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>
</div>
{open && (
<div
class={clsx('dropdown', cloak && 'invisible')}
ref={el => el && setItemWidth(el.offsetWidth)}
>
{Object.keys(children).map(key => (
<div
class="option"
onClick={() => {
setValue(key)
setOpen(false)
}}
>
{children[key]}
</div>
))}
</div>
)}
</div>
)
}

@ -1,13 +0,0 @@
import { useState } from 'preact/hooks'
export const Counter = ({}) => {
const [count, setCount] = useState(0)
return (
<div class="counter">
<button onClick={() => setCount(value => value - 1)}>-</button>
<div class="value">{count}</div>
<button onClick={() => setCount(value => value + 1)}>+</button>
</div>
)
}

@ -1,29 +0,0 @@
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
import type { JSX } from 'preact/jsx-runtime'
export const ShowMore = <T extends any>({
items,
pageSize,
children,
}: {
items: ReadonlySignal<T[]>
pageSize: number
children: (item: T) => JSX.Element
}) => {
const $shownItems = useSignal(pageSize)
const $paginatedItems = useComputed(() => {
return items.value.slice(0, $shownItems.value)
})
return (
<>
{$paginatedItems.value.map(children)}
<div class="show-more">
{$shownItems.value < items.value.length && (
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
)}
</div>
</>
)
}

@ -1,155 +0,0 @@
import { effect, useComputed, useSignal } from '@preact/signals'
import Fuse from 'fuse.js'
import { useEffect } from 'preact/hooks'
import { ShowMore } from './Paginate'
import { ComboBox } from './ComboBox'
type User = {
uid: string
gecos: string
}
const FILTERS = {
utenti: {
icon: 'person',
label: 'Utenti',
},
macchinisti: {
icon: 'construction',
label: 'Macchinisti',
},
rappstud: {
icon: 'account_balance',
label: 'Rappresentanti',
},
}
function applyPatches(users: User[]) {
users.forEach(user => {
// strip ",+" from the end of the gecos field
user.gecos = user.gecos.replace(/,+$/, '')
// capitalize the first letter of each word
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
})
// reverse the order of the users
users.reverse()
}
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
const RAPPSTUD = [
'smannella',
'lotti',
'rotolo',
'saccani',
'carbone',
'mburatti',
'ppuddu',
'marinari',
'evsilvestri',
'tateo',
'graccione',
'dilella',
'rocca',
'odetti',
'borso',
'numero',
]
export const UtentiPage = () => {
const $utentiData = useSignal<User[]>([])
const $filter = useSignal('utenti')
const $filteredData = useComputed(() =>
$filter.value === 'macchinisti'
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
: $filter.value === 'rappstud'
? $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,
)
useEffect(() => {
fetch('https://poisson.phc.dm.unipi.it/users.json')
.then(response => response.json())
.then(data => {
applyPatches(data)
$utentiData.value = data
$fuse.value.setCollection(data)
})
}, [])
return (
<>
<div class="search-bar">
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
{Object.fromEntries(
Object.entries(FILTERS).map(([k, v]) => [
k,
<>
<span class="material-symbols-outlined">{v.icon}</span> {v.label}
</>,
]),
)}
</ComboBox>
<div class="search">
<input
type="text"
placeholder="Cerca un utente Poisson..."
onInput={e => ($searchText.value = e.currentTarget.value)}
value={$searchText.value}
/>
<span class="material-symbols-outlined">search</span>
</div>
</div>
<div class="search-results">
{$searchResults.value ? (
<ShowMore items={$searchResults} pageSize={100}>
{poissonUser => (
<div class="search-result">
<div class="icon">
<span class="material-symbols-outlined">
{RAPPSTUD.includes(poissonUser.uid)
? 'account_balance'
: MACCHINISTI.includes(poissonUser.uid)
? 'construction'
: 'person'}
</span>
</div>
<div class="text">{poissonUser.gecos}</div>
<div class="right">
<a
href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}
target="_blank"
>
<span class="material-symbols-outlined">open_in_new</span>
</a>
</div>
</div>
)}
</ShowMore>
) : (
<>Nessun risultato</>
)}
</div>
</>
)
}

@ -1,69 +0,0 @@
import { useEffect, useMemo, useState } from 'preact/hooks'
export const trottleDebounce = <T extends any[], R>(
fn: (...args: T) => R,
delay: number,
options: { leading?: boolean; trailing?: boolean } = {},
): ((...args: T) => R | undefined) => {
let lastCall = 0
let lastResult: R | undefined
let lastArgs: T | undefined
let timeout: NodeJS.Timeout | undefined
const leading = options.leading ?? true
const trailing = options.trailing ?? true
return (...args: T): R | undefined => {
lastArgs = args
if (leading && Date.now() - lastCall >= delay) {
lastCall = Date.now()
lastResult = fn(...args)
} else {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
if (trailing && lastArgs) {
lastCall = Date.now()
lastResult = fn(...lastArgs)
}
}, delay)
}
return lastResult
}
}
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
export function clsx(...args: ClassValue[]): string {
return args
.flatMap(arg => {
if (typeof arg === 'string') {
return arg
} else if (Array.isArray(arg)) {
return clsx(...arg)
} else if (typeof arg === 'boolean') {
return []
} else if (typeof arg === 'object') {
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
} else {
return []
}
})
.join(' ')
}
export const isMobile = () => {
const [windowWidth, setWindowWidth] = useState(0)
useEffect(() => {
setWindowWidth(window.innerWidth)
const handleResize = () => setWindowWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowWidth < 1024
}

@ -1,9 +0,0 @@
<footer>
<div class="text">
<p>
&copy; PHC 2024 &nbsp;&bull;&nbsp; <a href="mailto:macchinisti@lists.dm.unipi.it"
>macchinisti@lists.dm.unipi.it</a
>
</p>
</div>
</footer>

@ -1,32 +0,0 @@
<header>
<a href="/" class="logo">
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
</a>
<input type="checkbox" id="header-menu-toggle" />
<div class="links desktop-only">
<a role="button" href="/utenti">Utenti</a>
<!-- <a role="button" href="/appunti">Appunti</a> -->
<a role="button" href="/notizie">Notizie</a>
<a role="button" href="/guide">Guide</a>
<a role="button" href="/storia">Storia</a>
<!-- <a class="primary" role="button" href="/login">Login</a> -->
</div>
<div class="mobile-only">
<label id="header-menu-toggle-menu" role="button" class="flat icon" for="header-menu-toggle">
<span class="material-symbols-outlined">menu</span>
</label>
<label id="header-menu-toggle-close" role="button" class="flat icon" for="header-menu-toggle">
<span class="material-symbols-outlined">close</span>
</label>
</div>
<div class="side-menu">
<div class="links">
<a role="button" href="/utenti">Utenti</a>
<!-- <a role="button" href="/appunti">Appunti</a> -->
<a role="button" href="/notizie">Notizie</a>
<a role="button" href="/guide">Guide</a>
<a role="button" href="/storia">Storia</a>
<!-- <a class="primary" role="button" href="/login">Login</a> -->
</div>
</div>
</header>

@ -1,14 +0,0 @@
---
const { year, title } = Astro.props
---
<div class="timeline-item">
<div class="content">
<div class="card">
<div class="title">{year} &bull; {title}</div>
<div class="text">
<slot />
</div>
</div>
</div>
</div>

@ -1,14 +0,0 @@
---
import { JSDOM } from 'jsdom'
import Container from './Container.astro'
const language = Astro.props['data-language'] ?? 'text'
const html = await Astro.slots.render('default')
const rawCode = new JSDOM(html).window.document.body.textContent
---
<pre {...Astro.props}><slot /></pre>
{language === 'astro' && <Container set:html={rawCode} />}

@ -1,9 +0,0 @@
---
const { size, ...rest } = Astro.props
---
<div class:list={['container', size ?? 'normal']} {...rest}>
<div class="content">
<slot />
</div>
</div>

@ -1,20 +0,0 @@
---
type Props = {
colors: string[]
}
const { colors } = Astro.props
---
<div class="palette">
{
colors.map(value => (
<>
<div class="color">
<div class="region" style={{ backgroundColor: value }} />
</div>
<div class="label">{value}</div>
</>
))
}
</div>

@ -1,51 +0,0 @@
import { z, defineCollection } from 'astro:content'
// Una notizia ha una data di pubblicazione ma non ha un autore in quanto sono
// notizie generiche sul PHC e non sono scritte da un autore specifico
const newsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.date(),
image: z
.object({
url: z.string(),
alt: z.string(),
})
.optional(),
}),
})
// Una guida ha un autore ma non ha una data di pubblicazione in quanto è un
// contenuto statico e non è importante sapere quando è stata pubblicata
const guidesCollection = defineCollection({
type: 'content',
schema: z.object({
id: z.string(),
title: z.string(),
description: z.string(),
series: z.string().optional(),
tags: z.array(z.string()),
}),
})
// 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()),
}),
})
// Export a single `collections` object to register your collection(s)
export const collections = {
news: newsCollection,
guides: guidesCollection,
seminarietti: seminariettiCollection,
}

@ -1,86 +0,0 @@
---
id: git-101-example
title: Git 101 (Esempio)
description: Una guida introduttiva alle basi di Git (Esempio)
author: Copilot
tags: [git]
---
Benvenuto alla guida introduttiva alle basi di Git. In questa guida imparerai come iniziare a usare Git per il controllo di versione dei tuoi progetti.
## Cos'è Git?
Git è un sistema di controllo di versione distribuito. Questo significa che puoi tenere traccia delle modifiche ai tuoi file e sincronizzarle con altri membri del tuo team.
## Installazione
Per iniziare ad usare Git, devi prima installarlo sul tuo computer. Puoi scaricare l'ultima versione di Git dal [sito ufficiale](https://git-scm.com/).
## Inizializzazione di un repository
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
$$
\int_0^1 x^2 \ dx
$$
```bash
git init
```
Questo creerà un nuovo repository $1+a+\zeta_7-\xi^2$ Git nella cartella del tuo progetto.
## Comandi vari
### Aggiunta di file
Ora che hai inizializzato il repository, puoi iniziare ad aggiungere file ad esso. Per aggiungere un file, esegui il seguente comando:
```bash
git add <nome del file>
```
Sostituisci `<nome del file>` con il nome del file che vuoi aggiungere.
### Commit delle modifiche
Una volta aggiunti i file, puoi fare un commit delle modifiche. Questo salverà le modifiche nel repository. Per fare un commit, esegui il seguente comando:
```bash
git commit -m "Messaggio del commit"
```
Sostituisci `"Messaggio del commit"` con un messaggio che descrive le modifiche che hai fatto.
### Sincronizzazione con un repository remoto
Ora che hai fatto un commit delle modifiche, puoi sincronizzare il repository con un repository remoto. Questo ti permetterà di condividere le tue modifiche con altri membri del tuo team. Per sincronizzare il repository con un repository remoto, esegui il seguente comando:
```bash
git remote add origin <url del repository remoto>
git push -u origin main
```
L'opzione `-u` imposta il repository remoto come repository predefinito per il ramo main, in modo che tu possa semplicemente eseguire `git push` in futuro per sincronizzare le tue modifiche con il repository remoto.
Sostituisci `<url del repository remoto>` con l'URL del repository remoto. Questo ti chiederà di inserire le tue credenziali per il repository remoto. Una volta fatto, le tue modifiche saranno sincronizzate con il repository remoto.
### Aggiornamento del repository
Una volta che il repository è sincronizzato con un repository remoto, puoi aggiornare il repository con le modifiche degli altri membri del tuo team. Per aggiornare il repository, esegui il seguente comando:
```bash
git pull
```
Questo aggiornerà il repository con le modifiche degli altri membri del tuo team.
## Conclusioni
Questo è solo un'introduzione alle basi di Git. Ci sono molte altre funzionalità che puoi esplorare, ma queste sono le basi per iniziare. Buon divertimento!

@ -1,71 +0,0 @@
---
id: pagina-poisson-con-astro
title: Pagina Poisson con Astro
description: Vediamo come creare una pagina Poisson moderna con Astro
author: Antonio De Lucreziis
tags: [astro, website]
---
In questa guida vedremo come creare una pagina Poisson moderna utilizzando Astro, un nuovo framework di sviluppo web statico.
## Setup
Se siete sul vostro pc installate VSCode ed il plugin di Astro.
Poi installiamo NodeJS (se siete su Windows è consigliato [installare WSL con `wsl --install`](https://learn.microsoft.com/en-us/windows/wsl/install) e poi installare i seguenti pacchetti nell'ambiente Linux)
- NodeJS: https://nodejs.org/en/download/package-manager
```bash
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm use --install-if-missing 20
node -v
npm -v
```
## Creazione di un nuovo progetto Astro
Per prima cosa dobbiamo creare un nuovo progetto di Astro sul nostro computer, possiamo scegliere [uno dei tanti temi disponibili per Astro](https://astro.build/themes/) o partire da un blog di esempio con il seguente comando:
```bash
npm create astro@latest -- --template blog
cd nome-del-progetto
npm install
```
Se ad esempio volessimo usare un tema come [Astro Nano](https://github.com/markhorn-dev/astro-nano) possiamo fare così:
```bash
git clone https://github.com/markhorn-dev/astro-nano sito-poisson
cd sito-poisson
npm install
```
L'ultima cosa importante che c'è da cambiare è che le pagine Poisson sono hostate su `https://poisson.phc.dm.unipi.it/~nomeutente/` che non è la radice del dominio del sito. Quindi dobbiamo cambiare il file `astro.config.mjs`:
```javascript
export default defineConfig({
...
base: '/~nomeutente/',
trailingSlash: 'always',
...
});
```
## Lavorare con Astro
Per vedere il nostro progetto in locale possiamo eseguire il comando:
```bash
npm run dev
```
A questo punto in base al tema scelto possiamo modificare i file dentro `src/pages` per cambiare il contenuto delle pagine. Molti temi sono preimpostati per scrivere contenuti in Markdown, ad esempio per il _template blog_ possiamo scrivere gli articoli per il nostro blog in `src/content/blog/$NOME_POST.md`.
## Deploy
Una volta creato il progetto possiamo caricare appunti e dispense nella cartella `/public`

@ -1,209 +0,0 @@
import Container from '../../components/meta/Container.astro'
import Palette from '../../components/meta/Palette.astro'
# Meta > Design
In questa pagina tento di spiegare come funziona il design di questo sito. I blocchi di codice con sfondo arancione chiaro sono esempi di codice Astro e sotto hanno un'anteprima del risultato _generata automaticamente_. Ad esempio
```astro
<p>Questo è un paragrafo</p>
```
Molti di questi esempi hanno alcuni stili impostati in `style` per mostrare come funzionano. Nella pratica invece è consigliato create una classe per ogni tipo di componente ed impostare le proprietà via CSS, ad esempio
```css
.my-custom-form {
--card-base: var(--palette-red);
max-width: 25rem;
}
```
```html
<div class="my-custom-form card">
<p>Questo è un paragrafo</p>
</div>
```
## Card
Le card sono uno dei componenti più importanti di questo sito. Sono utilizzate per mostrare i post, i progetti e le pagine. Ecco alcuni esempi per dare un'idea
### Esempio di base
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>
```
### Varianti
#### Grande
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>
```
### Low Level: Mixin SCSS
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`).
```scss
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
border: $size solid #222;
border-radius: $size * 2;
@if $shadow {
box-shadow: $offset $offset 0 0 #222;
}
@if $hoverable {
transition: all 64ms linear;
&:hover {
transform: translate(-1px, -1px);
box-shadow: $offset + 1 $offset + 1 0 0 #222;
}
}
}
```
Ad esempio tutti i bottoni utilizzano direttamente questo mixin senza cambiare i parametri di default.
### Sotto-componenti
#### Titolo
```astro
<div class="card">
<div class="title">Titolo</div>
</div>
```
#### Testo & Modificatori
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>
```
Altrimenti può essere inserito in un tag `<p>`.
```astro
<div class="card">
<div class="text">
<p>
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?
</p>
<p>
Quos sapiente amet numquam quis, libero odit eum, eius
perspiciatis repellat nesciunt cupiditate asperiores maiores?
</p>
</div>
</div>
```
C'è anche il modificatore `small` e `dimmed` per ridurre la grandezza del testo e renderlo grigio rispettivamente.
```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>
```
#### Tags
I tag sono una lista di link con `display: flex` e `flex-wrap: wrap`.
```astro
<div class="card" style="max-width: 25rem;">
<div class="tags">
<a href="#">#tag1</a>
<a href="#">#tagg2</a>
<a href="#">#tag3</a>
<a href="#">#taggg4</a>
<a href="#">#tagg5</a>
<a href="#">#taggg6</a>
<a href="#">#tag7</a>
<a href="#">#taggg8</a>
<a href="#">#tag9</a>
<a href="#">#taggggg10</a>
</div>
</div>
```
## Palette
Varie sezioni del sito utilizzano diverse palette di colori. Questa è la palette di base.
### Guide
<Palette
colors={[
'var(--guide-base)',
'var(--guide-darkest)',
'var(--guide-darker)',
'var(--guide-dark)',
'var(--guide-light)',
'var(--guide-lighter)',
'var(--guide-lightest)',
]}
/>
## Combo Box
I combo box sono un componente per fare dropdown scritto in Preact. Questo è un esempio di come funzionano.
```js
import { ComboBox } from '@/lib/components/ComboBox'
const [value, setValue] = useState('option-1')
```
```jsx
<ComboBox value={value} setValue={setValue}>
{{
'option-1': <>Option 1</>
'option-2': <>Option 2</>
'option-3': <>Option 3</>
}}
</ComboBox>
```

@ -1,12 +0,0 @@
---
# Questo documento è utilizzato nella homepage del sito nella card principale
title: Cos'è il PHC?
---
Il <span title="Pisa Happy Computing">**PHC**</span> è un laboratorio informatico, gestito dagli studenti del **Dipartimento di Matematica** di Pisa e nato nel 1999, che offre vari servizi agli studenti come [Poisson](https://poisson.phc.dm.unipi.it), che ospita le pagine degli studenti.
La sede del PHC è la [stanza 106](https://www.dm.unipi.it/mappa/?sel=638cd24b50cf34e03924a00c) del Dipartimento, dove si trovano i **macchinisti** per discutere e realizzare progetti [hardware](http://steffe.cs.dm.unipi.it/) e [software](https://lab.phc.dm.unipi.it/orario), e occuparsi di server autogestiti.
Le macchine del PHC girano principalmente Linux/Unix come sistemi operativi e i macchinisti sono grandi sostenitori di software [FOSS](https://it.wikipedia.org/wiki/Free_and_Open_Source_Software) (che loro stessi sviluppano sull'[istanza Gitea del PHC](https://git.phc.dm.unipi.it/phc)).
La lista dei vari macchinisti e di altri eventi notevoli si trova nella [pagina della storia](/storia) del PHC.

@ -1,37 +0,0 @@
---
title: Il Nuovo Sito del PHC
description: Benvenuti nel nuovo sito web del PHC, realizzato con la tecnologia web Astro! In questo articolo, ne vedremo le feature principali e cosa serba il futuro.
publishDate: 2024-10-05
---
# Il Nuovo Sito del PHC
Benvenuti nel nuovo sito web, realizzato con la tecnologia web [Astro](https://astro.build/)! In questo articolo, ne vedremo le feature principali e cosa serba il futuro.
## Astro: Un Potente Framework Statico
Astro è un framework statico che ci consente di creare siti web veloci e performanti. Grazie alla sua natura statica, il sito viene generato in anticipo, consentendo un caricamento rapido delle pagine e un'esperienza utente ottimale.
## Caratteristiche del Nuovo Sito
Il nuovo sito web realizzato in Astro offre diverse caratteristiche interessanti:
1. **Interattività**: Grazie all'utilizzo di Astro, siamo in grado di creare pagine web interattive che offrono un'esperienza coinvolgente agli utenti. Questo ci permette di creare una navigazione fluida e dinamica all'interno del sito.
2. **Pagina di Appunti dei Corsi**: Una delle prossime funzionalità che stiamo sviluppando è una pagina di appunti dei corsi interattiva e ricercabile. Questa pagina consentirà agli utenti di cercare e accedere facilmente agli appunti dei corsi, rendendo lo studio più efficiente e organizzato.
3. **Utenti Registrati**: In futuro, prevediamo di implementare un sistema di registrazione degli utenti. Ciò consentirà agli utenti di creare un account personalizzato e accedere a funzionalità esclusive, come mettere mi piace ai propri appunti preferiti o uplodare le proprie risorse.
## Cosa Aspettarsi in Futuro
Il nostro obiettivo è continuare a migliorare il sito web e offrire un'esperienza sempre più completa agli utenti. Alcune delle prossime caratteristiche che stiamo sviluppando includono:
- **Integrazione con Social Media**: Vogliamo consentire agli utenti di condividere facilmente il contenuto del sito web sui social media, ampliando così la visibilità e l'accessibilità del nostro materiale.
- **Miglioramenti dell'Interfaccia Utente**: Stiamo lavorando per migliorare l'aspetto e la sensazione del sito web, rendendolo più intuitivo e piacevole da navigare.
- **Espansione dei Contenuti**: Continueremo ad aggiungere nuovi articoli, tutorial e risorse per fornire agli utenti un'ampia gamma di contenuti di qualità.
## Conclusioni
Il nuovo sito web realizzato in Astro offre un'esperienza interattiva e performante. Con l'aggiunta di una pagina di appunti dei corsi ricercabile e la possibilità di registrarsi come utenti, stiamo lavorando per rendere il sito ancora più utile e coinvolgente. Continuate a seguirci per rimanere aggiornati sulle ultime novità e miglioramenti!

@ -1,5 +0,0 @@
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
const sql = new Database('out/website.sqlite')
export const db = drizzle(sql)

@ -1,4 +0,0 @@
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import { db } from './index'
migrate(db, { migrationsFolder: 'out/drizzle' })

@ -1,43 +0,0 @@
import { sql } from 'drizzle-orm'
import { text, sqliteTable } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
// id è l'id unico di questo utente, non è modificabile una volta creato l'utente.
id: text('id')
.primaryKey()
.default(sql`(lower(hex(randomblob(16))))`),
// Username è il nome leggibile di questo utente utilizzato anche per le
// route per singolo utente, deve essere unico nel sito.
//
// NOTE: Quando un utente accede per la prima volta di default gli viene
// chiesto se usare quello dell'account che sta usando o se cambiarlo
// (in teoria non dovrebbe essere un problema poterlo modificare
// successivamente).
username: text('username').unique().notNull(),
// FullName da mostrare in giro per il sito
fullName: text('fullname'),
// Email per eventuale contatto
email: text('email'),
})
export type User = typeof users.$inferSelect // return type when queried
export type InsertUser = typeof users.$inferInsert // insert type
export const accounts = sqliteTable('accounts', {
// id è l'id unico di questo account, non è modificabile una volta creato l'account.
id: text('id').primaryKey(),
userId: text('userId')
.notNull()
.references(() => users.id),
provider: text('provider').$type<'poisson' | 'ateneo'>().notNull(),
token: text('token').notNull(),
})
export type Account = typeof accounts.$inferSelect // return type when queried
export type InsertAccount = typeof accounts.$inferInsert // insert type

2
src/env.d.ts vendored

@ -1,2 +0,0 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

@ -1,16 +0,0 @@
---
import BaseLayout from './BaseLayout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
---
<BaseLayout {...Astro.props}>
<Header />
<main class="article card large">
<div class="text">
<slot />
</div>
</main>
<Footer />
</BaseLayout>

@ -1,59 +0,0 @@
---
import 'katex/dist/katex.min.css'
import '@fontsource/open-sans/latin.css'
import '@fontsource/source-sans-pro/latin.css'
import '@fontsource/source-code-pro/latin.css'
import '@fontsource/space-mono/latin.css'
import '@fontsource/iosevka/latin.css'
import '@fontsource-variable/material-symbols-outlined/full.css'
import '../styles/main.scss'
type Props = {
title?: string
description?: string
thumbnail?: string
/** Tags for the page, used for styling */
pageTags?: string | string[]
}
const { title, description, thumbnail, pageTags } = Astro.props
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content={title ?? 'PHC'} />
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
{thumbnail && <meta property="og:image" content={thumbnail} />}
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
<script>
import renderMathInElement from 'katex/contrib/auto-render'
document.addEventListener('DOMContentLoaded', function () {
renderMathInElement(document.body, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
],
throwOnError: false,
})
})
</script>
<title>{title}</title>
</head>
<body class:list={typeof pageTags === 'string' ? [pageTags] : pageTags}>
<slot />
</body>
</html>

@ -1,14 +0,0 @@
---
import BaseLayout from './BaseLayout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
---
<BaseLayout {...Astro.props}>
<Header />
<main>
<slot />
</main>
<Footer />
</BaseLayout>

@ -1,64 +0,0 @@
---
import PageLayout from '@layouts/PageLayout.astro'
import { AppuntiList, AppuntiCard } from '@client/Appunti'
---
<PageLayout pageTags="appunti">
<h1>Appunti & Dispense</h1>
<div class="search">
<input type="text" />
<span class="material-symbols-outlined">search</span>
</div>
<h2>In primo piano</h2>
<div class="appunti-scrollable center">
<AppuntiList>
<AppuntiCard client:load title="Appunti 1" author="someuser" courseYear="2023/2024" />
<AppuntiCard client:load course="Geometria 1" author="exampleuser" courseYear="2023/2024" />
<AppuntiCard client:load course="Algebra 1" author="anotheruser" courseYear="2023/2024" />
</AppuntiList>
</div>
<h2>Categorie</h2>
<h3>Analisi</h3>
<div class="appunti-scrollable">
<AppuntiList>
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
</AppuntiList>
</div>
<h3>Algebra Lineare</h3>
<div class="appunti-scrollable">
<AppuntiList>
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
</AppuntiList>
</div>
<h3>Geometria</h3>
<div class="appunti-scrollable">
<AppuntiList>
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
</AppuntiList>
</div>
</PageLayout>

@ -1,23 +0,0 @@
---
import { getCollection } from 'astro:content'
import ArticleLayout from '../../layouts/ArticleLayout.astro'
export async function getStaticPaths() {
const guides = await getCollection('guides')
return guides.map(entry => ({
params: { id: entry.slug },
props: { entry },
}))
}
const { entry } = Astro.props
const { Content } = await entry.render()
---
<ArticleLayout {...entry.data} 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>}
<Content />
</ArticleLayout>

@ -1,28 +0,0 @@
---
import { getCollection } from 'astro:content'
import PageLayout from '@layouts/PageLayout.astro'
const guides = await getCollection('guides')
---
<PageLayout pageTags="guide">
<h1>Guide</h1>
<div class="card-list">
{
guides.map(guide => (
<div class="card">
<div class="title">
<a href={`/guide/${guide.slug}`}>{guide.data.title}</a>
</div>
<div class="text">{guide.data.description}</div>
<div class="tags">
{guide.data.tags.map(tag => (
<a href={`/guide/tags/${tag}`}>#{tag}</a>
))}
</div>
</div>
))
}
</div>
</PageLayout>

@ -1,55 +0,0 @@
---
import { getCollection } from 'astro:content'
import type { CollectionEntry } from 'astro:content'
import PageLayout from '@/layouts/PageLayout.astro'
export async function getStaticPaths() {
const guides = await getCollection('guides')
const tags: string[] = []
guides.forEach(post => {
post.data.tags.forEach(tag => {
tags.push(tag)
})
})
return Array.from(new Set(tags)).map(tag => {
return {
params: { tag },
props: {
tag,
guides: guides.filter(post => post.data.tags.includes(tag)),
},
}
})
}
interface Props {
tag: string
guides: CollectionEntry<'guides'>[]
}
const { tag, guides } = Astro.props
---
<PageLayout pageTags="guide tag">
<h1>Guide > #{tag}</h1>
<div class="card-list">
{
guides.map(guide => (
<div class="card">
<div class="title">
<a href={`/guide/${guide.slug}`}>{guide.data.title}</a>
</div>
<div class="text">{guide.data.description}</div>
<div class="tags">
{guide.data.tags.map(tag => (
<a href={`/guide/tags/${tag}`}>#{tag}</a>
))}
</div>
</div>
))
}
</div>
</PageLayout>

@ -1,253 +0,0 @@
---
import { getCollection } from 'astro:content'
import PageLayout from '../layouts/PageLayout.astro'
import { Content as WhatPhcContent, frontmatter as whatsPhcFrontmatter } from '../content/meta/whats-phc.md'
const news = await getCollection('news')
const galleryCollage = (await Astro.glob('@/assets/gallery/*')).map(({ default: { src } }) => src)
---
<PageLayout pageTags="homepage">
<section class="principal">
<div class="circuit-layer">
<canvas id="circuits-art"></canvas>
<script src="../scripts/circuits-art.ts"></script>
</div>
<div class="logo">
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
</div>
<div class="whats-phc card large">
<input type="checkbox" class="hide" id="mobile-whats-phc-read-more" checked />
<div class="title">{whatsPhcFrontmatter.title}</div>
<div class="text">
<WhatPhcContent />
</div>
<div class="mobile-only">
<label for="mobile-whats-phc-read-more">
<div class="button">
<span>Mostra di più</span>
<span>Mostra meno</span>
</div>
</label>
</div>
</div>
</section>
<section class="news">
<div class="zig-zag">
<svg
width="100%"
height="2rem"
viewBox="0 0 1 1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMid meet"
>
<defs>
<pattern id="zig-zag-1" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
<path fill="#C2A8EB" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
</pattern>
</defs>
<rect fill="url(#zig-zag-1)" x="0" y="0" width="1000" height="1"></rect>
</svg>
</div>
<div class="title">Ultime Notizie</div>
<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',
})}
</div>
<div class="text">{newsItem.data.description}</div>
</div>
))
}
</div>
<a class="primary" href="/notizie" role="button">Vai all'Archivio</a>
</section>
<section class="projects">
<div class="zig-zag">
<svg
width="100%"
height="2rem"
viewBox="0 0 1 1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMid meet"
>
<defs>
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
</pattern>
</defs>
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>
</svg>
</div>
<div class="title">Progetti</div>
<div class="project-list">
<a target="_blank" href="https://git.phc.dm.unipi.it/" style="--masonry-height: 2;">
<div class="project" style="--card-bg: rgb(150, 197, 150);">
<div class="image">
<img
src="https://upload.wikimedia.org/wikipedia/commons/b/bb/Gitea_Logo.svg"
alt="logo Gitea"
/>
</div>
<div class="title">Gitea</div>
<div class="description">
<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.
</p>
</div>
</div>
</a>
<!-- <a href="#" style="--masonry-height: 2;">
<div class="project">
<div class="image">
<div class="box"></div>
</div>
<div class="title">PHC-Bot</div>
<div class="description">
Un bot con cui chattare per chiedere informazioni o supporto tecnico.
</div>
</div>
</a> -->
<a target="_blank" href="https://lab.phc.dm.unipi.it/orario/" style="--masonry-height: 1;">
<div class="project" style="--card-bg: #75ca75;">
<div class="image auto">
<img src="https://lab.phc.dm.unipi.it/orario/icon.png" alt="Logo Orario" />
</div>
<div class="title">Orario</div>
<div class="description">
Questo sito permette di visualizzare il proprio orario delle lezioni, con informazioni
sui docenti e le aule.
</div>
</div>
</a>
<a target="_blank" href="https://lab.phc.dm.unipi.it/problemi/" style="--masonry-height: 2;">
<div class="project" style="--card-bg: #aa88c0;">
<div class="image">
<img
src="https://lab.phc.dm.unipi.it/problemi/favicon/android-chrome-512x512.png"
alt="logo Problemi"
/>
</div>
<div class="title">Problemi</div>
<div class="description">
<p>
Bacheca di problemi di Matematica da risolvere, inviandone le soluzioni in LaTeX.
</p>
<p>La sera i problemi compaiono sullo schermo nell'atrio del Dipartimento.</p>
</div>
</div>
</a>
<a href="http://steffe.cs.dm.unipi.it/" style="--masonry-height: 2;">
<div class="project">
<div class="image cover">
<img src="https://steffe.lb.cs.dm.unipi.it/assets/img/logo.png" alt="Logo Cluster" />
</div>
<div class="title">Cluster "Steffè"</div>
<div class="description">
Cluster progettato ed assemblato durante il progetto speciale per la didattica
"Calcolo Parallelo dall'Infrastruttura alla Matematica".
</div>
</div>
</a>
<a target="_blank" href="https://seminarietti.phc.dm.unipi.it/" style="--masonry-height: 2;">
<div class="project" style="--card-bg: #bd9fec;">
<div class="image">
<img src="https://seminarietti.phc.dm.unipi.it/favicon.png" alt="Logo Seminarietti" />
</div>
<div class="title">Seminarietti</div>
<div class="description">
Varie conferenze del PHC su argomenti di informatica, matematica e tecnologia.
</div>
</div>
</a>
<a target="_blank" href="https://tutorato.phc.dm.unipi.it/" style="--masonry-height: 2;">
<div class="project" style="--card-bg: #c55;">
<div class="image">
<img src="https://tutorato.phc.dm.unipi.it/favicon.svg" alt="Logo Tutorato" />
</div>
<div class="title">Tutorato</div>
<div class="description">
Un sito con tutte le informazioni sui tutorati di Matematica, con tanto di archivio
degli anni passati.
</div>
</div>
</a>
</div>
</section>
<section class="wanna-be-macchinista">
<div class="zig-zag">
<svg
width="100%"
height="2rem"
viewBox="0 0 1 1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMid meet"
>
<defs>
<pattern id="zig-zag-3" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
<path fill="#888" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
</pattern>
</defs>
<rect fill="url(#zig-zag-3)" x="0" y="0" width="1000" height="1"></rect>
</svg>
</div>
<span></span>
<span></span>
<div class="card large" style="--card-base: #ddd;">
<div class="title">E cosa si fa in PHC?</div>
<div class="text">
<p>Nessuno lo sa di preciso, ma facciamo molte cose:</p>
<ul>
<li>amministrazione di sistemi Linux/Unix e macchine virtuali</li>
<li>supporto tecnico per installare e usare Linux sul proprio portatile</li>
<li>costruire, smontare ed aggiustare computer antichi e moderni</li>
<li>sviluppo di software backend e web development</li>
<li>organizzazione di seminari a tema tecnologico</li>
</ul>
</div>
</div>
<div class="gallery-collage">
{
galleryCollage.map((src, i) => (
<div class="card">
<img src={src} alt={`gallery image ${i}`} />
</div>
))
}
</div>
<div class="card large" style="--card-base: #ddd;">
<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!
</p>
<p>
L'unico prerequisito è la voglia di imparare! Di solito, c'è un periodo di "apprendistato"
nel quale si apprendono le basi, ed una volta superato potrai diventare un macchinista a
tutti gli effetti. <a href="mailto:macchinisti@lists.dm.unipi.it"> </a>
</p>
</div>
</div>
</section>
</PageLayout>

@ -1,22 +0,0 @@
---
import PageLayout from '../layouts/PageLayout.astro'
---
<PageLayout pageTags="login">
<h1>Login</h1>
<!-- form with username and password, and a button for oauth login -->
<form action="/login" method="post">
<h3 class="center">Accedi con Poisson</h3>
<input type="text" id="username" placeholder="Username" name="username" required />
<input type="password" id="password" placeholder="Password" name="password" required />
<button class="primary center" type="submit">Login</button>
<hr />
<h3 class="center">Accedi con Ateneo</h3>
<a href="/auth/ateneo" class="primary center" role="button">Login</a>
</form>
<!-- <span class="material-symbols-outlined">person</span> -->
</PageLayout>

@ -1,34 +0,0 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import Header from '../../components/Header.astro'
import Footer from '../../components/Footer.astro'
import CodeBlockPreview from '../../components/meta/CodeBlockPreview.astro'
import { Content, getHeadings } from '../../content/meta/design.mdx'
const headings = getHeadings()
---
<BaseLayout {...Astro.props} pageTags="design">
<Header />
<aside>
<nav>
<h3>Indice</h3>
<ul>
{
headings.map(heading => (
<li style={{ '--depth': heading.depth }}>
<a href={`#${heading.slug}`}>{heading.text}</a>
</li>
))
}
</ul>
</nav>
</aside>
<main class="text">
<Content components={{ pre: CodeBlockPreview }} />
</main>
<Footer />
</BaseLayout>

@ -1,20 +0,0 @@
---
import { getCollection } from 'astro:content'
import ArticleLayout from '../../layouts/ArticleLayout.astro'
export async function getStaticPaths() {
const guides = await getCollection('news')
return guides.map(entry => ({
params: { id: entry.slug },
props: { entry },
}))
}
const { entry } = Astro.props
const { Content } = await entry.render()
---
<ArticleLayout {...entry.data} pageTags={['notizia']}>
<Content />
</ArticleLayout>

@ -1,30 +0,0 @@
---
import { getCollection } from 'astro:content'
import PageLayout from '@layouts/PageLayout.astro'
const news = await getCollection('news')
---
<PageLayout pageTags="notizie">
<h1>Notizie</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',
})}
</div>
<div class="text">{newsItem.data.description}</div>
</div>
))
}
</div>
</PageLayout>

@ -1,101 +0,0 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import Timeline from '../components/Timeline.astro'
---
<BaseLayout {...Astro.props} pageTags={'storia'}>
<Header />
<main>
<h1>Storia</h1>
<div class="text">
<p>
Qui annoverariamo la storia del PHC in una timeline con gli eventi più salienti del progetto.
</p>
<p>
Per delle note storiche un po' più dettagliate, si legga l'ottima pagine sul <a
href="http://betti.dm.unipi.it/servizi/PHC.html">sito del dipartimento</a
>.
</p>
</div>
<div class="timeline">
<Timeline title="Tanti Francesco" year="2022">
<p>
Nel 2022, entrano a far parte del PHC <strong>Francesco Minnocci</strong>, <strong
>Francesco Manicastri</strong
> e <strong>Francesco Baldino</strong>
</p>
</Timeline>
<Timeline title="Nuovi macchinisti" year="2019">
<p>
Nel 2019, entrano a far parte del PHC <strong>Antonio De Lucreziis</strong> e <strong
>Illya Serdyuk</strong
>
</p>
</Timeline>
<Timeline title="Nuovi macchinisti" year="2018">
<p>
Nel 2018, entrano a far parte del PHC <strong>Francesco Caporali</strong> e <strong
>Letizia D'Achille</strong
>
</p>
</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/"
>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"
>logo</a
> creato da <strong>Michele Cerulli</strong>.
</p><img
class="dropdown"
src="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
alt="Logo"
/>
</Timeline>
<Timeline title="Seminari del PHC" year="2000">
<p>
Nel primo semestre del 2000, vengono tenuti diversi <a
href="https://web.archive.org/web/20010430151939/http://www.phc.unipi.it/seminari2000/"
>seminari</a
> su temi quali: installazione di programmi e sistemi operativi (come Linux), sicurezza informatica,
esperimenti con matematica e musica e creazione di pagine web.
</p>
</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
>.
</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.
</p>
</Timeline>
<Timeline title="Apertura di Poisson" year="~1994">
<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/"
>Web Archive</a
>
</p>
</Timeline>
</div>
</main>
<Footer />
</BaseLayout>

@ -1,10 +0,0 @@
---
import PageLayout from '../layouts/PageLayout.astro'
import { UtentiPage } from '../client/UtentiPage.tsx'
---
<PageLayout pageTags="utenti">
<h1>Utenti</h1>
<UtentiPage client:load />
</PageLayout>

@ -1,333 +0,0 @@
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
interface Grid<T> extends Iterable<[number, number, T]> {
has(point: [number, number]): boolean
get(point: [number, number]): T
set(point: [number, number], value: T): void
}
function createGrid<T>(): Grid<T> {
const cells: Record<string, T> = {}
return {
has([x, y]) {
return cells[`${x},${y}`] !== undefined
},
get([x, y]) {
return cells[`${x},${y}`]
},
set([x, y], value) {
cells[`${x},${y}`] = value
},
*[Symbol.iterator]() {
for (const [coord, value] of Object.entries(cells)) {
const [x, y] = coord.split(',').map(s => parseInt(s))
yield [x, y, value]
}
},
}
}
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
type WireDirection = 'down' | 'down-left' | 'down-right'
type WireSteps = { position: Point2; direction: WireCell }[]
type Point2 = [number, number]
type State = {
grid: Grid<WireCell>
queuedWire: {
index: number
steps: WireSteps
} | null
badTries: 0
}
type Renderer = {
timer: number
}
let renderer: Renderer | null = null
const RENDERER_FPS = 30
function setup() {
console.log('Setting up circuits art...')
$canvas.width = $canvas.clientWidth
$canvas.height = $canvas.clientHeight
const g = $canvas.getContext('2d')!
const state: State = {
grid: createGrid(),
queuedWire: null,
badTries: 0,
}
const startTime = new Date()
const handle = window.setInterval(() => {
const time = new Date().getTime() - startTime.getTime()
update(state, g.canvas.width, g.canvas.height, time)
render(g, state, time)
}, 1000 / RENDERER_FPS)
renderer = { timer: handle }
}
function update(state: State, width: number, height: number, time: number) {
const w = (width / CELL_SIZE) | 0
// const h = (height / CELL_SIZE) | 0
if (state.badTries > 1000) {
console.log('finished')
clearInterval(renderer!.timer)
}
if (!state.queuedWire) {
const sx = randomInt(0, w)
if (!state.grid.has([sx, 0])) {
const steps = generateWire(state.grid, [sx, 0])
if (steps.length < 7) {
state.badTries++
return
}
state.queuedWire = {
index: 0,
steps,
}
state.grid.set(state.queuedWire.steps[0].position, 'dot')
return
}
state.badTries++
} else {
const wire = state.queuedWire
const step = wire.steps[wire.index]
state.grid.set(step.position, step.direction)
if (wire.index + 1 < wire.steps.length) {
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
}
wire.index++
if (wire.index >= wire.steps.length) {
state.queuedWire = null
}
}
}
const CELL_SIZE = 27
const BACKGROUND_COLOR = '#ecffe3'
const WIRE_COLOR = '#a6ce94'
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
'down': g => {
g.strokeStyle = WIRE_COLOR
g.beginPath()
g.moveTo(0, 0)
g.lineTo(0, 1)
g.stroke()
},
'down-left': g => {
g.strokeStyle = WIRE_COLOR
g.beginPath()
g.moveTo(0, 0)
g.lineTo(-1, 1)
g.stroke()
},
'down-right': g => {
g.strokeStyle = WIRE_COLOR
g.beginPath()
g.moveTo(0, 0)
g.lineTo(+1, 1)
g.stroke()
},
'dot': g => {
g.fillStyle = BACKGROUND_COLOR
g.beginPath()
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
g.fill()
g.strokeStyle = WIRE_COLOR
g.beginPath()
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
g.stroke()
},
}
function render(g: CanvasRenderingContext2D, state: State, time: number) {
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
g.resetTransform()
g.scale(CELL_SIZE, CELL_SIZE)
g.lineWidth = 3 / CELL_SIZE
const w = (g.canvas.width / CELL_SIZE) | 0
const h = (g.canvas.height / CELL_SIZE) | 0
for (let y = 0; y <= h + 1; y++) {
for (let x = 0; x <= w + 1; x++) {
if (!state.grid.has([x, y])) continue
const cell = state.grid.get([x, y])
g.save()
g.translate(x, y)
// g.fillStyle = '#f008'
// g.beginPath()
// g.rect(-0.25, -0.25, 0.5, 0.5)
// g.fill()
RENDER_CELL[cell](g)
g.restore()
}
}
// if (state.queuedWire) {
// for (const step of state.queuedWire.steps) {
// const [x, y] = step.position
// g.fillStyle = '#00f8'
// g.save()
// g.translate(x, y)
// g.beginPath()
// g.rect(-0.25, -0.25, 0.5, 0.5)
// g.fill()
// g.restore()
// }
// }
// const [mx, my] = state.mouse
// g.save()
// g.fillStyle = '#0008'
// g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
// g.beginPath()
// g.rect(0, 0, 1, 1)
// g.fill()
// g.restore()
}
setup()
window.addEventListener('resize', () => {
if (renderer) {
clearInterval(renderer.timer)
}
setup()
})
function randomInt(from: number, to: number) {
return Math.floor(Math.random() * (to - from + 1))
}
function randomChoice<T>(choices: T[]): T {
return choices[randomInt(0, choices.length - 1)]
}
function randomWeightedChoice<T>(choices: [T, number][]) {
return
}
// 3 + 4 + 5 + 6
randomWeightedChoice([
['a', 3],
['b', 4],
['c', 5],
['d', 6],
])
const DIR_TO_VEC: Record<WireDirection, Point2> = {
['down']: [0, 1],
['down-left']: [-1, 1],
['down-right']: [+1, 1],
}
type ShortCircuitBoolean = boolean | (() => boolean)
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
/**
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
*/
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
['down']: ([x, y], grid) =>
!grid.has([x, y + 1]) &&
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'),
['down-right']: ([x, y], grid) =>
!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[] {
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
}
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'])
const steps: { position: Point2; direction: WireCell }[] = []
for (let i = 0; i < segmentLength; i++) {
const availableDirections = pruneDirections(grid, currentPosition, [
'down',
'down-left',
'down-right',
])
if (availableDirections.length === 0) {
break
} else {
const dir =
availableDirections.includes(currentDirection) && Math.random() < 0.25
? currentDirection
: randomChoice(availableDirections)
if (
(currentDirection === 'down-left' && dir === 'down-right') ||
(currentDirection === 'down-right' && dir === 'down-left')
) {
break
}
const [x, y] = currentPosition
const [dx, dy] = DIR_TO_VEC[dir]
steps.push({
position: [x, y],
direction: dir,
})
currentPosition = [x + dx, y + dy]
currentDirection = dir
}
}
const last = steps.at(-1)
if (last) {
last.direction = 'dot'
}
return steps
}

@ -1,781 +0,0 @@
// $news-bg: #fffbeb;
// $news-accent-bg: #f8e8b1;
@import './mixins.scss';
@layer component {
//
// Components - for complex parts of the UI like search bars or compound buttons
//
.material-symbols-outlined {
font-family: 'Material Symbols Outlined Variable';
font-weight: normal;
font-style: normal;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
display: inline-grid;
place-content: center;
font-size: 24px;
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24;
max-width: 32px;
}
.title {
font-family: 'Iosevka', monospace;
font-weight: 700;
font-size: 28px;
}
.search {
width: 100%;
height: 2.5rem;
@include neo-brutalist-card;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
cursor: pointer;
background: #fff;
&:hover,
&:hover input[type='text'] {
background: #f8f8f8;
}
input[type='text'] {
border: none;
box-shadow: none;
outline: none;
height: 100%;
padding: 0;
padding-left: 0.35rem;
}
.material-symbols-outlined {
padding: 0 0.5rem;
}
}
.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%;
display: flex;
flex-direction: column;
gap: 1rem;
.search-result {
width: 100%;
display: grid;
grid-template-columns: auto auto 1fr auto;
grid-template-areas: 'icon text . right';
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: #fff;
@include neo-brutalist-card;
a {
display: contents;
}
& > .icon {
grid-area: icon;
display: grid;
}
& > .text {
grid-area: text;
display: grid;
}
& > .right {
grid-area: right;
display: grid;
}
}
}
.appunti-scrollable {
display: flex;
overflow-x: auto;
.appunti-list {
padding: 2px;
padding-bottom: 1rem;
}
}
.appunti-list {
display: grid;
grid-auto-flow: column;
gap: 3rem;
overflow-x: auto;
max-width: 100%;
}
.appunti-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
color: #444;
& > .thumbnail {
width: 10rem;
aspect-ratio: 10 / 14;
background: #e0e0e0;
@include neo-brutalist-card($hoverable: true);
}
& > .thumbnail + * {
font-weight: 700;
padding-top: 0.25rem;
}
& > .title,
& > .course,
& > .author,
& > .course-year {
padding-left: 0.5rem;
font-size: 16px;
}
}
.article-list {
display: grid;
grid-auto-flow: row;
max-width: 100%;
gap: 2rem;
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
grid-auto-rows: auto;
.article {
display: grid;
grid-template-rows: auto auto auto;
background: var(--card-bg, $project-card-bg);
color: #000e;
@include neo-brutalist-card($size: 3px, $offset: 9px);
row-gap: 0.5rem;
padding: 1rem;
.description {
font-size: 16px;
}
.tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 14px;
font-weight: 600;
color: #555;
}
}
}
.news-list {
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;
// }
// }
// }
}
.timeline {
display: grid;
--timeline-track-size: 6rem;
--timeline-thickness: 6px;
--timeline-color: #333;
grid-template-columns: 1fr var(--timeline-track-size) 1fr;
@media screen and (max-width: $screen-desktop-min) {
--timeline-track-size: 3rem;
grid-template-columns: var(--timeline-track-size) 1fr;
}
max-width: 120ch;
& > .timeline-item {
position: relative;
display: grid;
& > .content {
padding: 2rem 1rem;
@media screen and (max-width: $screen-desktop-min) {
padding: 1rem 1rem 1rem 0;
}
display: grid;
grid-template-columns: auto;
& > .title {
font-size: 20px;
font-weight: 700;
}
& > .date {
font-size: 32px;
font-weight: 400;
color: #555;
}
}
// timeline vertical line
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: var(--timeline-thickness);
background: var(--timeline-color);
}
&:first-child::before {
top: 50%;
}
&:last-child::before {
bottom: 50%;
}
// timeline circle
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1rem;
height: 1rem;
border-radius: 50%;
background: var(--timeline-color);
}
&::before,
&::after {
grid-column: 1 / span 1;
}
@media screen and (min-width: $screen-desktop-min) {
&:nth-child(odd)::before {
grid-column: 2 / span 1;
}
&:nth-child(even)::before {
grid-column: 1 / span 1;
}
&:nth-child(odd)::after {
grid-column: 2 / span 1;
}
&:nth-child(even)::after {
grid-column: 1 / span 1;
}
}
.dropdown {
@include neo-brutalist-card;
max-width: 15rem;
position: absolute;
bottom: calc(-10rem - 1px);
left: 50%;
transform: translateX(calc(-50% - var(--timeline-track-size) / 2));
border-top: none;
border-radius: 0 0 0.5rem 0.5rem;
@media screen and (max-width: $screen-desktop-min) {
@include neo-brutalist-card;
position: static;
transform: none;
margin-left: auto;
margin-right: auto;
}
}
@media screen and (min-width: $screen-desktop-min) {
&:nth-child(odd) {
grid-column: 1 / span 2;
grid-template-columns: 1fr var(--timeline-track-size);
& > .content {
grid-column: 1;
}
.dropdown {
left: auto;
right: 50%;
transform: translateX(calc(50% - var(--timeline-track-size) / 2));
}
}
&:nth-child(even) {
grid-column: 2 / span 2;
grid-template-columns: var(--timeline-track-size) 1fr;
& > .content {
grid-column: 2;
}
.dropdown {
left: 50%;
transform: translateX(calc(-50% + var(--timeline-track-size) / 2));
}
}
}
@media screen and (max-width: $screen-desktop-min) {
grid-column: 1 / span 2;
grid-template-columns: var(--timeline-track-size) 1fr;
& > .content {
grid-column: 2;
}
}
}
}
//
// Cards
//
.card {
display: grid;
--card-base-internal: var(--card-base, #f8f8f8);
background: var(--card-base-internal);
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
@include neo-brutalist-card($size: 3px, $offset: 9px);
row-gap: 0.5rem;
padding: 1rem;
// Variants
&.large {
padding: 2rem;
@include neo-brutalist-card($size: 4px, $offset: 8px);
row-gap: 1rem;
& > .title {
font-size: 36px;
font-weight: 700;
}
& > .text {
font-size: 18px;
}
}
// Child Items
& > .title {
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
font-size: 26px;
font-weight: 700;
}
& > .text {
font-size: 16px;
&.small {
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
font-size: 14px;
font-weight: 600;
}
&.dimmed {
color: color-mix(in srgb, var(--card-base-internal), #000 50%);
}
}
& > .tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 14px;
font-weight: 600;
color: #555;
a {
color: color-mix(in srgb, var(--card-base-internal), #000 60%);
font-weight: 600;
&:hover {
text-decoration: underline solid 2px;
}
}
}
a:hover {
text-decoration: underline solid 2px;
}
a.title {
&:hover {
text-decoration: underline solid 3px;
}
}
}
//
// Card List
//
.card-list {
display: grid;
grid-auto-flow: row;
max-width: 100%;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
grid-auto-rows: auto;
& > .card {
max-width: 25rem;
}
@media screen and (max-width: $screen-desktop-min) {
grid-template-columns: 1fr;
}
}
.show-more {
place-self: center;
}
.search-bar {
display: grid;
grid-template-columns: auto 1fr;
width: 100%;
gap: 1rem;
@media screen and (max-width: $screen-desktop-min) {
grid-template-columns: 1fr;
& > .search {
grid-row: 1;
}
}
}
/* .filter-select {
width: 100%;
height: 2.5rem;
@include neo-brutalist-card;
display: grid;
align-items: center;
grid-auto-flow: column;
cursor: pointer;
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
background: var(--filter-bg-color, #ddd);
&:hover,
&:hover select {
background: var(--filter-bg-color-hover);
}
.material-symbols-outlined {
padding: 0 0.35rem;
}
select {
border: none;
box-shadow: none;
outline: none;
height: 100%;
appearance: none;
cursor: pointer;
padding: 0;
background: var(--filter-bg-color, #ddd);
}
} */
.combobox {
width: 100%;
height: 2.5rem;
position: relative;
padding: 0 0.25rem 0 0.25rem;
@include neo-brutalist-card;
cursor: pointer;
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
background: var(--filter-bg-color, #ddd);
.material-symbols-outlined {
padding: 0 0.35rem;
}
.selected {
height: 100%;
gap: 0.25rem;
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr auto;
align-items: center;
.content {
display: grid;
grid-auto-flow: column;
align-items: center;
justify-content: start;
}
}
.dropdown {
position: absolute;
z-index: 10;
top: calc(100% + 8px);
left: -3px;
@include neo-brutalist-card;
background: #fff;
.option {
height: 2rem;
padding: 0 0.5rem 0 0.25rem;
display: grid;
grid-auto-flow: column;
align-items: center;
justify-content: start;
&:hover {
background: #ddd;
}
}
}
@media screen and (max-width: $screen-desktop-min) {
.dropdown {
left: 3px;
right: 3px;
top: calc(100% + 9px);
}
}
}
.gallery-collage {
display: flex;
place-content: center;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
width: 100%;
max-width: 64rem;
& > .card {
padding: 3px;
overflow: hidden;
@media screen and (max-width: $screen-desktop-min) {
max-width: 100%;
}
img {
display: grid;
max-width: 25rem;
max-height: 25rem;
border-radius: 3px;
@media screen and (max-width: $screen-desktop-min) {
max-height: none;
width: 25rem;
max-width: 100%;
}
}
}
}
}

@ -1,130 +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: 'Source Sans Pro', sans-serif;
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;
}
}
}

@ -1,300 +0,0 @@
$black: #222;
$border-large: 4px solid $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;
$screen-desktop-min: 1024px;
@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: 'Open Sans', sans-serif;
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: $border-large;
background: $header-bg;
grid-column: 1 / -1;
top: 0;
position: sticky;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
padding-left: 0.5rem;
img {
height: 4rem;
}
}
.links {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0 1.5rem;
a {
font-family: 'Iosevka', monospace;
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: 20rem;
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: $border-large;
background: $footer-bg;
color: $footer-fg;
display: grid;
place-content: center;
font-family: 'Source Sans Pro', sans-serif;
font-size: 20px;
@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,17 +0,0 @@
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
border: $size solid #222;
border-radius: $size * 2;
@if $shadow {
box-shadow: $offset $offset 0 0 #222;
}
@if $hoverable {
transition: all 64ms linear;
&:hover {
transform: translate(-1px, -1px);
box-shadow: $offset + 1 $offset + 1 0 0 #222;
}
}
}

@ -1,749 +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 0 6rem;
background: $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;
max-width: 640px;
position: relative;
user-select: none;
img {
width: 100%;
filter: drop-shadow(6px 6px 0 $black) drop-shadow(4px 0 0 $black)
drop-shadow(0 4px 0 $black) drop-shadow(-4px 0 0 $black) drop-shadow(0 -4px 0 $black);
}
}
& > .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%);
}
}
}
}
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: $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: $homepage-projects-bg;
padding-bottom: 6rem;
.project-list {
width: calc(20rem * 3 + 1.5rem * 2 + 6rem * 2);
max-width: 100vw;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-auto-flow: dense;
& > * {
grid-row: span var(--masonry-height);
}
gap: 1.5rem;
padding: 0 6rem;
.project {
width: 100%;
height: 100%;
// background: #fcddff;
// background: #ffa89c;
background: var(--card-bg, $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;
.title {
font-size: 32px;
}
.image {
grid-row: span 2;
// place-self: center;
.box {
background: #0003;
border: 3px solid #0006;
border-radius: 6px;
width: 5rem;
height: 5rem;
}
img {
width: 5rem;
height: 5rem;
}
&.cover {
img {
object-fit: cover;
}
}
&.auto {
img {
width: auto;
height: auto;
}
}
}
.description {
font-size: 16px;
@extend .text;
}
transition: all 128ms ease-out;
&: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;
}
}
}
section.wanna-be-macchinista {
background: $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: #fdfdf0;
// justify-self: center;
// display: grid;
// grid-auto-flow: row;
// justify-items: center;
// padding: 3rem;
// gap: 3rem;
// width: 100%;
// .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 {
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 {
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;
@media screen and (max-width: $screen-desktop-min) {
box-shadow: none;
border: none;
border-radius: 0;
max-width: none;
margin: 0 auto;
}
}
}
.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%
);
}
}
//
// 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: 'Iosevka', monospace;
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', monospace;
font-size: 16px;
user-select: all;
}
}
}
}

@ -1,174 +0,0 @@
@function pow($number, $exponent) {
$value: 1;
@if $exponent > 0 {
@for $i from 1 through $exponent {
$value: $value * $number;
}
}
@return $value;
}
@mixin geometric-headings {
$base-font-size: 16px;
$heading-scale: 1.25;
@for $i from 1 through 5 {
$factor: pow($heading-scale, 5 - $i);
h#{$i} {
font-size: $base-font-size * $factor;
font-family: 'Iosevka', monospace;
font-weight: 700;
margin-bottom: 0.25rem;
}
// p + h#{$i} {
// margin-top: 0.75rem * $factor;
// }
}
}
@layer typography {
@include geometric-headings;
.text {
// text-align: justify;
// hyphens: auto;
&.center {
text-align: center;
}
line-height: 1.5;
& > * {
max-width: 46rem;
}
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;
font-family: 'Source Code Pro', monospace;
font-weight: 400;
font-size: 16px;
}
code {
border-radius: 0.25rem;
padding: 0.125rem 0.3rem;
}
pre {
margin: 2rem auto;
padding: 0.5rem 1rem;
// width: 100%;
max-width: 80ch;
width: fit-content;
line-height: 1.5;
border-radius: 0.25rem;
box-shadow: 0.25rem 0.25rem 0 0 #333;
border: 2px solid #333;
code {
padding: 0;
}
@media screen and (max-width: $screen-desktop-min) {
width: calc(100vw - 2rem);
max-width: none;
}
}
p {
line-height: 1.75;
margin: var(--paragraph-margin, 1rem) auto;
}
// p + 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:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
strong {
font-weight: 600;
}
em {
font-style: italic;
}
@include geometric-headings;
h1 {
margin-bottom: 2rem;
text-align: center;
}
h1,
h2,
h3,
h4 {
font-weight: 700;
color: #333;
// trick to fix anchor links with sticky header
padding-top: 7rem;
margin-top: -6.5rem;
}
h1,
h2,
h3,
h4,
ol,
ul {
margin-left: auto;
margin-right: auto;
}
a,
a:visited {
color: var(--zone-color, #1e6733);
font-weight: 600;
&:hover {
text-decoration: underline 2px solid;
}
}
@media screen and (max-width: 1024px) {
& > * {
margin: 0 0.75rem;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save