115 Commits
Author SHA1 Message Date
aziis98 0520e7641f Small auth module refactor 2022-07-31 01:02:57 +02:00
aziis98 45d41d714b Fixed dockerfile 2022-07-31 00:31:34 +02:00
aziis98 cb76d0cdf2 Added docker file 2022-07-30 19:37:12 +02:00
aziis98 dc53ee66cb Added some comments 2022-06-14 01:34:27 +02:00
aziis98 41b08b86e0 Added support for Sass 2022-06-14 01:34:15 +02:00
aziis98 1d61efeda0 Aggiunte tante cose relative al profilo utente 2022-06-13 02:36:31 +02:00
aziis98 39ad130e81 Added logout and some logging 2022-06-12 20:41:12 +02:00
aziis98 6a692fdf5f Working auth system 2022-06-11 16:56:39 +02:00
aziis98 b0f4eb069e Merge branch 'main' of git.phc.dm.unipi.it:phc/website 2022-06-07 19:19:35 +02:00
aziis98 0d7d24f1b9 Alcune modifiche 2022-06-07 19:04:50 +02:00
BachoSeven e6042c4e99 views/link.html: Fix links and add credenziali.phc 2022-06-07 16:12:04 +02:00
aziis98 751298f4c6 Tidied go.mod 2022-06-05 15:09:48 +02:00
aziis98 6cf6e54bbb Fixed typo 2022-06-05 14:47:21 +02:00
aziis98 b2a9355d1d Dualcoding con @bachoseven per la pagina "/storia" più qualche miglioria 2022-06-05 14:45:15 +02:00
BachoSeven e3b71962e5 feat: add json db for timeline (WIP); update fontawesome. 2022-06-03 17:12:33 +02:00
BachoSeven 2e2f15874b content: Update links.html 2022-06-03 17:10:08 +02:00
aziis98 5d3f082d62 Refactor da go-chi a go-fiber 2022-06-03 13:40:58 +02:00
aziis98 80ef1f11c2 Merge branch 'main' of git.phc.dm.unipi.it:phc/website 2022-06-02 20:01:09 +02:00
aziis98 cdb5d2ef07 Scheletro per la pagina sulla storia del PHC 2022-06-02 20:00:38 +02:00
aziis98 9393fa73b9 Nested makefile and libraries from CDN 2022-06-02 18:45:10 +02:00
BachoSeven 4c87ade7a0 README.md: aggiornata la documentazione 2022-05-31 17:12:52 +02:00
aziis98 9aea9bc3a8 Updated README.md 2022-05-31 01:53:41 +02:00
aziis98 c8e9ee964f Fix 2022-05-31 01:46:40 +02:00
aziis98 e6c46d75dc Aggiunto come impostare l'ambiente di development per il js 2022-05-31 01:46:22 +02:00
aziis98 58db3e3901 Cartella con file per la frontend, un makefile per costruire tutto e lista utenti con fuzzy search 2022-05-31 01:39:07 +02:00
aziis98 91998e8924 Icona per i macchinisti nella lista 2022-05-29 16:54:46 +02:00
aziis98 1ccb71d3a1 Pagina utenti caricata da json 2022-05-28 19:09:50 +02:00
aziis98 a53ee26e78 Refactor template renderer 2022-05-10 15:50:09 +02:00
BachoSeven 5b0fb7fa8a Cleanup 2022-04-06 22:39:34 +02:00
BachoSeven f775634fd3 Merge branch 'main' of git.phc.dm.unipi.it:phc/website 2022-04-06 22:34:18 +02:00
aziis98 ce2ec3e5ad Merge branch 'main' of git.phc.dm.unipi.it:phc/website 2022-04-06 22:28:56 +02:00
BachoSeven 163a6f8a2c Merge branch 'main' of git.phc.dm.unipi.it:phc/website 2022-04-04 17:36:57 +02:00
BachoSeven 7aed41dd0c Update email (once we have mailing list will be replaced and forwarded) 2022-04-04 17:36:14 +02:00
BachoSeven 57f0a97c68 Update script link 2022-04-03 14:35:51 +02:00
aziis98 349fb9bcf1 Refactor al nuovo nome del repo anche qua 2022-02-25 18:25:54 +01:00
aziis98 57f5764608 Refactored to phc/website 2022-02-25 17:06:48 +01:00
BachoSeven 8bd5f83fc0 Whitespace 2022-01-18 16:56:43 +01:00
aziis98 280c3aaef4 Refactor: Isolata in un pacchetto autonomo la struct che gestisce il registro degli articoli 2021-12-25 23:43:06 +01:00
aziis98 447e87b691 Aggiunta pagina appunti con struttura di base 2021-12-24 00:34:17 +01:00
aziis98 99371a1688 Refactor: Le route statiche ora sono aggiunte tutte insieme a blocco 2021-12-23 23:49:47 +01:00
aziis98 695720e44c Merge branch 'main' of github.com:phc-dm/phc-server 2021-12-23 23:31:47 +01:00
aziis98 1a2e2341d3 Migliorato il sistema dei template, ora è presente la cartella "views/partials/"
In particolare è stato generalizzato leggermente il sistema di loading dei template (ok forse un po' più del necessario) però ora è possibile aggiungere facilmente più cartelle di parziali e creare gerarchie di template più complicate.
2021-12-23 23:28:46 +01:00
BachoSeven 783bf20e21 chore: use mailto: in the footer email, minor rephrasing. 2021-12-23 20:24:51 +01:00
BachoSeven 650278828c views/link.html: use cards for links 2021-12-23 20:15:47 +01:00
aziis98 b79eb41438 Update README.md 2021-12-23 19:21:24 +01:00
aziis98 847f588e87 Cambiata la convenzione per i titoli delle pagine 2021-12-23 19:09:30 +01:00
aziis98 bfb5a61e14 Niente più blog in quanto ora c'è la sezione News 2021-12-23 18:56:27 +01:00
aziis98 106531603c ArticleRenderer è ora più generico e miglioramenti allo stile del Markdown 2021-12-23 18:54:54 +01:00
aziis98 1a27b2d91e Stili per i contenuti renderizzati da markdown come immagini e blocchi di codice 2021-12-23 15:35:57 +01:00
aziis98 b9444b4687 Riaggiunte le icone 2021-12-23 02:34:10 +01:00
aziis98 a2582c884b Fix: Nome del modulo e alcune cose nel README 2021-12-23 02:27:06 +01:00
aziis98 e727b2f71f Feature: Pagina notize dinamica e renderizzazione del markdown 2021-12-23 02:19:35 +01:00
aziis98 54f5d798e8 Migliorata pagina news 2021-12-23 00:11:12 +01:00
aziis98 e7cf338a8c Stili per elementi compound e migliorie a pagine utenti e link
Un elemento "compound" sarebbe ad esempio il campo di ricerca della pagina utenti formato da un campo di testo e da un bottone.
2021-12-22 23:54:22 +01:00
aziis98 cf21b9a143 Migliorati bottoni per pagina di login 2021-12-22 23:32:04 +01:00
aziis98 c04a161ccc Mergiate cose 2021-12-21 19:37:12 +01:00
aziis98 8bbfc57ea8 Fix public/, campi di testo e bottoni 2021-12-21 19:13:51 +01:00
aziis98 bdff6aadd0 Refactor: Changed the name of the "assets/" folder to "public/" 2021-12-21 18:28:51 +01:00
aziis98 41c654daea Added news page 2021-12-21 18:28:29 +01:00
aziis98 9a868120b8 Another fix 2021-12-21 18:24:01 +01:00
aziis98 b0f4a58221 Fix: Large zoom overlap 2021-12-21 17:58:06 +01:00
aziis98 4c0a6ed1c0 Fix: Scala sbagliata 2021-12-21 17:14:18 +01:00
aziis98 79c7cd9642 Feature: Sfondo animato con pattern di circuito stampato 2021-12-21 15:42:41 +01:00
aziis98 97266879e0 Nuovo stile homepage 2021-12-19 12:12:33 +01:00
aziis98 b9ac8a61f7 Merge: Aggiunte le cose che mi ero dimenticato di pullare 2021-12-19 11:22:45 +01:00
aziis98 4344c397f6 Ah ma dovevo fare pull 2021-12-19 11:19:57 +01:00
aziis98 1f099d4d53 Added a page-specific CSS class on <body> 2021-12-19 11:17:21 +01:00
BachoSeven b442af689c phrasing 2021-12-19 00:40:08 +01:00
BachoSeven 9ddcc9b0f5 Add views/link.html 2021-12-19 00:33:24 +01:00
BachoSeven 7a7fef0580 chore: refactor navbar, remove trailing whitespace 2021-12-19 00:02:42 +01:00
BachoSeven 9890bd2c58 chore: update .gitignore 2021-12-18 23:08:59 +01:00
aziis98 8a5e1a54c0 Pulizia di vecchio codice e qualche miglioria 2021-12-18 22:49:49 +01:00
aziis98 8e141af5df Miglioria: Rimosso dal body l'uso di fit-content e flexbox
In quanto non è proprio il massimo usare flexbox per il body e fit-content per l'altezza non è troppo supportato oltre che da Chrome.
2021-12-18 22:23:08 +01:00
aziis98 5d6d4bd4d4 Fix: Lieve modifica al codice per la dark-mode 2021-12-18 22:20:52 +01:00
aziis98 514973852c Minima responsività e ombra al logo 2021-12-18 21:55:35 +01:00
aziis98 647d192ee7 Feature: Bottone per dark mode, logo svg ed altre modifiche alla nav 2021-12-18 17:58:07 +01:00
BachoSeven 219a966860 feat: first draft for dark mode support 2021-12-18 13:53:56 +01:00
aziis98 a5f724d167 Nuovo screenshot e nuova convenzione organizzativa 2021-12-17 17:53:14 +01:00
aziis98 d98dc0bc3e style.css: Cambiato il colore di sfondo e aggiunta una variabile 2021-12-17 17:50:09 +01:00
aziis98 6c36b803e9 Primo test stilistico vagamente serio 2021-12-17 17:40:30 +01:00
aziis98 26268329d9 Nuovo logo 2021-12-17 14:56:44 +01:00
aziis98 5043cc2593 Semplificato il nome del gestore dei template e rinominata la cartella degli screenshot 2021-12-14 23:44:40 +01:00
aziis98 ef1183b35e Vari fix e riorganizzazione del codice 2021-12-14 23:37:16 +01:00
aziis98 9db584aab6 Mergiate le due versioni 2021-12-14 23:21:56 +01:00
aziis98 f3c45b469c Refactor: Rimpiazzato il framework echo con go-chi 2021-12-14 22:55:37 +01:00
aziis98 5f570cca7b Aggiornati screenshot sito 2020-08-27 17:45:03 +02:00
aziis98 eea1291887 base.html, style.css: Aggiunti drop down e relativi stili 2020-08-27 17:44:43 +02:00
aziis98 646a078131 Nuovi screenshot 2020-08-22 04:00:46 +02:00
aziis98 c7a3c2a153 style.css: Migliorato stile, v2 2020-08-22 03:59:07 +02:00
aziis98 77c73713d4 utenti.html: Aggiunte sezioni, lista di test 2020-08-22 03:58:40 +02:00
aziis98 168ef97460 login.html: Fixato testo 2020-08-22 03:58:01 +02:00
aziis98 4f1e72a39d home.html: Aggiunte sezioni 2020-08-22 03:57:43 +02:00
aziis98 0e8bee0520 auth.go: Riorganizzazione modulo di autenticazione 2020-08-21 23:01:44 +02:00
aziis98 5b088cdb8b README.md: Fixato link documentazione 2020-08-21 21:43:58 +02:00
aziis98 c61287f412 main.go: Aggiunto route per il blog da content-poisson 2020-08-21 19:33:09 +02:00
aziis98 246b82c5b0 README.md: Setup & Usage 2020-08-21 19:07:59 +02:00
aziis98 33c15ffaad blog/: Aggiunto sottomodulo content-poisson e symlink 2020-08-21 19:04:51 +02:00
aziis98 d0c9fdc3ef Aggiunti screenshots di debug 2020-08-21 18:58:25 +02:00
aziis98 95b7939960 README.md: Fixato un link 2020-08-21 03:59:37 +02:00
aziis98 8885f40c43 {home, login, utenti}.html, style.css: Nuovo stile sito 2020-08-21 03:46:16 +02:00
aziis98 935278357b base.html: Aggiunto CDN per KaTeX 2020-08-21 03:45:28 +02:00
aziis98 1c3040ee79 template_renderer.go: Migliorato ciclo di sviluppo 2020-08-21 03:45:20 +02:00
aziis98 ae8b2b2d51 Migrata la documentazione del README in phc-dm/documentazione 2020-08-20 02:37:11 +02:00
aziis98 764b569e31 Rimossa la cartella del blog che è gestita da content-poisson 2020-08-20 01:57:36 +02:00
aziis98 b7d65edbc9 Icone da Icons8, favicon temporanea, cambiato il framework (perché sì), i template ora sono cachate in produzione 2020-08-19 18:51:47 +02:00
aziis98 3a81824019 Basic site 2020-06-25 21:20:35 +02:00
aziis98 6f0b1467e8 Aggiunta struttura di base di un blog statico fatto con Hugo 2020-06-25 19:29:08 +02:00
aziis98 b19400f3ee Aggiunte nuove dipendenze e codice di esempio 2020-06-23 19:55:20 +02:00
aziis98 1510163d7b Fix e aggiunte immagini trasformate 2020-06-16 23:54:03 +02:00
aziis98 aa21c64880 Struttura di base, templating e interfaccia per il servizio di autenticazione 2020-03-08 22:02:03 +01:00
aziis98 bffe3056f7 Iniziato il nuovo sito e aggiunto il serving di file statici 2020-03-07 19:19:28 +01:00
aziis98 744d7e0860 minimal go web server 2020-03-04 01:24:55 +01:00
aziis98 f7cba43f7b Update README.md 2020-02-29 22:54:23 +01:00
aziis98 55711c1f0e Update README.md 2020-02-29 16:10:25 +01:00
aziis98 a7c5bee55b Initial commit 2020-02-29 16:04:48 +01:00
170 changed files with 5464 additions and 22068 deletions
+3
View File
@@ -0,0 +1,3 @@
Dockerfile
node_modules
.git
-50
View File
@@ -1,50 +0,0 @@
# This file defines a Drone pipeline that builds a static website with "npm run build". This
# pipeline must be marked as "Trusted" in the Drone project settings.
#
# We mount the target directory of the project at "/var/www/{project}" to the container
# "dist/" directory and the run the build. A caveat is that the container builds files
# with "root" permissions, so we need to fix those after each build with a second pipeline.
kind: pipeline
name: default
steps:
- name: deploy
image: node:latest
volumes:
- name: host-website-dist
path: /mnt/website
commands:
- npm install
- npm run build
- cp -rT ./dist /mnt/website
volumes:
- name: host-website-dist
host: # this volume is mounted on the host machine
path: /var/www/website
trigger:
branch:
- main
event:
- push
---
kind: pipeline
type: exec # this job is executed on the host machine
name: caddy-permissions
depends_on:
- default
steps:
- name: chown
commands:
- chown -R caddy:caddy /var/www/website
trigger:
branch:
- main
event:
- push
+15
View File
@@ -0,0 +1,15 @@
# Server config
MODE=development
HOST=localhost:8000
# Separate services
GIT_URL=https://git.phc.dm.unipi.it
CHAT_URL=https://chat.phc.dm.unipi.it
# Other
EMAIL=macchinisti@lists.dm.unipi.it
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
# AuthService
AUTH_SERVICE_HOST=:memory:
+14 -18
View File
@@ -1,23 +1,19 @@
# build output
dist/
# generated types
.astro/
# Environment files
.env
# dependencies
node_modules/
# Miscellaneous
tags
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# local data
# Local files
*.local*
# environment variables
.env
.env.production
# NodeJS
dist/
node_modules/
# macOS-specific files
.DS_Store
# Don't version generated files
public/css/
public/js/
# Executables
phc-website-server
-27
View File
@@ -1,27 +0,0 @@
/** @type {import("prettier").Config} */
export default {
printWidth: 120,
singleQuote: true,
quoteProps: 'consistent',
tabWidth: 4,
useTabs: false,
semi: false,
arrowParens: 'avoid',
plugins: ['prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
{
files: '*.{yml,yaml,json}',
excludeFiles: 'package-lock.json',
options: {
tabWidth: 2,
},
},
],
}
-11
View File
@@ -1,11 +0,0 @@
{
"npm.packageManager": "bun",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+25
View File
@@ -0,0 +1,25 @@
FROM node:18 AS frontend-builder
WORKDIR /frontend
COPY ./frontend/package.json ./package.json
COPY ./frontend/package-lock.json ./package-lock.json
RUN npm install
COPY ./frontend .
RUN make
FROM golang:1.18-alpine AS backend-builder
WORKDIR /backend
COPY go.mod go.sum ./
RUN go mod download -x
COPY . .
RUN go build -buildvcs=false -o website-server -v .
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=frontend-builder /frontend/dist ./frontend/dist
COPY --from=backend-builder /backend/website-server ./website-server
COPY ./views ./views
COPY ./news ./news
COPY ./public ./public
COPY ./.env ./.env
EXPOSE 8000
CMD ["./website-server"]
+37
View File
@@ -0,0 +1,37 @@
GO_SOURCES = $(shell find . -name '*.go')
GO_EXECUTABLE = phc-website-server
.PHONY: all
all: frontend go
#
# Build Frontend
#
.PHONY: setup
setup:
mkdir -p public/js
mkdir -p public/css
.PHONY: frontend
frontend: setup
$(MAKE) -C frontend/
cp frontend/dist/*.min.js public/js/
cp frontend/dist/*.css public/css/
#
# Build Server
#
.PHONY: go
go: $(GO_EXECUTABLE)
@echo "Compiled Server"
$(GO_EXECUTABLE): $(GO_SOURCES)
go build -o $(GO_EXECUTABLE) .
.PHONY: debug
debug:
@echo "GO_SOURCES = $(GO_SOURCES)"
@echo "GO_EXECUTABLE = $(GO_EXECUTABLE)"
+81 -17
View File
@@ -1,35 +1,99 @@
# PHC Website
# Nuovo Sito PHC
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
Repo del server del nuovo sito per il PHC.
## Installazione
## Dipendenze
- **Golang**
- `github.com/alecthomas/chroma`
- `github.com/alecthomas/repr`
- `github.com/go-chi/chi/v5`
- `github.com/joho/godotenv`
- `github.com/litao91/goldmark-mathjax`
- `github.com/yuin/goldmark`
- `github.com/yuin/goldmark-highlighting`
- `gopkg.in/yaml.v3`
- **NodeJS**
- AlpineJS
- FuseJS
## Setup
Per ottenere il progetto basta fare
```bash
bun install
$ git clone https://git.phc.dm.unipi.it/phc/website
$ cd frontend/
frontend/ $ npm install
frontend/ $ cd ..
$ make frontend
```
## Sviluppo
## Development
```bash
bun dev
### Setup
Copiare il file `.env.dev` in `.env` per dire al server di lavorare in modalità di development e su quale indirizzo servire il sito, poi avviare il server.
```bash shell
$ cp .env.dev .env
$ go run .
```
## Build
### Server
```bash
bun run build
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
```bash shell
$ find . -type f -name '*.go' | entr -r go run .
# O anche con fd...
$ fd -e go | entr -r go run .
```
## Deploy
### Frontend
Per ora c'è un file `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD. Al momento il sito è solo statico e non ha ancora una backend.
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
## Come Contribuire
```bash shell
$ make frontend
# O anche con un watcher...
$ fd -e js | entr make frontend
```
**Nota per Sviluppatori**: SE il branch `dev` non esiste o è indietro rispetto a `main`, bisogna portarlo avanti a `main` e poi continuare con le modifiche dal lì.
### Environment Variables
Sentitevi liberi di aprire una PR per qualsiasi modifica o aggiunta al sito web. Il branch `main` è protetto e corrisponde alla versione di produzione del sito web, le modifiche devono prima essere accettate su `dev` e poi mergeate su `main` da un amministratore.
- `MODE`
### Cose da fare
Può essere `production` (default) o `development`.
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
- `HOST`
Indirizzo (locale) sul quale servire il sito, di default è `localhost:8000`.
- `MAIL`
Indirizzo di posta elettronica per contattare gli ammistratori del sito,
che compare nel footer di ogni pagina.
- `<SERVIZIO>_URL`
Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale).
Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`.
## Altri Servizi
Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi.
Il servizio di autenticazione di default girerà su `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+113
View File
@@ -0,0 +1,113 @@
package articles
import (
"bytes"
"os"
"strings"
"time"
"git.phc.dm.unipi.it/phc/website/config"
"gopkg.in/yaml.v3"
)
type Article struct {
Id string
Title string
Description string
Tags []string
PublishDate time.Time
ArticlePath string
markdownSource string
renderedHTML string
}
func (article *Article) HasTag(tag string) bool {
for _, t := range article.Tags {
if t == tag {
return true
}
}
return false
}
func NewArticle(articlePath string) (*Article, error) {
article := &Article{
ArticlePath: articlePath,
}
err := article.load()
if err != nil {
return nil, err
}
return article, nil
}
func trimAll(vs []string) []string {
r := []string{}
for _, v := range vs {
r = append(r, strings.TrimSpace(v))
}
return r
}
func (article *Article) load() error {
fileBytes, err := os.ReadFile(article.ArticlePath)
if err != nil {
return err
}
source := string(fileBytes)
// TODO: Ehm bugia pare che esista "https://github.com/yuin/goldmark-meta" però non penso valga la pena aggiungerlo
parts := strings.SplitN(source, "---", 3)[1:]
frontMatterSource := parts[0]
markdownSource := parts[1]
var frontMatter struct {
Id string `yaml:"id"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Tags string `yaml:"tags"`
PublishDate string `yaml:"publish_date"`
}
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
return err
}
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
if err != nil {
return err
}
article.Id = frontMatter.Id
article.Title = frontMatter.Title
article.Description = frontMatter.Description
article.Tags = trimAll(strings.Split(frontMatter.Tags, ","))
article.PublishDate = publishDate
article.markdownSource = markdownSource
article.renderedHTML = ""
return nil
}
func (article *Article) Render() (string, error) {
if config.Mode == "development" {
article.load()
}
if article.renderedHTML == "" {
var buf bytes.Buffer
if err := articleMarkdown.Convert([]byte(article.markdownSource), &buf); err != nil {
return "", err
}
article.renderedHTML = buf.String()
}
return article.renderedHTML, nil
}
+77
View File
@@ -0,0 +1,77 @@
package articles
import (
"fmt"
"os"
"path"
"sort"
)
type Registry struct {
RootPath string
ArticleCache map[string]*Article
}
func NewRegistry(rootPath string) *Registry {
return &Registry{
rootPath,
map[string]*Article{},
}
}
func (registry *Registry) loadArticles() error {
entries, err := os.ReadDir(registry.RootPath)
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() {
article, err := NewArticle(path.Join(registry.RootPath, entry.Name()))
if err != nil {
return err
}
registry.ArticleCache[article.Id] = article
}
}
return nil
}
func (registry *Registry) GetArticle(id string) (*Article, error) {
article, present := registry.ArticleCache[id]
if !present {
err := registry.loadArticles()
if err != nil {
return nil, err
}
article, present := registry.ArticleCache[id]
if !present {
return nil, fmt.Errorf(`no article with id "%s"`, id)
}
return article, nil
}
return article, nil
}
func (registry *Registry) GetArticles() ([]*Article, error) {
err := registry.loadArticles()
if err != nil {
return nil, err
}
articles := []*Article{}
for _, article := range registry.ArticleCache {
articles = append(articles, article)
}
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishDate.After(articles[j].PublishDate)
})
return articles, nil
}
+60
View File
@@ -0,0 +1,60 @@
package articles
import (
"fmt"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
chromahtml "github.com/alecthomas/chroma/formatters/html"
mathjax "github.com/litao91/goldmark-mathjax"
highlighting "github.com/yuin/goldmark-highlighting"
)
var articleMarkdown goldmark.Markdown
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
lang, ok := c.Language()
if entering {
if ok {
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
return
}
w.WriteString("<pre><code>")
} else {
if ok {
w.WriteString("</pre></code>")
return
}
w.WriteString("</pre></code>")
}
}
func init() {
articleMarkdown = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Typographer,
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
mathjax.NewMathJax(),
highlighting.NewHighlighting(
highlighting.WithStyle("github"),
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
highlighting.WithFormatOptions(
chromahtml.PreventSurroundingPre(true),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
html.WithXHTML(),
),
)
}
-32
View File
@@ -1,32 +0,0 @@
import { defineConfig } from 'astro/config'
import preact from '@astrojs/preact'
import mdx from '@astrojs/mdx'
import remarkMath from 'remark-math'
import yaml from '@rollup/plugin-yaml'
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [yaml()],
},
server: {
port: 3000,
},
markdown: {
remarkPlugins: [remarkMath],
shikiConfig: {
theme: 'github-light',
},
},
integrations: [
preact({
compat: true,
}),
mdx({
remarkPlugins: [remarkMath],
}),
],
output: 'static',
})
+52
View File
@@ -0,0 +1,52 @@
package auth
import (
"fmt"
"git.phc.dm.unipi.it/phc/website/model"
)
// ErrInvalidSession is thrown when an AuthenticatorService is given a missing
// or invalid session token
var ErrInvalidSession = fmt.Errorf(`invalid session token`)
// Service is an authentication service abstraction. When a user is logged in a
// new session token is returned, this can be used to read and modify user
// properties without having to re-send the user password. (TODO: implement
// token renewal)
type Service interface {
// GetUser retrieves the user info given the username
GetUser(username string) (*model.User, error)
// GetUsers retrieves the full user list from the authentication service
GetUsers() ([]*model.User, error)
// GetSession retrieves the user session associated to a session token
GetSession(token string) (*model.Session, error)
// Login tries to log in a user given username and password and if successful returns a new user session
Login(username, password string) (*model.Session, error)
}
// UserForSession returns the user linked to the given session token, this is just a shortcut for calling [AuthenticatorService.GetSession] and then [AuthenticatorService.GetUser]
func UserForSession(as Service, token string) (*model.User, error) {
session, err := as.GetSession(token)
if err != nil {
return nil, err
}
user, err := as.GetUser(session.Username)
if err != nil {
return nil, err
}
return user, nil
}
// NewDefaultService create an AuthenticatorService given an "host" string,
// If host is ":memory:" then this uses the [auth.Memory] implementation,
// otherwise for now this defaults to [auth.LDAPAuthService]
func NewDefaultService(host string) Service {
if host == ":memory:" {
return exampleMemoryUsers
}
return &LDAPAuthService{host}
}
+136
View File
@@ -0,0 +1,136 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"path"
"git.phc.dm.unipi.it/phc/website/model"
)
// ldapUser represents an LDAP User, most fields are inherited from [auth.User]
type ldapUser struct {
Uid string `json:"username"`
NumericId int `json:"id"`
Name string `json:"name"`
Surname string `json:"surname"`
Email string `json:"email"`
Role string `json:"role"`
Gecos string `json:"gecos"`
}
// AsUser converts an [ldapUser] to an instance of [auth.User]
func (u ldapUser) AsUser() *model.User {
return &model.User{
Username: u.Uid,
Name: u.Name,
Surname: u.Surname,
Email: u.Email,
FullName: u.Gecos,
}
}
// LDAPAuthService just holds the remote host of the HTTP LDAP service to make requests to
type LDAPAuthService struct {
Host string
}
func NewLDAPAuthService(host string) Service {
return &LDAPAuthService{host}
}
// doGetRequest is a utility to make HTTP GET requests
func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error {
req, err := http.NewRequest(
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")),
)
if err != nil {
log.Printf(`GET %q resulted in %v`, url, err)
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf(`GET %q resulted in %v`, url, err)
return err
}
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
log.Printf(`GET %q resulted in %v`, url, err)
return err
}
return nil
}
// doPostRequest is a utility to make HTTP POST requests
func (a *LDAPAuthService) doPostRequest(url string, request interface{}, response interface{}) error {
jsonStr, err := json.Marshal(request)
if err != nil {
return err
}
req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), bytes.NewBuffer(jsonStr))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return json.NewDecoder(res.Body).Decode(response)
}
func (a *LDAPAuthService) GetUser(username string) (*model.User, error) {
var user ldapUser
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
return nil, err
}
return user.AsUser(), nil
}
func (a *LDAPAuthService) GetUsers() ([]*model.User, error) {
ldapUsers := []*ldapUser{}
if err := a.doGetRequest("/users", &ldapUsers); err != nil {
return nil, err
}
users := make([]*model.User, len(ldapUsers))
for i, u := range ldapUsers {
users[i] = u.AsUser()
}
return users, nil
}
func (a *LDAPAuthService) GetSession(token string) (*model.Session, error) {
var response model.Session
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
return nil, err
}
return &response, nil
}
func (a *LDAPAuthService) Login(username, password string) (*model.Session, error) {
reqBody := map[string]interface{}{
"username": username,
"password": password,
}
var response model.Session
if err := a.doPostRequest("/login", reqBody, &response); err != nil {
return nil, err
}
return &response, nil
}
+109
View File
@@ -0,0 +1,109 @@
package auth
import (
"fmt"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
var exampleMemoryUsers = &Memory{
Users: map[string]*memoryUser{
"aziis98": {
User: model.User{
Username: "aziis98",
Name: "Antonio",
Surname: "De Lucreziis",
Email: "aziis98@example.org",
}.WithDefaultFullName(),
Password: "123",
},
"bachoseven": {
User: model.User{
Username: "bachoseven",
Name: "Francesco",
Surname: "Minnocci",
Email: "bachoseven@example.org",
}.WithDefaultFullName(),
Password: "234",
},
},
Sessions: map[string]*memorySession{},
}
type memoryUser struct {
model.User
Password string `json:"-"`
}
func (u memoryUser) AsUser() *model.User {
return &model.User{
Username: u.Username,
Name: u.Name,
Surname: u.Surname,
FullName: u.FullName,
Email: u.Email,
}
}
type memorySession struct {
Username string
Token string
}
func (s memorySession) AsSession() *model.Session {
return &model.Session{
Username: s.Username,
Token: s.Token,
}
}
type Memory struct {
Users map[string]*memoryUser
Sessions map[string]*memorySession
}
func (m *Memory) GetUser(username string) (*model.User, error) {
user, ok := m.Users[username]
if !ok {
return nil, fmt.Errorf(`no user with that username`)
}
return user.AsUser(), nil
}
func (m *Memory) GetUsers() ([]*model.User, error) {
users := make([]*model.User, len(m.Users))
i := 0
for _, u := range m.Users {
users[i] = u.AsUser()
i++
}
return users, nil
}
func (m *Memory) GetSession(token string) (*model.Session, error) {
session, ok := m.Sessions[token]
if !ok {
return nil, ErrInvalidSession
}
return session.AsSession(), nil
}
func (m *Memory) Login(username string, password string) (*model.Session, error) {
user, ok := m.Users[username]
if !ok {
return nil, fmt.Errorf(`no user with that username`)
}
if user.Password != password {
return nil, fmt.Errorf(`invalid credentials`)
}
session := &memorySession{username, util.GenerateRandomString(15)}
m.Sessions[session.Token] = session
return session.AsSession(), nil
}
BIN
View File
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
package config
import (
"log"
"os"
"strings"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/joho/godotenv"
)
var Mode string
var Host string
var GitUrl string
var ChatUrl string
var Email string
var UserPagesBaseUrl string
var AuthServiceHost string
func loadEnv(target *string, name, defaultValue string) {
value := os.Getenv(name)
if len(strings.TrimSpace(value)) == 0 {
*target = defaultValue
} else {
*target = value
}
log.Printf("%s = %v", name, *target)
}
func Load() {
if err := godotenv.Load(); err != nil {
panic(err)
}
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// Production
loadEnv(&Mode, "MODE", "production")
loadEnv(&Host, "HOST", ":8080")
// Services
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
loadEnv(&ChatUrl, "CHAT_URL", "https://chat.example.org")
loadEnv(&Email, "EMAIL", "mail@example.org")
// Poisson
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
// AuthService
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
}
func Object() util.H {
return util.H{
"Mode": Mode,
"Host": Host,
"GitUrl": GitUrl,
"ChatUrl": ChatUrl,
"Email": Email,
"UserPagesBaseUrl": UserPagesBaseUrl,
"AuthServiceHost": AuthServiceHost,
}
}
+23
View File
@@ -0,0 +1,23 @@
STYLES_SOURCES = $(wildcard src/styles/*.scss)
SCRIPTS_SOURCES = $(wildcard src/*.js)
.PHONY: all
all: scripts styles
.PHONY: styles
styles: $(STYLES_SOURCES)
npm run build:styles
.PHONY: scripts
scripts: $(SCRIPTS_SOURCES)
npm run build:scripts
.PHONY: clean
clean:
rm -rf dist/
.PHONY: debug
debug:
@echo "STYLES_SOURCES = $(STYLES_SOURCES)"
@echo "SCRIPTS_SOURCES = $(SCRIPTS_SOURCES)"
+1338
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "Package to generate frontend files for phc/website",
"scripts": {
"build:scripts": "rollup -c",
"build:styles": "sass -s compressed --no-source-map src/styles/main.scss dist/main.css"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.3.0",
"rollup": "^2.75.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.52.3"
},
"dependencies": {
"alpinejs": "^3.10.2",
"fuse.js": "^6.6.2"
}
}
+31
View File
@@ -0,0 +1,31 @@
import { defineConfig } from 'rollup'
import { terser } from 'rollup-plugin-terser'
export default defineConfig([
{
input: 'src/utenti.js',
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
output: {
file: 'dist/utenti.min.js',
format: 'iife',
globals: {
// map library names to global constants
alpinejs: 'Alpine',
'fuse.js': 'Fuse',
},
},
plugins: [terser()],
},
{
input: 'src/profilo.js',
external: ['alpinejs'],
output: {
file: 'dist/profilo.min.js',
format: 'iife',
globals: {
alpinejs: 'Alpine',
},
},
plugins: [terser()],
},
])
+16
View File
@@ -0,0 +1,16 @@
import Alpine from 'alpinejs'
Alpine.data('profilo', () => ({
init() {
console.log('Profilo!')
},
}))
Alpine.data('passwordForm', () => ({
password: '',
passwordAgain: '',
passwordSame: true,
onUpdate() {
this.passwordSame = this.password === this.passwordAgain
},
}))
+69
View File
@@ -0,0 +1,69 @@
/* TODO: Don't use CDN and serve as static files */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
:root {
--fg-300: #777;
--fg-400: #666;
--fg-500: #333;
--fg-600: #222;
--bg-000: #f0f0f0;
--bg-100: #f0f0f0;
--bg-500: #eaeaea;
--bg-550: #ecedee;
--bg-600: #e4e5e7;
--bg-700: #d5d5d5;
--bg-750: #c8c8c8;
--bg-800: #c0c0c0;
--bg-850: #b8b8b8;
--accent-300: #5cc969;
--accent-400: #4eaa59;
--accent-500: #278542;
--accent-600: #2e974c;
--accent-700: #154d24;
--accent-800: #002d0d;
--ft-ss: 'Inter', sans-serif;
--ft-ss-wt-light: 300;
--ft-ss-wt-normal: 400;
--ft-ss-wt-medium: 500;
--ft-ss-wt-bold: 700;
--shadow-500: 0 0 16px 0 #00000018;
// Components
--text-input-bg: var(--bg-000);
--text-input-readonly-bg: var(--bg-600);
--text-input-readonly-fg: var(--fg-300);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
html {
height: 100%;
width: 100%;
}
body {
background: var(--bg-500);
color: var(--fg-500);
font-family: var(--ft-ss);
font-size: 17px;
font-weight: var(--ft-ss-wt-normal);
width: 100%;
min-height: 100%;
position: relative;
}
@import './typography.scss';
+78
View File
@@ -0,0 +1,78 @@
h1,
h2,
h3,
h4 {
margin: 0;
margin-top: 1rem;
margin-bottom: 0.5rem;
font-weight: var(--font-weight-light);
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.35rem;
}
h4 {
font-size: 1.2rem;
font-weight: var(--font-weight-bold);
}
p,
ul,
ol,
li {
margin: 0;
width: 70ch;
max-width: 100%;
line-height: 1.8;
}
p + p {
padding-top: 0.5rem;
}
ul,
ol {
padding: 0 0 0 1.5rem;
}
hr {
width: 50ch;
height: 1px;
margin: 0;
border: none;
background-color: var(--bg-darker-2);
}
pre {
margin: 0.5rem 0;
background: var(--bg-lighter);
border: 1px solid #cbcbcb;
border-radius: 2px;
box-shadow: 0 2px 4px 0 #00000033;
font-size: 90%;
display: flex;
overflow-x: auto;
}
pre > code {
display: block;
margin: 0.25rem;
}
p.center {
text-align: center;
}
+81
View File
@@ -0,0 +1,81 @@
import Alpine from 'alpinejs'
import Fuse from 'fuse.js'
const SHOW_MORE_INCREMENT = 15
const FUSE_OPTIONS = {
includeScore: true,
keys: [
'nome',
'cognome',
'tags',
{ name: 'nomeCompleto', getFn: user => `${user.nome} ${user.cognome}` },
],
}
const SORT_MODES = {
chronological: () => 0,
name: (a, b) => (a.nome < b.nome ? -1 : 1),
surname: (a, b) => (a.cognome < b.cognome ? -1 : 1),
}
function getSortedUserList(original, mode) {
return [...original].sort(SORT_MODES[mode])
}
Alpine.data('utenti', () => ({
searchField: '', // two-way binding for the search input field
sortMode: 'chronological', // two-way binding for the sorting mode
fetchedUsers: [], // hold complete user list
sortedUserBuffer: [], // Yet another buffer of the user list for the sort mode
fuse: new Fuse([], FUSE_OPTIONS), // current fuse instance, used to filter the list above
searchResultsBuffer: [], // stores the full current search
searchResults: [], // list to renderer on screen with a subset of the whole search results buffer
async init() {
// Get user list from server
const response = await fetch('/api/utenti')
this.fetchedUsers = await response.json()
// This will call the function "showMore()" when the user is near the end of the list
new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Near the bottom of the page')
this.showMore()
}
})
}).observe(this.$refs.spinner)
// Initialize with an empty query
this.updateSortMode()
this.updateSearch()
},
showMore() {
// setTimeout(() => {
// Updates the final "searchResults" list with more items from the previous buffer
const newCount = this.searchResults.length + SHOW_MORE_INCREMENT
this.searchResults = this.searchResultsBuffer.slice(0, newCount)
// }, 250) // For fun
},
setResults(list) {
this.searchResultsBuffer = list.filter(
entry => entry.score === undefined || entry.score <= 0.25
)
this.searchResults = this.searchResultsBuffer.slice(0, SHOW_MORE_INCREMENT)
},
updateSortMode() {
this.sortedUserBuffer = getSortedUserList(this.fetchedUsers, this.sortMode)
this.fuse.setCollection(this.sortedUserBuffer)
this.updateSearch()
},
updateSearch() {
console.time('search')
if (this.searchField.trim().length === 0) {
// Reset the result list
this.setResults(this.sortedUserBuffer.map(user => ({ item: user })))
} else {
// Update the result list with the new results
this.setResults(this.fuse.search(this.searchField))
}
console.timeEnd('search')
},
}))
+25
View File
@@ -0,0 +1,25 @@
module git.phc.dm.unipi.it/phc/website
go 1.18
require (
github.com/alecthomas/chroma v0.9.4
github.com/gofiber/fiber/v2 v2.34.0
github.com/gofiber/redirect/v2 v2.1.23
github.com/joho/godotenv v1.4.0
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f
github.com/yuin/goldmark v1.4.4
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/klauspost/compress v1.15.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.37.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
)
+76
View File
@@ -0,0 +1,76 @@
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
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.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+208
View File
@@ -0,0 +1,208 @@
package main
import (
"fmt"
"html/template"
"log"
"time"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/config"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/templates"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/redirect/v2"
)
func UserMiddleware(as auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Cookies("session-token")
user, _ := auth.UserForSession(as, token)
c.Locals("user", user)
return c.Next()
}
}
func main() {
config.Load()
app := fiber.New()
app.Use(logger.New())
app.Use(recover.New())
// Remove trailing slash from URLs
app.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/*/": "/$1",
},
}))
// Serve content statically from "./public", mounted on the "/public/" route
app.Static("/public/", "./public")
authService := auth.NewDefaultService(config.AuthServiceHost)
app.Use(UserMiddleware(authService))
// Templates & Renderer
renderer := templates.NewRenderer(
"./views/",
"./views/base.html",
"./views/partials/*.html",
)
newsArticlesRegistry := articles.NewRegistry("./news")
// Routes
actuallyStaticRoutes := map[string]string{
"/": "home.html",
"/link": "link.html",
"/login": "login.html",
"/utenti": "utenti.html",
}
for route, view := range actuallyStaticRoutes {
localView := view
app.Get(route, func(c *fiber.Ctx) error {
c.Type("html")
return renderer.Render(c, localView, util.H{
"User": c.Locals("user"),
})
})
}
app.Get("/api/utenti", func(c *fiber.Ctx) error {
utenti, err := authService.GetUsers()
if err != nil {
return err
}
return c.JSON(utenti)
})
app.Get("/api/profilo", func(c *fiber.Ctx) error {
user := c.Locals("user")
if user == nil {
return fmt.Errorf(`user not logged in`)
}
return c.JSON(user)
})
app.Get("/storia", func(c *fiber.Ctx) error {
storia, err := GetStoria()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "storia.html", util.H{
"User": c.Locals("user"),
"Storia": storia,
})
})
app.Get("/appunti", func(c *fiber.Ctx) error {
searchQuery := c.Query("q", "")
c.Type("html")
return renderer.Render(c, "appunti.html", util.H{
"User": c.Locals("user"),
"Query": searchQuery,
})
})
app.Get("/news", func(c *fiber.Ctx) error {
articles, err := newsArticlesRegistry.GetArticles()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news.html", util.H{
"User": c.Locals("user"),
"Articles": articles,
})
})
app.Post("/login", func(c *fiber.Ctx) error {
var loginForm struct {
Provider string `form:"provider"`
Username string `form:"username"`
Password string `form:"password"`
}
if err := c.BodyParser(&loginForm); err != nil {
return err
}
session, err := authService.Login(loginForm.Username, loginForm.Password)
if err != nil {
return err
}
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: session.Token,
Expires: inThreeDays,
})
return c.Redirect("/profilo")
})
app.Get("/profilo", func(c *fiber.Ctx) error {
user, ok := c.Locals("user").(*model.User)
if !ok || user == nil {
return fmt.Errorf(`user not logged in`)
}
c.Type("html")
return renderer.Render(c, "profilo.html", util.H{
"User": c.Locals("user"),
})
})
app.Get("/logout", func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: "",
Expires: time.Now(),
})
return c.Redirect("/")
})
app.Get("/news/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
article, err := newsArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news-base.html", util.H{
"User": c.Locals("user"),
"Article": article,
"ContentHTML": template.HTML(html),
})
})
log.Printf("Starting server on host %q", config.Host)
err := app.Listen(config.Host)
if err != nil {
log.Fatal(err)
}
}
+30
View File
@@ -0,0 +1,30 @@
package model
// User represents a user returned from AuthenticatorService
type User struct {
Username string `json:"username"`
Name string `json:"name"`
Surname string `json:"surname"`
// FullName is a separate field from Name and Surname because for example
// ldap stores them all as separate fields.
FullName string `json:"fullName"`
Email string `json:"email"`
}
// WithDefaultFullName is a utility that returns a copy of the given user with the full name set to the concatenation of the name and surname of the user.
func (u User) WithDefaultFullName() User {
return User{
Username: u.Username,
Name: u.Name,
Surname: u.Surname,
Email: u.Email,
FullName: u.Username + " " + u.Surname,
}
}
// Session represents a session returned from AuthenticatorService
type Session struct {
Username string `json:"username"`
Token string `json:"token"`
}
+91
View File
@@ -0,0 +1,91 @@
---
id: 2021-12-22-notizia-1
title: "Notizia 1"
tags: important, prova, test, foo, bar
publish_date: 2021/12/22 22:00
description: |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
---
## Heading 2
### Heading 3
#### Heading 4
Lorem ipsum dolor, sit amet consectetur "adipisicing" elit. Repudiandae -- optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora $1 + 1$ ea, cupiditate explicabo vel! Porro?
$$
\int_0^1 x^2 \mathrm d x
$$
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
![testing](https://picsum.photos/400/300)
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 5
- Item 5
- foo
```
type Article struct {
Id string
Title string
Description string
Tags []string
PublishDate time.Time
MarkdownSource string
renderedHTML string
}
```
- bar with some `code`
```go
type Article struct {
Id string
Title string
Description string
Tags []string
PublishDate time.Time
MarkdownSource string
renderedHTML string
}
```
#### Tables
<https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#tables>
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell.
The outer pipes (|) are optional, and you don't need to make the
raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
| Expression | Derivative | Integral |
| :---: | :---: | :---: |
| $x^a$ | $a x^{a-1}$ | $\displaystyle \frac{1}{a+1} x^{a+1} + c$ se $a \neq -1$ |
| $\sin x$ | $\cos x$ | $\displaystyle -\cos x + c$ |
| $e^x$ | $e^x$ | $\displaystyle e^x + c$ |
+37
View File
@@ -0,0 +1,37 @@
---
id: 2021-12-23-notizia-2
title: "Notizia 2"
tags: prova, test, foo, bar
publish_date: 2021/12/23 22:00
description: |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
---
## Heading 2
### Heading 3
#### Heading 4
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
![testing](https://picsum.photos/200/300)
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 5
- Item 5
- foo
- bar
```Makefile
foo
foo
```
+37
View File
@@ -0,0 +1,37 @@
---
id: 2021-12-24-notizia-3
title: "Notizia 3"
tags: prova, test, foo, bar
publish_date: 2021/12/24 18:00
description: |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
---
## Heading 2
### Heading 3
#### Heading 4
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
![testing](https://picsum.photos/200/300)
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 5
- Item 5
- foo
- bar
```Makefile
foo
foo
```
-11125
View File
File diff suppressed because it is too large Load Diff
-51
View File
@@ -1,51 +0,0 @@
{
"name": "website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "run-s astro:sync astro:dev",
"build": "run-s astro:build",
"astro:sync": "astro sync",
"astro:dev": "astro dev",
"astro:build": "astro check && astro build"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "9.0.0",
"@astrojs/preact": "4.0.0",
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
"@fontsource/iosevka": "^5.0.11",
"@fontsource/mononoki": "^5.0.11",
"@fontsource/open-sans": "^5.0.24",
"@fontsource/source-code-pro": "^5.0.16",
"@fontsource/source-sans-pro": "^5.0.8",
"@fontsource/space-mono": "^5.0.20",
"@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.7",
"@preact/signals": "^1.3.0",
"@types/jsdom": "^21.1.7",
"astro": "5.1.0",
"fuse.js": "^7.0.0",
"katex": "^0.16.9",
"lucide-static": "^0.468.0",
"marked": "^15.0.6",
"preact": "^10.19.6",
"typescript": "^5.3.3"
},
"devDependencies": {
"@astrojs/mdx": "4.0.2",
"@rollup/plugin-yaml": "^4.1.2",
"@types/katex": "^0.16.7",
"jsdom": "^24.1.1",
"linkedom": "^0.18.4",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.0",
"prettier-plugin-astro": "^0.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-math": "^6.0.0",
"remark-toc": "^9.0.0",
"sass": "^1.71.1",
"tsx": "^4.7.1"
}
}
-95
View File
@@ -1,95 +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.6 KiB

+342
View File
@@ -0,0 +1,342 @@
function pointInInterval(a, b, x) {
return a <= x && x <= b;
}
function intervalIntersectsInterval(a, b, c, d) {
if (c <= a) {
return a <= d;
}
return pointInInterval(a, b, c);
}
function rectIntersectRect(x1, y1, x2, y2, x3, y3, x4, y4) {
return intervalIntersectsInterval(x1, x2, x3, x4) && intervalIntersectsInterval(y1, y2, y3, y4);
}
function intersects(x1, y1, x2, y2, x3, y3, x4, y4) {
const a_minx = Math.min(x1, x2);
const a_maxx = Math.max(x1, x2);
const a_miny = Math.min(y1, y2);
const a_maxy = Math.max(y1, y2);
const b_minx = Math.min(x3, x4);
const b_maxx = Math.max(x3, x4);
const b_miny = Math.min(y3, y4);
const b_maxy = Math.max(y3, y4);
if (!rectIntersectRect(a_minx, a_miny, a_maxx, a_maxy, b_minx, b_miny, b_maxx, b_maxy))
return false;
const a_dx = x2 - x1;
const a_dy = y2 - y1;
const b_dx = x4 - x3;
const b_dy = y4 - y3;
const det = -b_dx * a_dy + a_dx * b_dy;
if (Math.abs(det) === 0 && -a_dy * (x3 - x1) + a_dx * (y3 - y1) === 0) {
return y1 <= y3 <= y2 || y1 <= y4 <= y2;
}
const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / det;
const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / det;
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
}
const $canvas = document.querySelector('#circuit-pattern');
let g = $canvas.getContext('2d');
let WIDTH = $canvas.offsetWidth;
let HEIGHT = $canvas.offsetHeight;
const TS = 20;
let ROWS = HEIGHT / TS;
let COLS = WIDTH / TS;
function updateCanvasDimensions() {
g = $canvas.getContext('2d');
WIDTH = $canvas.offsetWidth;
HEIGHT = $canvas.offsetHeight;
ROWS = HEIGHT / TS;
COLS = WIDTH / TS;
$canvas.width = WIDTH * devicePixelRatio;
$canvas.height = HEIGHT * devicePixelRatio;
g.scale(devicePixelRatio, devicePixelRatio);
}
window.addEventListener('resize', () => updateCanvasDimensions());
updateCanvasDimensions();
const state = {
wires: [], // :: [(Int, Int)]
nextWire: null,
nextWireIndex: 0,
nextSegmentLen: 0,
t: 0,
};
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomChoice(list) {
return list[randomInt(0, list.length - 1)];
}
const DIRS = {
left: [-1, 1],
center: [0, 1],
right: [+1, 1],
};
const NEXT_DIRS = {
left: ['center'],
center: ['left', 'right'],
right: ['center'],
};
function canPlaceSegment([[p1x, p1y], [p2x, p2y]]) {
return state.wires.every(wire => {
return everyWireSegment(wire, ([[sx, sy], [lx, ly]]) => {
return !intersects(sx, sy, lx, ly, p1x, p1y, p2x, p2y);
});
})
}
function everyWireSegment(wire, segmentFn) {
let [start, ...rest] = wire;
return rest.every((pt) => {
const r = segmentFn([start, pt]);
start = pt;
return r;
});
}
function generateWire() {
const pieceCount = Math.pow(randomFloat(1, 2.5), 2) | 0;
const startPoint = [
randomInt(0, COLS),
Math.pow(randomFloat(0, ROWS / 12), 2) | 0,
];
const sizes = Array(pieceCount).fill(0).map(() => randomInt(2, 4) | 0);
const dirs = [randomChoice(Object.keys(DIRS))];
for (let i = 0; i < pieceCount - 1; i++) {
const last = dirs[dirs.length - 1];
dirs.push(randomChoice(NEXT_DIRS[last]));
}
const wire = [startPoint];
for (let i = 0; i < pieceCount; i++) {
const [lpx, lpy] = wire[wire.length - 1];
const [dx, dy] = DIRS[dirs[i]];
const s = sizes[i];
const s_bonus = (dirs[i] === 'center' ? s * 2 : s) | 0;
wire.push([lpx + dx * s_bonus, lpy + dy * s_bonus]);
}
const ok = everyWireSegment(wire, segment => {
return canPlaceSegment(segment);
});
if (!ok)
return null;
return wire;
}
// // LinearintERPolation
function lerp(a, b, t) {
return t * (b - a) + a;
}
function lerpPoint([x1, y1], [x2, y2], t) {
return [lerp(x1, x2, t), lerp(y1, y2, t)];
}
const CELLS_PER_SECOND = 15;
let lastTime = new Date().getTime();
function update() {
const now = new Date().getTime();
const delta = (now - lastTime) / 1000;
lastTime = now;
if (state.nextWire) {
state.t += CELLS_PER_SECOND * delta;
if (state.t >= 1.0) {
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
state.nextWireIndex++;
}
if (state.nextWireIndex > state.nextWire.length) {
state.wires.push(state.nextWire);
state.nextWire = null;
}
} else {
const wire = generateWire();
if (wire) {
state.nextWire = wire;
state.nextWireIndex = 2;
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
}
}
}
function getColors() {
if (document.body.classList.contains('dark-mode')) {
return {
BACKGROUND: '#282828',
CIRCUIT: '#38302e',
}
} else {
return {
BACKGROUND: '#eaeaea',
CIRCUIT: '#d4d4d4',
};
}
}
function render() {
const { BACKGROUND, CIRCUIT } = getColors();
g.clearRect(0, 0, WIDTH, HEIGHT);
g.strokeStyle = CIRCUIT;
g.lineWidth = 3;
state.wires.forEach(wire => {
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
});
if (state.nextWire) {
const wirePart = state.nextWire.slice(0, state.nextWireIndex);
const wire = [
...wirePart.slice(0, wirePart.length - 1),
lerpPoint(
wirePart[wirePart.length - 2],
wirePart[wirePart.length - 1],
state.t
)
];
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
}
requestAnimationFrame(render);
}
// g.translate(0.5, 0.5);
window.addEventListener('DOMContentLoaded', () => {
// $canvas.classList.remove('hide');
render();
let i = 20;
while (i > 0) {
const wire = generateWire();
if (wire) {
state.wires.push(wire);
i--;
}
}
setInterval(() => {
update();
}, 1000 / 30);
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

-8
View File
@@ -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

+945
View File
@@ -0,0 +1,945 @@
/* TODO: Don't use CDN and serve as static files */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
:root {
--bg: #eaeaea;
--fg: #333;
--bg-lighter: #f0f0f0;
--bg-dark: hsl(220, 5%, 93%);
--bg-darker: hsl(220, 5%, 90%);
--bg-darker-2: #d5d5d5;
--bg-darker-2-1: #c8c8c8;
--bg-darker-3: #c0c0c0;
--bg-darker-4: #b8b8b8;
--accent-1: #278542;
--accent-1-fg: #154d24;
--card-date: #666;
--card-content: #222;
--font-sf: 'Inter', sans-serif;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
--shadow-1: 0 0 16px 0 #00000018;
--text-input-bg: #fff;
--text-input-readonly-bg: #e4e4e4;
--text-input-readonly-fg: #777;
--accent-2-lighter: #5cc969;
--accent-2: #4eaa59;
--accent-2-darker: #2e974c;
--accent-2-darkest: #002d0d;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
html {
height: 100%;
width: 100%;
}
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-sf);
font-size: 17px;
font-weight: var(--font-weight-normal);
width: 100%;
min-height: 100%;
position: relative;
}
.main {
max-width: 70ch;
margin: 0 auto;
padding-bottom: 8rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3rem;
}
nav {
padding: 1rem 0;
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-medium);
color: var(--accent-1-fg);
}
/* Tutti i link dentro la navbar sono speciali e non sembrano link */
nav .nav-element {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
text-decoration: none;
color: inherit;
gap: 0.5rem;
}
nav a:hover {
color: var(--accent-1-fg);
text-decoration: none;
}
nav .nav-logo {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: var(--shadow-1);
transition: transform 150ms ease-in-out;
}
nav .nav-logo:hover {
transform: scale(1.05);
}
nav .nav-logo img {
width: 150px;
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
}
nav .nav-main {
display: flex;
/* grid-template-columns: repeat(5, 1fr) 1fr auto 1fr; */
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: var(--shadow-1);
z-index: 1;
}
nav .nav-item.filler {
width: 6rem;
}
nav .nav-main .nav-element {
padding: 1rem 1.25rem;
cursor: pointer;
}
nav .nav-main .nav-button {
margin: 0 0.5rem;
/* padding: 0.5rem; */
aspect-ratio: 1;
width: 2rem;
height: 2rem;
/* background: var(--bg-darker-2); */
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--bg-darker-2);
border-radius: 1rem;
cursor: pointer;
}
nav .nav-main .nav-item {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-dark);
user-select: none;
z-index: 2;
}
nav .nav-main .nav-element:hover,
nav .nav-main .nav-button:hover {
background: var(--bg-darker);
}
nav .nav-main .nav-item.dropdown {
position: relative;
z-index: 1;
}
nav .nav-main .nav-item.dropdown .nav-items .nav-item:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
nav .nav-main .nav-item.dropdown .name {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-dark);
z-index: 3;
}
nav .nav-main .nav-item.dropdown .name:hover {
background: var(--bg-darker);
}
nav .nav-main .nav-item.dropdown .nav-items {
position: absolute;
top: 100%;
width: 100%;
display: none;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: var(--shadow-1);
}
/*
nav .nav-main .nav-item.dropdown .nav-items::before {
position: absolute;
content: '';
top: -2px;
height: 2px;
left: 0;
right: 0;
width: 100%;
background: var(--bg-darker);
} */
nav .nav-main .nav-item.dropdown .name:hover + .nav-items,
nav .nav-main .nav-item.dropdown .name + .nav-items:hover {
display: flex;
flex-direction: column;
}
/* .nav-main borders */
.nav-main > .nav-item:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.nav-main > .nav-item:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
footer {
position: absolute;
bottom: 0;
left: 50%;
height: 3rem;
width: 80ch;
transform: translate(-50%, 0);
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
border-bottom: none;
box-shadow: var(--shadow-1);
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
/* Components */
section {
display: flex;
flex-direction: column;
align-items: center;
}
.card-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-top: 1rem;
}
.card {
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: var(--shadow-1);
max-width: 60ch;
padding: 1rem;
display: flex;
flex-direction: column;
}
.card .title {
font-size: 22px;
font-weight: var(--font-weight-bold);
padding-bottom: 0.5rem;
}
.card .date {
font-size: 15px;
color: var(--card-date);
padding-bottom: 0.5rem;
}
.card .description {
font-weight: var(--font-weight-light);
color: var(--card-content);
}
.tags {
display: flex;
flex-direction: row;
gap: 0 0.5rem;
padding-top: 0.5rem;
}
.tags .tag {
height: 1.5rem;
border-radius: calc(1.5rem / 2);
padding: 0 calc(1.5rem / 2);
background: var(--bg-darker-2);
color: var(--card-date);
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
/* Typography */
h1,
h2,
h3,
h4 {
margin: 0;
margin-top: 1rem;
margin-bottom: 0.5rem;
font-weight: var(--font-weight-light);
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.35rem;
}
h4 {
font-size: 1.2rem;
font-weight: var(--font-weight-bold);
}
p,
ul,
ol,
li {
margin: 0;
width: 70ch;
max-width: 100%;
line-height: 1.8;
}
p + p {
padding-top: 0.5rem;
}
ul,
ol {
padding: 0 0 0 1.5rem;
}
hr {
width: 50ch;
height: 1px;
margin: 0;
border: none;
background-color: var(--bg-darker-2);
}
pre {
margin: 0.5rem 0;
background: var(--bg-lighter);
border: 1px solid #cbcbcb;
border-radius: 2px;
box-shadow: 0 2px 4px 0 #00000033;
font-size: 90%;
display: flex;
overflow-x: auto;
}
pre > code {
display: block;
margin: 0.25rem;
}
p.center {
text-align: center;
}
/* Controls */
a:not(.button) {
color: var(--accent-1-fg);
font-weight: var(--font-weight-medium);
text-decoration: none;
}
a:not(.button):hover {
color: var(--accent-1);
text-decoration: underline;
}
a.button {
text-decoration: none;
}
/* Buttons */
button,
.button {
display: inline-block;
font-family: var(--font-sf);
font-weight: var(--font-weight-medium);
font-size: 17px;
/* gray variant #b3b3b3 */
border: 1px solid var(--bg-darker-3);
/* gray variant #bfbfbf */
background: var(--bg-darker-2);
/* gray variant #333333 */
color: var(--fg);
height: 2rem;
border-radius: 4px;
padding: 0 1rem;
transition: all 100ms ease-in-out;
box-shadow: 0 4px 8px 0 #00000022;
cursor: pointer;
}
button:hover,
.button:hover {
background: var(--bg-darker-2-1);
box-shadow: 0 4px 8px 0 #00000033;
}
button.primary {
border: 1px solid var(--accent-2-darker);
background: var(--accent-2);
color: var(--accent-2-darkest);
}
button.primary:hover {
background: var(--accent-2-lighter);
}
button.icon {
padding: 0;
width: 2rem;
}
select {
font-family: var(--font-sf);
font-weight: var(--font-weight-medium);
font-size: 16px;
/* gray variant #b3b3b3 */
border: 1px solid var(--bg-darker-3);
/* gray variant #bfbfbf */
background: var(--bg-darker-2);
/* gray variant #333333 */
color: var(--fg);
height: 2rem;
border-radius: 4px;
padding: 0 0.25rem;
transition: all 100ms ease-in-out;
box-shadow: 0 4px 8px 0 #00000022;
cursor: pointer;
}
/* Text Fields */
input[type='text'],
input[type='password'] {
border: none;
background: none;
height: 2rem;
padding: 0 0.5rem;
font-size: 17px;
background: var(--text-input-bg);
color: var(--fg);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: 0 0 8px 0 #00000020;
font-family: var(--font-sf);
font-size: 17px;
font-weight: var(--font-weight-normal);
}
input[type='password'] {
font-family: caption;
}
input[type='text']:read-only,
input[type='password']:read-only {
background: var(--text-input-readonly-bg);
color: var(--text-input-readonly-fg);
box-shadow: 0 0 8px 0 #00000010;
}
input[type='text'].error,
input[type='password'].error {
background: #faa;
color: #311;
}
/* Compound Controls */
.compound {
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
box-shadow: 0 0 8px 0 #00000022;
border: 1px solid var(--bg-darker-3);
background: var(--bg-darker-2);
color: var(--fg);
}
.compound > .divider {
height: 2rem;
width: 1px;
background: var(--bg-darker-4);
}
.compound .icon {
width: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.compound > select {
background: none;
margin-right: 0.25rem;
}
.compound > button,
.compound > .button,
.compound > input,
.compound > select {
border: none;
box-shadow: none;
}
.compound > :not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.compound > :not(:last-child) {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* Forms */
form .field {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
form .field-set {
display: grid;
grid-template-columns: auto 1fr;
grid-auto-rows: auto;
gap: 0.5rem 1rem;
padding: 1rem;
}
form .field-set .fill {
grid-column: 1 / span 2;
}
form .field-set label {
grid-column: 1 / 2;
align-self: center;
justify-self: end;
font-weight: var(--font-weight-medium);
}
form .field-set input {
grid-column: 2 / 3;
}
/* Pages */
.page-home {
/* TODO: Sarebbe meglio se si riuscisse a capire come farlo senza */
overflow-x: hidden;
}
.page-home .nav-logo {
display: none;
}
.page-home .super {
position: absolute;
left: 50vw;
top: 50vh;
width: 90vw;
transform: translate(-50%, -50%);
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 2vmin;
}
.page-home canvas {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
right: 0;
pointer-events: none;
z-index: -1;
transition: opacity 1000ms ease-in-out;
opacity: 1;
}
.page-home canvas.hide {
opacity: 0;
}
.page-home .super .block.text {
max-width: 40ch;
font-size: 1.25rem;
}
.page-home .super .block.text h1 {
font-weight: var(--font-weight-medium);
}
.page-home .super .block.text p {
font-weight: var(--font-weight-light);
}
.page-home .super .block.image {
display: flex;
align-items: center;
justify-content: center;
}
.page-home .super .block.image img {
max-width: 80ch;
max-height: 50vh;
filter: drop-shadow(0 0 64px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 8px rgba(0, 0, 0, 0.35));
}
.page-home .main {
padding-top: calc(100vh - 6rem);
}
.page-utenti .user-list {
display: flex;
flex-direction: column;
width: 100%;
}
.page-utenti .user-item {
flex-direction: row;
width: 100%;
}
.page-utenti .user-item .icon {
width: 1.75rem;
display: grid;
place-content: center;
}
.page-utenti .spinner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 5rem;
color: var(--fg);
animation: rotate 1s linear infinite;
}
.page-storia .history-container {
--bar-size: 6px;
display: flex;
flex-direction: row;
margin: 5rem 0;
position: relative;
}
.page-storia .history-container .timeline-bar {
position: absolute;
top: 0;
bottom: 0;
left: -2rem;
width: var(--bar-size);
background: var(--bg-darker-4);
}
.page-storia .history-container .timeline-bar::before {
content: '';
position: absolute;
top: -3rem;
bottom: -3rem;
left: 0;
border-left: var(--bar-size) dashed var(--bg-darker-4);
}
.page-storia .history-container .events {
display: flex;
flex-direction: column;
gap: 1rem;
}
.page-storia .history-container .events .event::before {
--size: 1rem;
content: '';
position: absolute;
left: calc(-2rem + var(--bar-size) / 2 - var(--size) / 2);
transform: translate(0, 1rem);
width: var(--size);
height: var(--size);
border-radius: 100%;
background: var(--bg-darker-4);
}
.page-storia .history-container .events .event .title {
font-size: 22px;
display: flex;
align-items: center;
gap: 0.25rem;
}
.page-storia .history-container .events .event .date {
color: var(--card-date);
font-size: 18px;
}
.page-storia .history-container .events .spacer {
height: calc(var(--size) * 1rem);
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.search {
margin: 2rem 0;
display: flex;
flex-direction: row;
gap: 1rem;
}
.search input[type='text'] {
width: 50ch;
height: 2.5rem;
}
.search button {
width: 2.5rem;
height: 2.5rem;
}
/* Rendered Markdown */
.news-content p {
margin: 0.5rem 0;
}
.news-content {
display: block;
}
.news-content h1 {
text-align: center;
}
.news-content img {
display: block;
margin: 1rem auto;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: 0 2px 8px 0 #00000033;
}
.news-content .date {
font-size: 15px;
color: var(--card-date);
display: flex;
justify-content: center;
}
.news-content .tags,
.news-content .date {
justify-content: center;
}
.news-content table {
margin: 1rem auto;
}
/* Math */
.katex-display {
margin: 1rem 0;
font-size: 105%;
}
/* Tables */
table {
border-collapse: collapse;
}
table td,
table th {
padding: 0.5rem;
}
table td:not(:first-child),
table th:not(:first-child) {
border-left: 1px solid var(--bg-darker-3);
}
table td {
border-top: 1px solid var(--bg-darker-3);
}
table tbody tr:hover {
background: var(--bg-darker);
}
+92
View File
@@ -0,0 +1,92 @@
body.dark-mode {
--bg: #282828;
--fg: #a6cc92;
/* --fg: #6eac4d; */
/* Magari questo: */
/* --fg: #a3b09c; */
--bg-dark: hsl(10, 10%, 20%);
--bg-darker: hsl(10, 10%, 17%);
--bg-darker-2: #1d2021;
--bg-darker-3: #101111;
--accent-1: #154d24;
--accent-1-fg: #278542;
--card-date: #928374;
--card-content: #d4be98;
--font-sf: 'Inter', sans-serif;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
--shadow-1: 0 0 16px 0 #16182077;
--text-input-bg: var(--bg-darker);
--text-input-readonly-bg: hsl(10, 10%, 22%);
--text-input-readonly-fg: hsl(10, 10%, 40%);
--bg-darker-2-1: #c8c8c8;
--accent-2-lighter: #5cc969;
--accent-2: #4eaa59;
--accent-2-darker: #2e974c;
--accent-2-darkest: #002d0d;
}
.dark-mode button {
border: 1px solid var(--bg-darker-2);
background: var(--bg-darker);
color: #afafaf;
}
.dark-mode button:hover {
background: var(--bg-dark);
}
.dark-mode button.primary {
border: 1px solid #113a1c;
background: #1e6732;
color: #b7e3c3;
}
.dark-mode button.primary:hover {
background: #23773a;
}
.dark-mode nav .nav-logo img {
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
}
.dark-mode input[type] {
background: #4b4342;
}
.dark-mode input[type='text']:read-only,
.dark-mode input[type='password']:read-only {
background: var(--text-input-readonly-bg);
color: var(--text-input-readonly-fg);
box-shadow: 0 0 8px 0 #00000010;
}
.dark-mode input[type='text'].error,
.dark-mode input[type='password'].error {
background: #633;
color: #faa;
}
.dark-mode pre {
background: var(--bg-darker);
border: 1px solid var(--bg-darker-2);
}
/* Trick molto molto malvagio per non dover rendere il colorscheme dei blocchi di codice dinamici rispetto alla dark mode */
.dark-mode pre > code > span {
filter: invert(1);
}
.dark-mode table td,
.dark-mode table th {
border-color: var(--fg);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

-30
View File
@@ -1,30 +0,0 @@
/**
* @typedef {{
* image?: string,
* course?: string,
* title?: string,
* author: string,
* courseYear: string
* }} AppuntiCardProps
*/
/**
*
* @param {AppuntiCardProps} param0
* @returns
*/
export const AppuntiCard = ({ 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>
}
-60
View File
@@ -1,60 +0,0 @@
import { type ComponentChildren } from 'preact'
import { useState, useRef, useEffect } from 'preact/hooks'
import { clsx, isMobile } from './lib/util'
import { PhosphorIcon } from './Icon'
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> */}
<PhosphorIcon name="caret-down" />
</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>
)
}
-13
View File
@@ -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>
)
}
-119
View File
@@ -1,119 +0,0 @@
import { useEffect, useState } from 'preact/hooks'
import { Funnel } from '@phosphor-icons/react'
import { marked } from 'marked'
import extendedLatex from '@/client/lib/marked-latex'
marked.use(
extendedLatex({
lazy: false,
render: (formula: string, display: boolean) => {
return display ? '$$' + formula + '$$' : '$' + formula + '$'
},
}),
)
import type { Database } from '@/data/domande-esami.yaml'
const useRemoteValue = <T,>(url: string): T | null => {
const [value, setValue] = useState<T | null>(null)
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(value => setValue(value))
.catch(error => console.error(error))
}, [url])
return value
}
type Props = {
course: string
}
export const DomandeEsamiCourse = ({ course }: Props) => {
const database = useRemoteValue<Database>(`/domande-esami/api/${course}.json`)
if (!database) {
return <>Loading...</>
}
if ('requestIdleCallback' in window) {
// @ts-ignore
requestIdleCallback(() => window.renderMath())
} else {
// @ts-ignore
setTimeout(() => window.renderMath(), 100)
}
const courseTags = [
...new Set(
database.questions.filter(question => question.course === course).flatMap(question => question.tags),
),
]
const [selectedTag, setSelectedTag] = useState<string | null>(null)
const filteredQuestions = database.questions
.filter(question => question.course === course)
.filter(question => (selectedTag ? question.tags.includes(selectedTag) : true))
return (
<>
<div class="grid-center text-center">
<h3>
<a href="/domande-esami">Domande Orali</a>
</h3>
<h1>{database.names[course]}</h1>
</div>
{courseTags.length > 1 && (
<div class="card filter">
<div class="grid-h">
<Funnel />
<strong>Filtra Tag</strong>
</div>
<div class="flex-row-wrap">
{!selectedTag
? courseTags.map(tag => (
<div class="chip clickable" onClick={() => setSelectedTag(tag)}>
{tag}
</div>
))
: courseTags.map(tag => (
<div
class={tag === selectedTag ? 'chip clickable' : 'chip clickable disabled'}
onClick={() => setSelectedTag(tag === selectedTag ? null : tag)}
>
{tag}
</div>
))}
</div>
</div>
)}
<div class="wide-card-list" id="questions">
{filteredQuestions.length === 0 ? (
<div class="grid-center">
<em>No questions found</em>
</div>
) : (
filteredQuestions.map(question => (
<div class="card">
<div
class="text"
dangerouslySetInnerHTML={{
__html: marked(question.content, { async: false }),
}}
/>
<div class="metadata">
{question.tags.map(tag => (
<div class="chip small">{tag}</div>
))}
</div>
</div>
))
)}
</div>
</>
)
}
-21
View File
@@ -1,21 +0,0 @@
const icons = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`, {
eager: true,
}),
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
)
type Props = {
name: string
}
export const PhosphorIcon = ({ name }: Props) => {
const icon = icons[name]
if (!icon) {
throw new Error(`Icon "${name}" not found`)
}
return <img class="phosphor-icon" src={icon.default.src} alt={name} />
}
-765
View File
@@ -1,765 +0,0 @@
import { useState, useEffect } from 'preact/hooks'
import { render } from 'preact'
// Tipi per la gestione dei dati
type TipoStudente = 'triennale' | 'magistrale'
interface Corso {
nome: string
anno: '1' | '2' | '3' | 'M' | 'istituzioni'
cfu: number
passFailOnly?: boolean // Per materie senza voto (pass/fail)
}
interface CorsoSelezionato {
id: string
nome: string
cfu: number
voto: number | null
lode: boolean
passFailOnly?: boolean // Per materie senza voto (pass/fail)
}
interface CorsoCustom {
nome: string
cfu: number
}
// Dati dei corsi aggiornati dalla tabella ufficiale
const CORSI_DISPONIBILI: Corso[] = [
// Primo Anno
{ nome: 'Analisi matematica 1', anno: '1', cfu: 15 },
{ nome: 'Aritmetica', anno: '1', cfu: 9 },
{ nome: 'Fisica I con laboratorio', anno: '1', cfu: 9 },
{ nome: 'Fondamenti di programmazione con laboratorio', anno: '1', cfu: 9 },
{ nome: 'Geometria 1', anno: '1', cfu: 15 },
{ nome: 'Laboratorio di introduzione alla matematica computazionale', anno: '1', cfu: 6, passFailOnly: true },
{ nome: 'Laboratorio di comunicazione mediante calcolatore', anno: '1', cfu: 3, passFailOnly: true },
// Secondo Anno
{ nome: 'Algebra 1', anno: '2', cfu: 6 },
{ nome: 'Algoritmi e strutture dati', anno: '2', cfu: 6 },
{ nome: 'Analisi matematica 2', anno: '2', cfu: 12 },
{ nome: 'Analisi numerica con laboratorio', anno: '2', cfu: 9 },
{ nome: 'Elementi di probabilità e statistica', anno: '2', cfu: 6 },
{ nome: 'Geometria 2', anno: '2', cfu: 12 },
{ nome: 'Inglese scientifico', anno: '2', cfu: 6, passFailOnly: true },
{ nome: 'Laboratorio didattico di matematica computazionale', anno: '2', cfu: 3, passFailOnly: true },
// Terzo Anno
{ nome: 'Algebra 2', anno: '3', cfu: 6 },
{ nome: 'Analisi matematica 3', anno: '3', cfu: 6 },
{ nome: 'Analisi reale', anno: '3', cfu: 6 },
{ nome: 'Calcolo scientifico', anno: '3', cfu: 6 },
{ nome: 'Elementi di analisi complessa', anno: '3', cfu: 6 },
{ nome: 'Elementi di calcolo delle variazioni', anno: '3', cfu: 6 },
{ nome: 'Elementi di geometria algebrica', anno: '3', cfu: 6 },
{ nome: 'Elementi di meccanica celeste', anno: '3', cfu: 6 },
{ nome: 'Elementi di teoria degli insiemi', anno: '3', cfu: 6 },
{ nome: 'Elementi di teoria delle rappresentazioni', anno: '3', cfu: 6 },
{ nome: 'Elementi di topologia algebrica', anno: '3', cfu: 6 },
{ nome: 'Equazioni alle derivate parziali', anno: '3', cfu: 6 },
{ nome: 'Fisica II', anno: '3', cfu: 9 },
{ nome: 'Fisica III', anno: '3', cfu: 6 },
{ nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 },
{ nome: 'Gruppi e rappresentazioni', anno: '3', cfu: 6 },
{ nome: 'Laboratorio computazionale', anno: '3', cfu: 6 },
{ nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 },
{ nome: 'Logica matematica', anno: '3', cfu: 6 },
{ nome: 'Matematiche elementari da un punto di vista superiore: aritmetica', anno: '3', cfu: 6 },
{ nome: 'Matematiche elementari da un punto di vista superiore: geometria', anno: '3', cfu: 6 },
{ nome: 'Meccanica razionale', anno: '3', cfu: 6 },
{ nome: 'Metodi numerici per equazioni differenziali ordinarie', anno: '3', cfu: 6 },
{ nome: 'Metodi topologici in analisi globale', anno: '3', cfu: 6 },
{ nome: 'Ottimizzazione non lineare', anno: '3', cfu: 6 },
{ nome: 'Probabilità', anno: '3', cfu: 6 },
{ nome: 'Ricerca operativa', anno: '3', cfu: 6 },
{ nome: 'Sistemi dinamici', anno: '3', cfu: 6 },
{ nome: 'Spazi di Sobolev', anno: '3', cfu: 6 },
{ nome: 'Statistica matematica', anno: '3', cfu: 6 },
{ nome: 'Storia della matematica', anno: '3', cfu: 6 },
{ nome: 'Teoria algebrica dei numeri 1', anno: '3', cfu: 6 },
{ nome: 'Teoria dei campi e teoria di Galois', anno: '3', cfu: 6 },
{ nome: 'Teoria dei numeri elementare', anno: '3', cfu: 6 },
{ nome: 'Teoria della misura', anno: '3', cfu: 6 },
// Istituzioni (Magistrale)
{ nome: 'Istituzioni di algebra', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di analisi matematica', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di analisi numerica', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di didattica della matematica', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di fisica matematica', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di geometria', anno: 'istituzioni', cfu: 11 },
{ nome: 'Istituzioni di probabilità', anno: 'istituzioni', cfu: 11 },
// Materie a scelta (Magistrale)
{ nome: '4-varietà', anno: 'M', cfu: 6 },
{ nome: 'Algebra superiore A', anno: 'M', cfu: 6 },
{ nome: 'Algebre e gruppi di Lie', anno: 'M', cfu: 6 },
{ nome: 'Analisi armonica', anno: 'M', cfu: 6 },
{ nome: 'Analisi complessa A', anno: 'M', cfu: 6 },
{ nome: 'Analisi complessa B', anno: 'M', cfu: 6 },
{ nome: 'Analisi convessa', anno: 'M', cfu: 6 },
{ nome: 'Analisi dei dati', anno: 'M', cfu: 6 },
{ nome: 'Analisi non standard', anno: 'M', cfu: 6 },
{ nome: 'Analisi reale', anno: 'M', cfu: 6 },
{ nome: 'Analisi su spazi gaussiani', anno: 'M', cfu: 6 },
{ nome: 'Analisi superiore', anno: 'M', cfu: 6 },
{ nome: 'Analisi superiore A', anno: 'M', cfu: 6 },
{ nome: 'Analisi superiore B', anno: 'M', cfu: 6 },
{ nome: 'Aspetti matematici nella computazione quantistica', anno: 'M', cfu: 6 },
{ nome: 'Calcolo delle variazioni B', anno: 'M', cfu: 6 },
{ nome: 'Calcolo della variazioni A', anno: 'M', cfu: 6 }, // Variante nome
{ nome: 'Combinatoria algebrica', anno: 'M', cfu: 6 },
{ nome: 'Complementi di analisi funzionale', anno: 'M', cfu: 6 },
{ nome: 'Complementi di meccanica razionale', anno: 'M', cfu: 6 },
{ nome: 'Crittografia post-quantistica', anno: 'M', cfu: 6 },
{ nome: 'Curve ellittiche', anno: 'M', cfu: 6 },
{ nome: 'Determinazione orbitale', anno: 'M', cfu: 6 },
{ nome: 'Didattica della matematica e nuove tecnologie', anno: 'M', cfu: 6 },
{ nome: 'Dinamica del sistema solare', anno: 'M', cfu: 6 },
{ nome: 'Dinamica iperbolica', anno: 'M', cfu: 6 },
{ nome: 'Dinamica olomorfa', anno: 'M', cfu: 6 },
{ nome: 'Elementi di calcolo in gruppi omogenei', anno: 'M', cfu: 6 },
{ nome: 'Equazioni della fluidodinamica', anno: 'M', cfu: 6 },
{ nome: 'Equazioni differenziali stocastiche e applicazioni', anno: 'M', cfu: 6 },
{ nome: 'Equazioni ellittiche', anno: 'M', cfu: 6 },
{ nome: 'Finanza matematica', anno: 'M', cfu: 6 },
{ nome: 'Fisica matematica', anno: 'M', cfu: 6 },
{ nome: 'Forme modulari', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica B', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica C', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica complessa', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica D', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica E', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica F', anno: 'M', cfu: 6 },
{ nome: 'Geometria algebrica G', anno: 'M', cfu: 6 },
{ nome: 'Geometria e analisi complessa', anno: 'M', cfu: 6 },
{ nome: 'Geometria differenziale complessa', anno: 'M', cfu: 6 },
{ nome: 'Geometria iperbolica', anno: 'M', cfu: 6 },
{ nome: 'Geometria riemanniana', anno: 'M', cfu: 6 },
{ nome: 'Gruppi algebrici lineari', anno: 'M', cfu: 6 },
{ nome: 'Gruppi di Coxeter', anno: 'M', cfu: 6 },
{ nome: 'Gruppi di Galois e gruppi fondamentali', anno: 'M', cfu: 6 },
{ nome: 'Meccanica celeste', anno: 'M', cfu: 6 },
{ nome: 'Meccanica spaziale', anno: 'M', cfu: 6 },
{ nome: 'Meccanica superiore', anno: 'M', cfu: 6 },
{ nome: 'Metodi di analisi armonica in analisi non lineare', anno: 'M', cfu: 6 },
{ nome: 'Metodi di approssimazione', anno: 'M', cfu: 6 },
{ nome: 'Metodi matematici della crittografia', anno: 'M', cfu: 6 },
{ nome: 'Metodi matematici della meccanica quantistica', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per catene di Markov', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per equazioni alle derivate parziali', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per il calcolo tensoriale', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per il controllo ottimo', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per la grafica', anno: 'M', cfu: 6 },
{ nome: 'Metodi numerici per problemi inversi', anno: 'M', cfu: 6 },
{ nome: "Metodi probabilistici per l'algebra lineare numerica", anno: 'M', cfu: 6 },
{ nome: 'Modelli matematici in biomedicina e fisica matematica', anno: 'M', cfu: 6 },
{ nome: 'Origini e sviluppo delle matematiche moderne', anno: 'M', cfu: 6 },
{ nome: 'Probabilità superiore', anno: 'M', cfu: 6 },
{ nome: 'Problemi e metodi della ricerca in didattica della matematica', anno: 'M', cfu: 6 },
{ nome: 'Problemi e metodi in storia della matematica', anno: 'M', cfu: 6 },
{ nome: 'Sistemi dinamici aleatori', anno: 'M', cfu: 6 },
{ nome: 'Statistica superiore', anno: 'M', cfu: 6 },
{ nome: 'Storia della matematica antica e della sua tradizione', anno: 'M', cfu: 6 },
{ nome: 'Superfici di Riemann e curve algebriche', anno: 'M', cfu: 6 },
{ nome: 'Tecnologie per la didattica', anno: 'M', cfu: 6 },
{ nome: 'Teoria algebrica dei numeri 2', anno: 'M', cfu: 6 },
{ nome: 'Teoria analitica dei numeri A', anno: 'M', cfu: 6 },
{ nome: 'Teoria dei giochi', anno: 'M', cfu: 6 },
{ nome: 'Teoria dei modelli', anno: 'M', cfu: 6 },
{ nome: 'Teoria dei nodi A', anno: 'M', cfu: 6 },
{ nome: 'Teoria degli insiemi', anno: 'M', cfu: 6 },
{ nome: 'Teoria degli insiemi A', anno: 'M', cfu: 6 },
{ nome: 'Teoria degli insiemi B', anno: 'M', cfu: 6 },
{ nome: 'Teoria delle categorie', anno: 'M', cfu: 6 },
{ nome: 'Teoria delle rappresentazioni A', anno: 'M', cfu: 6 },
{ nome: "Teoria e metodi dell'ottimizzazione", anno: 'M', cfu: 6 },
{ nome: 'Teoria ergodica', anno: 'M', cfu: 6 },
{ nome: 'Teoria geometrica della misura', anno: 'M', cfu: 6 },
{ nome: 'Topologia algebrica', anno: 'M', cfu: 6 },
{ nome: 'Topologia algebrica A', anno: 'M', cfu: 6 },
{ nome: 'Topologia algebrica B', anno: 'M', cfu: 6 },
{ nome: 'Topologia differenziale', anno: 'M', cfu: 6 },
{ nome: 'Topologia e geometria in bassa dimensione', anno: 'M', cfu: 6 },
{ nome: 'Ultrafiltri e metodi non-standard', anno: 'M', cfu: 6 },
]
export function MediaPesataApp() {
// Funzioni per localStorage
const loadFromStorage = () => {
try {
const savedData = localStorage.getItem('media-pesata-data')
if (savedData) {
const parsed = JSON.parse(savedData)
return {
tipoStudente: parsed.tipoStudente || 'triennale',
corsiSelezionati: parsed.corsiSelezionati || [],
sezioniAperte: parsed.sezioniAperte || {},
mostraRisultati: parsed.mostraRisultati || false,
}
}
} catch (error) {
console.warn('Errore nel caricamento dei dati salvati:', error)
}
return {
tipoStudente: 'triennale' as TipoStudente,
corsiSelezionati: [],
sezioniAperte: {},
mostraRisultati: false,
}
}
const saveToStorage = (data: any) => {
try {
localStorage.setItem('media-pesata-data', JSON.stringify(data))
} catch (error) {
console.warn('Errore nel salvataggio dei dati:', error)
}
}
// Inizializzazione con dati salvati
// const initialData = loadFromStorage()
const [tipoStudente, setTipoStudente] = useState<TipoStudente>('triennale')
const [corsiSelezionati, setCorsiSelezionati] = useState<CorsoSelezionato[]>([])
const [showCustomForm, setShowCustomForm] = useState(false)
const [customCorso, setCustomCorso] = useState<CorsoCustom>({ nome: '', cfu: 0 })
const [sezioniAperte, setSezioniAperte] = useState<Record<string, boolean>>({})
const [mostraRisultati, setMostraRisultati] = useState(false)
// Load data from localStorage on mount
useEffect(() => {
const initialData = loadFromStorage()
setTipoStudente(initialData.tipoStudente)
setCorsiSelezionati(initialData.corsiSelezionati)
setSezioniAperte(initialData.sezioniAperte)
setMostraRisultati(initialData.mostraRisultati)
}, [])
// Salva automaticamente quando cambiano i dati importanti
useEffect(() => {
const dataToSave = {
tipoStudente,
corsiSelezionati,
sezioniAperte,
mostraRisultati,
}
saveToStorage(dataToSave)
}, [tipoStudente, corsiSelezionati, sezioniAperte, mostraRisultati])
const toggleSezione = (nomeSezione: string) => {
setSezioniAperte(prev => ({
...prev,
[nomeSezione]: !prev[nomeSezione],
}))
}
const calcolaMedia = () => {
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
if (corsiConVoto.length === 0) {
alert('Inserisci almeno un voto per calcolare la media!')
return
}
setMostraRisultati(true)
}
// Funzioni per la gestione dei corsi
const aggiungiCorso = (corso: Corso) => {
// Controlla se la materia esiste già
const materiaEsiste = corsiSelezionati.some(c => c.nome.toLowerCase() === corso.nome.toLowerCase())
if (materiaEsiste) {
alert('Questa materia è già stata aggiunta')
return
}
// Controllo per magistrali: massimo 3 istituzioni
if (tipoStudente === 'magistrale' && corso.anno === 'istituzioni') {
const istituzioniAttuali = corsiSelezionati.filter(c => c.nome.toLowerCase().includes('istituzioni')).length
if (istituzioniAttuali >= 3) {
alert('Puoi selezionare al massimo 3 istituzioni per la magistrale')
return
}
}
const id = Date.now().toString()
const nuovoCorso: CorsoSelezionato = {
id,
nome: corso.nome,
cfu: corso.cfu,
voto: null,
lode: false,
passFailOnly: corso.passFailOnly,
}
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
}
const aggiungiCorsoCustom = () => {
// Validazioni
if (!customCorso.nome.trim()) {
alert('Il nome della materia non può essere vuoto')
return
}
if (customCorso.cfu <= 0 || customCorso.cfu > 30) {
alert('I CFU devono essere tra 1 e 30')
return
}
if (!Number.isInteger(customCorso.cfu)) {
alert('I CFU devono essere un numero intero')
return
}
// Controlla se la materia esiste già
const materiaEsiste = corsiSelezionati.some(
corso => corso.nome.toLowerCase().trim() === customCorso.nome.toLowerCase().trim(),
)
if (materiaEsiste) {
alert('Questa materia è già stata aggiunta')
return
}
const id = Date.now().toString()
const nuovoCorso: CorsoSelezionato = {
id,
nome: customCorso.nome.trim(),
cfu: customCorso.cfu,
voto: null,
lode: false,
}
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
setCustomCorso({ nome: '', cfu: 0 })
setShowCustomForm(false)
}
const rimuoviCorso = (id: string) => {
setCorsiSelezionati(corsiSelezionati.filter(corso => corso.id !== id))
}
const aggiornaVoto = (id: string, voto: number | null) => {
// Validazione voto: deve essere tra 18 e 30 e intero
if (voto !== null && (voto < 18 || voto > 30 || !Number.isInteger(voto))) {
return // Ignora valori non validi
}
setCorsiSelezionati(
corsiSelezionati.map(corso =>
corso.id === id ? { ...corso, voto, lode: voto !== 30 ? false : corso.lode } : corso,
),
)
}
const aggiornaLode = (id: string, lode: boolean) => {
setCorsiSelezionati(corsiSelezionati.map(corso => (corso.id === id ? { ...corso, lode } : corso)))
}
const resetTutto = () => {
if (corsiSelezionati.length > 0) {
if (confirm('Sei sicuro di voler cancellare tutte le materie selezionate?')) {
setCorsiSelezionati([])
setCustomCorso({ nome: '', cfu: 0 })
setShowCustomForm(false)
setSezioniAperte({})
setMostraRisultati(false)
// Pulisce anche il localStorage
try {
localStorage.removeItem('media-pesata-data')
} catch (error) {
console.warn('Errore nella pulizia del localStorage:', error)
}
}
}
}
// Calcoli della media pesata
const calcolaMediaPesata = () => {
// Escludi le materie pass/fail dal calcolo
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
if (corsiConVoto.length === 0) {
return {
mediaPesata: 0,
votoAmmissione: 0,
massimoVotoLaurea: 0,
conLode: false,
bonusLodi: 0,
errore: 'Nessun voto inserito per il calcolo della media',
}
}
// Calcola CFU totali con voto
const cfuTotaliConVoto = corsiConVoto.reduce((sum, corso) => sum + corso.cfu, 0)
const cfuDaEscludere = tipoStudente === 'triennale' ? 15 : 9
// Se i CFU sono insufficienti per l'esclusione, avvisa l'utente
if (cfuTotaliConVoto < cfuDaEscludere) {
return {
mediaPesata: 0,
votoAmmissione: 0,
massimoVotoLaurea: 0,
conLode: false,
bonusLodi: 0,
errore: `Hai inserito solo ${cfuTotaliConVoto} CFU con voto. Servono almeno ${cfuDaEscludere} CFU per applicare le regole di esclusione.`,
}
}
// Ordina per voto crescente
const corsiOrdinati = [...corsiConVoto].sort((a, b) => a.voto! - b.voto!)
let cfuEsclusi = 0
const corsiValidi: CorsoSelezionato[] = []
for (const corso of corsiOrdinati) {
if (cfuEsclusi < cfuDaEscludere) {
const cfuRimanentiDaEscludere = cfuDaEscludere - cfuEsclusi
if (corso.cfu <= cfuRimanentiDaEscludere) {
// Escludi tutto il corso
cfuEsclusi += corso.cfu
} else {
// Escludi solo una parte del corso
const cfuValidi = corso.cfu - cfuRimanentiDaEscludere
corsiValidi.push({ ...corso, cfu: cfuValidi })
cfuEsclusi = cfuDaEscludere
}
} else {
corsiValidi.push(corso)
}
}
// Calcola media pesata
const sommaPesata = corsiValidi.reduce((sum, corso) => sum + corso.voto! * corso.cfu, 0)
const sommaCfu = corsiValidi.reduce((sum, corso) => sum + corso.cfu, 0)
const mediaPesata = sommaCfu > 0 ? sommaPesata / sommaCfu : 0
// Calcola bonus lodi
const bonusLodi = corsiConVoto.reduce((bonus, corso) => {
if (corso.lode) {
return bonus + (corso.cfu > 6 ? 0.5 : 0.25)
}
return bonus
}, 0)
// Cap del bonus lodi basato sul tipo di studente
const capBonusLodi = tipoStudente === 'triennale' ? 1.5 : 2
const bonusLodiFinal = Math.min(bonusLodi, capBonusLodi)
// Voto di ammissione alla laurea (media pesata * 11/3)
const votoAmmissione = (mediaPesata * 11) / 3
// Voto di ammissione finale = voto ammissione + bonus lodi
const votoAmmissioneFinale = votoAmmissione + bonusLodiFinal
// Massimo voto di laurea possibile = voto ammissione + 10 (cappato a 110)
const massimoVotoLaurea = Math.min(votoAmmissioneFinale + 10, 110)
const conLode = massimoVotoLaurea === 110
return {
mediaPesata: Math.round(mediaPesata * 100) / 100,
votoAmmissione: Math.round(votoAmmissioneFinale * 100) / 100,
massimoVotoLaurea: Math.round(massimoVotoLaurea * 100) / 100,
conLode,
bonusLodi: Math.round(bonusLodiFinal * 100) / 100,
errore: null,
}
}
// Filtra corsi disponibili in base al tipo di studente
const getCorsiDisponibili = () => {
if (tipoStudente === 'triennale') {
return CORSI_DISPONIBILI.filter(corso => corso.anno !== 'istituzioni')
} else {
return CORSI_DISPONIBILI.filter(
corso => corso.anno === 'istituzioni' || corso.anno === '3' || corso.anno === 'M',
)
}
}
// Raggruppa corsi per categoria
const raggruppaCorsi = () => {
const corsi = getCorsiDisponibili()
const gruppi: Record<string, Corso[]> = {}
if (tipoStudente === 'triennale') {
gruppi['Primo Anno'] = corsi.filter(c => c.anno === '1')
gruppi['Secondo Anno'] = corsi.filter(c => c.anno === '2')
gruppi['Terzo Anno'] = corsi.filter(c => c.anno === '3')
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === 'M')
} else {
// Per magistrali: prima le istituzioni, poi tutto il resto come "Materie a Scelta"
gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni')
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === '3' || c.anno === 'M')
}
return gruppi
}
const cambiaTipoStudente = (nuovoTipo: TipoStudente) => {
setTipoStudente(nuovoTipo)
}
const gruppiCorsi = raggruppaCorsi()
const risultati = calcolaMediaPesata()
const totaleCfu = corsiSelezionati.reduce((sum, corso) => sum + corso.cfu, 0)
const maxCfu = tipoStudente === 'triennale' ? 171 : 93 // 180 − 9 e 120 − 27 per la tesi
const cfuError = totaleCfu > maxCfu
return (
<div class="media-pesata-app">
{/* Selezione tipo studente */}
<div class="card student-type-switcher wide">
<div class="grid-center">
<h2>Corso di Laurea</h2>
<div class="compound-button">
<button
class={tipoStudente === 'triennale' ? 'active' : ''}
onClick={() => cambiaTipoStudente('triennale')}
>
Triennale
</button>
<button
class={tipoStudente === 'magistrale' ? 'active' : ''}
onClick={() => cambiaTipoStudente('magistrale')}
>
Magistrale
</button>
</div>
</div>
</div>
{/* Counter CFU */}
<div class={`cfu-counter wide ${cfuError ? 'error' : ''}`}>
<h3>
CFU Totali: {totaleCfu}/{maxCfu}
{tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}
</h3>
{cfuError && <p class="error-text">⚠️ Hai superato il limite di CFU consentiti!</p>}
</div>
{/* Sezione selezione corsi */}
<div class="card">
<div class="title">
<h2>Seleziona Materie</h2>
</div>
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
<div key={categoria} class="course-category">
<button onClick={() => toggleSezione(categoria)}>
<div class="h-flex">
{categoria}
<div class="spacer"></div>
<span class={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}>▶</span>
</div>
</button>
{sezioniAperte[categoria] && (
<div class="course-grid">
{corsi.map((corso, index) => (
<button
key={index}
class="course-button"
onClick={() => aggiungiCorso(corso)}
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
>
<span class="course-name">{corso.nome}</span>
<span class="course-cfu">{corso.cfu} CFU</span>
</button>
))}
</div>
)}
</div>
))}
{/* Form per materia custom */}
<div class="custom-course">
<h3>Materia Personalizzata</h3>
{!showCustomForm ? (
<button onClick={() => setShowCustomForm(true)}>+ Aggiungi Materia Personalizzata</button>
) : (
<div class="custom-form">
<input
type="text"
placeholder="Nome materia"
value={customCorso.nome}
onChange={e =>
setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value })
}
/>
<input
type="number"
placeholder="CFU"
min="1"
max="30"
step="1"
value={customCorso.cfu || ''}
onChange={e =>
setCustomCorso({
...customCorso,
cfu: parseInt((e.target as HTMLInputElement).value) || 0,
})
}
/>
<button onClick={aggiungiCorsoCustom}>Aggiungi</button>
<button onClick={() => setShowCustomForm(false)}>Annulla</button>
</div>
)}
</div>
</div>
{/* Sezione lista corsi selezionati */}
<div class="card">
<div class="h-flex">
<div class="title">
<h2>Materie Selezionate</h2>
</div>
<div class="spacer"></div>
{corsiSelezionati.length > 0 && <button onClick={resetTutto}>🗑️ Cancella Tutto</button>}
</div>
{corsiSelezionati.length === 0 ? (
<p>Nessuna materia selezionata</p>
) : (
<div class="courses-list">
{corsiSelezionati.map(corso => (
<div key={corso.id} class="course-item">
<span class="course-name">{corso.nome}</span>
<span class="course-cfu">{corso.cfu} CFU</span>
{!corso.passFailOnly && (
<div class="course-grade tall">
<input
type="number"
placeholder="Voto"
min="18"
max="30"
step="1"
value={corso.voto || ''}
onChange={e =>
aggiornaVoto(
corso.id,
parseInt((e.target as HTMLInputElement).value) || null,
)
}
/>
<label class={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
<input
class="star"
type="checkbox"
checked={corso.lode}
disabled={corso.voto !== 30}
onChange={e =>
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
}
/>
Lode
</label>
</div>
)}
<div class="actions tall">
<button
class="icon remove-btn"
onClick={() => rimuoviCorso(corso.id)}
title="Rimuovi materia"
>
×
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Pulsante Calcola */}
{corsiSelezionati.length > 0 && (
<div class="calculate-section wide">
<button onClick={calcolaMedia}>🧮 Calcola Media e Voto di Laurea</button>
</div>
)}
{/* Risultati */}
{risultati && mostraRisultati && (
<div class="results wide">
<h2>Risultati</h2>
{risultati.errore ? (
<div class="error-message">
<p>⚠️ {risultati.errore}</p>
</div>
) : (
<div class="results-grid">
<div class="result-item">
<span class="label">Media Pesata:</span>
<span class="value">{risultati.mediaPesata}</span>
</div>
<div class="result-item">
<span class="label">Bonus Lodi:</span>
<span class="value">+{risultati.bonusLodi}</span>
</div>
<div class="result-item highlight">
<span class="label">Voto di Ammissione:</span>
<span class="value">{risultati.votoAmmissione}</span>
</div>
<div class="result-item highlight">
<span class="label">Massimo Voto Di Laurea Possibile:</span>
<span class="value">
{risultati.massimoVotoLaurea}
{risultati.conLode && <span class="lode-badge">(+lode)</span>}
</span>
</div>
</div>
)}
</div>
)}
{/* Nota informativa */}
<div class="card wide">
<h3>📋 Come viene calcolata la media</h3>
<div class="info-content">
<h4>Regole di esclusione CFU:</h4>
<ul>
<li>
<strong>Triennale:</strong> I 15 CFU con i voti più bassi vengono esclusi dalla media
</li>
<li>
<strong>Magistrale:</strong> I 9 CFU con i voti più bassi vengono esclusi dalla media
</li>
<li>Se un corso ha più CFU di quelli da escludere, viene diviso proporzionalmente</li>
</ul>
<h4>Calcolo del voto finale:</h4>
<ul>
<li>
<strong>Media pesata:</strong> Somma dei (voto × CFU) diviso per i CFU totali
</li>
<li>
<strong>Bonus lodi:</strong> +0.5 per lodi in materie &gt; 6 CFU, +0.25 per lodi in materie
≤ 6 CFU (max +1.5 per triennale, max +2 per magistrale)
</li>
<li>
<strong>Voto di laurea:</strong> (Voto finale × 11) ÷ 3
</li>
</ul>
<h4>Note:</h4>
<ul>
<li>
Le materie <strong>Pass/Fail</strong> non contribuiscono al calcolo della media
</li>
<li>Il voto finale è limitato a 30</li>
<li>Per i magistrali: massimo 3 istituzioni selezionabili</li>
</ul>
</div>
</div>
</div>
)
}
// Funzione per inizializzare l'app
// export function initMediaPesataApp() {
// const container = document.getElementById('media-pesata-app')
// if (container) {
// render(<MediaPesataApp />, container)
// }
// }
// export default MediaPesataApp
-29
View File
@@ -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>
</>
)
}
-166
View File
@@ -1,166 +0,0 @@
import { useComputed, useSignal } from '@preact/signals'
import Fuse from 'fuse.js'
import { useEffect } from 'preact/hooks'
import { ShowMore } from './Paginate'
import { ComboBox } from './ComboBox'
import { PhosphorIcon } from './Icon'
type User = {
uid: string
gecos: string
}
const FILTERS = {
utenti: {
icon: 'user',
label: 'Utenti',
},
macchinisti: {
icon: 'wrench',
label: 'Macchinisti',
},
rappstud: {
icon: 'bank',
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} */}
<PhosphorIcon name={v.icon} />
{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> */}
<PhosphorIcon name="magnifying-glass" />
</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> */}
<PhosphorIcon
name={
RAPPSTUD.includes(poissonUser.uid)
? 'bank'
: MACCHINISTI.includes(poissonUser.uid)
? 'wrench'
: 'user'
}
/>
</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> */}
<PhosphorIcon name="arrow-square-out" />
</a>
</div>
</div>
)}
</ShowMore>
) : (
<>Nessun risultato</>
)}
</div>
</>
)
}
-27
View File
@@ -1,27 +0,0 @@
const $debugConsole = document.createElement('div')
$debugConsole.style.position = 'fixed'
$debugConsole.style.bottom = '0'
$debugConsole.style.left = '0'
$debugConsole.style.width = '100%'
$debugConsole.style.height = '25vh'
$debugConsole.style.backgroundColor = 'black'
$debugConsole.style.color = 'white'
$debugConsole.style.overflow = 'auto'
$debugConsole.style.padding = '10px'
$debugConsole.style.boxSizing = 'border-box'
$debugConsole.style.fontFamily = 'monospace'
$debugConsole.style.zIndex = '9999'
$debugConsole.style.fontSize = '15px'
$debugConsole.style.opacity = '0.8'
document.body.appendChild($debugConsole)
function logDebugConsole(...args) {
$debugConsole.innerHTML += args.join(' ') + '<br>'
}
console.error = logDebugConsole
console.warn = logDebugConsole
console.log = logDebugConsole
console.debug = logDebugConsole
View File
-83
View File
@@ -1,83 +0,0 @@
// took from: https://github.com/sxyazi/marked-extended-latex
// this has a peer dependency bug
const CLASS_NAME = 'latex-b172fea480b'
const extBlock = options => ({
name: 'latex-block',
level: 'block',
start(src) {
return src.match(/\$\$[^\$]/)?.index ?? -1
},
tokenizer(src, tokens) {
const match = /^\$\$([^\$]+)\$\$/.exec(src)
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
},
renderer(token) {
if (!options.lazy) return options.render(token.formula, true)
return `<span class="${CLASS_NAME}" block>${token.formula}</span>`
},
})
const extInline = options => ({
name: 'latex',
level: 'inline',
start(src) {
return src.match(/\$[^\$]/)?.index ?? -1
},
tokenizer(src, tokens) {
const match = /^\$([^\$]+)\$/.exec(src)
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
},
renderer(token) {
if (!options.lazy) return options.render(token.formula, false)
return `<span class="${CLASS_NAME}">${token.formula}</span>`
},
})
let observer
/* istanbul ignore next */
export default (options = {}) => {
/* istanbul ignore next */
if (options.lazy && options.env !== 'test') {
observer = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue
}
const span = entry.target
self.unobserve(span)
Promise.resolve(options.render(span.innerText, span.hasAttribute('block'))).then(html => {
span.innerHTML = html
})
span.classList.add('latex-rendered')
}
},
{ threshold: 1.0 },
)
}
return {
extensions: [extBlock(options), extInline(options)],
}
}
/* istanbul ignore next */
export const observe = () => {
if (!observer) {
return
}
observer.disconnect()
document.querySelectorAll(`span.${CLASS_NAME}:not(.latex-rendered)`).forEach(span => {
observer.observe(span)
})
}
/* istanbul ignore next */
export const disconnect = () => {
observer?.disconnect()
}
-69
View File
@@ -1,69 +0,0 @@
import { useEffect, 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
}
-50
View File
@@ -1,50 +0,0 @@
---
import PhosphorIcon from './PhosphorIcon.astro'
const ICONS_MAP: Record<string, string> = {
github: 'github-logo',
linkedin: 'linkedin-logo',
website: 'globe',
mail: 'mailbox',
}
type Props = {
fullName: string
description: string
image: ImageMetadata
entranceDate: number
exitDate?: number
founder?: boolean
social?: {
github?: string
linkedin?: string
website?: string
mail?: string
}
}
const { fullName, description, image, entranceDate, exitDate, founder, social } = Astro.props
---
<div class="bubble">
<img src={image.src} alt={fullName.toLowerCase()} />
<div class="title">{fullName}</div>
<div class="date">{entranceDate}&mdash;{exitDate ?? 'Presente'}</div>
{founder && <div class="founder">Fondatore</div>}
<div class="description">{description}</div>
{
social && (
<div class="social">
{Object.entries(social).map(([key, value]) => (
<a href={value} target="_blank" rel="noopener noreferrer">
<PhosphorIcon name={ICONS_MAP[key] ?? 'question-mark'} />
</a>
))}
</div>
)
}
</div>
-12
View File
@@ -1,12 +0,0 @@
---
type Props = {
large?: boolean
style?: string
}
const { large, ...props } = Astro.props
---
<div class:list={['card', large && 'large']} {...props}>
<slot />
</div>
-9
View File
@@ -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>
-57
View File
@@ -1,57 +0,0 @@
---
const links = [
{ href: '/utenti', text: 'Utenti' },
// { href: '/macchinisti', text: 'Macchinisti' },
// { href: '/appunti', text: 'Appunti' },
{ href: '/notizie', text: 'Notizie' },
{ href: '/guide', text: 'Guide' },
{ href: '/domande-esami', text: 'Domande Orali' },
{ href: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto
{ href: '/storia', text: 'Storia' },
// { href: '/login', text: 'Login' },
]
---
<header>
<!-- main logo on the left -->
<a href="/" class="logo">
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
</a>
<!-- hidden checkbox for mobile js-less sidebar interaction -->
<input type="checkbox" id="header-menu-toggle" />
<!-- desktop navbar links -->
<div class="links desktop-only">
{
links.map(link => (
<a role="button" href={link.href}>
{link.text}
</a>
))
}
</div>
<!-- sidebar menu for mobile -->
<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>
<!-- sidebar menu only visible on mobile when #header-menu-toggle is checked -->
<div class="side-menu">
<div class="links">
{
links.map(link => (
<a role="button" href={link.href}>
{link.text}
</a>
))
}
</div>
</div>
</header>
-26
View File
@@ -1,26 +0,0 @@
---
type Props = {
color: string
}
const { color } = Astro.props
const patternId = 'zig-zag-' + color.slice(1)
---
<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={patternId} x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
<path fill={color} d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
</pattern>
</defs>
<rect fill={`url(#${patternId})`} x="0" y="0" width="1000" height="1"></rect>
</svg>
</div>
-21
View File
@@ -1,21 +0,0 @@
---
import { Image } from 'astro:assets'
type Props = {
name: string
}
const { name } = Astro.props
const icons = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`),
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
)
if (!icons[name]) {
throw new Error(`Icon "${name}" not found`)
}
---
<Image class="phosphor-icon" src={icons[name]()} alt={name} />
-24
View File
@@ -1,24 +0,0 @@
---
// Card.astro
export interface Props {
title: string
href: string
imgSrc?: string
style?: string
}
const { href, imgSrc, style, title } = Astro.props
---
<a target="_blank" href={href} style={style}>
<div class="project">
<div class="image">
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
</div>
<div class="title">{title}</div>
<div class="description text">
<slot />
</div>
</div>
</a>
-14
View File
@@ -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} />}
-9
View File
@@ -1,9 +0,0 @@
---
const { size, ...rest } = Astro.props
---
<div class:list={['container', size ?? 'normal']} {...rest}>
<div class="content">
<slot />
</div>
</div>
-20
View File
@@ -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>
-58
View File
@@ -1,58 +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(),
author: 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()),
}),
})
const metaCollection = defineCollection({
type: 'content',
schema: z.any(),
})
// Export a single `collections` object to register your collection(s)
export const collections = {
news: newsCollection,
guides: guidesCollection,
seminarietti: seminariettiCollection,
meta: metaCollection,
}
-353
View File
@@ -1,353 +0,0 @@
---
id: git-101
title: Git 101
description: Una guida 📚 introduttiva alle basi di Git
author: Luca Lombardo
tags: [git, gitea]
---
Git è un sistema di controllo di versione distribuito creato per gestire progetti di qualsiasi dimensione, mantenendo traccia delle modifiche al codice sorgente. Questa guida ci accompagnerà dai concetti di base fino alle funzionalità avanzate.
---
## **1. Introduzione a Git**
### **Cos'è Git?**
- **Sistema di controllo di versione**: Gestisce le modifiche al codice sorgente nel tempo.
- **Distribuito**: Ogni sviluppatore ha una copia del repository.
- **Veloce e leggero**: Ottimizzato per la velocità e le prestazioni.
### **Perché usare Git?**
- **Tracciabilità**: Ogni modifica è tracciata e reversibile.
- **Collaborazione**: Più persone possono lavorare sullo stesso progetto.
- **Backup**: Repository remoto per il backup del codice.
- **Branching**: Lavoriamo su nuove funzionalità senza influenzare il codice principale.
---
## **2. Installazione**
> Se ci troviamo al dipartimento di matematica a Pisa, è già installato su tutte le macchine dell'aula 3 ed aula 4!
### **Windows**
1. Scarichiamo [Git for Windows](https://git-scm.com/download/win).
2. Seguiamo il wizard di installazione.
3. Durante l'installazione:
- Selezioniamo "Git Bash" come terminale.
- Configuriamo un editor di testo (es. Vim o Nano).
### **macOS**
1. Usiamo `brew` per installare Git:
```bash
brew install git
```
### **Linux**
1. Installiamo Git usando il nostro gestore di pacchetti:
- **Debian/Ubuntu**:
```bash
sudo apt update
sudo apt install git
```
- **Arch Linux**:
```bash
sudo pacman -S git
```
---
## **3. Configurazione iniziale**
Una volta installato, configuriamo Git con il nostro nome e indirizzo email:
```bash
git config --global user.name "Il Nostro Nome"
git config --global user.email "nostro@email.com"
```
### **Verifica configurazione**
```bash
git config --list
```
---
## **4. Concetti fondamentali**
### **Repository**
- **Repository locale**: Una cartella sul nostro computer che contiene il nostro progetto.
- **Repository remoto**: Una versione del progetto ospitata su un server (es. GitHub, GitLab).
### **Branch**
Un ramo permette di lavorare su modifiche isolate rispetto al codice principale (branch `main` o `master`).
### **Commit**
Una snapshot del nostro codice in un determinato momento.
---
## **5. Creazione e gestione di un repository**
### **Inizializzare un nuovo repository**
Se stiamo iniziando un nuovo progetto, possiamo creare un nuovo repository con il comando:
```bash
git init
```
### **Clonare un repository esistente**
Se invece vogliamo lavorare su un progetto esistente, possiamo clonare da remoto con:
```bash
git clone <URL>
```
---
## **6. Lavorare con Git**
### **Aggiungere file**
Aggiungiamo file allo stage per includerli nel prossimo commit:
```bash
git add <nome-file>
# Oppure, per aggiungere tutti i file:
git add .
```
---
### **Commit**
Il comando **`git commit`** è utilizzato per registrare le modifiche nel repository locale. Ogni commit è una snapshot del progetto, contenente tutte le modifiche che sono state aggiunte tramite `git add`.
#### Come funziona:
1. **Aggiungiamo modifiche all'area di staging**:
Prima di fare un commit, dobbiamo aggiungere i file che vogliamo includere al prossimo commit usando `git add`. Questo comando prepara i file per essere salvati nella cronologia del repository.
Esempio:
```bash
git add <nome-file>
# Oppure per aggiungere tutti i file modificati
git add .
```
2. **Effettuiamo il commit**:
Una volta che i file sono nell'area di staging, possiamo fare un commit. Ogni commit dovrebbe avere un messaggio descrittivo che spieghi cosa è stato cambiato nel progetto.
Comando per fare un commit:
```bash
git commit -m "Descrizione chiara del cambiamento"
```
L'opzione `-m` permette di aggiungere il messaggio direttamente dalla linea di comando. Se omettiamo `-m`, Git aprirà un editor di testo per scrivere il messaggio di commit.
#### Cosa succede dietro le quinte:
- Git salva lo stato dei file nell'area di staging in un commit, che viene aggiunto alla cronologia del repository locale.
- Ogni commit ha un identificatore unico (hash) che consente di risalire facilmente alle modifiche in qualsiasi momento.
---
### **Push**
**`git push`** è il comando che ci permette di inviare le modifiche dal nostro repository locale a un repository remoto (ad esempio su GitHub, GitLab, Bitbucket, ecc.).
#### Come funziona:
1. Dopo aver fatto uno o più commit locali, dobbiamo inviare queste modifiche al repository remoto.
2. Per fare questo, usiamo il comando **`git push`** seguito dal nome del remoto (di solito `origin` per il repository remoto di default) e dal nome del branch (di solito `main` o `master`, ma potrebbe essere qualsiasi altro nome di branch che stiamo utilizzando).
Comando per inviare le modifiche:
```bash
git push origin main
```
#### Cosa succede dietro le quinte:
- Git confronta il nostro branch locale con il branch remoto. Se ci sono nuovi commit nel branch remoto che non sono ancora nel nostro branch locale, ci verrà richiesto di fare un **pull** per aggiornare prima di fare il push.
- Il nostro repository locale viene sincronizzato con il remoto, rendendo le modifiche visibili a tutti gli altri che hanno accesso al repository remoto.
#### Errori comuni:
- Se il repository remoto è stato aggiornato nel frattempo da qualcun altro (ad esempio, con un altro push), riceveremo un errore che ci avvisa che dobbiamo fare prima un `git pull` per sincronizzare il nostro lavoro.
---
### **Pull**
**`git pull`** è il comando che ci permette di scaricare e integrare le modifiche dal repository remoto al nostro repository locale. È una combinazione di due comandi: **`git fetch`** (scarica i cambiamenti dal remoto) e **`git merge`** (integra questi cambiamenti nel nostro branch attuale).
#### Come funziona:
1. Se altri collaboratori hanno fatto modifiche al repository remoto, possiamo ottenere queste modifiche con **`git pull`**. Questo comando aggiorna il nostro branch locale con le modifiche più recenti dal repository remoto.
2. Eseguiamo il comando:
```bash
git pull origin main
```
In questo caso, `origin` è il nome del repository remoto (il nome predefinito quando cloni un repository), e `main` è il branch che vogliamo aggiornare.
#### Cosa succede dietro le quinte:
- **`git fetch`** scarica tutte le modifiche dal repository remoto, ma non le integra ancora nel nostro codice.
- **`git merge`** unisce le modifiche scaricate al nostro branch attuale, risolvendo eventuali conflitti, se necessario.
#### Errori comuni:
- Se ci sono conflitti tra il nostro lavoro e quello degli altri, Git ci avviserà che dovremo risolverli manualmente. Dopo aver risolto i conflitti, dovremo aggiungere i file risolti (`git add`) e completare il merge con un commit.
## **7. Lavorare con branch**
### Creare un nuovo branch
Per creare un nuovo branch in Git, utilizziamo il comando:
```bash
git branch <nome-branch>
```
Sostituiamo `<nome-branch>` con il nome desiderato per il nuovo branch.
### Spostarsi su un branch
Per spostarci su un branch esistente, usiamo:
```bash
git checkout <nome-branch>
```
Oppure, per creare e spostarci su un nuovo branch in un solo comando:
```bash
git switch -c <nome-branch>
```
### Unire un branch nel branch principale
Per unire un branch nel branch principale (di solito chiamato `main`):
1. Spostiamoci sul branch principale:
```bash
git checkout main
```
2. Eseguiamo il merge del branch desiderato:
```bash
git merge <nome-branch>
```
Sostituiamo `<nome-branch>` con il nome del branch che vogliamo unire.
### Risoluzione dei conflitti
Quando due persone modificano lo stesso file, Git può generare un conflitto. Ecco come risolverlo:
1. Identifichiamo il file in conflitto:
```bash
git status
```
2. Modifichiamo manualmente il file per risolvere il conflitto. Cerchiamo i segni di conflitto (`<<<<<<<`, `=======`, `>>>>>>>`) e scegliamo quali modifiche mantenere.
3. Aggiungiamo il file risolto allo stage:
```bash
git add <file>
```
4. Concludiamo con un commit per salvare le modifiche risolte:
```bash
git commit
```
---
## **9. Comandi utili**
### **Visualizzare le differenze**
```bash
git diff
```
### **Annullare modifiche**
1. **Prima del commit**:
```bash
git checkout -- <file>
```
2. **Dopo il commit**:
```bash
git reset --soft HEAD~1
```
### **Eliminare un branch**
```bash
git branch -d <nome-branch>
```
---
## **10. Best practices**
- Scriviamo messaggi di commit chiari e descrittivi.
- Creiamo branch per nuove funzionalità o bugfix.
- Sincronizziamo frequentemente il nostro repository locale con quello remoto.
---
## **11. Risorse aggiuntive**
- [Documentazione ufficiale di Git](https://git-scm.com/doc)
- [Guida interattiva Learn Git Branching](https://learngitbranching.js.org/)
- [GitHub Docs](https://docs.github.com/)
@@ -1,80 +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. Per prima cosa installeremo NodeJS sul nostro computer, poi creeremo un nuovo progetto Astro e infine dopo averlo generato, lo caricheremo su Poisson.
## 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`.
## Appunti
Una volta creato il progetto possiamo caricare appunti e dispense nella cartella `/public`
## Deploy
Per caricare il nostro sito su Poisson possiamo usare il comando `rsync`:
```bash
npm run build
rsync -avz dist/ username@poisson.phc.dm.unipi.it:public_html/
```
Dove `username` è il nostro username Poisson. Da notare che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore.
@@ -1,111 +0,0 @@
---
id: deploy-with-github-actions
title: Deploy automatico per Poisson da GitHub
description: Come impostare il deploy automatico per la propria pagina Poisson tramite le GitHub Actions
author: Antonio De Lucreziis
tags: [github, poisson, sito]
---
Supponiamo di avere un sito web statico che vogliamo caricare su Poisson, ad esempio un progetto NodeJS che genera in `dist/` o `out/` i file da caricare. Come possiamo automatizzare il processo di deploy su Poisson?
Vedremo come deployare il nostro sito, e successivamente come automatizzare il deployment con le GitHub Actions.
## Setup manuale
Come primo approccio, potremmo compilare il nostro progetto in locale e poi caricare i file su Poisson utilizzando `rsync`, ad esempio come segue:
```bash
$ npm run build
$ rsync -avz dist/ <username>@poisson.phc.dm.unipi.it:public_html/
```
(osserviamo che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore)
## GitHub Actions
Per automatizzare questo processo possiamo caricare il nostro progetto su GitHub ed aggiungere un _workflow_ che esegue il build e il deploy ogni volta che facciamo un push sul branch `main`. Ad esempio, possiamo creare un file `.github/workflows/deploy-poison.yaml` con quanto segue:
```yaml
name: Deploy to Poisson
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Write SSH keys
run: |
install -m 600 -D /dev/null ~/.ssh/known_hosts
install -m 600 -D /dev/null ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '23'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Deploy
run: rsync -cavz dist/ ${{ secrets.SSH_USER }}@poisson.phc.dm.unipi.it:public_html/
```
## Comando rsync
Il comando `rsync` ha le seguenti opzioni:
- `-c` per controllare i file tramite checksum invece che per data e dimensione (che sono sempre diverse visto che stiamo ricosruendo il sito ogni volta con le GitHub Actions)
- `-a` per copiare ricorsivamente i file e mantenere i permessi
- `-v` per mostrare i file copiati
- `-z` per comprimere i file durante il trasferimento
## SSH Segreti
Per stabilire una connessione SSH a Poisson dalle GitHub Actions in modo sicuro, dobbiamo aggiungere alcuni segreti alla nostra repository. Vediamo meglio il workflow:
```yaml
- name: Write SSH keys
run: |
install -m 600 -D /dev/null ~/.ssh/known_hosts
install -m 600 -D /dev/null ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
```
Questa è la parte più importante del workflow, che permette di autenticarsi su Poisson senza dover inserire la password ogni volta (cosa che non possiamo materialmente fare dall GitHub Actions).
Per farlo, creiamo in locale una coppia di chiavi SSH apposta per le GitHub Actions e aggiungiamo la chiave pubblica su Poisson. Per farlo, possiamo seguire questi passaggi:
```bash
$ ssh-keygen -t ed25519 -C "deploy@github-actions" -f actions-deploy-key
$ ssh-copy-id -i actions-deploy-key <username>@poisson.phc.dm.unipi.it
```
Qui generiamo una chiave ssh utilizzando l'algoritmo `ed25519` (leggermente più consigliato rispetto a `rsa`, in particolare ha anche chiavi più corte), `-C` aggiunge semplicemente un commento alla chiave e `-f` specifica il file in cui salvare la chiave.
Poi eseguendo `cat actions-deploy-key` possiamo copiare il contenuto della chiave privata ed aggiungiamo il contenuto in un segreto chiamato `SSH_PRIVATE_KEY` nella nostra repository GitHub.
Poi, per evitare che la connessione venga rifiutata, eseguiamo in locale anche uno scan delle chiavi SSH di Poisson:
```bash
$ ssh-keyscan poisson.phc.dm.unipi.it
```
(se l'output è vuoto riprovare con `ssh-keyscan -4 ...`) e copiamo l'output in un segreto della nostra repository chiamato `SSH_KNOWN_HOSTS`.
Infine possiamo aggiungere anche un segreto `SSH_USER` con il nostro username o modificare anche direttamente il workflow ed inserire l'username direttamente nel file.
Ora ogni volta che facciamo un push sul branch `main` il nostro sito verrà automaticamente costruito e caricato su Poisson!
@@ -1,85 +0,0 @@
---
id: attivazione-poisson
title: Come attivare il proprio account Poisson
description: Guida all'attivazione dell'account Poisson, con istruzioni per il primo accesso e configurazione del proprio sito
author: Luca Lombardo
tags: [poisson, sito, ssh]
---
Poisson è un server autogestito dalla comunità studentesca di matematica, da sempre gestito con cura dai membri del PHC. Ogni studentə ha la possibilità di attivare un account personale, che consente l'accesso alla macchina via SSH e la creazione di uno spazio web personale.
## Come richiedere un account
Se non si è mai creato un account Poisson, è necessario inviare una richiesta via email a **macchinisti@lists.dm.unipi.it** includendo:
- Nome
- Cognome
- Username di ateneo (quello associato alla propria email istituzionale)
Nella mail è sufficiente specificare che si desidera attivare un account Poisson. I "macchinisti" si occuperanno di attivare l'account il prima possibile.
### Come ottenere le credenziali
Dopo l'attivazione, le credenziali del proprio account saranno disponibili accedendo al seguente sito:
<p align="center">
<a href="https://credenziali.phc.dm.unipi.it/">https://credenziali.phc.dm.unipi.it/</a>
</p>
Assicuriamoci di accedere con le credenziali di ateneo per recuperare username e password assegnati.
## Primo accesso al server
Per accedere a Poisson via SSH, è necessario:
1. Aprire un terminale (su Linux/Mac) o utilizzare un client SSH come PuTTY (su Windows).
2. Eseguire il comando:
```bash
ssh <username>@poisson.phc.dm.unipi.it
```
Dove `<username>` è il proprio username che è stato fornito con le credenziali.
## Configurazione della pagina web
Dopo aver effettuato il primo accesso, è possibile configurare la propria pagina web personale. Nella home directory del proprio account, è presente una cartella `public_html` in cui è possibile inserire i file necessari per la propria pagina web.
Vediamo un piccolo esempio di file `index.html` che possiamo creare:
```html
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sergio Steffè</title>
</head>
<body>
<h1>Sergio Steffè</h1>
<img
src="https://people.cs.dm.unipi.it/steffe/sergio.jpg"
alt="Foto di Sergio Steffè"
style="max-width: 300px; border-radius: 10px;"
/>
<p>Ciao! Sono Sergio Steffè.</p>
<p>
Email:
<a href="mailto:sergio.steffe@example.com">sergio.steffe@example.com</a>
</p>
</body>
</html>
```
Una volta salvato il file `index.html` nella cartella `public_html`, sarà possibile visualizzarlo accedendo al seguente indirizzo:
https://poisson.phc.dm.unipi.it/~<username>
Dove `<username>` è il proprio username.
### Creazione di pagine web più complesse
Per creare pagine web più complesse, suggeriamo di utilizzare il framework [Astro](https://astro.build/), che permette di creare siti web statici in modo semplice e veloce. [Abbiamo scritto una guida](/guide/pagina-poisson-con-astro) su come iniziare a utilizzare Astro per creare la nostra pagina web e caricarla su Poisson.
@@ -1,34 +0,0 @@
---
id: recupero-password
title: MI SONO SCORDATO LA PASSWORD DI POISSON COME FACCIO ORA?
description: Hai dimenticato la tua password Poisson? Niente paura, vediamo cosa fare! 💪
author: Luca Lombardo
tags: [poisson, password]
---
Se per qualche strano motivo hai dimenticato la tua password Poisson, niente paura! La procedura è super semplice:
1. Scrivi una mail a **macchinisti@lists.dm.unipi.it**.
2. Nella mail, specifica il tuo nome, cognome, username di ateneo (quello della tua email istituzionale) e se lo ricordi, anche il tuo username Poisson.
3. Attendi pazientemente la risposta dei macchinisti, che ti resetteranno la password e ti invieranno una nuova.
E voilà, in un batter d'occhio sarai di nuovo pronto a entrare nel fantastico mondo di Poisson.
## Template Email di Recupero
**Oggetto**: Recupero Password Poisson
```
Salve,
sono NOME COGNOME, la mia email istituzionale è
N.COGNOME@studenti.unipi.it {ed il mio username Poisson
è USERNAME / ma non ricordo il mio username Poisson}.
Grazie mille!
```
---
**Nota:** Non preoccuparti, capita a tutti di dimenticare una password ogni tanto. Se vuoi evitare che succeda di nuovo, prova ad usare un password manager come [Bitwarden](https://bitwarden.com/) 😉
@@ -1,68 +0,0 @@
---
id: eliminare-sito-poisson
title: Come tolgo la mia pagina Poisson da Google?
description: Sei divenuto troppo famoso 🤩 e vuoi eliminare la tua pagina Poisson dai risultati di ricerca di Google? Ecco come farlo!
author: Antonio De Lucreziis
tags: [poisson, sito]
---
Hai un nuovo sito web e vuoi che venga indicizzato da Google prima della tua pagina Poisson? Oppure sei diventato troppo famoso, e vuoi eliminare la tua pagina Poisson dai risultati di ricerca di Google? In entrambi i casi, vediamo come fare.
Ricorda che la tua pagina Poisson è indicizzata da Google in quanto pubblicamente accessibile. Per "toglierla" dai risultati di ricerca dovremo chiedere a Google di non indicizzarla più, e quindi innanzitutto dire a Google che siamo i proprietari di quella pagina (altrimenti non ci permetterà di rimuoverla).
> **Attenzione**: in questo articolo, sostituisci sempre `USERNAME` con il tuo username Poisson (non `n.cognome`, quello è il tuo username di Ateneo).
## Cancellazione pagina Poisson
Invece di eliminare la tua pagina Poisson, puoi anche solo ridirezionarla al tuo nuovo sito web. Se fossi proprio sicuro di volerla eliminare, vediamo ora come fare.
A questo punto, se vogliamo anche eliminare tutta la pagina Poisson, possiamo farlo con il comando
```bash
$ ssh USERNAME@poisson.phc.dm.unipi.it
$ cd public_html
$ rm -rf *
```
> **Attenzione!** La cartella `public_html` nella propria home in realtà è un **link simbolico** alla cartella `public_html`, che fisicamente sta altrove. Per sostituire o cancellare il contenuto della propria pagina Poisson, dovremo eliminare tutti i file contenuti nella cartella `public_html`, _ma non la cartella stessa_.
## Google Search Console
### 1. Verifica della proprietà
Prima di tutto visita la pagina <https://search.google.com/search-console>, fai l'accesso con un account Google di tuo piacimento e clicca su "Aggiungi proprietà". Poisson utilizza la convenzione che le pagine personali sono raggiungibili all'indirizzo `https://poisson.phc.dm.unipi.it/~USERNAME`, quindi seleziona l'opzione "Prefisso URL" / "URL prefix" per poi inserire l'URL della tua pagina Poisson (es. `https://poisson.phc.dm.unipi.it/~USERNAME`).
Dopo una breve attesa, Google ci chiederà di verificare la nostra proprietà con alcuni metodi. Il metodo più semplice è quello di inserire un file di verifica nella nostra cartella pubblica. Scarica il file di verifica e copialo nella tua cartella pubblica: se ad esempio il file si chiama `google1234567890.html`, puoi caricarlo nella tua cartella pubblica con il seguente comando ([clicca QUI se non ricordi le credenziali](/guide/recupero-password)):
```bash
# Vai nella cartella in cui hai scaricato il file di verifica
$ ls
... google1234567890.html ...
# Copia il file nella public_html su Poisson
$ scp google1234567890.html USERNAME@poisson.phc.dm.unipi.it:public_html/
```
Dopo qualche minuto, torniamo su Google Search Console e clicchiamo su "Verifica". Se tutto è andato a buon fine, Google ci darà accesso ai dati di indicizzazione della nostra pagina Poisson.
### 2. Richiesta di Rimozione
Nella barra laterale di sinistra, nella sezione "Indicizzazione", clicchiamo su "Rimozioni", dopodichè in "Rimozioni Temporanee" clicchiamo su "Nuova Richiesta" e selezioniamo l'opzione "Rimuovi tutti gli URL con questo prefisso". Come prefisso, andiamo ad inserire `https://poisson.phc.dm.unipi.it/~USERNAME` per rimuovere la nostra pagina Poisson e tutte le sue sottopagine. Infine, clicchiamo su "Invia richiesta di rimozione" per confermare.
### 3. Attesa...
Considera che Google potrebbe metterci fino a qualche giorno per rimuovere le pagine indicizzate. Ben fatto! Ora la tua pagina Poisson non sarà più indicizzata da Google.
## Redirect
Se invece vuoi che la tua pagina Poisson rimandi al tuo nuovo sito web, puoi impostare un redirect. Per fare ciò, ti basterà creare un file `.htaccess` nella cartella `public_html/` della tua home con il seguente contenuto:
```apache
RedirectMatch 307 /~USERNAME/.* https://www.mio-nuovo-sito.com/
```
Qui, il codice `307` indica un redirect temporaneo; se sei sicuro di volere un redirect permanente puoi usare direttamente il codice `301` (la differenza tecnica tra i due è che il primo non salva la cache del redirect nel browser, mentre il secondo sì).
## Conclusioni
Per rimuovere la tua pagina Poisson dai risultati di ricerca di altri motori di ricerca come Bing, DuckDuckGo, ecc., bisogna seguire procedure simili. Se fosse proprio necessario scrivi pure a noi macchinisti, e ricorda sempre che la tua pagina Poisson è pubblica e accessibile a tutti, quindi non metterci cose che non vuoi che siano pubbliche.

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