41 Commits
Author SHA1 Message Date
BachoSeven eeba6356af Update README.md 2022-09-19 21:48:21 +02:00
BachoSeven 23757ad9b0 dark-theme: fix bg-darker-2-1 2022-09-18 16:17:59 +02:00
BachoSeven 9d518dd4f8 /profilo: use full name 2022-09-18 16:06:35 +02:00
BachoSeven 3ca159e809 auth/ldap.go: remove excess debug logging 2022-09-18 15:15:18 +02:00
BachoSeven 855bc8fa25 auth: fix typo 2022-09-18 13:24:00 +02:00
BachoSeven f7350bc6a8 lista_utenti: fix typo 2022-09-18 13:08:06 +02:00
BachoSeven 4b12c3d59d lista_utenti: handle rappresentanti in :auth: 2022-09-18 13:05:43 +02:00
BachoSeven 47a9d32670 Minor fix in README 2022-09-18 12:47:19 +02:00
BachoSeven 6f308377ef Use url.JoinPath instead of path.Join for URLs. 2022-09-18 01:06:53 +02:00
BachoSeven d7f9f2935d home: start filling descriptions 2022-09-18 01:06:10 +02:00
BachoSeven ddbf06d7d0 don't make undefined routes clickable; fix comment 2022-09-17 22:26:47 +02:00
BachoSeven cd0383245b lista_utenti: add rappresentanti to the user tags. 2022-09-17 22:26:03 +02:00
BachoSeven cff45dc92f Add illya 2022-09-01 15:58:35 +02:00
BachoSeven 346459f822 Minor edits 2022-08-28 18:51:46 +02:00
aziis98 6cccf78e45 Aggiunto supporto per il markdown alle descrizioni degli item in storia e convertito a YAML 2022-08-28 18:45:45 +02:00
aziis98 192d8ea944 chore: Rimosso file utenti.go e spostati file .json in _content/ 2022-08-28 18:11:08 +02:00
aziis98 805d9a7ba5 Estratto il servizio lista utenti e qualche altra modifica 2022-08-28 17:58:44 +02:00
BachoSeven 85a0a21d15 feat: update users via Ldap 2022-08-28 12:23:24 +02:00
BachoSeven 4be4a68912 storia.html: Fix typo 2022-08-28 12:22:52 +02:00
BachoSeven 5decb85552 chore: Refactor storia.go into its own module. 2022-08-28 11:16:07 +02:00
BachoSeven 3d0bd18aef favicon; revamp ed aggiunte a storia.html, rimando alla pagina PHC su
betti.dm.unipi.it.
2022-08-28 01:31:57 +02:00
aziis98 489d7f161d Alcuni update al README e watcher per la frontend 2022-08-27 23:39:13 +02:00
aziis98 6e90f8c32f Aggiunta pagina per appunti condivisi con setup minimale di Preact 2022-08-27 22:58:13 +02:00
aziis98 5337849bc7 Ora il default in .env.dev è lo stesso di config.go 2022-08-27 22:55:59 +02:00
BachoSeven f0a9a17fc0 feed: Fix articles' links' URLs 2022-08-27 22:12:47 +02:00
BachoSeven 1ddc71f64b feeds: fix articles Link(s) 2022-08-27 22:05:01 +02:00
BachoSeven 684859c197 Finalize/fix RSS feeds 2022-08-27 21:35:05 +02:00
BachoSeven c1a701353a feat: RSS feeds for {news,guide} 2022-08-27 20:17:08 +02:00
BachoSeven 9c10e1b8fc feat: Add guide page 2022-08-27 12:14:24 +02:00
BachoSeven 9553dbe252 storia: add Icon and Description fields. 2022-08-26 17:46:38 +02:00
BachoSeven 03bae38ebe profilo.html: fix wording 2022-08-26 17:46:09 +02:00
BachoSeven f875c9c6bf base.html: add e-mail icon 2022-08-26 17:45:35 +02:00
BachoSeven cfb62677db Cleanup .gitignore 2022-08-26 17:45:07 +02:00
aziis98 67f8421516 Removed old homepage animation script 2022-08-19 18:37:57 +02:00
aziis98 a5039610e1 Update README 2022-08-19 18:32:22 +02:00
aziis98 1f90dacbdd Update frontend 2022-08-19 18:30:22 +02:00
aziis98 c9d02d22dd Updated README 2022-08-19 17:23:45 +02:00
aziis98 0f7bde6232 Updated gitignore 2022-08-19 15:14:48 +02:00
aziis98 860cc76503 Minor fixes 2022-08-18 18:26:38 +02:00
aziis98 227619e5eb Still refactoring, better _frontend and _public structuring 2022-08-18 18:11:51 +02:00
aziis98 92cee16cc8 Big refactor 2022-08-18 17:08:05 +02:00
86 changed files with 3566 additions and 2367 deletions
+8 -1
View File
@@ -1,6 +1,6 @@
# Server config # Server config
MODE=development MODE=development
HOST=localhost:8000 HOST=:8080
# Separate services # Separate services
GIT_URL=https://git.phc.dm.unipi.it GIT_URL=https://git.phc.dm.unipi.it
@@ -9,7 +9,14 @@ CHAT_URL=https://chat.phc.dm.unipi.it
# Other # Other
EMAIL=macchinisti@lists.dm.unipi.it EMAIL=macchinisti@lists.dm.unipi.it
# Base URL
BASE_URL=localhost:8080
# Lista Utenti
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~ USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
# AuthService # AuthService
AUTH_SERVICE_HOST=:memory: AUTH_SERVICE_HOST=:memory:
# Origine per Lista Utenti
LISTA_UTENTI=utenti-poisson-2022.local.json
+6 -8
View File
@@ -1,19 +1,17 @@
# Environment files # Environment files
.env .env
# Miscellaneous
tags
# Local files # Local files
.vscode/
*.local* *.local*
# NodeJS # Generated output
out/
dist/ dist/
node_modules/
# Don't version generated files # NodeJS
public/css/ node_modules/
public/js/
# Executables # Executables
phc-website-server phc-website-server
!phc-website-server/
+19 -15
View File
@@ -1,25 +1,29 @@
FROM node:18 AS frontend-builder FROM node:18-alpine AS frontend-builder
WORKDIR /frontend RUN apk add make
COPY ./frontend/package.json ./package.json WORKDIR /_frontend
COPY ./frontend/package-lock.json ./package-lock.json RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
RUN npm install COPY ./_frontend/pnpm-lock.yaml ./
COPY ./frontend . RUN pnpm fetch --prod
RUN make COPY ./_frontend ./
RUN pnpm install -r --offline --prod
RUN make build
FROM golang:1.18-alpine AS backend-builder FROM golang:1.19-alpine AS backend-builder
RUN apk add make
WORKDIR /backend WORKDIR /backend
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download -x RUN go mod download -x
COPY . . COPY ./ ./
RUN go build -buildvcs=false -o website-server -v . RUN make backend
FROM alpine:latest AS runner FROM alpine:latest AS runner
WORKDIR /app WORKDIR /app
COPY --from=frontend-builder /frontend/dist ./frontend/dist COPY --from=frontend-builder /_frontend/out ./_frontend/out
COPY --from=backend-builder /backend/website-server ./website-server COPY --from=backend-builder /backend/phc-website-server ./phc-website-server
COPY ./views ./views COPY ./_views ./_views
COPY ./news ./news COPY ./_content/news ./_content/news
COPY ./public ./public COPY ./_content/guide ./_content/guide
COPY ./_public ./_public
COPY ./.env ./.env COPY ./.env ./.env
EXPOSE 8000 EXPOSE 8000
CMD ["./website-server"] CMD ["./website-server"]
+13 -15
View File
@@ -3,33 +3,31 @@ GO_SOURCES = $(shell find . -name '*.go')
GO_EXECUTABLE = phc-website-server GO_EXECUTABLE = phc-website-server
.PHONY: all .PHONY: all
all: frontend go all: frontend backend
# #
# Build Frontend # Build Frontend
# #
.PHONY: setup
setup:
mkdir -p public/js
mkdir -p public/css
.PHONY: frontend .PHONY: frontend
frontend: setup frontend:
$(MAKE) -C frontend/ $(MAKE) -C _frontend/
cp frontend/dist/*.min.js public/js/ @echo "Built Frontend"
cp frontend/dist/*.css public/css/
# #
# Build Server # Build Backend
# #
.PHONY: go .PHONY: backend
go: $(GO_EXECUTABLE) backend: $(GO_EXECUTABLE)
@echo "Compiled Server" @echo "Built Backend"
$(GO_EXECUTABLE): $(GO_SOURCES) $(GO_EXECUTABLE): $(GO_SOURCES)
go build -o $(GO_EXECUTABLE) . go build -o $(GO_EXECUTABLE) ./cmd/phc-website-server
#
# Debug
#
.PHONY: debug .PHONY: debug
debug: debug:
+141 -60
View File
@@ -1,77 +1,142 @@
# Nuovo Sito PHC # phc/website
Repo del server del nuovo sito per il PHC. Backend e frontend del nuovo sito per il PHC.
## Dipendenze ## Usage
- **Golang** To setup the project
- `github.com/alecthomas/chroma` ```bash shell
# Clone the repo
- `github.com/alecthomas/repr`
- `github.com/go-chi/chi/v5`
- `github.com/joho/godotenv`
- `github.com/litao91/goldmark-mathjax`
- `github.com/yuin/goldmark`
- `github.com/yuin/goldmark-highlighting`
- `gopkg.in/yaml.v3`
- **NodeJS**
- AlpineJS
- FuseJS
## Setup
Per ottenere il progetto basta fare
```bash
$ git clone https://git.phc.dm.unipi.it/phc/website $ git clone https://git.phc.dm.unipi.it/phc/website
$ cd frontend/ $ cd _frontend
frontend/ $ npm install $ make setup
frontend/ $ cd ..
$ make frontend
``` ```
## Development To just do a full build and start the project run
### Setup
Copiare il file `.env.dev` in `.env` per dire al server di lavorare in modalità di development e su quale indirizzo servire il sito, poi avviare il server.
```bash shell ```bash shell
# Setup local development env file
$ cp .env.dev .env $ cp .env.dev .env
$ go run .
# Setup and do a full build of frontend and backend
$ make all
# Compile and start the server
$ go run -v ./cmd/phc-website-server
``` ```
### Server Alternativamente se si sta modificando in live il codice è comodo usare [`entr`](https://github.com/eradman/entr) e `fd` (un'alternativa a `find`)
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
```bash shell ```bash shell
$ find . -type f -name '*.go' | entr -r go run . # Restart server when go files change
# O anche con fd... $ printf '%s\n' $(echo **/*.go) | entr -r go run ./cmd/phc-website-server
$ fd -e go | entr -r go run .
``` ```
### Frontend 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
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
```bash shell ```bash shell
$ make frontend # Recompile files inside "_frontend/src" on change
# O anche con un watcher... _frontend/ $ pnpm run watch
$ fd -e js | entr make frontend
``` ```
### Environment Variables ## 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` - `MODE`
@@ -79,21 +144,37 @@ $ fd -e js | entr make frontend
- `HOST` - `HOST`
Indirizzo (locale) sul quale servire il sito, di default è `localhost:8000`. Indirizzo sul quale servire il sito, di default è `:8080`.
- `MAIL` - `EMAIL`
Indirizzo di posta elettronica per contattare gli ammistratori del sito, Indirizzo di posta elettronica per contattare gli amministratori del sito,
che compare nel footer di ogni pagina. che compare nel footer di ogni pagina.
- `<SERVIZIO>_URL` - `<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). Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale).
Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`. 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/~`
- `AUTH_SERVICE_HOST`
Indirizzo del servizio generico di autenticazione.
## Altri Servizi ## Altri Servizi
Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi. Questo progetto dipende dal servizio `phc/auth-service` che permettere agli utenti di autenticarsi usando vari meccanismi.
Il servizio di autenticazione di default girerà su `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/) 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/)
+19
View File
@@ -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
+31
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
["delucreziis", "serdyuk", "minnocci", "manicastri"]
+1
View File
@@ -0,0 +1 @@
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
+38
View File
@@ -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

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+23
View File
@@ -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/
+35
View File
@@ -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"
}
}
+1634
View File
File diff suppressed because it is too large Load Diff
+74
View File
@@ -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(),
],
},
])
+24
View File
@@ -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')
)
+41
View File
@@ -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()
})
+378
View File
@@ -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)
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"noImplicitAny": true,
"sourceMap": true,
"noEmitOnError": true
},
"filesGlob": ["./src/**/*.ts"]
}

