Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeba6356af
|
||
|
|
23757ad9b0
|
||
|
|
9d518dd4f8
|
||
|
|
3ca159e809
|
||
|
|
855bc8fa25
|
||
|
|
f7350bc6a8
|
||
|
|
4b12c3d59d
|
||
|
|
47a9d32670
|
||
|
|
6f308377ef
|
||
|
|
d7f9f2935d
|
||
|
|
ddbf06d7d0
|
||
|
|
cd0383245b
|
||
|
|
cff45dc92f
|
||
|
|
346459f822
|
||
|
|
6cccf78e45 | ||
|
|
192d8ea944 | ||
|
|
805d9a7ba5 | ||
|
|
85a0a21d15
|
||
|
|
4be4a68912
|
||
|
|
5decb85552
|
||
|
|
3d0bd18aef
|
||
|
|
489d7f161d | ||
|
|
6e90f8c32f | ||
|
|
5337849bc7 | ||
|
|
f0a9a17fc0
|
||
|
|
1ddc71f64b
|
||
|
|
684859c197
|
||
|
|
c1a701353a
|
||
|
|
9c10e1b8fc
|
||
|
|
9553dbe252
|
||
|
|
03bae38ebe
|
||
|
|
f875c9c6bf
|
||
|
|
cfb62677db
|
||
|
|
67f8421516 | ||
|
|
a5039610e1 | ||
|
|
1f90dacbdd | ||
|
|
c9d02d22dd | ||
|
|
0f7bde6232 | ||
|
|
860cc76503 | ||
|
|
227619e5eb | ||
|
|
92cee16cc8 |
@@ -1,6 +1,6 @@
|
||||
# Server config
|
||||
MODE=development
|
||||
HOST=localhost:8000
|
||||
HOST=:8080
|
||||
|
||||
# Separate services
|
||||
GIT_URL=https://git.phc.dm.unipi.it
|
||||
@@ -9,7 +9,14 @@ CHAT_URL=https://chat.phc.dm.unipi.it
|
||||
# Other
|
||||
EMAIL=macchinisti@lists.dm.unipi.it
|
||||
|
||||
# Base URL
|
||||
BASE_URL=localhost:8080
|
||||
|
||||
# Lista Utenti
|
||||
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
|
||||
|
||||
# AuthService
|
||||
AUTH_SERVICE_HOST=:memory:
|
||||
|
||||
# Origine per Lista Utenti
|
||||
LISTA_UTENTI=utenti-poisson-2022.local.json
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Miscellaneous
|
||||
tags
|
||||
|
||||
# Local files
|
||||
.vscode/
|
||||
*.local*
|
||||
|
||||
# NodeJS
|
||||
# Generated output
|
||||
out/
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# Don't version generated files
|
||||
public/css/
|
||||
public/js/
|
||||
# NodeJS
|
||||
node_modules/
|
||||
|
||||
# Executables
|
||||
phc-website-server
|
||||
!phc-website-server/
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
FROM node:18 AS frontend-builder
|
||||
WORKDIR /frontend
|
||||
COPY ./frontend/package.json ./package.json
|
||||
COPY ./frontend/package-lock.json ./package-lock.json
|
||||
RUN npm install
|
||||
COPY ./frontend .
|
||||
RUN make
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /_frontend
|
||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
COPY ./_frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm fetch --prod
|
||||
COPY ./_frontend ./
|
||||
RUN pnpm install -r --offline --prod
|
||||
RUN make build
|
||||
|
||||
FROM golang:1.18-alpine AS backend-builder
|
||||
FROM golang:1.19-alpine AS backend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /backend
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
COPY . .
|
||||
RUN go build -buildvcs=false -o website-server -v .
|
||||
COPY ./ ./
|
||||
RUN make backend
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=frontend-builder /frontend/dist ./frontend/dist
|
||||
COPY --from=backend-builder /backend/website-server ./website-server
|
||||
COPY ./views ./views
|
||||
COPY ./news ./news
|
||||
COPY ./public ./public
|
||||
COPY --from=frontend-builder /_frontend/out ./_frontend/out
|
||||
COPY --from=backend-builder /backend/phc-website-server ./phc-website-server
|
||||
COPY ./_views ./_views
|
||||
COPY ./_content/news ./_content/news
|
||||
COPY ./_content/guide ./_content/guide
|
||||
COPY ./_public ./_public
|
||||
COPY ./.env ./.env
|
||||
EXPOSE 8000
|
||||
CMD ["./website-server"]
|
||||
|
||||
@@ -3,33 +3,31 @@ GO_SOURCES = $(shell find . -name '*.go')
|
||||
GO_EXECUTABLE = phc-website-server
|
||||
|
||||
.PHONY: all
|
||||
all: frontend go
|
||||
all: frontend backend
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
mkdir -p public/js
|
||||
mkdir -p public/css
|
||||
|
||||
.PHONY: frontend
|
||||
frontend: setup
|
||||
$(MAKE) -C frontend/
|
||||
cp frontend/dist/*.min.js public/js/
|
||||
cp frontend/dist/*.css public/css/
|
||||
frontend:
|
||||
$(MAKE) -C _frontend/
|
||||
@echo "Built Frontend"
|
||||
|
||||
#
|
||||
# Build Server
|
||||
# Build Backend
|
||||
#
|
||||
|
||||
.PHONY: go
|
||||
go: $(GO_EXECUTABLE)
|
||||
@echo "Compiled Server"
|
||||
.PHONY: backend
|
||||
backend: $(GO_EXECUTABLE)
|
||||
@echo "Built Backend"
|
||||
|
||||
$(GO_EXECUTABLE): $(GO_SOURCES)
|
||||
go build -o $(GO_EXECUTABLE) .
|
||||
go build -o $(GO_EXECUTABLE) ./cmd/phc-website-server
|
||||
|
||||
#
|
||||
# Debug
|
||||
#
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
|
||||
@@ -1,99 +1,180 @@
|
||||
# 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`
|
||||
|
||||
- `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
|
||||
```bash shell
|
||||
# Clone the repo
|
||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
||||
$ cd frontend/
|
||||
frontend/ $ npm install
|
||||
frontend/ $ cd ..
|
||||
$ make frontend
|
||||
$ cd _frontend
|
||||
$ make setup
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### 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.
|
||||
To just do a full build and start the project run
|
||||
|
||||
```bash shell
|
||||
# Setup local development env file
|
||||
$ 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
|
||||
|
||||
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
|
||||
Alternativamente se si sta modificando in live il codice è comodo usare [`entr`](https://github.com/eradman/entr) e `fd` (un'alternativa a `find`)
|
||||
|
||||
```bash shell
|
||||
$ find . -type f -name '*.go' | entr -r go run .
|
||||
# O anche con fd...
|
||||
$ fd -e go | entr -r go run .
|
||||
# Restart server when go files change
|
||||
$ printf '%s\n' $(echo **/*.go) | entr -r go run ./cmd/phc-website-server
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
|
||||
Per ora non c'è ancora nessun sistema alternativo ma se si sta modificando codice della frontend in `_frontend/` invece di riavviare ogni volta il server conviene lanciare in parallelo un watcher con
|
||||
|
||||
```bash shell
|
||||
$ make frontend
|
||||
# O anche con un watcher...
|
||||
$ fd -e js | entr make frontend
|
||||
# Recompile files inside "_frontend/src" on change
|
||||
_frontend/ $ pnpm run watch
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
## Backend
|
||||
|
||||
- `MODE`
|
||||
### Dependencies
|
||||
|
||||
- `github.com/gofiber/fiber/v2`
|
||||
|
||||
Backend server framework.
|
||||
|
||||
- `github.com/joho/godotenv`
|
||||
|
||||
Library used to load `.env` config files.
|
||||
|
||||
- `github.com/yuin/goldmark`
|
||||
|
||||
Along with `github.com/yuin/goldmark-highlighting`, `github.com/alecthomas/chroma` and `github.com/litao91/goldmark-mathjax` are used to render Markdown articles and pages with latex expression and syntax highlighting.
|
||||
|
||||
- `gopkg.in/yaml.v3`
|
||||
|
||||
Used to load YAML frontmatter in Markdown documents.
|
||||
|
||||
- `github.com/alecthomas/repr`
|
||||
|
||||
Utility to pretty print Go values.
|
||||
|
||||
### Architecture
|
||||
|
||||
The go project is organized as a library and the main server executable is inside `cmd/phc-website-server` that starts the server provided by the `server/` package and injects all concrete service instances.
|
||||
|
||||
After a recent refactor all code that interacts with services is inside the `handler/` package that abstracts away the HTTP server dependency to test more easily each route.
|
||||
|
||||
The `handler/` has a `Context` type that for now is just used to pass around the user if the HTTP request contained a session token.
|
||||
|
||||
The `model/` package provides some common types used in the whole application.
|
||||
|
||||
The `config/` package is just a singleton loaded when the application is started providing config values loaded from a `.env` file.
|
||||
|
||||
#### Services
|
||||
|
||||
All other Go packages are services used by the `handler/` packages that provide authentication (`auth/`), template rendering (`templates/`), articles loading (`articles/`).
|
||||
|
||||
Some very small services for now just have a single file and are `storia.go` and `utenti.go` (each provides a `-Service` interface and a default concrete implementation).
|
||||
|
||||
## Frontend
|
||||
|
||||
**Warning.** Forse a breve ci sarà un lieve refactor da AlpineJS a Preact perché è più comodo.
|
||||
|
||||
All frontend code is stored inside the `_frontend/` directory. (This will be implied in paths from now on)
|
||||
|
||||
This is a NodeJS project that uses <https://pnpm.io/> as a package manager.
|
||||
|
||||
This project compiles javascript files using _RollupJS_ (a tree shaking js bundler) and scss files using _sass_.
|
||||
|
||||
### Javascript
|
||||
|
||||
These files a processed by RollupJS into `out/`
|
||||
|
||||
- `base.js`
|
||||
|
||||
Script che avvia KaTeX e il _theme switcher_.
|
||||
|
||||
- `utenti.js`
|
||||
|
||||
Script che si occupa di mostrare la lista degli utenti con fuzzy search.
|
||||
|
||||
This script is imported by `_views/utenti.html` along side its dependencies loaded from common CDNs
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
- `profilo.js`
|
||||
|
||||
Script che aggiunge un minimo di interattività alla pagina renderizzata da `_views/profilo.html`
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="/public/profilo.min.js"></script>
|
||||
```
|
||||
|
||||
- `homepage-art.ts`
|
||||
|
||||
Script che renderizza l'animazione della homepage.
|
||||
|
||||
|
||||
- `appunti-condivisi.jsx`
|
||||
|
||||
Script che aggiunte l'interattività alla pagina `/appunti/condivisi` utilizzando Preact come libreria di UI.
|
||||
|
||||
```html
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODE`
|
||||
|
||||
Può essere `production` (default) o `development`.
|
||||
|
||||
- `HOST`
|
||||
- `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.
|
||||
|
||||
- `<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).
|
||||
|
||||
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
|
||||
|
||||
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/)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
id: guida-git
|
||||
title: "Guida a Git"
|
||||
tags: git
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Vedremo come creare una repository Git per tracciare i cambiamenti di una serie di file nel tempo, e molto altro.
|
||||
---
|
||||
|
||||
## A cosa serve
|
||||
|
||||
## Creiamo una repository git
|
||||
|
||||
Nonostante per grossi progetti sia comodo usare server Git come github.com, che essendo popolare attirerà l'attenzione di più potenziali __contributors__, per progetti personali o
|
||||
di piccola scala va benissimo utilizzare un server **self-hostato**: in questo caso noi useremo il server del PHC.
|
||||
|
||||
Per prima cosa, visitiamo git.phc.dm.unipi.it ed accediamo con le credenziali di
|
||||
|
||||
## Comandi di base
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
id: guida-terminale
|
||||
title: "Guida al Terminale"
|
||||
tags: shell, zsh, vim, ssh
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
|
||||
---
|
||||
|
||||
## 1. Shell & Terminale: differenze?
|
||||
|
||||
- introduzione storica + bash
|
||||
|
||||
### Le Basi
|
||||
|
||||
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
|
||||
- copiare/incollare testo
|
||||
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
|
||||
- chiedere aiuto: man, info
|
||||
|
||||
### Zsh: una shell moderna e potente
|
||||
|
||||
https://wiki.archlinux.org/title/Zsh
|
||||
|
||||
## 2. Vim: l'Editor intramontabile
|
||||
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
|
||||
|
||||
## 3. E la TTY?
|
||||
Come utilizzarla, quando è utile, limitazioni(no scrollback)
|
||||
|
||||
## SSH: la magia della remote shell
|
||||
@@ -0,0 +1 @@
|
||||
["delucreziis", "serdyuk", "minnocci", "manicastri"]
|
||||
@@ -0,0 +1 @@
|
||||
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
|
||||
@@ -0,0 +1,38 @@
|
||||
macchinisti:
|
||||
- uid: minnocci
|
||||
fullName: Francesco Minnocci
|
||||
entryDate: 2022/02
|
||||
- uid: manicastri
|
||||
fullName: Francesco Manicastri
|
||||
entryDate: 2022/05
|
||||
- uid: delucreziis
|
||||
fullName: Antonio De Lucreziis
|
||||
entryDate: 2019/09
|
||||
- uid: serdyuk
|
||||
fullName: Illya Serdyuk
|
||||
entryDate: 2019/12
|
||||
- uid: caporali
|
||||
fullName: Francesco Caporali
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
- uid: dachille
|
||||
fullName: Letizia D'Achille
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
eventi:
|
||||
- type: simple
|
||||
title: Nuovo sito del PHC
|
||||
description: |
|
||||
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
|
||||
date: 2022/09
|
||||
icon: fa-solid fa-star
|
||||
- type: simple
|
||||
title: Data di pubblicazione del sito Poisson originale
|
||||
date: 1996/06/17
|
||||
description: |
|
||||
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
|
||||
icon: fa-solid fa-upload
|
||||
- type: simple
|
||||
title: Apertura del PHC
|
||||
date: 1994
|
||||
icon: fa-solid fa-flag-checkered
|
||||
|
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 |
@@ -0,0 +1,23 @@
|
||||
|
||||
# https://pnpm.io/ is better at storing packages than npm, if you still want to use npm just run "make NPM=npm <target>"
|
||||
NPM = pnpm
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: all
|
||||
all: setup build
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
$(NPM) install
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
mkdir -p out/
|
||||
$(NPM) run build
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf out/
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Package to generate frontend files for phc/website",
|
||||
"scripts": {
|
||||
"build": "npm-run-all -p build:*",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"build:js": "rollup -c",
|
||||
"watch:js": "rollup -c -w",
|
||||
"build:css": "sass -s compressed src/styles/main.scss out/next.css",
|
||||
"watch:css": "sass -w src/styles/main.scss out/next.css"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "aziis98 <antonio.delucreziis@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-typescript": "^8.3.4",
|
||||
"esbuild": "^0.15.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^2.75.3",
|
||||
"rollup-plugin-esbuild": "^4.9.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.52.3",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.10.2",
|
||||
"fuse.js": "^6.6.2",
|
||||
"preact": "^10.10.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { defineConfig } from 'rollup'
|
||||
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import esbuild from 'rollup-plugin-esbuild'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'src/base.js',
|
||||
output: {
|
||||
file: 'out/base.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/utenti.js',
|
||||
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
|
||||
output: {
|
||||
file: 'out/utenti.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
// map library names to global constants
|
||||
alpinejs: 'Alpine',
|
||||
'fuse.js': 'Fuse',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/profilo.js',
|
||||
external: ['alpinejs'],
|
||||
output: {
|
||||
file: 'out/profilo.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
alpinejs: 'Alpine',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/homepage-art.ts',
|
||||
output: {
|
||||
file: 'out/homepage-art.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [esbuild({ minify: true })],
|
||||
},
|
||||
{
|
||||
input: 'src/appunti-condivisi.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,24 @@
|
||||
import { render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
const Counter = ({}) => {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
const increment = () => {
|
||||
setValue(value => value + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
Counter: {value} <button onClick={increment}>+</button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
<>
|
||||
<h1>Preact is Working!</h1>
|
||||
<Counter />
|
||||
</>,
|
||||
document.querySelector('#app')
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $toggle = document.querySelector('#toggle-dark-mode')
|
||||
const $toggleIcon = document.querySelector('#toggle-dark-mode i')
|
||||
|
||||
// Loads preferred dark from from localStorage or defaults to media query.
|
||||
let prefersDarkMode =
|
||||
localStorage.getItem('theme') !== undefined
|
||||
? localStorage.getItem('theme') === 'dark'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
function storePrefersDarkMode(mode) {
|
||||
prefersDarkMode = mode
|
||||
localStorage.setItem('theme', mode ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
function displayToggle() {
|
||||
document.body.classList.toggle('dark-mode', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode)
|
||||
|
||||
document.dispatchEvent(new CustomEvent('theme:change'))
|
||||
}
|
||||
|
||||
$toggle.addEventListener('click', () => {
|
||||
storePrefersDarkMode(!prefersDarkMode)
|
||||
displayToggle()
|
||||
})
|
||||
|
||||
displayToggle()
|
||||
})
|
||||
@@ -0,0 +1,378 @@
|
||||
type Point2i = [number, number]
|
||||
|
||||
type WireDirection = 'down-left' | 'down' | 'down-right'
|
||||
|
||||
type TipPosition = false | 'begin' | 'end' | 'begin-end'
|
||||
|
||||
type WirePiece = {
|
||||
direction: WireDirection
|
||||
lerp: number
|
||||
tipPosition: TipPosition
|
||||
}
|
||||
|
||||
type LatticePoint = string
|
||||
|
||||
function toLatticePoint(x: number, y: number): LatticePoint {
|
||||
return `${x | 0},${y | 0}`
|
||||
}
|
||||
|
||||
function fromLatticePoint(p: LatticePoint): Point2i {
|
||||
const [x, y] = p.split(',').map(s => parseInt(s))
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
type WireNode = { point: Point2i; direction: WireDirection }
|
||||
|
||||
type Wire = WireNode[]
|
||||
|
||||
type World = {
|
||||
dimensions: Point2i
|
||||
|
||||
wirePieces: { [point: LatticePoint]: WirePiece }
|
||||
wiresQueue: { wire: Wire; cursor: number }[]
|
||||
}
|
||||
|
||||
function randomChoice<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right'])
|
||||
|
||||
const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => {
|
||||
if (direction === 'down') return [x, y + 1]
|
||||
if (direction === 'down-left') return [x - 1, y + 1]
|
||||
if (direction === 'down-right') return [x + 1, y + 1]
|
||||
throw 'invalid'
|
||||
}
|
||||
|
||||
function checkPoint(world: World, [x, y]: [number, number]): boolean {
|
||||
return !!world.wirePieces[toLatticePoint(x, y)]
|
||||
}
|
||||
|
||||
function wireIntersects(world: World, wire: Wire): boolean {
|
||||
return wire.some(({ point: [x, y], direction }) => {
|
||||
// TODO: The point check actually "doubly" depends on direction
|
||||
if (direction === 'down') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1])
|
||||
}
|
||||
if (direction === 'down-left') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y])
|
||||
}
|
||||
if (direction === 'down-right') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y])
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function generateWire(world: World): Wire {
|
||||
const [w, h] = world.dimensions
|
||||
|
||||
const randomPoint = (): [number, number] => [
|
||||
Math.floor(Math.random() * w),
|
||||
Math.floor(Math.pow(Math.random(), 2) * h * 0.5),
|
||||
]
|
||||
|
||||
const wireLength = 3 + Math.floor(Math.random() * 10)
|
||||
const wire: Wire = [
|
||||
{
|
||||
point: randomPoint(),
|
||||
direction: randomDirection(),
|
||||
},
|
||||
]
|
||||
|
||||
let prev = wire[0]
|
||||
let dir = prev.direction
|
||||
|
||||
for (let i = 0; i < wireLength; i++) {
|
||||
const p = nextPoint(prev.point, dir)
|
||||
|
||||
if (Math.random() < 0.325) {
|
||||
// change direction
|
||||
if (dir === 'down') {
|
||||
dir = randomChoice(['down-left', 'down-right'])
|
||||
} else {
|
||||
dir = 'down'
|
||||
}
|
||||
}
|
||||
|
||||
wire.push({
|
||||
point: p,
|
||||
direction: dir,
|
||||
})
|
||||
prev = wire[wire.length - 1]
|
||||
}
|
||||
|
||||
return wire
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
return {
|
||||
backgroundColor: '#282828',
|
||||
circuitColor: '#38302e',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: '#eaeaea',
|
||||
circuitColor: '#d4d4d4',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Art {
|
||||
static CELL_SIZE = 28
|
||||
static TIP_RADIUS = 4
|
||||
static WIRE_LERP_SPEED = 25 // units / seconds
|
||||
|
||||
renewGraphicsContext: boolean = true
|
||||
dirty: boolean
|
||||
|
||||
world: World
|
||||
|
||||
constructor($canvas: HTMLCanvasElement) {
|
||||
let g: CanvasRenderingContext2D
|
||||
|
||||
let unMount = this.setup($canvas)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.renewGraphicsContext = true
|
||||
unMount()
|
||||
unMount = this.setup($canvas)
|
||||
})
|
||||
|
||||
document.addEventListener('theme:change', () => {
|
||||
this.dirty = true
|
||||
})
|
||||
|
||||
const renderFn = () => {
|
||||
if (this.renewGraphicsContext) {
|
||||
$canvas.width = $canvas.offsetWidth * devicePixelRatio
|
||||
$canvas.height = $canvas.offsetHeight * devicePixelRatio
|
||||
|
||||
g = $canvas.getContext('2d')!
|
||||
g.scale(devicePixelRatio, devicePixelRatio)
|
||||
}
|
||||
|
||||
if (this.dirty || this.renewGraphicsContext) {
|
||||
console.log('Rendering')
|
||||
this.render(g, $canvas.offsetWidth, $canvas.offsetHeight)
|
||||
}
|
||||
|
||||
this.dirty = false
|
||||
this.renewGraphicsContext = false
|
||||
requestAnimationFrame(renderFn)
|
||||
}
|
||||
|
||||
renderFn()
|
||||
}
|
||||
|
||||
setup($canvas: HTMLCanvasElement) {
|
||||
this.world = {
|
||||
dimensions: [
|
||||
Math.ceil($canvas.offsetWidth / Art.CELL_SIZE),
|
||||
Math.ceil($canvas.offsetHeight / Art.CELL_SIZE),
|
||||
],
|
||||
wirePieces: {},
|
||||
wiresQueue: [],
|
||||
}
|
||||
|
||||
let failedTries = 0
|
||||
|
||||
const wireGeneratorTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('Trying to generate wire')
|
||||
if (failedTries > 400) {
|
||||
console.log('Stopped generating wires')
|
||||
clearInterval(wireGeneratorTimer)
|
||||
return
|
||||
}
|
||||
|
||||
const wire = generateWire(this.world)
|
||||
if (!wireIntersects(this.world, wire)) {
|
||||
failedTries = 0
|
||||
this.world.wiresQueue.push({ wire, cursor: 0 })
|
||||
} else {
|
||||
failedTries++
|
||||
}
|
||||
}, 10)
|
||||
|
||||
let pieceLerpBeginTime = new Date().getTime()
|
||||
const wireQueueTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
// console.log('Interpolating queued wire')
|
||||
|
||||
// get top wire to add
|
||||
const wireInterp = this.world.wiresQueue[0]
|
||||
if (wireInterp.cursor < wireInterp.wire.length) {
|
||||
const currentNode = wireInterp.wire[wireInterp.cursor]
|
||||
const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED
|
||||
|
||||
const now = new Date().getTime()
|
||||
if (now > pieceLerpEndTime) {
|
||||
wireInterp.cursor++
|
||||
pieceLerpBeginTime = new Date().getTime()
|
||||
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
direction: currentNode.direction,
|
||||
lerp: 1,
|
||||
tipPosition:
|
||||
wireInterp.cursor === 1
|
||||
? 'begin'
|
||||
: wireInterp.cursor === wireInterp.wire.length
|
||||
? 'end'
|
||||
: false,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
return
|
||||
}
|
||||
|
||||
const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
...currentNode,
|
||||
tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end',
|
||||
lerp,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
} else {
|
||||
this.world.wiresQueue.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}, 1000 / 60)
|
||||
|
||||
const unMount = () => {
|
||||
clearInterval(wireGeneratorTimer)
|
||||
clearInterval(wireQueueTimer)
|
||||
}
|
||||
|
||||
// document.addEventListener('keypress', e => {
|
||||
// if (e.key === 'r') {
|
||||
// unMount()
|
||||
// this.setup($canvas)
|
||||
// }
|
||||
// })
|
||||
|
||||
return unMount
|
||||
}
|
||||
|
||||
render(g: CanvasRenderingContext2D, width: number, height: number) {
|
||||
g.clearRect(0, 0, width, height)
|
||||
|
||||
const { backgroundColor, circuitColor } = getTheme()
|
||||
|
||||
// Grid
|
||||
// g.lineWidth = 1
|
||||
// g.strokeStyle = '#ddd'
|
||||
// g.beginPath()
|
||||
// for (let i = 0; i < height / Art.CELL_SIZE; i++) {
|
||||
// g.moveTo(0, i * Art.CELL_SIZE)
|
||||
// g.lineTo(width, i * Art.CELL_SIZE)
|
||||
// }
|
||||
// for (let j = 0; j < width / Art.CELL_SIZE; j++) {
|
||||
// g.moveTo(j * Art.CELL_SIZE, 0)
|
||||
// g.lineTo(j * Art.CELL_SIZE, height)
|
||||
// }
|
||||
// g.stroke()
|
||||
|
||||
g.lineWidth = 3
|
||||
g.strokeStyle = circuitColor
|
||||
g.lineCap = 'round'
|
||||
g.lineJoin = 'round'
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
g.beginPath()
|
||||
g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE)
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down':
|
||||
g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down-right':
|
||||
g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
}
|
||||
g.stroke()
|
||||
}
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
const drawTip = () => {
|
||||
if (
|
||||
y !== 0 &&
|
||||
(piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end')
|
||||
) {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = (x - piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = (x + piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (piece.tipPosition) {
|
||||
g.fillStyle = backgroundColor
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.fill()
|
||||
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement
|
||||
new Art($canvas)
|
||||
@@ -0,0 +1,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-darker: hsl(10, 10%, 17%);
|
||||
--bg-darker-2: #1d2021;
|
||||
--bg-darker-2-1: #1d5021;
|
||||
--bg-darker-3: #101111;
|
||||
|
||||
--accent-1: #154d24;
|
||||
@@ -28,7 +29,6 @@ body.dark-mode {
|
||||
--text-input-readonly-bg: hsl(10, 10%, 22%);
|
||||
--text-input-readonly-fg: hsl(10, 10%, 40%);
|
||||
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
@@ -0,0 +1,14 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
</section>
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,44 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ template "title" . }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
|
||||
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
|
||||
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
|
||||
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/public/base.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/public/style.css">
|
||||
<link rel="stylesheet" href="/public/theme-dark.css">
|
||||
<link rel="icon" type="image/png" href="/public/icons/icons8-electronics-100.png" />
|
||||
</head>
|
||||
|
||||
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
|
||||
{{ template "navbar" . }}
|
||||
|
||||
<div class="main">
|
||||
{{ template "body" . }}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="footer-item">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Article.Title }} • Guide • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section class="news-content">
|
||||
<h1>{{ .Article.Title }}</h1>
|
||||
<div class="date">
|
||||
{{ .Article.PublishDate.Format "2006/01/02" }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Article.Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ .ContentHTML }}
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Guide • 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}}
|
||||
@@ -3,7 +3,7 @@
|
||||
{{define "title"}}Home • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<canvas id="circuit-pattern"></canvas>
|
||||
<canvas id="wires-animation"></canvas>
|
||||
|
||||
<!-- Non avevo molta fantasia per il nome di questa classe -->
|
||||
<section class="super">
|
||||
@@ -14,8 +14,18 @@
|
||||
<h1>
|
||||
Cos'è il PHC?
|
||||
</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>Voluptatibus sequi praesentium similique, quos ex illo autem quidem!</p>
|
||||
<p>
|
||||
Il PHC è un progetto di sperimentazione tecnologica di alcuni studenti di Matematica di Pisa che va avanti fin dal 1994.
|
||||
</p>
|
||||
<p>
|
||||
Fisicamente, occupa la stanza 106 del Dipartimento, mentre questo sito ne è la sede virtuale. Qui si trovano vari servizi offerti agli studenti, e ne si documenta lo sviluppo nella pagina <a href="/news">Notizie</a>.
|
||||
</p>
|
||||
<p>
|
||||
Nella sezione <b>Progetti</b> si trovano alcune delle attività e dei servizi del PHC, mentre <a href="/utenti">Utenti</a> contiene l'indice delle pagine personali degli studenti, che sono storicamente uno dei progetti più importanti del PHC.
|
||||
</p>
|
||||
<p>
|
||||
Per più informazioni sul PHC, si consulti la sezione <a href="/storia">Storia</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,18 +100,19 @@
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
<!-- <h2>Altro...</h2> -->
|
||||
<div class="card">
|
||||
<div class="title">Vuoi diventare un macchinista?</div>
|
||||
<!-- <h3 style="font-weight: 500;">Vuoi diventare un macchinista?</h3> -->
|
||||
<div class="title">
|
||||
Vuoi diventare un macchinista?
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Ti interessa (o interesserebbe) smanettare al PC, montare e smontare i cose? Stai spesso in dipartimento? Allora
|
||||
puoi venire a parlare con noi per diventare un apprendista macchinista per poi entrare nel PHC.
|
||||
Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
|
||||
Stai spesso in dipartimento? Allora fai pure un salto in PHC a parlare con noi, per diventare apprendista (e magari un giorno macchinista) o semplicemente per imparare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/public/home.js"></script>
|
||||
<script src="/public/homepage-art.min.js"></script>
|
||||
{{end}}
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h2>
|
||||
Feed RSS
|
||||
<a href="/news/rss"> <i class="fa-solid fa-rss"></i></a>
|
||||
</h2>
|
||||
<h1>
|
||||
<i class="far fa-newspaper"></i>
|
||||
Notizie Importanti
|
||||
@@ -60,4 +64,4 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -12,14 +12,14 @@
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<a class="nav-element" href="/progetti">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Progetti
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
@@ -35,14 +35,14 @@
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<a class="nav-element" href="/risorse">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Risorse
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
@@ -85,4 +85,4 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -2,9 +2,9 @@
|
||||
{{define "title"}}Profilo di @{{ .User.Username }} • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<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">
|
||||
<h1>Profilo di @{{ .User.Username }}</h1>
|
||||
<h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
|
||||
<p class="center">
|
||||
<a class="button" href="/logout">Logout</a>
|
||||
</p>
|
||||
@@ -97,9 +97,9 @@
|
||||
<section>
|
||||
<h2>Recupero Credenziali Poisson</h2>
|
||||
<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
|
||||
sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -1,5 +1,5 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Utenti • PHC{{end}}
|
||||
{{define "title"}}Storia • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
@@ -7,7 +7,11 @@
|
||||
Storia del PHC
|
||||
</h1>
|
||||
<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>
|
||||
<div class="history-container">
|
||||
<div class="timeline-bar"></div>
|
||||
@@ -16,16 +20,22 @@
|
||||
{{ if eq .Type "simple" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
{{ if .Icon }}
|
||||
<i class="{{ .Icon }}"></i>
|
||||
{{ end }}
|
||||
{{ .Title }}
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
<div class="description">
|
||||
{{ raw .Description }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "entry-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
Ingresso di
|
||||
<i class="fa-solid fa-sign-in"></i>
|
||||
Ingresso di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
@@ -34,8 +44,8 @@
|
||||
{{ if eq .Type "exit-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
Uscita di
|
||||
<i class="fa-solid fa-sign-out"></i>
|
||||
Uscita di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
{{template "base" .}}
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Utenti • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/js/utenti.min.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
<section x-data="utenti">
|
||||
<h1>
|
||||
<i class="fas fa-users"></i>
|
||||
@@ -37,15 +37,20 @@
|
||||
<template x-for="entry in searchResults" :key="entry.item.uid">
|
||||
<div class="user-item card">
|
||||
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
|
||||
sortMode === 'surname' ?
|
||||
entry.item.cognome + ' ' + entry.item.nome :
|
||||
sortMode === 'surname' ?
|
||||
entry.item.cognome + ' ' + entry.item.nome :
|
||||
entry.item.nome + ' ' + entry.item.cognome
|
||||
"></a>
|
||||
<template x-if="entry.item.macchinista">
|
||||
<template x-if="entry.item.tags?.includes('macchinista')">
|
||||
<div class="icon" title="Macchinista">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="entry.item.tags?.includes('rappresentante')">
|
||||
<div class="icon" title="Rappresentante">
|
||||
<i class="fas fa-landmark"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template x-if="entry.score">
|
||||
<span x-text="entry.score"></span>
|
||||
</template> -->
|
||||
@@ -102,7 +102,7 @@ func (article *Article) Render() (string, error) {
|
||||
|
||||
if article.renderedHTML == "" {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
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
|
||||
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() {
|
||||
articleMarkdown = goldmark.New(
|
||||
Markdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
extension.Typographer,
|
||||
|
||||
@@ -48,5 +48,5 @@ func NewDefaultService(host string) Service {
|
||||
return exampleMemoryUsers
|
||||
}
|
||||
|
||||
return &LDAPAuthService{host}
|
||||
return newLDAPAuthService(host)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
urlpkg "net/url"
|
||||
|
||||
"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
|
||||
type LDAPAuthService struct {
|
||||
// ldapAuthService just holds the remote host of the HTTP LDAP service to make requests to
|
||||
type ldapAuthService struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func NewLDAPAuthService(host string) Service {
|
||||
return &LDAPAuthService{host}
|
||||
func newLDAPAuthService(host string) Service {
|
||||
return &ldapAuthService{host}
|
||||
}
|
||||
|
||||
// doGetRequest is a utility to make HTTP GET requests
|
||||
func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error {
|
||||
func (a *ldapAuthService) doGetRequest(url string, response interface{}) error {
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")),
|
||||
"GET", u, bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
|
||||
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
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -89,7 +99,7 @@ func (a *LDAPAuthService) doPostRequest(url string, request interface{}, respons
|
||||
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
|
||||
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
|
||||
return nil, err
|
||||
@@ -98,7 +108,7 @@ func (a *LDAPAuthService) GetUser(username string) (*model.User, error) {
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (a *LDAPAuthService) GetUsers() ([]*model.User, error) {
|
||||
func (a *ldapAuthService) GetUsers() ([]*model.User, error) {
|
||||
ldapUsers := []*ldapUser{}
|
||||
if err := a.doGetRequest("/users", &ldapUsers); err != nil {
|
||||
return nil, err
|
||||
@@ -112,7 +122,7 @@ func (a *LDAPAuthService) GetUsers() ([]*model.User, error) {
|
||||
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
|
||||
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
|
||||
return nil, err
|
||||
@@ -121,7 +131,7 @@ func (a *LDAPAuthService) GetSession(token string) (*model.Session, error) {
|
||||
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{}{
|
||||
"username": username,
|
||||
"password": password,
|
||||
|
||||
@@ -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,10 +16,15 @@ var GitUrl string
|
||||
var ChatUrl string
|
||||
var Email string
|
||||
|
||||
var BaseUrl string
|
||||
|
||||
var UserPagesBaseUrl string
|
||||
|
||||
var AuthServiceHost string
|
||||
|
||||
// ListaUtenti è un file json da cui leggere la lista degli utenti oppure ":auth:" per utilizzare il servizio di autenticazione
|
||||
var ListaUtenti string
|
||||
|
||||
func loadEnv(target *string, name, defaultValue string) {
|
||||
value := os.Getenv(name)
|
||||
if len(strings.TrimSpace(value)) == 0 {
|
||||
@@ -41,6 +46,7 @@ func Load() {
|
||||
// Production
|
||||
loadEnv(&Mode, "MODE", "production")
|
||||
loadEnv(&Host, "HOST", ":8080")
|
||||
loadEnv(&BaseUrl, "BASE_URL", "localhost:8080")
|
||||
|
||||
// Services
|
||||
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
|
||||
@@ -50,12 +56,15 @@ func Load() {
|
||||
// Poisson
|
||||
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
|
||||
|
||||
// AuthService
|
||||
// Auth
|
||||
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
|
||||
|
||||
// Altro
|
||||
loadEnv(&ListaUtenti, "LISTA_UTENTI", ":auth:")
|
||||
}
|
||||
|
||||
func Object() util.H {
|
||||
return util.H{
|
||||
func Object() util.Map {
|
||||
return util.Map{
|
||||
"Mode": Mode,
|
||||
"Host": Host,
|
||||
|
||||
@@ -63,8 +72,12 @@ func Object() util.H {
|
||||
"ChatUrl": ChatUrl,
|
||||
"Email": Email,
|
||||
|
||||
"BaseUrl": BaseUrl,
|
||||
|
||||
"UserPagesBaseUrl": UserPagesBaseUrl,
|
||||
|
||||
"AuthServiceHost": AuthServiceHost,
|
||||
|
||||
"ListaUtenti": ListaUtenti,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()],
|
||||
},
|
||||
])
|
||||
@@ -1,6 +1,6 @@
|
||||
module git.phc.dm.unipi.it/phc/website
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.9.4
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/gorilla/feeds v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
|
||||
@@ -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/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package handler
|
||||
|
||||
type ContextKey[T any] string
|
||||
|
||||
type Context map[string]any
|
||||
|
||||
func GetContextValue[T any](ctx Context, key ContextKey[T]) T {
|
||||
value, present := ctx[string(key)]
|
||||
if !present {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
typedValue, _ := value.(T)
|
||||
|
||||
return typedValue
|
||||
}
|
||||
|
||||
func SetContextValue[T any](ctx Context, key ContextKey[T], value T) {
|
||||
ctx[string(key)] = value
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/rss"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
}
|
||||
|
||||
//
|
||||
// Typed context
|
||||
//
|
||||
|
||||
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
|
||||
const UserKey ContextKey[*model.User] = "user"
|
||||
|
||||
func (ctx Context) getUser() *model.User {
|
||||
return GetContextValue(ctx, UserKey)
|
||||
}
|
||||
|
||||
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
|
||||
type DefaultHandler struct {
|
||||
AuthService auth.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
ListaUtenti lista_utenti.Service
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
|
||||
return h.Renderer.Render(w, view, util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
|
||||
utenti, err := h.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
|
||||
utenti, err := h.ListaUtenti.GetUtenti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
|
||||
storia, err := h.Storia.GetStoria()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "storia.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Storia": storia,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Query": query,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
|
||||
session, err := h.AuthService.Login(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUser(token string) *model.User {
|
||||
user, _ := auth.UserForSession(h.AuthService, token)
|
||||
return user
|
||||
}
|
||||
|
||||
var ErrNoUser = fmt.Errorf(`user not logged in`)
|
||||
|
||||
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return nil, ErrNoUser
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "profilo.html", util.Map{
|
||||
"User": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
|
||||
registry, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
|
||||
|
||||
return newsFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
|
||||
registry, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
|
||||
|
||||
return guideFeed.WriteRss(w)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type authListaUtenti struct {
|
||||
AuthService auth.Service
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
authUsers, err := a.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.ListUser, 0, len(authUsers))
|
||||
for _, u := range authUsers {
|
||||
users = append(users, &model.ListUser{
|
||||
Uid: u.Username,
|
||||
Nome: u.Name,
|
||||
Cognome: u.Surname,
|
||||
Tags: []string{},
|
||||
})
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, a.Macchinisti)
|
||||
mergeRappresentanti(users, a.Rappresentanti)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type jsonListaUtenti struct {
|
||||
Path string
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newJsonListaUtenti(path string) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
var users []*model.ListUser
|
||||
|
||||
f, err := os.Open(j.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, j.Macchinisti)
|
||||
mergeRappresentanti(users, j.Rappresentanti)
|
||||
|
||||
log.Printf("Caricata lista di %d utenti", len(users))
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetUtenti() ([]*model.ListUser, error)
|
||||
}
|
||||
|
||||
// New crea un nuovo servizio che lista gli utenti utilizzando AuthService se config è ":auth:" oppure un file json
|
||||
func New(authService auth.Service, config string) (Service, error) {
|
||||
if config == ":auth:" {
|
||||
return newAuthListaUtenti(authService)
|
||||
}
|
||||
|
||||
return newJsonListaUtenti(config)
|
||||
}
|
||||
|
||||
func loadMacchinisti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/macchinisti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macchinisti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&macchinisti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(macchinisti...), nil
|
||||
}
|
||||
|
||||
func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := macchinisti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "macchinista")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRappresentanti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/rappresentanti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(rappresentanti...), nil
|
||||
}
|
||||
|
||||
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := rappresentanti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "rappresentante")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package model
|
||||
|
||||
type ListUser struct {
|
||||
Uid string `json:"uid"`
|
||||
Nome string `json:"nome"`
|
||||
Cognome string `json:"cognome"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user returned from AuthenticatorService
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
func GenerateRssFeed(entries []*articles.Article, sectionName string, title string, link string, description string) *feeds.Feed {
|
||||
|
||||
// Initialize RSS Feed
|
||||
feed := &feeds.Feed{
|
||||
Title: title,
|
||||
Link: &feeds.Link{Href: link},
|
||||
Description: description,
|
||||
}
|
||||
|
||||
var feedItems []*feeds.Item
|
||||
|
||||
// Add items to RSS feeds
|
||||
for _, entry := range entries {
|
||||
feedItems = append(feedItems,
|
||||
&feeds.Item{
|
||||
Id: entry.Id,
|
||||
Title: entry.Title,
|
||||
Link: &feeds.Link{Href: config.BaseUrl + "/" + sectionName + "/" + entry.Id},
|
||||
Description: entry.Description,
|
||||
Created: entry.PublishDate,
|
||||
})
|
||||
}
|
||||
feed.Items = feedItems
|
||||
|
||||
return feed
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/redirect/v2"
|
||||
)
|
||||
|
||||
func UserMiddleware(h handler.Service) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Cookies("session-token")
|
||||
user := h.HandleUser(token)
|
||||
c.Locals("user", user)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContext(ctx *fiber.Ctx) handler.Context {
|
||||
// the "_" here is required because Go cannot cast <nil> of type interface{} to *model.User, in other words <nil> of type *model.User and <nil> of type interface{} are different type. In this case the "_" returns a boolean that tells whether the cast succeeded or not, if it is false the user variable gets assigned its default zero value that is <nil> of type *model.User
|
||||
user, _ := ctx.Locals("user").(*model.User)
|
||||
|
||||
context := handler.Context{}
|
||||
handler.SetContextValue(context, handler.UserKey, user)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
func NewFiberServer(h handler.Service) *fiber.App {
|
||||
app := fiber.New()
|
||||
routes(h, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// routes defines routes under "/"
|
||||
func routes(h handler.Service, r fiber.Router) {
|
||||
//
|
||||
// Initial setup
|
||||
//
|
||||
|
||||
r.Use(logger.New())
|
||||
r.Use(recover.New())
|
||||
|
||||
// Remove trailing slash from URLs
|
||||
r.Use(redirect.New(redirect.Config{
|
||||
Rules: map[string]string{
|
||||
"/*/": "/$1",
|
||||
},
|
||||
}))
|
||||
|
||||
// Serve generated css and js files and the static "./_public" folder
|
||||
r.Static("/public/", "./_frontend/out")
|
||||
r.Static("/public/", "./_public")
|
||||
|
||||
// Process all request and add user to the request context if there is a session cookie
|
||||
r.Use(UserMiddleware(h))
|
||||
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
|
||||
r.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "home.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/link", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "link.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/login", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "login.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "utenti.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/storia", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStoriaPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti", func(c *fiber.Ctx) error {
|
||||
query := c.Query("q", "")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleQueryAppunti(c, query, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleAppuntiCondivisiPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleNewsPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleGuidePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleNewsFeedPage(c)
|
||||
})
|
||||
|
||||
r.Get("/guide/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleGuideFeedPage(c)
|
||||
})
|
||||
|
||||
r.Post("/login", func(c *fiber.Ctx) error {
|
||||
var loginForm struct {
|
||||
Provider string `form:"provider"`
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&loginForm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := h.HandleLogin(loginForm.Username, loginForm.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: session.Token,
|
||||
Expires: inThreeDays,
|
||||
})
|
||||
|
||||
return c.Redirect("/profilo")
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleProfilePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/logout", func(c *fiber.Ctx) error {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
})
|
||||
|
||||
return c.Redirect("/")
|
||||
})
|
||||
|
||||
r.Get("/news/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleNewsArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleGuideArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
routesApi(h, r.Group("/api"))
|
||||
}
|
||||
|
||||
// routesApi defines routes under "/api"
|
||||
func routesApi(h handler.Service, r fiber.Router) {
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
utenti, err := h.HandleListaUtenti()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(utenti)
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
user, err := h.HandleRequiredUser(CreateContext(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +1,58 @@
|
||||
package main
|
||||
package storia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"bytes"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"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
|
||||
|
||||
func (m byEventDateDescending) Len() int {
|
||||
@@ -23,82 +65,8 @@ func (m byEventDateDescending) Less(i, j int) bool {
|
||||
return m[i].Date > m[j].Date
|
||||
}
|
||||
|
||||
type Macchinista struct {
|
||||
Uid string `json:"uid"`
|
||||
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()
|
||||
func (db *JsonFileStoria) GetStoria() ([]*GenericEvent, error) {
|
||||
history, err := db.GetRawStoria()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,5 +93,61 @@ func GetStoria() ([]*GenericEvent, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
@@ -23,7 +24,9 @@ func NewCacheTemplate(loadPatterns ...string) *CachedTemplate {
|
||||
PreLoadTemplatePatterns: loadPatterns,
|
||||
cachedTmpl: nil,
|
||||
}
|
||||
template.Must(cachedTemplate.Load(template.New("")))
|
||||
|
||||
template.Must(cachedTemplate.Load(nil))
|
||||
|
||||
return cachedTemplate
|
||||
}
|
||||
|
||||
@@ -33,7 +36,11 @@ func (ct *CachedTemplate) Load(t *template.Template) (*template.Template, error)
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
|
||||
if config.Mode == "development" {
|
||||
cachedTemplate.Reload()
|
||||
}
|
||||
|
||||
newData := util.H{}
|
||||
newData := util.Map{}
|
||||
newData.Apply(data)
|
||||
newData["Page"] = util.H{
|
||||
newData["Page"] = util.Map{
|
||||
// Used to inject a page specific class on <body>
|
||||
"Name": strings.TrimSuffix(path.Base(name), ".html"),
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
package util
|
||||
@@ -1,10 +1,10 @@
|
||||
package util
|
||||
|
||||
// H is a shortcut for creating a json-like object
|
||||
type H map[string]interface{}
|
||||
// Map is a shortcut for creating a json-like object
|
||||
type Map map[string]interface{}
|
||||
|
||||
// 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 k, v := range source {
|
||||
target[k] = v
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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 {
|
||||
b := make([]byte, n)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}}
|
||||