Compare commits

..

No commits in common. 'main' and 'dev-old' have entirely different histories.

@ -0,0 +1,3 @@
Dockerfile
node_modules
.git

@ -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

@ -0,0 +1,22 @@
# Server config
MODE=development
HOST=:8080
# 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
# Base URL
BASE_URL=localhost:8080
# Lista Utenti
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
# AuthService
AUTH_SERVICE_HOST=:memory:
# Origine per Lista Utenti
LISTA_UTENTI=utenti-poisson-2022.local.json

32
.gitignore vendored

@ -1,23 +1,17 @@
# build output # Environment files
dist/ .env
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# local data # Local files
.vscode/
*.local* *.local*
# environment variables # Generated output
.env out/
.env.production dist/
# NodeJS
node_modules/
# macOS-specific files # Executables
.DS_Store phc-website-server
!phc-website-server/

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

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

@ -0,0 +1,29 @@
FROM node:18-alpine AS frontend-builder
RUN apk add make
WORKDIR /_frontend
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
COPY ./_frontend/pnpm-lock.yaml ./
RUN pnpm fetch --prod
COPY ./_frontend ./
RUN pnpm install -r --offline --prod
RUN make build
FROM golang:1.19-alpine AS backend-builder
RUN apk add make
WORKDIR /backend
COPY go.mod go.sum ./
RUN go mod download -x
COPY ./ ./
RUN make backend
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=frontend-builder /_frontend/out ./_frontend/out
COPY --from=backend-builder /backend/phc-website-server ./phc-website-server
COPY ./_views ./_views
COPY ./_content/news ./_content/news
COPY ./_content/guide ./_content/guide
COPY ./_public ./_public
COPY ./.env ./.env
EXPOSE 8000
CMD ["./website-server"]

@ -0,0 +1,35 @@
GO_SOURCES = $(shell find . -name '*.go')
GO_EXECUTABLE = phc-website-server
.PHONY: all
all: frontend backend
#
# Build Frontend
#
.PHONY: frontend
frontend:
$(MAKE) -C _frontend/
@echo "Built Frontend"
#
# Build Backend
#
.PHONY: backend
backend: $(GO_EXECUTABLE)
@echo "Built Backend"
$(GO_EXECUTABLE): $(GO_SOURCES)
go build -o $(GO_EXECUTABLE) ./cmd/phc-website-server
#
# Debug
#
.PHONY: debug
debug:
@echo "GO_SOURCES = $(GO_SOURCES)"
@echo "GO_EXECUTABLE = $(GO_EXECUTABLE)"

@ -1,42 +1,180 @@
# PHC Website # phc/website
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web. Backend e frontend del nuovo sito per il PHC.
## Installazione ## Usage
```bash To setup the project
bun install
```bash shell
# Clone the repo
$ git clone https://git.phc.dm.unipi.it/phc/website
$ cd _frontend
$ make setup
``` ```
## Sviluppo To just do a full build and start the project run
```bash ```bash shell
bun dev # Setup local development env file
``` $ cp .env.dev .env
## Build # Setup and do a full build of frontend and backend
$ make all
```bash # Compile and start the server
bun build $ go run -v ./cmd/phc-website-server
``` ```
## Deploy [TODO] Alternativamente se si sta modificando in live il codice è comodo usare [`entr`](https://github.com/eradman/entr) e `fd` (un'alternativa a `find`)
```bash shell
# Restart server when go files change
$ printf '%s\n' $(echo **/*.go) | entr -r go run ./cmd/phc-website-server
```
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro). Per ora non c'è ancora nessun sistema alternativo ma se si sta modificando codice della frontend in `_frontend/` invece di riavviare ogni volta il server conviene lanciare in parallelo un watcher con
```bash ```bash shell
docker build -t phc-website . # Recompile files inside "_frontend/src" on change
docker run -p 3000:3000 phc-website _frontend/ $ pnpm run watch
``` ```
C'è anche un `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD. ## Backend
### Dependencies
- `github.com/gofiber/fiber/v2`
Backend server framework.
- `github.com/joho/godotenv`
Library used to load `.env` config files.
- `github.com/yuin/goldmark`
Along with `github.com/yuin/goldmark-highlighting`, `github.com/alecthomas/chroma` and `github.com/litao91/goldmark-mathjax` are used to render Markdown articles and pages with latex expression and syntax highlighting.
- `gopkg.in/yaml.v3`
Used to load YAML frontmatter in Markdown documents.
- `github.com/alecthomas/repr`
Utility to pretty print Go values.
### Architecture
The go project is organized as a library and the main server executable is inside `cmd/phc-website-server` that starts the server provided by the `server/` package and injects all concrete service instances.
After a recent refactor all code that interacts with services is inside the `handler/` package that abstracts away the HTTP server dependency to test more easily each route.
The `handler/` has a `Context` type that for now is just used to pass around the user if the HTTP request contained a session token.
The `model/` package provides some common types used in the whole application.
The `config/` package is just a singleton loaded when the application is started providing config values loaded from a `.env` file.
#### Services
All other Go packages are services used by the `handler/` packages that provide authentication (`auth/`), template rendering (`templates/`), articles loading (`articles/`).
Some very small services for now just have a single file and are `storia.go` and `utenti.go` (each provides a `-Service` interface and a default concrete implementation).
## Frontend
**Warning.** Forse a breve ci sarà un lieve refactor da AlpineJS a Preact perché è più comodo.
All frontend code is stored inside the `_frontend/` directory. (This will be implied in paths from now on)
This is a NodeJS project that uses <https://pnpm.io/> as a package manager.
This project compiles javascript files using _RollupJS_ (a tree shaking js bundler) and scss files using _sass_.
### Javascript
These files a processed by RollupJS into `out/`
- `base.js`
Script che avvia KaTeX e il _theme switcher_.
- `utenti.js`
Script che si occupa di mostrare la lista degli utenti con fuzzy search.
This script is imported by `_views/utenti.html` along side its dependencies loaded from common CDNs
```html
...
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
<script src="/public/utenti.min.js"></script>
...
```
- `profilo.js`
Script che aggiunge un minimo di interattività alla pagina renderizzata da `_views/profilo.html`
```html
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/public/profilo.min.js"></script>
```
- `homepage-art.ts`
Script che renderizza l'animazione della homepage.
- `appunti-condivisi.jsx`
Script che aggiunte l'interattività alla pagina `/appunti/condivisi` utilizzando Preact come libreria di UI.
```html
<script src="/public/appunti-condivisi.min.js"></script>
```
## Environment Variables
- `MODE`
Può essere `production` (default) o `development`.
- `HOST`
Indirizzo sul quale servire il sito, di default è `:8080`.
- `EMAIL`
Indirizzo di posta elettronica per contattare gli amministratori 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 solo i seguenti
- `GIT_URL`
- `CHAT_URL`
- `BASE_URL`
Base dell'url per i link nel sito, per adesso usato per il feed RSS. In locale conviene impostarlo come `HOST` magari premettendo il protocollo (eg. `http://localhost:8080`)
- `USER_PAGES_BASE_URL`
Base dell'url per le pagine utente di Poisson, di default `https://poisson.phc.dm.unipi.it/~`
## Come Contribuire - `AUTH_SERVICE_HOST`
**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ì. Indirizzo del servizio generico di autenticazione.
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. ## Altri Servizi
### Cose da fare Questo progetto dipende dal servizio `phc/auth-service` che permettere agli utenti di autenticarsi usando vari meccanismi.
- Aggiungere guide in [src/content/guides/](./src/content/guides/) Il servizio di autenticazione di default girerà su `http://localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)

@ -0,0 +1,19 @@
---
id: guida-git
title: "Guida a Git"
tags: git
publish_date: 2022/08/27 22:00
description: |
Vedremo come creare una repository Git per tracciare i cambiamenti di una serie di file nel tempo, e molto altro.
---
## A cosa serve
## Creiamo una repository git
Nonostante per grossi progetti sia comodo usare server Git come github.com, che essendo popolare attirerà l'attenzione di più potenziali __contributors__, per progetti personali o
di piccola scala va benissimo utilizzare un server **self-hostato**: in questo caso noi useremo il server del PHC.
Per prima cosa, visitiamo git.phc.dm.unipi.it ed accediamo con le credenziali di
## Comandi di base

@ -0,0 +1,31 @@
---
id: guida-terminale
title: "Guida al Terminale"
tags: shell, zsh, vim, ssh
publish_date: 2022/08/27 22:00
description: |
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
---
## 1. Shell & Terminale: differenze?
- introduzione storica + bash
### Le Basi
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
- copiare/incollare testo
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
- chiedere aiuto: man, info
### Zsh: una shell moderna e potente
https://wiki.archlinux.org/title/Zsh
## 2. Vim: l'Editor intramontabile
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
## 3. E la TTY?
Come utilizzarla, quando è utile, limitazioni(no scrollback)
## SSH: la magia della remote shell

@ -0,0 +1 @@
["delucreziis", "serdyuk", "minnocci", "manicastri"]

@ -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$ |

@ -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
```