Before

Width:  |  Height:  |  Size: 790 B

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

@@ -8,6 +8,7 @@ body.dark-mode {
--bg-dark: hsl(10, 10%, 20%); --bg-dark: hsl(10, 10%, 20%);
--bg-darker: hsl(10, 10%, 17%); --bg-darker: hsl(10, 10%, 17%);
--bg-darker-2: #1d2021; --bg-darker-2: #1d2021;
--bg-darker-2-1: #1d5021;
--bg-darker-3: #101111; --bg-darker-3: #101111;
--accent-1: #154d24; --accent-1: #154d24;
@@ -28,7 +29,6 @@ body.dark-mode {
--text-input-readonly-bg: hsl(10, 10%, 22%); --text-input-readonly-bg: hsl(10, 10%, 22%);
--text-input-readonly-fg: hsl(10, 10%, 40%); --text-input-readonly-fg: hsl(10, 10%, 40%);
--bg-darker-2-1: #c8c8c8;
--accent-2-lighter: #5cc969; --accent-2-lighter: #5cc969;
--accent-2: #4eaa59; --accent-2: #4eaa59;
--accent-2-darker: #2e974c; --accent-2-darker: #2e974c;
+14
View File
@@ -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}}
+44
View File
@@ -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}}
+18
View File
@@ -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}}
+37
View File
@@ -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}}
+20 -9
View File
@@ -3,7 +3,7 @@
{{define "title"}}Home &bull; PHC{{end}} {{define "title"}}Home &bull; PHC{{end}}
{{define "body"}} {{define "body"}}
<canvas id="circuit-pattern"></canvas> <canvas id="wires-animation"></canvas>
<!-- Non avevo molta fantasia per il nome di questa classe --> <!-- Non avevo molta fantasia per il nome di questa classe -->
<section class="super"> <section class="super">
@@ -14,8 +14,18 @@
<h1> <h1>
Cos'è il PHC? Cos'è il PHC?
</h1> </h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum vel maiores dolorum dicta, repellat nulla repellendus amet fuga voluptas molestiae, magni</p> <p>
<p>Voluptatibus sequi praesentium similique, quos ex illo autem quidem!</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> </div>
</section> </section>
@@ -90,18 +100,19 @@
</section> </section>
<hr> <hr>
<section> <section>
<!-- <h2>Altro...</h2> -->
<div class="card"> <div class="card">
<div class="title">Vuoi diventare un macchinista?</div> <div class="title">
<!-- <h3 style="font-weight: 500;">Vuoi diventare un macchinista?</h3> --> Vuoi diventare un macchinista?
<i class="fas fa-wrench"></i>
</div>
<div class="description"> <div class="description">
<p> <p>
Ti interessa (o interesserebbe) smanettare al PC, montare e smontare i cose? Stai spesso in dipartimento? Allora Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
puoi venire a parlare con noi per diventare un apprendista macchinista per poi entrare nel PHC. 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> </p>
</div> </div>
</div> </div>
</section> </section>
<script src="/public/home.js"></script> <script src="/public/homepage-art.min.js"></script>
{{end}} {{end}}
+4
View File
@@ -4,6 +4,10 @@
{{define "body"}} {{define "body"}}
<section> <section>
<h2>
Feed RSS
<a href="/news/rss"> <i class="fa-solid fa-rss"></i></a>
</h2>
<h1> <h1>
<i class="far fa-newspaper"></i> <i class="far fa-newspaper"></i>
Notizie Importanti Notizie Importanti
@@ -12,14 +12,14 @@
</div> </div>
<div class="nav-item dropdown"> <div class="nav-item dropdown">
<div class="name"> <div class="name">
<a class="nav-element" href="/progetti"> <div class="nav-element">
<div class="icon"> <div class="icon">
<i class="fas fa-chevron-down"></i> <i class="fas fa-chevron-down"></i>
</div> </div>
<div class="label"> <div class="label">
Progetti Progetti
</div> </div>
</a> </div>
</div> </div>
<div class="nav-items"> <div class="nav-items">
<div class="nav-item"> <div class="nav-item">
@@ -35,14 +35,14 @@
</div> </div>
<div class="nav-item dropdown"> <div class="nav-item dropdown">
<div class="name"> <div class="name">
<a class="nav-element" href="/risorse"> <div class="nav-element">
<div class="icon"> <div class="icon">
<i class="fas fa-chevron-down"></i> <i class="fas fa-chevron-down"></i>
</div> </div>
<div class="label"> <div class="label">
Risorse Risorse
</div> </div>
</a> </div>
</div> </div>
<div class="nav-items"> <div class="nav-items">
<div class="nav-item"> <div class="nav-item">
+3 -3
View File
@@ -2,9 +2,9 @@
{{define "title"}}Profilo di @{{ .User.Username }} &bull; PHC{{end}} {{define "title"}}Profilo di @{{ .User.Username }} &bull; PHC{{end}}
{{define "body"}} {{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script> <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/public/js/profilo.min.js"></script> <script src="/public/profilo.min.js"></script>
<section x-data="profilo"> <section x-data="profilo">
<h1>Profilo di @{{ .User.Username }}</h1> <h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
<p class="center"> <p class="center">
<a class="button" href="/logout">Logout</a> <a class="button" href="/logout">Logout</a>
</p> </p>
@@ -97,7 +97,7 @@
<section> <section>
<h2>Recupero Credenziali Poisson</h2> <h2>Recupero Credenziali Poisson</h2>
<p> <p>
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una email 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 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>. sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
</p> </p>
+14 -4
View File
@@ -1,5 +1,5 @@
{{template "base" .}} {{template "base" .}}
{{define "title"}}Utenti &bull; PHC{{end}} {{define "title"}}Storia &bull; PHC{{end}}
{{define "body"}} {{define "body"}}
<section> <section>
<h1> <h1>
@@ -7,7 +7,11 @@
Storia del PHC Storia del PHC
</h1> </h1>
<p> <p>
In questa pagina è raccontata la storia del PHC con alcuni degli eventi principali. Al momento è <i>work in progress</i> ma pianifichiamo di aggiungere almeno le date di ingresso di tutti i macchinisti. 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> </p>
<div class="history-container"> <div class="history-container">
<div class="timeline-bar"></div> <div class="timeline-bar"></div>
@@ -16,16 +20,22 @@
{{ if eq .Type "simple" }} {{ if eq .Type "simple" }}
<div class="event"> <div class="event">
<div class="title"> <div class="title">
{{ if .Icon }}
<i class="{{ .Icon }}"></i>
{{ end }}
{{ .Title }} {{ .Title }}
</div> </div>
<div class="date">{{ .Date }}</div> <div class="date">{{ .Date }}</div>
<div class="description">
{{ raw .Description }}
</div>
</div> </div>
{{ end }} {{ end }}
{{ if eq .Type "entry-macchinista" }} {{ if eq .Type "entry-macchinista" }}
<div class="event"> <div class="event">
<div class="title"> <div class="title">
Ingresso di
<i class="fa-solid fa-sign-in"></i> <i class="fa-solid fa-sign-in"></i>
Ingresso di
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a> <a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
</div> </div>
<div class="date">{{ .Date }}</div> <div class="date">{{ .Date }}</div>
@@ -34,8 +44,8 @@
{{ if eq .Type "exit-macchinista" }} {{ if eq .Type "exit-macchinista" }}
<div class="event"> <div class="event">
<div class="title"> <div class="title">
Uscita di
<i class="fa-solid fa-sign-out"></i> <i class="fa-solid fa-sign-out"></i>
Uscita di
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a> <a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
</div> </div>
<div class="date">{{ .Date }}</div> <div class="date">{{ .Date }}</div>
+7 -2
View File
@@ -3,7 +3,7 @@
{{define "body"}} {{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script> <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="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
<script src="/public/js/utenti.min.js"></script> <script src="/public/utenti.min.js"></script>
<section x-data="utenti"> <section x-data="utenti">
<h1> <h1>
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
@@ -41,11 +41,16 @@
entry.item.cognome + ' ' + entry.item.nome : entry.item.cognome + ' ' + entry.item.nome :
entry.item.nome + ' ' + entry.item.cognome entry.item.nome + ' ' + entry.item.cognome
"></a> "></a>
<template x-if="entry.item.macchinista"> <template x-if="entry.item.tags?.includes('macchinista')">
<div class="icon" title="Macchinista"> <div class="icon" title="Macchinista">
<i class="fas fa-wrench"></i> <i class="fas fa-wrench"></i>
</div> </div>
</template> </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"> <!-- <template x-if="entry.score">
<span x-text="entry.score"></span> <span x-text="entry.score"></span>
</template> --> </template> -->
+1 -1
View File
@@ -102,7 +102,7 @@ func (article *Article) Render() (string, error) {
if article.renderedHTML == "" { if article.renderedHTML == "" {
var buf bytes.Buffer var buf bytes.Buffer
if err := articleMarkdown.Convert([]byte(article.markdownSource), &buf); err != nil { if err := Markdown.Convert([]byte(article.markdownSource), &buf); err != nil {
return "", err return "", err
} }
+2 -2
View File
@@ -14,7 +14,7 @@ import (
highlighting "github.com/yuin/goldmark-highlighting" highlighting "github.com/yuin/goldmark-highlighting"
) )
var articleMarkdown goldmark.Markdown var Markdown goldmark.Markdown
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27 // https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
@@ -35,7 +35,7 @@ func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, e
} }
func init() { func init() {
articleMarkdown = goldmark.New( Markdown = goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,
extension.Typographer, extension.Typographer,
+1 -1
View File
@@ -48,5 +48,5 @@ func NewDefaultService(host string) Service {
return exampleMemoryUsers return exampleMemoryUsers
} }
return &LDAPAuthService{host} return newLDAPAuthService(host)
} }
+23 -13
View File
@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"path" urlpkg "net/url"
"git.phc.dm.unipi.it/phc/website/model" "git.phc.dm.unipi.it/phc/website/model"
) )
@@ -33,19 +33,24 @@ func (u ldapUser) AsUser() *model.User {
} }
} }
// LDAPAuthService just holds the remote host of the HTTP LDAP service to make requests to // ldapAuthService just holds the remote host of the HTTP LDAP service to make requests to
type LDAPAuthService struct { type ldapAuthService struct {
Host string Host string
} }
func NewLDAPAuthService(host string) Service { func newLDAPAuthService(host string) Service {
return &LDAPAuthService{host} return &ldapAuthService{host}
} }
// doGetRequest is a utility to make HTTP GET requests // doGetRequest is a utility to make HTTP GET requests
func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error { 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( req, err := http.NewRequest(
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")), "GET", u, bytes.NewBuffer([]byte("")),
) )
if err != nil { if err != nil {
@@ -68,13 +73,18 @@ func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error {
} }
// doPostRequest is a utility to make HTTP POST requests // doPostRequest is a utility to make HTTP POST requests
func (a *LDAPAuthService) doPostRequest(url string, request interface{}, response interface{}) error { func (a *ldapAuthService) doPostRequest(url string, request interface{}, response interface{}) error {
jsonStr, err := json.Marshal(request) jsonStr, err := json.Marshal(request)
if err != nil { if err != nil {
return err return err
} }
req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), bytes.NewBuffer(jsonStr)) 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 { if err != nil {
return err return err
} }
@@ -89,7 +99,7 @@ func (a *LDAPAuthService) doPostRequest(url string, request interface{}, respons
return json.NewDecoder(res.Body).Decode(response) return json.NewDecoder(res.Body).Decode(response)
} }
func (a *LDAPAuthService) GetUser(username string) (*model.User, error) { func (a *ldapAuthService) GetUser(username string) (*model.User, error) {
var user ldapUser var user ldapUser
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil { if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
return nil, err return nil, err
@@ -98,7 +108,7 @@ func (a *LDAPAuthService) GetUser(username string) (*model.User, error) {
return user.AsUser(), nil return user.AsUser(), nil
} }
func (a *LDAPAuthService) GetUsers() ([]*model.User, error) { func (a *ldapAuthService) GetUsers() ([]*model.User, error) {
ldapUsers := []*ldapUser{} ldapUsers := []*ldapUser{}
if err := a.doGetRequest("/users", &ldapUsers); err != nil { if err := a.doGetRequest("/users", &ldapUsers); err != nil {
return nil, err return nil, err
@@ -112,7 +122,7 @@ func (a *LDAPAuthService) GetUsers() ([]*model.User, error) {
return users, nil return users, nil
} }
func (a *LDAPAuthService) GetSession(token string) (*model.Session, error) { func (a *ldapAuthService) GetSession(token string) (*model.Session, error) {
var response model.Session var response model.Session
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil { if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
return nil, err return nil, err
@@ -121,7 +131,7 @@ func (a *LDAPAuthService) GetSession(token string) (*model.Session, error) {
return &response, nil return &response, nil
} }
func (a *LDAPAuthService) Login(username, password string) (*model.Session, error) { func (a *ldapAuthService) Login(username, password string) (*model.Session, error) {
reqBody := map[string]interface{}{ reqBody := map[string]interface{}{
"username": username, "username": username,
"password": password, "password": password,
+47
View File
@@ -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)
}
}
+16 -3
View File
@@ -16,10 +16,15 @@ var GitUrl string
var ChatUrl string var ChatUrl string
var Email string var Email string
var BaseUrl string
var UserPagesBaseUrl string var UserPagesBaseUrl string
var AuthServiceHost 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) { func loadEnv(target *string, name, defaultValue string) {
value := os.Getenv(name) value := os.Getenv(name)
if len(strings.TrimSpace(value)) == 0 { if len(strings.TrimSpace(value)) == 0 {
@@ -41,6 +46,7 @@ func Load() {
// Production // Production
loadEnv(&Mode, "MODE", "production") loadEnv(&Mode, "MODE", "production")
loadEnv(&Host, "HOST", ":8080") loadEnv(&Host, "HOST", ":8080")
loadEnv(&BaseUrl, "BASE_URL", "localhost:8080")
// Services // Services
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org") loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
@@ -50,12 +56,15 @@ func Load() {
// Poisson // Poisson
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~") loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
// AuthService // Auth
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535") loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
// Altro
loadEnv(&ListaUtenti, "LISTA_UTENTI", ":auth:")
} }
func Object() util.H { func Object() util.Map {
return util.H{ return util.Map{
"Mode": Mode, "Mode": Mode,
"Host": Host, "Host": Host,
@@ -63,8 +72,12 @@ func Object() util.H {
"ChatUrl": ChatUrl, "ChatUrl": ChatUrl,
"Email": Email, "Email": Email,
"BaseUrl": BaseUrl,
"UserPagesBaseUrl": UserPagesBaseUrl, "UserPagesBaseUrl": UserPagesBaseUrl,
"AuthServiceHost": AuthServiceHost, "AuthServiceHost": AuthServiceHost,
"ListaUtenti": ListaUtenti,
} }
} }
-23
View File
@@ -1,23 +0,0 @@
STYLES_SOURCES = $(wildcard src/styles/*.scss)
SCRIPTS_SOURCES = $(wildcard src/*.js)
.PHONY: all
all: scripts styles
.PHONY: styles
styles: $(STYLES_SOURCES)
npm run build:styles
.PHONY: scripts
scripts: $(SCRIPTS_SOURCES)
npm run build:scripts
.PHONY: clean
clean:
rm -rf dist/
.PHONY: debug
debug:
@echo "STYLES_SOURCES = $(STYLES_SOURCES)"
@echo "SCRIPTS_SOURCES = $(SCRIPTS_SOURCES)"
-1338
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -1,22 +0,0 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "Package to generate frontend files for phc/website",
"scripts": {
"build:scripts": "rollup -c",
"build:styles": "sass -s compressed --no-source-map src/styles/main.scss dist/main.css"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.3.0",
"rollup": "^2.75.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.52.3"
},
"dependencies": {
"alpinejs": "^3.10.2",
"fuse.js": "^6.6.2"
}
}
-31
View File
@@ -1,31 +0,0 @@
import { defineConfig } from 'rollup'
import { terser } from 'rollup-plugin-terser'
export default defineConfig([
{
input: 'src/utenti.js',
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
output: {
file: 'dist/utenti.min.js',
format: 'iife',
globals: {
// map library names to global constants
alpinejs: 'Alpine',
'fuse.js': 'Fuse',
},
},
plugins: [terser()],
},
{
input: 'src/profilo.js',
external: ['alpinejs'],
output: {
file: 'dist/profilo.min.js',
format: 'iife',
globals: {
alpinejs: 'Alpine',
},
},
plugins: [terser()],
},
])
+2 -1
View File
@@ -1,6 +1,6 @@
module git.phc.dm.unipi.it/phc/website module git.phc.dm.unipi.it/phc/website
go 1.18 go 1.19
require ( require (
github.com/alecthomas/chroma v0.9.4 github.com/alecthomas/chroma v0.9.4
@@ -17,6 +17,7 @@ require (
require ( require (
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/dlclark/regexp2 v1.4.0 // 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/klauspost/compress v1.15.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.37.0 // indirect github.com/valyala/fasthttp v1.37.0 // indirect
+2
View File
@@ -19,6 +19,8 @@ github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSE
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U= 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 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0= 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 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+21
View File
@@ -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
}
+223
View File
@@ -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)
}
+50
View File
@@ -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
}
+51
View File
@@ -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
}
+69
View File
@@ -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")
}
}
}
-208
View File
@@ -1,208 +0,0 @@
package main
import (
"fmt"
"html/template"
"log"
"time"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/config"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/templates"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/redirect/v2"
)
func UserMiddleware(as auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Cookies("session-token")
user, _ := auth.UserForSession(as, token)
c.Locals("user", user)
return c.Next()
}
}
func main() {
config.Load()
app := fiber.New()
app.Use(logger.New())
app.Use(recover.New())
// Remove trailing slash from URLs
app.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/*/": "/$1",
},
}))
// Serve content statically from "./public", mounted on the "/public/" route
app.Static("/public/", "./public")
authService := auth.NewDefaultService(config.AuthServiceHost)
app.Use(UserMiddleware(authService))
// Templates & Renderer
renderer := templates.NewRenderer(
"./views/",
"./views/base.html",
"./views/partials/*.html",
)
newsArticlesRegistry := articles.NewRegistry("./news")
// Routes
actuallyStaticRoutes := map[string]string{
"/": "home.html",
"/link": "link.html",
"/login": "login.html",
"/utenti": "utenti.html",
}
for route, view := range actuallyStaticRoutes {
localView := view
app.Get(route, func(c *fiber.Ctx) error {
c.Type("html")
return renderer.Render(c, localView, util.H{
"User": c.Locals("user"),
})
})
}
app.Get("/api/utenti", func(c *fiber.Ctx) error {
utenti, err := authService.GetUsers()
if err != nil {
return err
}
return c.JSON(utenti)
})
app.Get("/api/profilo", func(c *fiber.Ctx) error {
user := c.Locals("user")
if user == nil {
return fmt.Errorf(`user not logged in`)
}
return c.JSON(user)
})
app.Get("/storia", func(c *fiber.Ctx) error {
storia, err := GetStoria()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "storia.html", util.H{
"User": c.Locals("user"),
"Storia": storia,
})
})
app.Get("/appunti", func(c *fiber.Ctx) error {
searchQuery := c.Query("q", "")
c.Type("html")
return renderer.Render(c, "appunti.html", util.H{
"User": c.Locals("user"),
"Query": searchQuery,
})
})
app.Get("/news", func(c *fiber.Ctx) error {
articles, err := newsArticlesRegistry.GetArticles()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news.html", util.H{
"User": c.Locals("user"),
"Articles": articles,
})
})
app.Post("/login", func(c *fiber.Ctx) error {
var loginForm struct {
Provider string `form:"provider"`
Username string `form:"username"`
Password string `form:"password"`
}
if err := c.BodyParser(&loginForm); err != nil {
return err
}
session, err := authService.Login(loginForm.Username, loginForm.Password)
if err != nil {
return err
}
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: session.Token,
Expires: inThreeDays,
})
return c.Redirect("/profilo")
})
app.Get("/profilo", func(c *fiber.Ctx) error {
user, ok := c.Locals("user").(*model.User)
if !ok || user == nil {
return fmt.Errorf(`user not logged in`)
}
c.Type("html")
return renderer.Render(c, "profilo.html", util.H{
"User": c.Locals("user"),
})
})
app.Get("/logout", func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: "",
Expires: time.Now(),
})
return c.Redirect("/")
})
app.Get("/news/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
article, err := newsArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news-base.html", util.H{
"User": c.Locals("user"),
"Article": article,
"ContentHTML": template.HTML(html),
})
})
log.Printf("Starting server on host %q", config.Host)
err := app.Listen(config.Host)
if err != nil {
log.Fatal(err)
}
}
+7
View File
@@ -1,5 +1,12 @@
package model 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 // User represents a user returned from AuthenticatorService
type User struct { type User struct {
Username string `json:"username"` Username string `json:"username"`
-342
View File
@@ -1,342 +0,0 @@
function pointInInterval(a, b, x) {
return a <= x && x <= b;
}
function intervalIntersectsInterval(a, b, c, d) {
if (c <= a) {
return a <= d;
}
return pointInInterval(a, b, c);
}
function rectIntersectRect(x1, y1, x2, y2, x3, y3, x4, y4) {
return intervalIntersectsInterval(x1, x2, x3, x4) && intervalIntersectsInterval(y1, y2, y3, y4);
}
function intersects(x1, y1, x2, y2, x3, y3, x4, y4) {
const a_minx = Math.min(x1, x2);
const a_maxx = Math.max(x1, x2);
const a_miny = Math.min(y1, y2);
const a_maxy = Math.max(y1, y2);
const b_minx = Math.min(x3, x4);
const b_maxx = Math.max(x3, x4);
const b_miny = Math.min(y3, y4);
const b_maxy = Math.max(y3, y4);
if (!rectIntersectRect(a_minx, a_miny, a_maxx, a_maxy, b_minx, b_miny, b_maxx, b_maxy))
return false;
const a_dx = x2 - x1;
const a_dy = y2 - y1;
const b_dx = x4 - x3;
const b_dy = y4 - y3;
const det = -b_dx * a_dy + a_dx * b_dy;
if (Math.abs(det) === 0 && -a_dy * (x3 - x1) + a_dx * (y3 - y1) === 0) {
return y1 <= y3 <= y2 || y1 <= y4 <= y2;
}
const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / det;
const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / det;
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
}
const $canvas = document.querySelector('#circuit-pattern');
let g = $canvas.getContext('2d');
let WIDTH = $canvas.offsetWidth;
let HEIGHT = $canvas.offsetHeight;
const TS = 20;
let ROWS = HEIGHT / TS;
let COLS = WIDTH / TS;
function updateCanvasDimensions() {
g = $canvas.getContext('2d');
WIDTH = $canvas.offsetWidth;
HEIGHT = $canvas.offsetHeight;
ROWS = HEIGHT / TS;
COLS = WIDTH / TS;
$canvas.width = WIDTH * devicePixelRatio;
$canvas.height = HEIGHT * devicePixelRatio;
g.scale(devicePixelRatio, devicePixelRatio);
}
window.addEventListener('resize', () => updateCanvasDimensions());
updateCanvasDimensions();
const state = {
wires: [], // :: [(Int, Int)]
nextWire: null,
nextWireIndex: 0,
nextSegmentLen: 0,
t: 0,
};
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomChoice(list) {
return list[randomInt(0, list.length - 1)];
}
const DIRS = {
left: [-1, 1],
center: [0, 1],
right: [+1, 1],
};
const NEXT_DIRS = {
left: ['center'],
center: ['left', 'right'],
right: ['center'],
};
function canPlaceSegment([[p1x, p1y], [p2x, p2y]]) {
return state.wires.every(wire => {
return everyWireSegment(wire, ([[sx, sy], [lx, ly]]) => {
return !intersects(sx, sy, lx, ly, p1x, p1y, p2x, p2y);
});
})
}
function everyWireSegment(wire, segmentFn) {
let [start, ...rest] = wire;
return rest.every((pt) => {
const r = segmentFn([start, pt]);
start = pt;
return r;
});
}
function generateWire() {
const pieceCount = Math.pow(randomFloat(1, 2.5), 2) | 0;
const startPoint = [
randomInt(0, COLS),
Math.pow(randomFloat(0, ROWS / 12), 2) | 0,
];
const sizes = Array(pieceCount).fill(0).map(() => randomInt(2, 4) | 0);
const dirs = [randomChoice(Object.keys(DIRS))];
for (let i = 0; i < pieceCount - 1; i++) {
const last = dirs[dirs.length - 1];
dirs.push(randomChoice(NEXT_DIRS[last]));
}
const wire = [startPoint];
for (let i = 0; i < pieceCount; i++) {
const [lpx, lpy] = wire[wire.length - 1];
const [dx, dy] = DIRS[dirs[i]];
const s = sizes[i];
const s_bonus = (dirs[i] === 'center' ? s * 2 : s) | 0;
wire.push([lpx + dx * s_bonus, lpy + dy * s_bonus]);
}
const ok = everyWireSegment(wire, segment => {
return canPlaceSegment(segment);
});
if (!ok)
return null;
return wire;
}
// // LinearintERPolation
function lerp(a, b, t) {
return t * (b - a) + a;
}
function lerpPoint([x1, y1], [x2, y2], t) {
return [lerp(x1, x2, t), lerp(y1, y2, t)];
}
const CELLS_PER_SECOND = 15;
let lastTime = new Date().getTime();
function update() {
const now = new Date().getTime();
const delta = (now - lastTime) / 1000;
lastTime = now;
if (state.nextWire) {
state.t += CELLS_PER_SECOND * delta;
if (state.t >= 1.0) {
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
state.nextWireIndex++;
}
if (state.nextWireIndex > state.nextWire.length) {
state.wires.push(state.nextWire);
state.nextWire = null;
}
} else {
const wire = generateWire();
if (wire) {
state.nextWire = wire;
state.nextWireIndex = 2;
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
}
}
}
function getColors() {
if (document.body.classList.contains('dark-mode')) {
return {
BACKGROUND: '#282828',
CIRCUIT: '#38302e',
}
} else {
return {
BACKGROUND: '#eaeaea',
CIRCUIT: '#d4d4d4',
};
}
}
function render() {
const { BACKGROUND, CIRCUIT } = getColors();
g.clearRect(0, 0, WIDTH, HEIGHT);
g.strokeStyle = CIRCUIT;
g.lineWidth = 3;
state.wires.forEach(wire => {
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
});
if (state.nextWire) {
const wirePart = state.nextWire.slice(0, state.nextWireIndex);
const wire = [
...wirePart.slice(0, wirePart.length - 1),
lerpPoint(
wirePart[wirePart.length - 2],
wirePart[wirePart.length - 1],
state.t
)
];
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
}
requestAnimationFrame(render);
}
// g.translate(0.5, 0.5);
window.addEventListener('DOMContentLoaded', () => {
// $canvas.classList.remove('hide');
render();
let i = 20;
while (i > 0) {
const wire = generateWire();
if (wire) {
state.wires.push(wire);
i--;
}
}
setInterval(() => {
update();
}, 1000 / 30);
})
+34
View File
@@ -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
}
+202
View File
@@ -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)
})
}
-44
View File
@@ -1,44 +0,0 @@
{
"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/??"
},
{
"uid": "caporali",
"fullName": "Francesco Caporali",
"entryDate": "2018/??",
"exitDate": "2022/03"
},
{
"uid": "dachille",
"fullName": "Letizia D'Achille",
"entryDate": "2018/??",
"exitDate": "2022/03"
}
],
"eventi": [
{
"type": "simple",
"title": "Nuovo sito del PHC",
"description": "TBA",
"date": "2022/??"
},
{
"type": "simple",
"title": "Apertura del PHC",
"date": "1994"
}
]
}
+103 -79
View File
@@ -1,16 +1,58 @@
package main package storia
import ( import (
"encoding/json" "bytes"
"io/ioutil"
"math" "math"
"os"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/util" "git.phc.dm.unipi.it/phc/website/util"
"gopkg.in/yaml.v3"
) )
type StoriaService interface {
GetStoria() ([]*GenericEvent, error)
}
type GenericEvent struct {
// Campi principali
Type string `yaml:"type"`
Date string `yaml:"date"`
// Altri campi utilizzati ogni tanto
Title string `yaml:"title"`
Description string `yaml:"description"`
// Se il tipo di evento è riferito ad un utente usiamo anche questo campo apposta
Uid string `yaml:"uid"`
FullName string `yaml:"fullName"`
// Se il tipo è "spacer" questa è la sua dimensione
Size int `yaml:"size"`
// Icona opzionale per il tipo "simple"
Icon string `yaml:"icon"`
}
type Macchinista struct {
Uid string `yaml:"uid"`
FullName string `yaml:"fullName"`
EntryDate string `yaml:"entryDate"`
ExitDate string `yaml:"exitDate"`
}
type storiaDB struct {
Macchinisti []*Macchinista `yaml:"macchinisti"`
GenericEvent []*GenericEvent `yaml:"eventi"`
}
type JsonFileStoria struct {
Path string
}
type byEventDateDescending []*GenericEvent type byEventDateDescending []*GenericEvent
func (m byEventDateDescending) Len() int { func (m byEventDateDescending) Len() int {
@@ -23,82 +65,8 @@ func (m byEventDateDescending) Less(i, j int) bool {
return m[i].Date > m[j].Date return m[i].Date > m[j].Date
} }
type Macchinista struct { func (db *JsonFileStoria) GetStoria() ([]*GenericEvent, error) {
Uid string `json:"uid"` history, err := db.GetRawStoria()
FullName string `json:"fullName"`
EntryDate string `json:"entryDate"`
ExitDate string `json:"exitDate"`
}
type GenericEvent struct {
// Campi principali
Type string `json:"type"`
Date string `json:"date"`
// Altri campi utilizzati ogni tanto
Title string `json:"title"`
Description string `json:"description"`
// Se il tipo di evento è riferito ad un utente usiamo anche questo campo apposta
Uid string `json:"uid"`
FullName string `json:"fullName"`
// Se il tipo è "spacer" questa è la sua dimensione
Size int `json:"size"`
}
type HistoryDB struct {
Macchinisti []*Macchinista `json:"macchinisti"`
GenericEvent []*GenericEvent `json:"eventi"`
}
func GetRawHistory() (*HistoryDB, error) {
var rawHistory HistoryDB
historyDB, err := ioutil.ReadFile("./storia.json")
if err != nil {
return nil, err
}
if err := json.Unmarshal(historyDB, &rawHistory); err != nil {
return nil, err
}
return &rawHistory, nil
}
func getDateYear(date string) int {
year, err := strconv.Atoi(strings.Split(date, "/")[0])
if err != nil {
panic(err) // Tanto nel caso si nota in fase di sviluppo visto che "storia.json" è statico
}
return year
}
func withSpacers(events []*GenericEvent) []*GenericEvent {
// crea un nuovo slice con capacità già almeno "len(events)"
newEvents := make([]*GenericEvent, 0, len(events))
for i := 0; i < len(events)-1; i++ {
newEvents = append(newEvents, events[i])
delta := util.Abs(getDateYear(events[i+1].Date) - getDateYear(events[i].Date))
if delta > 1 {
logDelta := int(math.Log(float64(delta)))
newEvents = append(newEvents, &GenericEvent{
Type: "spacer",
Size: logDelta,
})
}
}
newEvents = append(newEvents, events[len(events)-1])
return newEvents
}
func GetStoria() ([]*GenericEvent, error) {
history, err := GetRawHistory()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -125,5 +93,61 @@ func GetStoria() ([]*GenericEvent, error) {
sort.Sort(byEventDateDescending(events)) sort.Sort(byEventDateDescending(events))
// render descriptions to markdown
for _, event := range events {
var buf bytes.Buffer
if err := articles.Markdown.Convert([]byte(event.Description), &buf); err != nil {
return nil, err
}
event.Description = buf.String()
}
return withSpacers(events), nil return withSpacers(events), nil
} }
func (db *JsonFileStoria) GetRawStoria() (*storiaDB, error) {
var rawHistory storiaDB
f, err := os.Open(db.Path)
if err != nil {
return nil, err
}
if err := yaml.NewDecoder(f).Decode(&rawHistory); err != nil {
return nil, err
}
return &rawHistory, nil
}
func withSpacers(events []*GenericEvent) []*GenericEvent {
// crea un nuovo slice con capacità già almeno "len(events)"
newEvents := make([]*GenericEvent, 0, len(events))
for i := 0; i < len(events)-1; i++ {
newEvents = append(newEvents, events[i])
delta := util.Abs(getDateYear(events[i+1].Date) - getDateYear(events[i].Date))
if delta > 1 {
logDelta := int(math.Log(float64(delta)))
newEvents = append(newEvents, &GenericEvent{
Type: "spacer",
Size: logDelta,
})
}
}
newEvents = append(newEvents, events[len(events)-1])
return newEvents
}
func getDateYear(date string) int {
year, err := strconv.Atoi(strings.Split(date, "/")[0])
if err != nil {
panic(err) // Tanto nel caso si nota in fase di sviluppo visto che "_content/storia.yaml" è statico
}
return year
}
+12 -5
View File
@@ -1,6 +1,7 @@
package templates package templates
import ( import (
"fmt"
"html/template" "html/template"
"io" "io"
"log" "log"
@@ -23,7 +24,9 @@ func NewCacheTemplate(loadPatterns ...string) *CachedTemplate {
PreLoadTemplatePatterns: loadPatterns, PreLoadTemplatePatterns: loadPatterns,
cachedTmpl: nil, cachedTmpl: nil,
} }
template.Must(cachedTemplate.Load(template.New("")))
template.Must(cachedTemplate.Load(nil))
return cachedTemplate return cachedTemplate
} }
@@ -33,7 +36,11 @@ func (ct *CachedTemplate) Load(t *template.Template) (*template.Template, error)
return ct.cachedTmpl, nil return ct.cachedTmpl, nil
} }
ct.cachedTmpl = t ct.cachedTmpl = template.New("").Funcs(template.FuncMap{
"raw": func(value any) template.HTML {
return template.HTML(fmt.Sprint(value))
},
})
for _, pattern := range ct.PreLoadTemplatePatterns { for _, pattern := range ct.PreLoadTemplatePatterns {
var err error var err error
@@ -91,16 +98,16 @@ func (r *TemplateRenderer) Load(name string) *CachedTemplate {
} }
// Render the template, also injects "Page" and "Config" values in the template // Render the template, also injects "Page" and "Config" values in the template
func (r *TemplateRenderer) Render(w io.Writer, name string, data util.H) error { func (r *TemplateRenderer) Render(w io.Writer, name string, data util.Map) error {
cachedTemplate := r.Load(name) cachedTemplate := r.Load(name)
if config.Mode == "development" { if config.Mode == "development" {
cachedTemplate.Reload() cachedTemplate.Reload()
} }
newData := util.H{} newData := util.Map{}
newData.Apply(data) newData.Apply(data)
newData["Page"] = util.H{ newData["Page"] = util.Map{
// Used to inject a page specific class on <body> // Used to inject a page specific class on <body>
"Name": strings.TrimSuffix(path.Base(name), ".html"), "Name": strings.TrimSuffix(path.Base(name), ".html"),
} }
-41
View File
@@ -1,41 +0,0 @@
package main
import (
"encoding/json"
"io/ioutil"
)
type UserInfo struct {
Uid string `json:"uid"`
Nome string `json:"nome"`
Cognome string `json:"cognome"`
Tags []string `json:"tags"`
IsMacchinista bool `json:"macchinista,omitempty"`
}
func GetUtenti() ([]UserInfo, error) {
history, err := GetRawHistory()
if err != nil {
return nil, err
}
// Creo un set per velocizzare l'utilizzo dopo
macchinisti := map[string]bool{}
for _, m := range history.Macchinisti {
macchinisti[m.Uid] = true
}
var users []UserInfo
usersJsonData, err := ioutil.ReadFile("./utenti-poisson-2022.local.json")
if err != nil {
return nil, err
}
if err := json.Unmarshal(usersJsonData, &users); err != nil {
return nil, err
}
return users, nil
}
-1
View File
@@ -1 +0,0 @@
package util
+3 -3
View File
@@ -1,10 +1,10 @@
package util package util
// H is a shortcut for creating a json-like object // Map is a shortcut for creating a json-like object
type H map[string]interface{} type Map map[string]interface{}
// Apply is like Object.apply from JS and merges the given objects on top of target // Apply is like Object.apply from JS and merges the given objects on top of target
func (target H) Apply(sources ...H) H { func (target Map) Apply(sources ...Map) Map {
for _, source := range sources { for _, source := range sources {
for k, v := range source { for k, v := range source {
target[k] = v target[k] = v
+1
View File
@@ -6,6 +6,7 @@ import (
"log" "log"
) )
// GenerateRandomString generates a random base64 string from a random array of "n" bytes (to prevent padding let n be a multiple of 3)
func GenerateRandomString(n int) string { func GenerateRandomString(n int) string {
b := make([]byte, n) b := make([]byte, n)
+26
View File
@@ -0,0 +1,26 @@
package util
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](elements ...T) Set[T] {
set := Set[T]{}
for _, e := range elements {
set[e] = struct{}{}
}
return set
}
func (set Set[T]) Add(value T) {
set[value] = struct{}{}
}
func (set Set[T]) Remove(value T) {
delete(set, value)
}
func (set Set[T]) Has(value T) bool {
_, found := set[value]
return found
}
-78
View File
@@ -1,78 +0,0 @@
{{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>
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 prefered 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);
}
$toggle.addEventListener('click', () => {
storePrefersDarkMode(!prefersDarkMode);
displayToggle();
});
displayToggle();
});
</script>
<link rel="stylesheet" href="/public/style.css">
<link rel="stylesheet" href="/public/theme-dark.css">
</head>
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
{{ template "navbar" . }}
<div class="main">
{{ template "body" . }}
</div>
<footer>
<div class="footer-item">
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
</div>
</footer>
</body>
</html>
{{end}}