@ -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
```

@ -0,0 +1 @@
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]

@ -0,0 +1,38 @@
macchinisti:
- uid: minnocci
fullName: Francesco Minnocci
entryDate: 2022/02
- uid: manicastri
fullName: Francesco Manicastri
entryDate: 2022/05
- uid: delucreziis
fullName: Antonio De Lucreziis
entryDate: 2019/09
- uid: serdyuk
fullName: Illya Serdyuk
entryDate: 2019/12
- uid: caporali
fullName: Francesco Caporali
entryDate: 2018/09
exitDate: 2022/03
- uid: dachille
fullName: Letizia D'Achille
entryDate: 2018/09
exitDate: 2022/03
eventi:
- type: simple
title: Nuovo sito del PHC
description: |
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
date: 2022/09
icon: fa-solid fa-star
- type: simple
title: Data di pubblicazione del sito Poisson originale
date: 1996/06/17
description: |
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
icon: fa-solid fa-upload
- type: simple
title: Apertura del PHC
date: 1994
icon: fa-solid fa-flag-checkered

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

@ -0,0 +1,23 @@
# https://pnpm.io/ is better at storing packages than npm, if you still want to use npm just run "make NPM=npm <target>"
NPM = pnpm
#
# Build Frontend
#
.PHONY: all
all: setup build
.PHONY: setup
setup:
$(NPM) install
.PHONY: build
build:
mkdir -p out/
$(NPM) run build
.PHONY: clean
clean:
rm -rf out/

@ -0,0 +1,35 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "Package to generate frontend files for phc/website",
"scripts": {
"build": "npm-run-all -p build:*",
"watch": "npm-run-all -p watch:*",
"build:js": "rollup -c",
"watch:js": "rollup -c -w",
"build:css": "sass -s compressed src/styles/main.scss out/next.css",
"watch:css": "sass -w src/styles/main.scss out/next.css"
},
"keywords": [],
"author": "aziis98 <antonio.delucreziis@gmail.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/preset-react": "^7.18.6",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.3.4",
"esbuild": "^0.15.5",
"npm-run-all": "^4.1.5",
"rollup": "^2.75.3",
"rollup-plugin-esbuild": "^4.9.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.52.3",
"typescript": "^4.7.4"
},
"dependencies": {
"alpinejs": "^3.10.2",
"fuse.js": "^6.6.2",
"preact": "^10.10.6"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,74 @@
import { defineConfig } from 'rollup'
import resolve from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import esbuild from 'rollup-plugin-esbuild'
export default defineConfig([
{
input: 'src/base.js',
output: {
file: 'out/base.min.js',
format: 'iife',
},
plugins: [terser()],
},
{
input: 'src/utenti.js',
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
output: {
file: 'out/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: 'out/profilo.min.js',
format: 'iife',
globals: {
alpinejs: 'Alpine',
},
},
plugins: [terser()],
},
{
input: 'src/homepage-art.ts',
output: {
file: 'out/homepage-art.min.js',
format: 'iife',
},
plugins: [esbuild({ minify: true })],
},
{
input: 'src/appunti-condivisi.jsx',
output: {
file: 'out/appunti-condivisi.min.js',
format: 'iife',
},
plugins: [
resolve(),
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-react',
{
runtime: 'automatic',
importSource: 'preact',
},
],
],
}),
terser(),
],
},
])

@ -0,0 +1,24 @@
import { render } from 'preact'
import { useState } from 'preact/hooks'
const Counter = ({}) => {
const [value, setValue] = useState(0)
const increment = () => {
setValue(value => value + 1)
}
return (
<p>
Counter: {value} <button onClick={increment}>+</button>
</p>
)
}
render(
<>
<h1>Preact is Working!</h1>
<Counter />
</>,
document.querySelector('#app')
)

@ -0,0 +1,41 @@
document.addEventListener('DOMContentLoaded', function () {
renderMathInElement(document.body, {
delimiters: [
{ left: '$', right: '$', display: false },
{ left: '$$', right: '$$', display: true },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
],
})
})
document.addEventListener('DOMContentLoaded', () => {
const $toggle = document.querySelector('#toggle-dark-mode')
const $toggleIcon = document.querySelector('#toggle-dark-mode i')
// Loads preferred dark from from localStorage or defaults to media query.
let prefersDarkMode =
localStorage.getItem('theme') !== undefined
? localStorage.getItem('theme') === 'dark'
: window.matchMedia('(prefers-color-scheme: dark)').matches
function storePrefersDarkMode(mode) {
prefersDarkMode = mode
localStorage.setItem('theme', mode ? 'dark' : 'light')
}
function displayToggle() {
document.body.classList.toggle('dark-mode', prefersDarkMode)
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode)
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode)
document.dispatchEvent(new CustomEvent('theme:change'))
}
$toggle.addEventListener('click', () => {
storePrefersDarkMode(!prefersDarkMode)
displayToggle()
})
displayToggle()
})

@ -0,0 +1,378 @@
type Point2i = [number, number]
type WireDirection = 'down-left' | 'down' | 'down-right'
type TipPosition = false | 'begin' | 'end' | 'begin-end'
type WirePiece = {
direction: WireDirection
lerp: number
tipPosition: TipPosition
}
type LatticePoint = string
function toLatticePoint(x: number, y: number): LatticePoint {
return `${x | 0},${y | 0}`
}
function fromLatticePoint(p: LatticePoint): Point2i {
const [x, y] = p.split(',').map(s => parseInt(s))
return [x, y]
}
type WireNode = { point: Point2i; direction: WireDirection }
type Wire = WireNode[]
type World = {
dimensions: Point2i
wirePieces: { [point: LatticePoint]: WirePiece }
wiresQueue: { wire: Wire; cursor: number }[]
}
function randomChoice<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right'])
const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => {
if (direction === 'down') return [x, y + 1]
if (direction === 'down-left') return [x - 1, y + 1]
if (direction === 'down-right') return [x + 1, y + 1]
throw 'invalid'
}
function checkPoint(world: World, [x, y]: [number, number]): boolean {
return !!world.wirePieces[toLatticePoint(x, y)]
}
function wireIntersects(world: World, wire: Wire): boolean {
return wire.some(({ point: [x, y], direction }) => {
// TODO: The point check actually "doubly" depends on direction
if (direction === 'down') {
return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1])
}
if (direction === 'down-left') {
return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y])
}
if (direction === 'down-right') {
return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y])
}
return false
})
}
function generateWire(world: World): Wire {
const [w, h] = world.dimensions
const randomPoint = (): [number, number] => [
Math.floor(Math.random() * w),
Math.floor(Math.pow(Math.random(), 2) * h * 0.5),
]
const wireLength = 3 + Math.floor(Math.random() * 10)
const wire: Wire = [
{
point: randomPoint(),
direction: randomDirection(),
},
]
let prev = wire[0]
let dir = prev.direction
for (let i = 0; i < wireLength; i++) {
const p = nextPoint(prev.point, dir)
if (Math.random() < 0.325) {
// change direction
if (dir === 'down') {
dir = randomChoice(['down-left', 'down-right'])
} else {
dir = 'down'
}
}
wire.push({
point: p,
direction: dir,
})
prev = wire[wire.length - 1]
}
return wire
}
function getTheme() {
if (document.body.classList.contains('dark-mode')) {
return {
backgroundColor: '#282828',
circuitColor: '#38302e',
}
} else {
return {
backgroundColor: '#eaeaea',
circuitColor: '#d4d4d4',
}
}
}
class Art {
static CELL_SIZE = 28
static TIP_RADIUS = 4
static WIRE_LERP_SPEED = 25 // units / seconds
renewGraphicsContext: boolean = true
dirty: boolean
world: World
constructor($canvas: HTMLCanvasElement) {
let g: CanvasRenderingContext2D
let unMount = this.setup($canvas)
window.addEventListener('resize', () => {
this.renewGraphicsContext = true
unMount()
unMount = this.setup($canvas)
})
document.addEventListener('theme:change', () => {
this.dirty = true
})
const renderFn = () => {
if (this.renewGraphicsContext) {
$canvas.width = $canvas.offsetWidth * devicePixelRatio
$canvas.height = $canvas.offsetHeight * devicePixelRatio
g = $canvas.getContext('2d')!
g.scale(devicePixelRatio, devicePixelRatio)
}
if (this.dirty || this.renewGraphicsContext) {
console.log('Rendering')
this.render(g, $canvas.offsetWidth, $canvas.offsetHeight)
}
this.dirty = false
this.renewGraphicsContext = false
requestAnimationFrame(renderFn)
}
renderFn()
}
setup($canvas: HTMLCanvasElement) {
this.world = {
dimensions: [
Math.ceil($canvas.offsetWidth / Art.CELL_SIZE),
Math.ceil($canvas.offsetHeight / Art.CELL_SIZE),
],
wirePieces: {},
wiresQueue: [],
}
let failedTries = 0
const wireGeneratorTimer = setInterval(() => {
if (this.world.wiresQueue.length > 0) {
return
}
// console.log('Trying to generate wire')
if (failedTries > 400) {
console.log('Stopped generating wires')
clearInterval(wireGeneratorTimer)
return
}
const wire = generateWire(this.world)
if (!wireIntersects(this.world, wire)) {
failedTries = 0
this.world.wiresQueue.push({ wire, cursor: 0 })
} else {
failedTries++
}
}, 10)
let pieceLerpBeginTime = new Date().getTime()
const wireQueueTimer = setInterval(() => {
if (this.world.wiresQueue.length > 0) {
// console.log('Interpolating queued wire')
// get top wire to add
const wireInterp = this.world.wiresQueue[0]
if (wireInterp.cursor < wireInterp.wire.length) {
const currentNode = wireInterp.wire[wireInterp.cursor]
const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED
const now = new Date().getTime()
if (now > pieceLerpEndTime) {
wireInterp.cursor++
pieceLerpBeginTime = new Date().getTime()
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
direction: currentNode.direction,
lerp: 1,
tipPosition:
wireInterp.cursor === 1
? 'begin'
: wireInterp.cursor === wireInterp.wire.length
? 'end'
: false,
}
this.dirty = true
return
}
const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
...currentNode,
tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end',
lerp,
}
this.dirty = true
} else {
this.world.wiresQueue.splice(0, 1)
}
}
}, 1000 / 60)
const unMount = () => {
clearInterval(wireGeneratorTimer)
clearInterval(wireQueueTimer)
}
// document.addEventListener('keypress', e => {
// if (e.key === 'r') {
// unMount()
// this.setup($canvas)
// }
// })
return unMount
}
render(g: CanvasRenderingContext2D, width: number, height: number) {
g.clearRect(0, 0, width, height)
const { backgroundColor, circuitColor } = getTheme()
// Grid
// g.lineWidth = 1
// g.strokeStyle = '#ddd'
// g.beginPath()
// for (let i = 0; i < height / Art.CELL_SIZE; i++) {
// g.moveTo(0, i * Art.CELL_SIZE)
// g.lineTo(width, i * Art.CELL_SIZE)
// }
// for (let j = 0; j < width / Art.CELL_SIZE; j++) {
// g.moveTo(j * Art.CELL_SIZE, 0)
// g.lineTo(j * Art.CELL_SIZE, height)
// }
// g.stroke()
g.lineWidth = 3
g.strokeStyle = circuitColor
g.lineCap = 'round'
g.lineJoin = 'round'
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
const [x, y] = fromLatticePoint(lp)
g.beginPath()
g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE)
switch (piece.direction) {
case 'down-left':
g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
case 'down':
g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
case 'down-right':
g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
}
g.stroke()
}
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
const [x, y] = fromLatticePoint(lp)
const drawTip = () => {
if (
y !== 0 &&
(piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end')
) {
switch (piece.direction) {
case 'down-left':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down-right':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
}
}
if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') {
switch (piece.direction) {
case 'down-left':
{
const cx = (x - piece.lerp) * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down':
{
const cx = x * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down-right':
{
const cx = (x + piece.lerp) * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
}
}
}
if (piece.tipPosition) {
g.fillStyle = backgroundColor
g.beginPath()
drawTip()
g.fill()
g.beginPath()
drawTip()
g.stroke()
}
}
}
}
const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement
new Art($canvas)

@ -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
},
}))

@ -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';

@ -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;
}

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

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"noImplicitAny": true,
"sourceMap": true,
"noEmitOnError": true
},
"filesGlob": ["./src/**/*.ts"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 4.6 MiB

@ -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);
}

@ -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-2-1: #1d5021;
--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%);
--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);
}

@ -0,0 +1,14 @@
{{template "base" .}}
{{define "title"}}Condivisione Appunti &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-book"></i>
Condivisione Appunti
</h1>
<div id="app"></div>
</section>
<script src="/public/appunti-condivisi.min.js"></script>
{{end}}

@ -0,0 +1,43 @@
{{template "base" .}}
{{define "title"}}Appunti &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-book"></i>
Raccolta degli Appunti
</h1>
<p>
Questa è la raccolta degli appunti presenti su Poisson. Cerca il titolo della dispensa, il nome e cognome o l'username dell'autore oppure scrivi il nome del corso rispetto a cui filtrare. Altrimenti in cima compariranno gli appunti più "gettonati".
</p>
<p>
Puoi aggiungere le tue dispense dalla <a href="/appunti/condivisi">pagina personale di condivisione</a>.
</p>
<div class="search">
<div class="compound">
<input type="text" id="search-field" placeholder="Prova con &quot;Geometria 1&quot; o &quot;Mezzedimi&quot;..." autocomplete="off" {{ if .Query }}value="{{ .Query }}"{{ end }}>
<button class="icon">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="appunti-list">
<code>TODO: Lista work in progress</code>
</div>
<script>
// Aggiorna automaticamente la barra dell'URL con la query corrente
document.querySelector('#search-field').addEventListener('input', function() {
const q = this.value.trim();
history.replaceState({}, 'Appunti',
q.length === 0 ? '/appunti' : '/appunti?q=' + encodeURIComponent(q)
);
});
</script>
</section>
<script>
</script>
{{end}}

@ -0,0 +1,44 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ template "title" . }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
crossorigin="anonymous"></script>
<script src="/public/base.min.js"></script>
<link rel="stylesheet" href="/public/style.css">
<link rel="stylesheet" href="/public/theme-dark.css">
<link rel="icon" type="image/png" href="/public/icons/icons8-electronics-100.png" />
</head>
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
{{ template "navbar" . }}
<div class="main">
{{ template "body" . }}
</div>
<footer>
<div class="footer-item">
<i class="fa-solid fa-envelope"></i>
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
</div>
</footer>
</body>
</html>
{{end}}

@ -0,0 +1,18 @@
{{template "base" .}}
{{define "title"}}{{ .Article.Title }} &bull; Guide &bull; PHC{{end}}
{{define "body"}}
<section class="news-content">
<h1>{{ .Article.Title }}</h1>
<div class="date">
{{ .Article.PublishDate.Format "2006/01/02" }}
</div>
<div class="tags">
{{ range .Article.Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
{{ .ContentHTML }}
</section>
{{end}}

@ -0,0 +1,37 @@
{{template "base" .}}
{{define "title"}}Guide &bull; PHC{{end}}
{{define "body"}}
<section>
<h2>
Feed RSS
<a href="/guide/rss"> <i class="fa-solid fa-rss"></i></a>
</h2>
<h1>
<i class="fa-solid fa-person-chalkboard"></i>
Articoli
</h1>
<div class="card-list">
{{ range .Articles }}
<div class="card">
<div class="title">
<a href="/guide/{{ .Id }}">
{{ .Title }}
</a>
</div>
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
<div class="description">
<p>{{ .Description }}</p>
</div>
<div class="tags">
{{ range .Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</section>
{{end}}

@ -0,0 +1,118 @@
{{template "base" .}}
{{define "title"}}Home &bull; PHC{{end}}
{{define "body"}}
<canvas id="wires-animation"></canvas>
<!-- Non avevo molta fantasia per il nome di questa classe -->
<section class="super">
<div class="block image">
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
</div>
<div class="block text">
<h1>
Cos'è il PHC?
</h1>
<p>
Il PHC è un progetto di sperimentazione tecnologica di alcuni studenti di Matematica di Pisa che va avanti fin dal 1994.
</p>
<p>
Fisicamente, occupa la stanza 106 del Dipartimento, mentre questo sito ne è la sede virtuale. Qui si trovano vari servizi offerti agli studenti, e ne si documenta lo sviluppo nella pagina <a href="/news">Notizie</a>.
</p>
<p>
Nella sezione <b>Progetti</b> si trovano alcune delle attività e dei servizi del PHC, mentre <a href="/utenti">Utenti</a> contiene l'indice delle pagine personali degli studenti, che sono storicamente uno dei progetti più importanti del PHC.
</p>
<p>
Per più informazioni sul PHC, si consulti la sezione <a href="/storia">Storia</a>
</p>
</div>
</section>
<section>
<h2>Notizie Importanti</h2>
<div class="card-list">
<div class="card">
<div class="title">
<a href="/news/notizia-super-wow-1">News 1</a>
</div>
<div class="date">yyyy-mm-dd</div>
<div class="description">
<p>
much doge, 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...
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="/news/notizia-super-wow-2">News 2</a>
</div>
<div class="date">yyyy-mm-dd</div>
<div class="description">
<p>
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...
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="/news/notizia-super-wow-3">News 3</a>
</div>
<div class="date">yyyy-mm-dd</div>
<div class="description">
<p>
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...
</p>
</div>
</div>
</div>
</section>
<section>
<!-- TODO: Progetti/Servizi/Cose fornite -->
<h2>Progetti</h2>
<div class="card-list">
<div class="card">
<div class="title">Project title</div>
<div class="description">
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
</p>
</div>
</div>
<div class="card">
<div class="title">Project title</div>
<div class="description">
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
</p>
</div>
</div>
<div class="card">
<div class="title">Project title</div>
<div class="description">
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
</p>
</div>
</div>
</div>
</section>
<hr>
<section>
<div class="card">
<div class="title">
Vuoi diventare un macchinista?
<i class="fas fa-wrench"></i>
</div>
<div class="description">
<p>
Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
Stai spesso in dipartimento? Allora fai pure un salto in PHC a parlare con noi, per diventare apprendista (e magari un giorno macchinista) o semplicemente per imparare.
</p>
</div>
</div>
</section>
<script src="/public/homepage-art.min.js"></script>
{{end}}

@ -0,0 +1,89 @@
{{template "base" .}}
{{define "title"}}Link Utili &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-link"></i>
Link Utili
</h1>
<p class="center">
Questo è un elenco di alcuni indirizzi potenzialmente utili
</p>
<ul>
<div class="card-list">
<div class="card">
<div class="title">
<a href="https://linktr.ee/aulastudenti">Aula Studenti</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="https://credenziali.phc.dm.unipi.it/">Credenziali Poisson</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson.
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="http://detexify.kirelabs.org/classify.html">Non trovi un simbolo Latex?</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex.
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="https://git.phc.dm.unipi.it/phc/dm-scripts/src/branch/main/printa4">Script per stampare in dipartimento dal proprio PC</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto!
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="https://github.com/SuzanneSoy/WiTeX">Wikipedia+LaTex??</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i <a href="https://en.wikipedia.org/wiki/Abstract_nonsense">tuoi</a> <a href="https://en.wikipedia.org/wiki/New_Math">articoli</a> <a href="https://en.wikipedia.org/wiki/%C3%89variste_Galois#Final_days">preferiti</a> di Wikipedia con un typesetting in stile LaTex!
</p>
</div>
</div>
<div class="card">
<div class="title">
<a href="https://www.dm.unipi.it">CdS Matematica</a>
<i class="fas fa-external-link-alt"></i>
</div>
<div class="description">
<p>
La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica
</p>
</div>
</div>
</div>
</ul>
</section>
<script>
</script>
{{end}}

@ -0,0 +1,55 @@
{{template "base" .}}
{{define "title"}}Accedi &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-user"></i>
Account di Poisson
</h1>
<div class="card-list">
<form class="card" action="/login" method="POST">
<div class="title">
<i class="fas fa-sign-in-alt"></i>
Accedi
</div>
<p>
Inserisci le tue credenziali di Poisson per accedere
</p>
<div class="field-set">
<!-- <label for="login-provider">Provider:</label> -->
<input type="hidden" name="provider" id="login-provider" value="poisson-ldap">
<label for="login-username">Username:</label>
<input type="text" name="username" id="login-username">
<label for="login-password">Password:</label>
<input type="password" name="password" id="login-password">
</div>
<div class="field">
<button class="primary">Accedi</button>
</div>
</form>
</div>
</section>
<section>
<h2>Ottenere un account</h2>
<p>
Dal 2022 in nuovi utenti hanno bisogno di compilare un modulo se vogliono ottenere un account<sup>1</sup>, scarica il <a href="#">modulo di richiesta</a> e portacelo in PHC o inviacelo via email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>.
</p>
<p>
1: In realtà il modulo ancora non esiste
</p>
</section>
<section>
<h2>Recupero credenziali</h2>
<p>
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una
email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
</p>
</section>
{{end}}

@ -0,0 +1,18 @@
{{template "base" .}}
{{define "title"}}{{ .Article.Title }} &bull; News &bull; PHC{{end}}
{{define "body"}}
<section class="news-content">
<h1>{{ .Article.Title }}</h1>
<div class="date">
{{ .Article.PublishDate.Format "2006/01/02" }}
</div>
<div class="tags">
{{ range .Article.Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
{{ .ContentHTML }}
</section>
{{end}}

@ -0,0 +1,67 @@
{{template "base" .}}
{{define "title"}}News &bull; PHC{{end}}
{{define "body"}}
<section>
<h2>
Feed RSS
<a href="/news/rss"> <i class="fa-solid fa-rss"></i></a>
</h2>
<h1>
<i class="far fa-newspaper"></i>
Notizie Importanti
</h1>
<div class="card-list">
{{ range .Articles }}
{{ if .HasTag "important" }}
<div class="card">
<div class="title">
<a href="/news/{{ .Id }}">
{{ .Title }}
</a>
</div>
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
<div class="description">
<p>{{ .Description }}</p>
</div>
<div class="tags">
{{ range .Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
</div>
</section>
<section>
<h2>
<i class="fas fa-history"></i>
Archivio notizie
</h2>
<div class="card-list">
{{ range .Articles }}
{{ if not (.HasTag "important") }}
<div class="card">
<div class="title">
<a href="/news/{{ .Id }}">
{{ .Title }}
</a>
</div>
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
<div class="description">
<p>{{ .Description }}</p>
</div>
<div class="tags">
{{ range .Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
</div>
</section>
{{end}}

@ -0,0 +1,88 @@
{{ define "navbar" }}
<nav>
<!-- Site -->
<div class="nav-logo">
<a class="nav-element" href="/">
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
</a>
</div>
<div class="nav-main">
<div class="nav-item">
<a class="nav-element" href="/utenti">Utenti</a>
</div>
<div class="nav-item dropdown">
<div class="name">
<div class="nav-element">
<div class="icon">
<i class="fas fa-chevron-down"></i>
</div>
<div class="label">
Progetti
</div>
</div>
</div>
<div class="nav-items">
<div class="nav-item">
<a class="nav-element" href="{{ .Config.GitUrl }}">Gitea</a>
</div>
<div class="nav-item">
<a class="nav-element" href="{{ .Config.ChatUrl }}">Zulip</a>
</div>
<div class="nav-item">
<a class="nav-element" href="/seminari">Seminari</a>
</div>
</div>
</div>
<div class="nav-item dropdown">
<div class="name">
<div class="nav-element">
<div class="icon">
<i class="fas fa-chevron-down"></i>
</div>
<div class="label">
Risorse
</div>
</div>
</div>
<div class="nav-items">
<div class="nav-item">
<a class="nav-element" href="/news">News</a>
</div>
<div class="nav-item">
<a class="nav-element" href="/appunti">Appunti</a>
</div>
<div class="nav-item">
<a class="nav-element" href="/guide">Guide</a>
</div>
<div class="nav-item">
<a class="nav-element" href="/link">Link Utili</a>
</div>
</div>
</div>
<div class="nav-item">
<a class="nav-element" href="/storia">Storia</a>
</div>
<!-- <div class="nav-item">
<a class="nav-element" href="/about">About</a>
</div> -->
<div class="nav-item filler"></div>
<!-- User Related -->
<div class="nav-item">
<div id="toggle-dark-mode" class="nav-button">
<i class="fas fa-moon"></i>
</div>
</div>
{{if .User}}
<div class="nav-item">
<a class="nav-element" href="/profilo">@{{ .User.Username }}</a>
</div>
{{else}}
<div class="nav-item">
<a class="nav-element" href="/login">Accedi</a>
</div>
{{end}}
</div>
</nav>
{{ end }}

@ -0,0 +1,105 @@
{{template "base" .}}
{{define "title"}}Profilo di @{{ .User.Username }} &bull; PHC{{end}}
{{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/public/profilo.min.js"></script>
<section x-data="profilo">
<h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
<p class="center">
<a class="button" href="/logout">Logout</a>
</p>
</section>
<section>
<h2>Appunti e Dispense</h2>
<p>
Per gestire i tuoi appunti e le tue dispense caricate nella sezione <a href="/appunti">Appunti</a> del sito vai
<a href="/appunti/condivisi">alla pagina appunti condivisi</a>.
</p>
</section>
<section>
<h2>Impostazioni</h2>
<div class="card-list">
<form class="card" action="/profile/impostazioni" method="POST">
<div class="title">
Sito PHC
</div>
<p>
Aggiorna i campi relativi al sito del PHC, l'immagine del profilo verrà visualizzata sulla tua pagina utente <a href="#">phc.dm.unipi.it/u/{{ .User.Username }}</a> e nella lista di <a href="/utenti">tutti gli utenti</a>.
</p>
<div class="field-set">
<label for="website-nickname">Nickname</label>
<input type="text" name="nickname" id="website-nickname" placeholder="{{ .User.Username }}">
<label for="website-profile-picture">Immagine Profilo</label>
<input type="file" name="picture" id="website-profile-picture">
</div>
<p>
Puoi anche aggiungere link ad altri tuoi profili su siti esterni (quando riempi l'ultimo campo se ne crea uno nuovo sotto)
</p>
<div class="field-set">
<label for="website-link-1">Link 1</label>
<input type="text" name="link" id="website-link-1"
placeholder="https://github.com/...">
<label for="website-link-2">Link 2</label>
<input type="text" name="link" id="website-link-2"
placeholder="https://twitter.com/...">
<label for="website-link-3">Link 3</label>
<input type="text" name="link" id="website-link-3"
placeholder="https://news.ycombinator.com/user?id=...">
</div>
<div class="field">
<button class="primary">Aggiorna</button>
</div>
</form>
<form class="card" action="/profile/impostazioni" method="POST">
<div class="title">
Informazioni Utente
</div>
<p>
Aggiorna i campi modificabili dell'account Poisson
</p>
<div class="field-set">
<label for="user-username">Username</label>
<input type="text" readonly name="username" id="user-username" value="{{ .User.Username }}">
<label for="user-name">Nome</label>
<input type="text" readonly name="name" id="user-name" value="{{ .User.Name }}">
<label for="user-surname">Cognome</label>
<input type="text" readonly name="surname" id="user-surname" value="{{ .User.Surname }}">
<label for="user-email">Email</label>
<input type="text" readonly name="email" id="user-email" value="{{ .User.Email }}">
</div>
<div class="field">
<button class="primary">Aggiorna</button>
</div>
</form>
<form x-data="passwordForm" class="card" action="/profile/impostazioni" method="POST">
<div class="title">
Modifica Password
</div>
<p>
Aggiorna la password di Poisson
</p>
<div class="field-set">
<label for="pass-username">Username</label>
<input type="text" readonly name="username" id="pass-username" value="{{ .User.Username }}">
<label for="pass-password">Password</label>
<input type="password" name="password" id="pass-password" :class="!passwordSame ? 'error' : ''"
x-model="password" @input="onUpdate" placeholder="In realtà anche questo non funge...">
<label for="pass-password-2">Ripeti Password</label>
<input type="password" name="password-2" id="pass-password-2" :class="!passwordSame ? 'error' : ''"
x-model="passwordAgain" @input="onUpdate" placeholder="In realtà anche questo non funge...">
</div>
<div class="field">
<button class="primary">Aggiorna Password</button>
</div>
</form>
</div>
</section>
<section>
<h2>Recupero Credenziali Poisson</h2>
<p>
Per il recupero credenziali, puoi venire direttamente in PHC a parlarne con noi, oppure inviarci una email
all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul
sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
</p>
</section>
{{end}}

@ -0,0 +1,61 @@
{{template "base" .}}
{{define "title"}}Storia &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-clock"></i>
Storia del PHC
</h1>
<p>
Qui è annoverata la storia del PHC tramite una linea temporale di alcuni fra gli eventi principali. Al momento è un <i>work in progress</i>, ma aggiungeremo almeno le date di ingresso di tutti i macchinisti.
</p>
<p>
Per delle note storiche un po' più dettagliate, si legga l'ottima pagine sul sito del dipartimento:
<a href="http://betti.dm.unipi.it/servizi/PHC.html">http://betti.dm.unipi.it/servizi/PHC.html</a>.
</p>
<div class="history-container">
<div class="timeline-bar"></div>
<div class="events">
{{ range .Storia }}
{{ if eq .Type "simple" }}
<div class="event">
<div class="title">
{{ if .Icon }}
<i class="{{ .Icon }}"></i>
{{ end }}
{{ .Title }}
</div>
<div class="date">{{ .Date }}</div>
<div class="description">
{{ raw .Description }}
</div>
</div>
{{ end }}
{{ if eq .Type "entry-macchinista" }}
<div class="event">
<div class="title">
<i class="fa-solid fa-sign-in"></i>
Ingresso di
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
</div>
<div class="date">{{ .Date }}</div>
</div>
{{ end }}
{{ if eq .Type "exit-macchinista" }}
<div class="event">
<div class="title">
<i class="fa-solid fa-sign-out"></i>
Uscita di
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
</div>
<div class="date">{{ .Date }}</div>
</div>
{{ end }}
{{ if eq .Type "spacer" }}
<div class="spacer" style="--size: {{ .Size }}"></div>
{{ end }}
{{ end }}
</div>
</div>
</section>
{{end}}

@ -0,0 +1,64 @@
{{template "base" .}}
{{define "title"}}Utenti &bull; PHC{{end}}
{{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
<script src="/public/utenti.min.js"></script>
<section x-data="utenti">
<h1>
<i class="fas fa-users"></i>
Lista degli Utenti
</h1>
<p>
Questa è la lista di tutti gli utenti con un account su Poisson. Scrivi nome, cognome o
username di un utente per filtrare la lista in tempo reale. Altrimenti di base in cima
compariranno gli utenti con più "follower".
</p>
<div class="search">
<div class="compound padded">
<div class="icon" title="Ordina per">
<i class="fas fa-sort"></i>
</div>
<div class="divider"></div>
<select x-model="sortMode" @click="updateSortMode()">
<option value="chronological">Cronologico</option>
<option value="name">Nome</option>
<option value="surname">Cognome</option>
</select>
</div>
<div class="compound">
<input type="text" x-model="searchField" @input="updateSearch()" placeholder="Cerca..." autocomplete="off" />
<button class="icon" title="In realtà questo tasto è finto">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="user-list card-list">
<template x-for="entry in searchResults" :key="entry.item.uid">
<div class="user-item card">
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
sortMode === 'surname' ?
entry.item.cognome + ' ' + entry.item.nome :
entry.item.nome + ' ' + entry.item.cognome
"></a>
<template x-if="entry.item.tags?.includes('macchinista')">
<div class="icon" title="Macchinista">
<i class="fas fa-wrench"></i>
</div>
</template>
<template x-if="entry.item.tags?.includes('rappresentante')">
<div class="icon" title="Rappresentante">
<i class="fas fa-landmark"></i>
</div>
</template>
<!-- <template x-if="entry.score">
<span x-text="entry.score"></span>
</template> -->
</div>
</template>
<div class="spinner" x-ref="spinner" x-show="searchResults.length < searchResultsBuffer.length">
<i class="fas fa-hourglass"></i>
</div>
</div>
</section>
{{end}}

@ -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 := Markdown.Convert([]byte(article.markdownSource), &buf); err != nil {
return "", err
}
article.renderedHTML = buf.String()
}
return article.renderedHTML, nil
}

@ -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
}

@ -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 Markdown 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() {
Markdown = 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(),
),
)
}

@ -1,18 +0,0 @@
import { defineConfig } from 'astro/config'
import preact from '@astrojs/preact'
import mdx from '@astrojs/mdx'
// https://astro.build/config
export default defineConfig({
server: {
port: 3000,
},
markdown: {
shikiConfig: {
theme: 'github-light',
},
},
integrations: [preact(), mdx()],
output: 'static'
})

@ -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 newLDAPAuthService(host)
}

@ -0,0 +1,146 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
urlpkg "net/url"
"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 {
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
if err != nil {
return err
}
req, err := http.NewRequest(
"GET", u, 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
}
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
if err != nil {
return err
}
req, err := http.NewRequest("POST", u, 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
}

@ -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
}

Binary file not shown.

@ -0,0 +1,47 @@
package main
import (
"log"
"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/handler"
"git.phc.dm.unipi.it/phc/website/lista_utenti"
"git.phc.dm.unipi.it/phc/website/server"
"git.phc.dm.unipi.it/phc/website/storia"
"git.phc.dm.unipi.it/phc/website/templates"
)
func main() {
config.Load()
authService := auth.NewDefaultService(config.AuthServiceHost)
listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti)
if err != nil {
log.Fatal(err)
}
h := &handler.DefaultHandler{
AuthService: authService,
Renderer: templates.NewRenderer(
"./_views/",
"./_views/base.html",
"./_views/partials/*.html",
),
NewsArticlesRegistry: articles.NewRegistry("./_content/news"),
GuideArticlesRegistry: articles.NewRegistry("./_content/guide"),
Storia: &storia.JsonFileStoria{
Path: "./_content/storia.yaml",
},
ListaUtenti: listaUtentiService,
}
app := server.NewFiberServer(h)
log.Printf("Starting server on host %q", config.Host)
if err := app.Listen(config.Host); err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,83 @@
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 BaseUrl string
var UserPagesBaseUrl string
var AuthServiceHost string
// ListaUtenti è un file json da cui leggere la lista degli utenti oppure ":auth:" per utilizzare il servizio di autenticazione
var ListaUtenti 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")
loadEnv(&BaseUrl, "BASE_URL", "localhost: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/~")
// Auth
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
// Altro
loadEnv(&ListaUtenti, "LISTA_UTENTI", ":auth:")
}
func Object() util.Map {
return util.Map{
"Mode": Mode,
"Host": Host,
"GitUrl": GitUrl,
"ChatUrl": ChatUrl,
"Email": Email,
"BaseUrl": BaseUrl,
"UserPagesBaseUrl": UserPagesBaseUrl,
"AuthServiceHost": AuthServiceHost,
"ListaUtenti": ListaUtenti,
}
}

@ -0,0 +1,26 @@
module git.phc.dm.unipi.it/phc/website
go 1.19
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/gorilla/feeds v1.1.1 // 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
)

@ -0,0 +1,78 @@
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/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
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=

@ -0,0 +1,21 @@
package handler
type ContextKey[T any] string
type Context map[string]any
func GetContextValue[T any](ctx Context, key ContextKey[T]) T {
value, present := ctx[string(key)]
if !present {
var zero T
return zero
}
typedValue, _ := value.(T)
return typedValue
}
func SetContextValue[T any](ctx Context, key ContextKey[T], value T) {
ctx[string(key)] = value
}

@ -0,0 +1,223 @@
package handler
import (
"fmt"
"html/template"
"io"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/lista_utenti"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/rss"
"git.phc.dm.unipi.it/phc/website/storia"
"git.phc.dm.unipi.it/phc/website/templates"
"git.phc.dm.unipi.it/phc/website/util"
)
type Service interface {
HandleStaticPage(w io.Writer, view string, ctx Context) error
HandleUtenti() ([]*model.User, error)
HandleListaUtenti() ([]*model.ListUser, error)
HandleStoriaPage(w io.Writer, ctx Context) error
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
HandleNewsPage(w io.Writer, ctx Context) error
HandleGuidePage(w io.Writer, ctx Context) error
HandleLogin(username, password string) (*model.Session, error)
HandleUser(token string) *model.User
HandleRequiredUser(ctx Context) (*model.User, error)
HandleProfilePage(w io.Writer, ctx Context) error
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
HandleNewsFeedPage(w io.Writer) error
HandleGuideFeedPage(w io.Writer) error
}
//
// Typed context
//
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
const UserKey ContextKey[*model.User] = "user"
func (ctx Context) getUser() *model.User {
return GetContextValue(ctx, UserKey)
}
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
type DefaultHandler struct {
AuthService auth.Service
Renderer *templates.TemplateRenderer
NewsArticlesRegistry *articles.Registry
GuideArticlesRegistry *articles.Registry
ListaUtenti lista_utenti.Service
Storia storia.StoriaService
}
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
return h.Renderer.Render(w, view, util.Map{
"User": ctx.getUser(),
})
}
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
utenti, err := h.AuthService.GetUsers()
if err != nil {
return nil, err
}
return utenti, nil
}
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
utenti, err := h.ListaUtenti.GetUtenti()
if err != nil {
return nil, err
}
return utenti, nil
}
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
storia, err := h.Storia.GetStoria()
if err != nil {
return err
}
return h.Renderer.Render(w, "storia.html", util.Map{
"User": ctx.getUser(),
"Storia": storia,
})
}
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
return h.Renderer.Render(w, "appunti.html", util.Map{
"User": ctx.getUser(),
"Query": query,
})
}
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
"User": ctx.getUser(),
})
}
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
articles, err := h.NewsArticlesRegistry.GetArticles()
if err != nil {
return err
}
return h.Renderer.Render(w, "news.html", util.Map{
"User": ctx.getUser(),
"Articles": articles,
})
}
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
articles, err := h.GuideArticlesRegistry.GetArticles()
if err != nil {
return err
}
return h.Renderer.Render(w, "guide.html", util.Map{
"User": ctx.getUser(),
"Articles": articles,
})
}
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
session, err := h.AuthService.Login(username, password)
if err != nil {
return nil, err
}
return session, nil
}
func (h *DefaultHandler) HandleUser(token string) *model.User {
user, _ := auth.UserForSession(h.AuthService, token)
return user
}
var ErrNoUser = fmt.Errorf(`user not logged in`)
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
user := ctx.getUser()
if user == nil {
return nil, ErrNoUser
}
return user, nil
}
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
user := ctx.getUser()
if user == nil {
return ErrNoUser
}
return h.Renderer.Render(w, "profilo.html", util.Map{
"User": user,
})
}
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
return h.Renderer.Render(w, "news-base.html", util.Map{
"User": ctx.getUser(),
"Article": article,
"ContentHTML": template.HTML(html),
})
}
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
return h.Renderer.Render(w, "guide-base.html", util.Map{
"User": ctx.getUser(),
"Article": article,
"ContentHTML": template.HTML(html),
})
}
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
registry, err := h.NewsArticlesRegistry.GetArticles()
if err != nil {
return err
}
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
return newsFeed.WriteRss(w)
}
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
registry, err := h.GuideArticlesRegistry.GetArticles()
if err != nil {
return err
}
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
return guideFeed.WriteRss(w)
}

@ -0,0 +1,50 @@
package lista_utenti
import (
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type authListaUtenti struct {
AuthService auth.Service
Macchinisti util.Set[string]
Rappresentanti util.Set[string]
}
func newAuthListaUtenti(authService auth.Service) (Service, error) {
macchinisti, err := loadMacchinisti()
if err != nil {
return nil, err
}
rappresentanti, err := loadRappresentanti()
if err != nil {
return nil, err
}
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
}
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
authUsers, err := a.AuthService.GetUsers()
if err != nil {
return nil, err
}
users := make([]*model.ListUser, 0, len(authUsers))
for _, u := range authUsers {
users = append(users, &model.ListUser{
Uid: u.Username,
Nome: u.Name,
Cognome: u.Surname,
Tags: []string{},
})
}
mergeMacchinisti(users, a.Macchinisti)
mergeRappresentanti(users, a.Rappresentanti)
return users, nil
}

@ -0,0 +1,51 @@
package lista_utenti
import (
"encoding/json"
"log"
"os"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type jsonListaUtenti struct {
Path string
Macchinisti util.Set[string]
Rappresentanti util.Set[string]
}
func newJsonListaUtenti(path string) (Service, error) {
macchinisti, err := loadMacchinisti()
if err != nil {
return nil, err
}
rappresentanti, err := loadRappresentanti()
if err != nil {
return nil, err
}
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
}
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
var users []*model.ListUser
f, err := os.Open(j.Path)
if err != nil {
return nil, err
}
if err := json.NewDecoder(f).Decode(&users); err != nil {
return nil, err
}
mergeMacchinisti(users, j.Macchinisti)
mergeRappresentanti(users, j.Rappresentanti)
log.Printf("Caricata lista di %d utenti", len(users))
return users, nil
}

@ -0,0 +1,69 @@
package lista_utenti
import (
"encoding/json"
"os"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type Service interface {
GetUtenti() ([]*model.ListUser, error)
}
// New crea un nuovo servizio che lista gli utenti utilizzando AuthService se config è ":auth:" oppure un file json
func New(authService auth.Service, config string) (Service, error) {
if config == ":auth:" {
return newAuthListaUtenti(authService)
}
return newJsonListaUtenti(config)
}
func loadMacchinisti() (util.Set[string], error) {
f, err := os.Open("_content/macchinisti.json")
if err != nil {
return nil, err
}
macchinisti := []string{}
if err := json.NewDecoder(f).Decode(&macchinisti); err != nil {
return nil, err
}
return util.NewSet(macchinisti...), nil
}
func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
for _, user := range users {
if _, found := macchinisti[user.Uid]; found {
user.Tags = append(user.Tags, "macchinista")
}
}
}
func loadRappresentanti() (util.Set[string], error) {
f, err := os.Open("_content/rappresentanti.json")
if err != nil {
return nil, err
}
rappresentanti := []string{}
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
return nil, err
}
return util.NewSet(rappresentanti...), nil
}
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
for _, user := range users {
if _, found := rappresentanti[user.Uid]; found {
user.Tags = append(user.Tags, "rappresentante")
}
}
}

@ -0,0 +1,37 @@
package model
type ListUser struct {
Uid string `json:"uid"`
Nome string `json:"nome"`
Cognome string `json:"cognome"`
Tags []string `json:"tags,omitempty"`
}
// 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"`
}

@ -1,44 +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": "^8.3.4",
"@astrojs/preact": "^3.5.3",
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
"@fontsource/iosevka": "^5.0.11",
"@fontsource/mononoki": "^5.0.11",
"@fontsource/open-sans": "^5.0.24",
"@fontsource/source-code-pro": "^5.0.16",
"@fontsource/source-sans-pro": "^5.0.8",
"@fontsource/space-mono": "^5.0.20",
"@preact/signals": "^1.3.0",
"@types/jsdom": "^21.1.7",
"astro": "^4.15.11",
"fuse.js": "^7.0.0",
"katex": "^0.16.9",
"preact": "^10.19.6",
"typescript": "^5.3.3"
},
"devDependencies": {
"@astrojs/mdx": "^3.1.7",
"@types/katex": "^0.16.7",
"jsdom": "^24.1.1",
"linkedom": "^0.18.4",
"npm-run-all": "^4.1.5",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-math": "^6.0.0",
"remark-toc": "^9.0.0",
"sass": "^1.71.1",
"tsx": "^4.7.1"
}
}

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

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

Before

Width:  |  Height:  |  Size: 344 B

@ -0,0 +1,34 @@
package rss
import (
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/config"
"github.com/gorilla/feeds"
)
func GenerateRssFeed(entries []*articles.Article, sectionName string, title string, link string, description string) *feeds.Feed {
// Initialize RSS Feed
feed := &feeds.Feed{
Title: title,
Link: &feeds.Link{Href: link},
Description: description,
}
var feedItems []*feeds.Item
// Add items to RSS feeds
for _, entry := range entries {
feedItems = append(feedItems,
&feeds.Item{
Id: entry.Id,
Title: entry.Title,
Link: &feeds.Link{Href: config.BaseUrl + "/" + sectionName + "/" + entry.Id},
Description: entry.Description,
Created: entry.PublishDate,
})
}
feed.Items = feedItems
return feed
}

@ -0,0 +1,202 @@
package server
import (
"time"
"git.phc.dm.unipi.it/phc/website/handler"
"git.phc.dm.unipi.it/phc/website/model"
"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(h handler.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Cookies("session-token")
user := h.HandleUser(token)
c.Locals("user", user)
return c.Next()
}
}
func CreateContext(ctx *fiber.Ctx) handler.Context {
// the "_" here is required because Go cannot cast <nil> of type interface{} to *model.User, in other words <nil> of type *model.User and <nil> of type interface{} are different type. In this case the "_" returns a boolean that tells whether the cast succeeded or not, if it is false the user variable gets assigned its default zero value that is <nil> of type *model.User
user, _ := ctx.Locals("user").(*model.User)
context := handler.Context{}
handler.SetContextValue(context, handler.UserKey, user)
return context
}
func NewFiberServer(h handler.Service) *fiber.App {
app := fiber.New()
routes(h, app)
return app
}
// routes defines routes under "/"
func routes(h handler.Service, r fiber.Router) {
//
// Initial setup
//
r.Use(logger.New())
r.Use(recover.New())
// Remove trailing slash from URLs
r.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/*/": "/$1",
},
}))
// Serve generated css and js files and the static "./_public" folder
r.Static("/public/", "./_frontend/out")
r.Static("/public/", "./_public")
// Process all request and add user to the request context if there is a session cookie
r.Use(UserMiddleware(h))
//
// Pages
//
r.Get("/", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "home.html", CreateContext(c))
})
r.Get("/link", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "link.html", CreateContext(c))
})
r.Get("/login", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "login.html", CreateContext(c))
})
r.Get("/utenti", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "utenti.html", CreateContext(c))
})
r.Get("/storia", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStoriaPage(c, CreateContext(c))
})
r.Get("/appunti", func(c *fiber.Ctx) error {
query := c.Query("q", "")
c.Type("html")
return h.HandleQueryAppunti(c, query, CreateContext(c))
})
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleAppuntiCondivisiPage(c, CreateContext(c))
})
r.Get("/news", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleNewsPage(c, CreateContext(c))
})
r.Get("/guide", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleGuidePage(c, CreateContext(c))
})
r.Get("/news/rss", func(c *fiber.Ctx) error {
c.Type("xml")
return h.HandleNewsFeedPage(c)
})
r.Get("/guide/rss", func(c *fiber.Ctx) error {
c.Type("xml")
return h.HandleGuideFeedPage(c)
})
r.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 := h.HandleLogin(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")
})
r.Get("/profilo", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleProfilePage(c, CreateContext(c))
})
r.Get("/logout", func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: "",
Expires: time.Now(),
})
return c.Redirect("/")
})
r.Get("/news/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
c.Type("html")
return h.HandleNewsArticlePage(c, articleID, CreateContext(c))
})
r.Get("/guide/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
c.Type("html")
return h.HandleGuideArticlePage(c, articleID, CreateContext(c))
})
routesApi(h, r.Group("/api"))
}
// routesApi defines routes under "/api"
func routesApi(h handler.Service, r fiber.Router) {
r.Get("/utenti", func(c *fiber.Ctx) error {
utenti, err := h.HandleListaUtenti()
if err != nil {
return err
}
return c.JSON(utenti)
})
r.Get("/profilo", func(c *fiber.Ctx) error {
user, err := h.HandleRequiredUser(CreateContext(c))
if err != nil {
return err
}
return c.JSON(user)
})
}

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: 2.0 KiB

@ -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>
}

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

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

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

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

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

@ -1,15 +0,0 @@
---
import type { AstroBuiltinProps } from 'astro'
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
type Props = {
large?: boolean
style?: string
}
const { large, ...props } = Astro.props
---
<div class:list={['card', large && 'large']} {...props}>
<slot />
</div>

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

Loading…
Cancel
Save