41 Commits
Author SHA1 Message Date
BachoSeven eeba6356af Update README.md 2022-09-19 21:48:21 +02:00
BachoSeven 23757ad9b0 dark-theme: fix bg-darker-2-1 2022-09-18 16:17:59 +02:00
BachoSeven 9d518dd4f8 /profilo: use full name 2022-09-18 16:06:35 +02:00
BachoSeven 3ca159e809 auth/ldap.go: remove excess debug logging 2022-09-18 15:15:18 +02:00
BachoSeven 855bc8fa25 auth: fix typo 2022-09-18 13:24:00 +02:00
BachoSeven f7350bc6a8 lista_utenti: fix typo 2022-09-18 13:08:06 +02:00
BachoSeven 4b12c3d59d lista_utenti: handle rappresentanti in :auth: 2022-09-18 13:05:43 +02:00
BachoSeven 47a9d32670 Minor fix in README 2022-09-18 12:47:19 +02:00
BachoSeven 6f308377ef Use url.JoinPath instead of path.Join for URLs. 2022-09-18 01:06:53 +02:00
BachoSeven d7f9f2935d home: start filling descriptions 2022-09-18 01:06:10 +02:00
BachoSeven ddbf06d7d0 don't make undefined routes clickable; fix comment 2022-09-17 22:26:47 +02:00
BachoSeven cd0383245b lista_utenti: add rappresentanti to the user tags. 2022-09-17 22:26:03 +02:00
BachoSeven cff45dc92f Add illya 2022-09-01 15:58:35 +02:00
BachoSeven 346459f822 Minor edits 2022-08-28 18:51:46 +02:00
aziis98 6cccf78e45 Aggiunto supporto per il markdown alle descrizioni degli item in storia e convertito a YAML 2022-08-28 18:45:45 +02:00
aziis98 192d8ea944 chore: Rimosso file utenti.go e spostati file .json in _content/ 2022-08-28 18:11:08 +02:00
aziis98 805d9a7ba5 Estratto il servizio lista utenti e qualche altra modifica 2022-08-28 17:58:44 +02:00
BachoSeven 85a0a21d15 feat: update users via Ldap 2022-08-28 12:23:24 +02:00
BachoSeven 4be4a68912 storia.html: Fix typo 2022-08-28 12:22:52 +02:00
BachoSeven 5decb85552 chore: Refactor storia.go into its own module. 2022-08-28 11:16:07 +02:00
BachoSeven 3d0bd18aef favicon; revamp ed aggiunte a storia.html, rimando alla pagina PHC su
betti.dm.unipi.it.
2022-08-28 01:31:57 +02:00
aziis98 489d7f161d Alcuni update al README e watcher per la frontend 2022-08-27 23:39:13 +02:00
aziis98 6e90f8c32f Aggiunta pagina per appunti condivisi con setup minimale di Preact 2022-08-27 22:58:13 +02:00
aziis98 5337849bc7 Ora il default in .env.dev è lo stesso di config.go 2022-08-27 22:55:59 +02:00
BachoSeven f0a9a17fc0 feed: Fix articles' links' URLs 2022-08-27 22:12:47 +02:00
BachoSeven 1ddc71f64b feeds: fix articles Link(s) 2022-08-27 22:05:01 +02:00
BachoSeven 684859c197 Finalize/fix RSS feeds 2022-08-27 21:35:05 +02:00
BachoSeven c1a701353a feat: RSS feeds for {news,guide} 2022-08-27 20:17:08 +02:00
BachoSeven 9c10e1b8fc feat: Add guide page 2022-08-27 12:14:24 +02:00
BachoSeven 9553dbe252 storia: add Icon and Description fields. 2022-08-26 17:46:38 +02:00
BachoSeven 03bae38ebe profilo.html: fix wording 2022-08-26 17:46:09 +02:00
BachoSeven f875c9c6bf base.html: add e-mail icon 2022-08-26 17:45:35 +02:00
BachoSeven cfb62677db Cleanup .gitignore 2022-08-26 17:45:07 +02:00
aziis98 67f8421516 Removed old homepage animation script 2022-08-19 18:37:57 +02:00
aziis98 a5039610e1 Update README 2022-08-19 18:32:22 +02:00
aziis98 1f90dacbdd Update frontend 2022-08-19 18:30:22 +02:00
aziis98 c9d02d22dd Updated README 2022-08-19 17:23:45 +02:00
aziis98 0f7bde6232 Updated gitignore 2022-08-19 15:14:48 +02:00
aziis98 860cc76503 Minor fixes 2022-08-18 18:26:38 +02:00
aziis98 227619e5eb Still refactoring, better _frontend and _public structuring 2022-08-18 18:11:51 +02:00
aziis98 92cee16cc8 Big refactor 2022-08-18 17:08:05 +02:00
86 changed files with 3566 additions and 2367 deletions
+8 -1
View File
@@ -1,6 +1,6 @@
# Server config
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
+6 -8
View File
@@ -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/
+19 -15
View File
@@ -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"]
+13 -15
View File
@@ -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:
+144 -63
View File
@@ -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/)
+19
View File
@@ -0,0 +1,19 @@
---
id: guida-git
title: "Guida a Git"
tags: git
publish_date: 2022/08/27 22:00
description: |
Vedremo come creare una repository Git per tracciare i cambiamenti di una serie di file nel tempo, e molto altro.
---
## A cosa serve
## Creiamo una repository git
Nonostante per grossi progetti sia comodo usare server Git come github.com, che essendo popolare attirerà l'attenzione di più potenziali __contributors__, per progetti personali o
di piccola scala va benissimo utilizzare un server **self-hostato**: in questo caso noi useremo il server del PHC.
Per prima cosa, visitiamo git.phc.dm.unipi.it ed accediamo con le credenziali di
## Comandi di base
+31
View File
@@ -0,0 +1,31 @@
---
id: guida-terminale
title: "Guida al Terminale"
tags: shell, zsh, vim, ssh
publish_date: 2022/08/27 22:00
description: |
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
---
## 1. Shell & Terminale: differenze?
- introduzione storica + bash
### Le Basi
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
- copiare/incollare testo
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
- chiedere aiuto: man, info
### Zsh: una shell moderna e potente
https://wiki.archlinux.org/title/Zsh
## 2. Vim: l'Editor intramontabile
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
## 3. E la TTY?
Come utilizzarla, quando è utile, limitazioni(no scrollback)
## SSH: la magia della remote shell
+1
View File
@@ -0,0 +1 @@
["delucreziis", "serdyuk", "minnocci", "manicastri"]
+1
View File
@@ -0,0 +1 @@
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
+38
View File
@@ -0,0 +1,38 @@
macchinisti:
- uid: minnocci
fullName: Francesco Minnocci
entryDate: 2022/02
- uid: manicastri
fullName: Francesco Manicastri
entryDate: 2022/05
- uid: delucreziis
fullName: Antonio De Lucreziis
entryDate: 2019/09
- uid: serdyuk
fullName: Illya Serdyuk
entryDate: 2019/12
- uid: caporali
fullName: Francesco Caporali
entryDate: 2018/09
exitDate: 2022/03
- uid: dachille
fullName: Letizia D'Achille
entryDate: 2018/09
exitDate: 2022/03
eventi:
- type: simple
title: Nuovo sito del PHC
description: |
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
date: 2022/09
icon: fa-solid fa-star
- type: simple
title: Data di pubblicazione del sito Poisson originale
date: 1996/06/17
description: |
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
icon: fa-solid fa-upload
- type: simple
title: Apertura del PHC
date: 1994
icon: fa-solid fa-flag-checkered

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+23
View File
@@ -0,0 +1,23 @@
# https://pnpm.io/ is better at storing packages than npm, if you still want to use npm just run "make NPM=npm <target>"
NPM = pnpm
#
# Build Frontend
#
.PHONY: all
all: setup build
.PHONY: setup
setup:
$(NPM) install
.PHONY: build
build:
mkdir -p out/
$(NPM) run build
.PHONY: clean
clean:
rm -rf out/
+35
View File
@@ -0,0 +1,35 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "Package to generate frontend files for phc/website",
"scripts": {
"build": "npm-run-all -p build:*",
"watch": "npm-run-all -p watch:*",
"build:js": "rollup -c",
"watch:js": "rollup -c -w",
"build:css": "sass -s compressed src/styles/main.scss out/next.css",
"watch:css": "sass -w src/styles/main.scss out/next.css"
},
"keywords": [],
"author": "aziis98 <antonio.delucreziis@gmail.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/preset-react": "^7.18.6",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.3.4",
"esbuild": "^0.15.5",
"npm-run-all": "^4.1.5",
"rollup": "^2.75.3",
"rollup-plugin-esbuild": "^4.9.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.52.3",
"typescript": "^4.7.4"
},
"dependencies": {
"alpinejs": "^3.10.2",
"fuse.js": "^6.6.2",
"preact": "^10.10.6"
}
}
+1634
View File
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
import { defineConfig } from 'rollup'
import resolve from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import esbuild from 'rollup-plugin-esbuild'
export default defineConfig([
{
input: 'src/base.js',
output: {
file: 'out/base.min.js',
format: 'iife',
},
plugins: [terser()],
},
{
input: 'src/utenti.js',
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
output: {
file: 'out/utenti.min.js',
format: 'iife',
globals: {
// map library names to global constants
alpinejs: 'Alpine',
'fuse.js': 'Fuse',
},
},
plugins: [terser()],
},
{
input: 'src/profilo.js',
external: ['alpinejs'],
output: {
file: 'out/profilo.min.js',
format: 'iife',
globals: {
alpinejs: 'Alpine',
},
},
plugins: [terser()],
},
{
input: 'src/homepage-art.ts',
output: {
file: 'out/homepage-art.min.js',
format: 'iife',
},
plugins: [esbuild({ minify: true })],
},
{
input: 'src/appunti-condivisi.jsx',
output: {
file: 'out/appunti-condivisi.min.js',
format: 'iife',
},
plugins: [
resolve(),
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-react',
{
runtime: 'automatic',
importSource: 'preact',
},
],
],
}),
terser(),
],
},
])
+24
View File
@@ -0,0 +1,24 @@
import { render } from 'preact'
import { useState } from 'preact/hooks'
const Counter = ({}) => {
const [value, setValue] = useState(0)
const increment = () => {
setValue(value => value + 1)
}
return (
<p>
Counter: {value} <button onClick={increment}>+</button>
</p>
)
}
render(
<>
<h1>Preact is Working!</h1>
<Counter />
</>,
document.querySelector('#app')
)
+41
View File
@@ -0,0 +1,41 @@
document.addEventListener('DOMContentLoaded', function () {
renderMathInElement(document.body, {
delimiters: [
{ left: '$', right: '$', display: false },
{ left: '$$', right: '$$', display: true },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
],
})
})
document.addEventListener('DOMContentLoaded', () => {
const $toggle = document.querySelector('#toggle-dark-mode')
const $toggleIcon = document.querySelector('#toggle-dark-mode i')
// Loads preferred dark from from localStorage or defaults to media query.
let prefersDarkMode =
localStorage.getItem('theme') !== undefined
? localStorage.getItem('theme') === 'dark'
: window.matchMedia('(prefers-color-scheme: dark)').matches
function storePrefersDarkMode(mode) {
prefersDarkMode = mode
localStorage.setItem('theme', mode ? 'dark' : 'light')
}
function displayToggle() {
document.body.classList.toggle('dark-mode', prefersDarkMode)
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode)
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode)
document.dispatchEvent(new CustomEvent('theme:change'))
}
$toggle.addEventListener('click', () => {
storePrefersDarkMode(!prefersDarkMode)
displayToggle()
})
displayToggle()
})
+378
View File
@@ -0,0 +1,378 @@
type Point2i = [number, number]
type WireDirection = 'down-left' | 'down' | 'down-right'
type TipPosition = false | 'begin' | 'end' | 'begin-end'
type WirePiece = {
direction: WireDirection
lerp: number
tipPosition: TipPosition
}
type LatticePoint = string
function toLatticePoint(x: number, y: number): LatticePoint {
return `${x | 0},${y | 0}`
}
function fromLatticePoint(p: LatticePoint): Point2i {
const [x, y] = p.split(',').map(s => parseInt(s))
return [x, y]
}
type WireNode = { point: Point2i; direction: WireDirection }
type Wire = WireNode[]
type World = {
dimensions: Point2i
wirePieces: { [point: LatticePoint]: WirePiece }
wiresQueue: { wire: Wire; cursor: number }[]
}
function randomChoice<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right'])
const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => {
if (direction === 'down') return [x, y + 1]
if (direction === 'down-left') return [x - 1, y + 1]
if (direction === 'down-right') return [x + 1, y + 1]
throw 'invalid'
}
function checkPoint(world: World, [x, y]: [number, number]): boolean {
return !!world.wirePieces[toLatticePoint(x, y)]
}
function wireIntersects(world: World, wire: Wire): boolean {
return wire.some(({ point: [x, y], direction }) => {
// TODO: The point check actually "doubly" depends on direction
if (direction === 'down') {
return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1])
}
if (direction === 'down-left') {
return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y])
}
if (direction === 'down-right') {
return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y])
}
return false
})
}
function generateWire(world: World): Wire {
const [w, h] = world.dimensions
const randomPoint = (): [number, number] => [
Math.floor(Math.random() * w),
Math.floor(Math.pow(Math.random(), 2) * h * 0.5),
]
const wireLength = 3 + Math.floor(Math.random() * 10)
const wire: Wire = [
{
point: randomPoint(),
direction: randomDirection(),
},
]
let prev = wire[0]
let dir = prev.direction
for (let i = 0; i < wireLength; i++) {
const p = nextPoint(prev.point, dir)
if (Math.random() < 0.325) {
// change direction
if (dir === 'down') {
dir = randomChoice(['down-left', 'down-right'])
} else {
dir = 'down'
}
}
wire.push({
point: p,
direction: dir,
})
prev = wire[wire.length - 1]
}
return wire
}
function getTheme() {
if (document.body.classList.contains('dark-mode')) {
return {
backgroundColor: '#282828',
circuitColor: '#38302e',
}
} else {
return {
backgroundColor: '#eaeaea',
circuitColor: '#d4d4d4',
}
}
}
class Art {
static CELL_SIZE = 28
static TIP_RADIUS = 4
static WIRE_LERP_SPEED = 25 // units / seconds
renewGraphicsContext: boolean = true
dirty: boolean
world: World
constructor($canvas: HTMLCanvasElement) {
let g: CanvasRenderingContext2D
let unMount = this.setup($canvas)
window.addEventListener('resize', () => {
this.renewGraphicsContext = true
unMount()
unMount = this.setup($canvas)
})
document.addEventListener('theme:change', () => {
this.dirty = true
})
const renderFn = () => {
if (this.renewGraphicsContext) {
$canvas.width = $canvas.offsetWidth * devicePixelRatio
$canvas.height = $canvas.offsetHeight * devicePixelRatio
g = $canvas.getContext('2d')!
g.scale(devicePixelRatio, devicePixelRatio)
}
if (this.dirty || this.renewGraphicsContext) {
console.log('Rendering')
this.render(g, $canvas.offsetWidth, $canvas.offsetHeight)
}
this.dirty = false
this.renewGraphicsContext = false
requestAnimationFrame(renderFn)
}
renderFn()
}
setup($canvas: HTMLCanvasElement) {
this.world = {
dimensions: [
Math.ceil($canvas.offsetWidth / Art.CELL_SIZE),
Math.ceil($canvas.offsetHeight / Art.CELL_SIZE),
],
wirePieces: {},
wiresQueue: [],
}
let failedTries = 0
const wireGeneratorTimer = setInterval(() => {
if (this.world.wiresQueue.length > 0) {
return
}
// console.log('Trying to generate wire')
if (failedTries > 400) {
console.log('Stopped generating wires')
clearInterval(wireGeneratorTimer)
return
}
const wire = generateWire(this.world)
if (!wireIntersects(this.world, wire)) {
failedTries = 0
this.world.wiresQueue.push({ wire, cursor: 0 })
} else {
failedTries++
}
}, 10)
let pieceLerpBeginTime = new Date().getTime()
const wireQueueTimer = setInterval(() => {
if (this.world.wiresQueue.length > 0) {
// console.log('Interpolating queued wire')
// get top wire to add
const wireInterp = this.world.wiresQueue[0]
if (wireInterp.cursor < wireInterp.wire.length) {
const currentNode = wireInterp.wire[wireInterp.cursor]
const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED
const now = new Date().getTime()
if (now > pieceLerpEndTime) {
wireInterp.cursor++
pieceLerpBeginTime = new Date().getTime()
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
direction: currentNode.direction,
lerp: 1,
tipPosition:
wireInterp.cursor === 1
? 'begin'
: wireInterp.cursor === wireInterp.wire.length
? 'end'
: false,
}
this.dirty = true
return
}
const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
...currentNode,
tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end',
lerp,
}
this.dirty = true
} else {
this.world.wiresQueue.splice(0, 1)
}
}
}, 1000 / 60)
const unMount = () => {
clearInterval(wireGeneratorTimer)
clearInterval(wireQueueTimer)
}
// document.addEventListener('keypress', e => {
// if (e.key === 'r') {
// unMount()
// this.setup($canvas)
// }
// })
return unMount
}
render(g: CanvasRenderingContext2D, width: number, height: number) {
g.clearRect(0, 0, width, height)
const { backgroundColor, circuitColor } = getTheme()
// Grid
// g.lineWidth = 1
// g.strokeStyle = '#ddd'
// g.beginPath()
// for (let i = 0; i < height / Art.CELL_SIZE; i++) {
// g.moveTo(0, i * Art.CELL_SIZE)
// g.lineTo(width, i * Art.CELL_SIZE)
// }
// for (let j = 0; j < width / Art.CELL_SIZE; j++) {
// g.moveTo(j * Art.CELL_SIZE, 0)
// g.lineTo(j * Art.CELL_SIZE, height)
// }
// g.stroke()
g.lineWidth = 3
g.strokeStyle = circuitColor
g.lineCap = 'round'
g.lineJoin = 'round'
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
const [x, y] = fromLatticePoint(lp)
g.beginPath()
g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE)
switch (piece.direction) {
case 'down-left':
g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
case 'down':
g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
case 'down-right':
g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
break
}
g.stroke()
}
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
const [x, y] = fromLatticePoint(lp)
const drawTip = () => {
if (
y !== 0 &&
(piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end')
) {
switch (piece.direction) {
case 'down-left':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down-right':
{
const cx = x * Art.CELL_SIZE
const cy = y * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
}
}
if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') {
switch (piece.direction) {
case 'down-left':
{
const cx = (x - piece.lerp) * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down':
{
const cx = x * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
case 'down-right':
{
const cx = (x + piece.lerp) * Art.CELL_SIZE
const cy = (y + piece.lerp) * Art.CELL_SIZE
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
}
break
}
}
}
if (piece.tipPosition) {
g.fillStyle = backgroundColor
g.beginPath()
drawTip()
g.fill()
g.beginPath()
drawTip()
g.stroke()
}
}
}
}
const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement
new Art($canvas)
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"noImplicitAny": true,
"sourceMap": true,
"noEmitOnError": true
},
"filesGlob": ["./src/**/*.ts"]
}

Before

Width:  |  Height:  |  Size: 790 B

After

Width:  |  Height:  |  Size: 790 B

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 4.6 MiB

@@ -8,6 +8,7 @@ body.dark-mode {
--bg-dark: hsl(10, 10%, 20%);
--bg-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;
+14
View File
@@ -0,0 +1,14 @@
{{template "base" .}}
{{define "title"}}Condivisione Appunti &bull; PHC{{end}}
{{define "body"}}
<section>
<h1>
<i class="fas fa-book"></i>
Condivisione Appunti
</h1>
<div id="app"></div>
</section>
<script src="/public/appunti-condivisi.min.js"></script>
{{end}}
+44
View File
@@ -0,0 +1,44 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ template "title" . }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
crossorigin="anonymous"></script>
<script src="/public/base.min.js"></script>
<link rel="stylesheet" href="/public/style.css">
<link rel="stylesheet" href="/public/theme-dark.css">
<link rel="icon" type="image/png" href="/public/icons/icons8-electronics-100.png" />
</head>
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
{{ template "navbar" . }}
<div class="main">
{{ template "body" . }}
</div>
<footer>
<div class="footer-item">
<i class="fa-solid fa-envelope"></i>
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
</div>
</footer>
</body>
</html>
{{end}}
+18
View File
@@ -0,0 +1,18 @@
{{template "base" .}}
{{define "title"}}{{ .Article.Title }} &bull; Guide &bull; PHC{{end}}
{{define "body"}}
<section class="news-content">
<h1>{{ .Article.Title }}</h1>
<div class="date">
{{ .Article.PublishDate.Format "2006/01/02" }}
</div>
<div class="tags">
{{ range .Article.Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
{{ .ContentHTML }}
</section>
{{end}}
+37
View File
@@ -0,0 +1,37 @@
{{template "base" .}}
{{define "title"}}Guide &bull; PHC{{end}}
{{define "body"}}
<section>
<h2>
Feed RSS
<a href="/guide/rss"> <i class="fa-solid fa-rss"></i></a>
</h2>
<h1>
<i class="fa-solid fa-person-chalkboard"></i>
Articoli
</h1>
<div class="card-list">
{{ range .Articles }}
<div class="card">
<div class="title">
<a href="/guide/{{ .Id }}">
{{ .Title }}
</a>
</div>
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
<div class="description">
<p>{{ .Description }}</p>
</div>
<div class="tags">
{{ range .Tags }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</section>
{{end}}
+20 -9
View File
@@ -3,7 +3,7 @@
{{define "title"}}Home &bull; PHC{{end}}
{{define "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}}
+5 -1
View File
@@ -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 }}
+4 -4
View File
@@ -2,9 +2,9 @@
{{define "title"}}Profilo di @{{ .User.Username }} &bull; PHC{{end}}
{{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/public/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}}
+14 -4
View File
@@ -1,5 +1,5 @@
{{template "base" .}}
{{define "title"}}Utenti &bull; PHC{{end}}
{{define "title"}}Storia &bull; 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>
+10 -5
View File
@@ -1,9 +1,9 @@
{{template "base" .}}
{{template "base" .}}
{{define "title"}}Utenti &bull; PHC{{end}}
{{define "body"}}
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
<script src="/public/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> -->
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -48,5 +48,5 @@ func NewDefaultService(host string) Service {
return exampleMemoryUsers
}
return &LDAPAuthService{host}
return newLDAPAuthService(host)
}
+23 -13
View File
@@ -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,
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"log"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/config"
"git.phc.dm.unipi.it/phc/website/handler"
"git.phc.dm.unipi.it/phc/website/lista_utenti"
"git.phc.dm.unipi.it/phc/website/server"
"git.phc.dm.unipi.it/phc/website/storia"
"git.phc.dm.unipi.it/phc/website/templates"
)
func main() {
config.Load()
authService := auth.NewDefaultService(config.AuthServiceHost)
listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti)
if err != nil {
log.Fatal(err)
}
h := &handler.DefaultHandler{
AuthService: authService,
Renderer: templates.NewRenderer(
"./_views/",
"./_views/base.html",
"./_views/partials/*.html",
),
NewsArticlesRegistry: articles.NewRegistry("./_content/news"),
GuideArticlesRegistry: articles.NewRegistry("./_content/guide"),
Storia: &storia.JsonFileStoria{
Path: "./_content/storia.yaml",
},
ListaUtenti: listaUtentiService,
}
app := server.NewFiberServer(h)
log.Printf("Starting server on host %q", config.Host)
if err := app.Listen(config.Host); err != nil {
log.Fatal(err)
}
}
+16 -3
View File
@@ -16,10 +16,15 @@ var GitUrl string
var ChatUrl string
var 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,
}
}
-23
View File
@@ -1,23 +0,0 @@
STYLES_SOURCES = $(wildcard src/styles/*.scss)
SCRIPTS_SOURCES = $(wildcard src/*.js)
.PHONY: all
all: scripts styles
.PHONY: styles
styles: $(STYLES_SOURCES)
npm run build:styles
.PHONY: scripts
scripts: $(SCRIPTS_SOURCES)
npm run build:scripts
.PHONY: clean
clean:
rm -rf dist/
.PHONY: debug
debug:
@echo "STYLES_SOURCES = $(STYLES_SOURCES)"
@echo "SCRIPTS_SOURCES = $(SCRIPTS_SOURCES)"
-1338
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -1,22 +0,0 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "Package to generate frontend files for phc/website",
"scripts": {
"build:scripts": "rollup -c",
"build:styles": "sass -s compressed --no-source-map src/styles/main.scss dist/main.css"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.3.0",
"rollup": "^2.75.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.52.3"
},
"dependencies": {
"alpinejs": "^3.10.2",
"fuse.js": "^6.6.2"
}
}
-31
View File
@@ -1,31 +0,0 @@
import { defineConfig } from 'rollup'
import { terser } from 'rollup-plugin-terser'
export default defineConfig([
{
input: 'src/utenti.js',
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
output: {
file: 'dist/utenti.min.js',
format: 'iife',
globals: {
// map library names to global constants
alpinejs: 'Alpine',
'fuse.js': 'Fuse',
},
},
plugins: [terser()],
},
{
input: 'src/profilo.js',
external: ['alpinejs'],
output: {
file: 'dist/profilo.min.js',
format: 'iife',
globals: {
alpinejs: 'Alpine',
},
},
plugins: [terser()],
},
])
+2 -1
View File
@@ -1,6 +1,6 @@
module git.phc.dm.unipi.it/phc/website
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
+2
View File
@@ -19,6 +19,8 @@ github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSE
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
github.com/gofiber/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=
+21
View File
@@ -0,0 +1,21 @@
package handler
type ContextKey[T any] string
type Context map[string]any
func GetContextValue[T any](ctx Context, key ContextKey[T]) T {
value, present := ctx[string(key)]
if !present {
var zero T
return zero
}
typedValue, _ := value.(T)
return typedValue
}
func SetContextValue[T any](ctx Context, key ContextKey[T], value T) {
ctx[string(key)] = value
}
+223
View File
@@ -0,0 +1,223 @@
package handler
import (
"fmt"
"html/template"
"io"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/lista_utenti"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/rss"
"git.phc.dm.unipi.it/phc/website/storia"
"git.phc.dm.unipi.it/phc/website/templates"
"git.phc.dm.unipi.it/phc/website/util"
)
type Service interface {
HandleStaticPage(w io.Writer, view string, ctx Context) error
HandleUtenti() ([]*model.User, error)
HandleListaUtenti() ([]*model.ListUser, error)
HandleStoriaPage(w io.Writer, ctx Context) error
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
HandleNewsPage(w io.Writer, ctx Context) error
HandleGuidePage(w io.Writer, ctx Context) error
HandleLogin(username, password string) (*model.Session, error)
HandleUser(token string) *model.User
HandleRequiredUser(ctx Context) (*model.User, error)
HandleProfilePage(w io.Writer, ctx Context) error
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
HandleNewsFeedPage(w io.Writer) error
HandleGuideFeedPage(w io.Writer) error
}
//
// Typed context
//
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
const UserKey ContextKey[*model.User] = "user"
func (ctx Context) getUser() *model.User {
return GetContextValue(ctx, UserKey)
}
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
type DefaultHandler struct {
AuthService auth.Service
Renderer *templates.TemplateRenderer
NewsArticlesRegistry *articles.Registry
GuideArticlesRegistry *articles.Registry
ListaUtenti lista_utenti.Service
Storia storia.StoriaService
}
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
return h.Renderer.Render(w, view, util.Map{
"User": ctx.getUser(),
})
}
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
utenti, err := h.AuthService.GetUsers()
if err != nil {
return nil, err
}
return utenti, nil
}
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
utenti, err := h.ListaUtenti.GetUtenti()
if err != nil {
return nil, err
}
return utenti, nil
}
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
storia, err := h.Storia.GetStoria()
if err != nil {
return err
}
return h.Renderer.Render(w, "storia.html", util.Map{
"User": ctx.getUser(),
"Storia": storia,
})
}
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
return h.Renderer.Render(w, "appunti.html", util.Map{
"User": ctx.getUser(),
"Query": query,
})
}
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
"User": ctx.getUser(),
})
}
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
articles, err := h.NewsArticlesRegistry.GetArticles()
if err != nil {
return err
}
return h.Renderer.Render(w, "news.html", util.Map{
"User": ctx.getUser(),
"Articles": articles,
})
}
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
articles, err := h.GuideArticlesRegistry.GetArticles()
if err != nil {
return err
}
return h.Renderer.Render(w, "guide.html", util.Map{
"User": ctx.getUser(),
"Articles": articles,
})
}
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
session, err := h.AuthService.Login(username, password)
if err != nil {
return nil, err
}
return session, nil
}
func (h *DefaultHandler) HandleUser(token string) *model.User {
user, _ := auth.UserForSession(h.AuthService, token)
return user
}
var ErrNoUser = fmt.Errorf(`user not logged in`)
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
user := ctx.getUser()
if user == nil {
return nil, ErrNoUser
}
return user, nil
}
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
user := ctx.getUser()
if user == nil {
return ErrNoUser
}
return h.Renderer.Render(w, "profilo.html", util.Map{
"User": user,
})
}
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
return h.Renderer.Render(w, "news-base.html", util.Map{
"User": ctx.getUser(),
"Article": article,
"ContentHTML": template.HTML(html),
})
}
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
return h.Renderer.Render(w, "guide-base.html", util.Map{
"User": ctx.getUser(),
"Article": article,
"ContentHTML": template.HTML(html),
})
}
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
registry, err := h.NewsArticlesRegistry.GetArticles()
if err != nil {
return err
}
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
return newsFeed.WriteRss(w)
}
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
registry, err := h.GuideArticlesRegistry.GetArticles()
if err != nil {
return err
}
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
return guideFeed.WriteRss(w)
}
+50
View File
@@ -0,0 +1,50 @@
package lista_utenti
import (
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type authListaUtenti struct {
AuthService auth.Service
Macchinisti util.Set[string]
Rappresentanti util.Set[string]
}
func newAuthListaUtenti(authService auth.Service) (Service, error) {
macchinisti, err := loadMacchinisti()
if err != nil {
return nil, err
}
rappresentanti, err := loadRappresentanti()
if err != nil {
return nil, err
}
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
}
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
authUsers, err := a.AuthService.GetUsers()
if err != nil {
return nil, err
}
users := make([]*model.ListUser, 0, len(authUsers))
for _, u := range authUsers {
users = append(users, &model.ListUser{
Uid: u.Username,
Nome: u.Name,
Cognome: u.Surname,
Tags: []string{},
})
}
mergeMacchinisti(users, a.Macchinisti)
mergeRappresentanti(users, a.Rappresentanti)
return users, nil
}
+51
View File
@@ -0,0 +1,51 @@
package lista_utenti
import (
"encoding/json"
"log"
"os"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type jsonListaUtenti struct {
Path string
Macchinisti util.Set[string]
Rappresentanti util.Set[string]
}
func newJsonListaUtenti(path string) (Service, error) {
macchinisti, err := loadMacchinisti()
if err != nil {
return nil, err
}
rappresentanti, err := loadRappresentanti()
if err != nil {
return nil, err
}
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
}
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
var users []*model.ListUser
f, err := os.Open(j.Path)
if err != nil {
return nil, err
}
if err := json.NewDecoder(f).Decode(&users); err != nil {
return nil, err
}
mergeMacchinisti(users, j.Macchinisti)
mergeRappresentanti(users, j.Rappresentanti)
log.Printf("Caricata lista di %d utenti", len(users))
return users, nil
}
+69
View File
@@ -0,0 +1,69 @@
package lista_utenti
import (
"encoding/json"
"os"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/util"
)
type Service interface {
GetUtenti() ([]*model.ListUser, error)
}
// New crea un nuovo servizio che lista gli utenti utilizzando AuthService se config è ":auth:" oppure un file json
func New(authService auth.Service, config string) (Service, error) {
if config == ":auth:" {
return newAuthListaUtenti(authService)
}
return newJsonListaUtenti(config)
}
func loadMacchinisti() (util.Set[string], error) {
f, err := os.Open("_content/macchinisti.json")
if err != nil {
return nil, err
}
macchinisti := []string{}
if err := json.NewDecoder(f).Decode(&macchinisti); err != nil {
return nil, err
}
return util.NewSet(macchinisti...), nil
}
func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
for _, user := range users {
if _, found := macchinisti[user.Uid]; found {
user.Tags = append(user.Tags, "macchinista")
}
}
}
func loadRappresentanti() (util.Set[string], error) {
f, err := os.Open("_content/rappresentanti.json")
if err != nil {
return nil, err
}
rappresentanti := []string{}
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
return nil, err
}
return util.NewSet(rappresentanti...), nil
}
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
for _, user := range users {
if _, found := rappresentanti[user.Uid]; found {
user.Tags = append(user.Tags, "rappresentante")
}
}
}
-208
View File
@@ -1,208 +0,0 @@
package main
import (
"fmt"
"html/template"
"log"
"time"
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/auth"
"git.phc.dm.unipi.it/phc/website/config"
"git.phc.dm.unipi.it/phc/website/model"
"git.phc.dm.unipi.it/phc/website/templates"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/redirect/v2"
)
func UserMiddleware(as auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Cookies("session-token")
user, _ := auth.UserForSession(as, token)
c.Locals("user", user)
return c.Next()
}
}
func main() {
config.Load()
app := fiber.New()
app.Use(logger.New())
app.Use(recover.New())
// Remove trailing slash from URLs
app.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/*/": "/$1",
},
}))
// Serve content statically from "./public", mounted on the "/public/" route
app.Static("/public/", "./public")
authService := auth.NewDefaultService(config.AuthServiceHost)
app.Use(UserMiddleware(authService))
// Templates & Renderer
renderer := templates.NewRenderer(
"./views/",
"./views/base.html",
"./views/partials/*.html",
)
newsArticlesRegistry := articles.NewRegistry("./news")
// Routes
actuallyStaticRoutes := map[string]string{
"/": "home.html",
"/link": "link.html",
"/login": "login.html",
"/utenti": "utenti.html",
}
for route, view := range actuallyStaticRoutes {
localView := view
app.Get(route, func(c *fiber.Ctx) error {
c.Type("html")
return renderer.Render(c, localView, util.H{
"User": c.Locals("user"),
})
})
}
app.Get("/api/utenti", func(c *fiber.Ctx) error {
utenti, err := authService.GetUsers()
if err != nil {
return err
}
return c.JSON(utenti)
})
app.Get("/api/profilo", func(c *fiber.Ctx) error {
user := c.Locals("user")
if user == nil {
return fmt.Errorf(`user not logged in`)
}
return c.JSON(user)
})
app.Get("/storia", func(c *fiber.Ctx) error {
storia, err := GetStoria()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "storia.html", util.H{
"User": c.Locals("user"),
"Storia": storia,
})
})
app.Get("/appunti", func(c *fiber.Ctx) error {
searchQuery := c.Query("q", "")
c.Type("html")
return renderer.Render(c, "appunti.html", util.H{
"User": c.Locals("user"),
"Query": searchQuery,
})
})
app.Get("/news", func(c *fiber.Ctx) error {
articles, err := newsArticlesRegistry.GetArticles()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news.html", util.H{
"User": c.Locals("user"),
"Articles": articles,
})
})
app.Post("/login", func(c *fiber.Ctx) error {
var loginForm struct {
Provider string `form:"provider"`
Username string `form:"username"`
Password string `form:"password"`
}
if err := c.BodyParser(&loginForm); err != nil {
return err
}
session, err := authService.Login(loginForm.Username, loginForm.Password)
if err != nil {
return err
}
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: session.Token,
Expires: inThreeDays,
})
return c.Redirect("/profilo")
})
app.Get("/profilo", func(c *fiber.Ctx) error {
user, ok := c.Locals("user").(*model.User)
if !ok || user == nil {
return fmt.Errorf(`user not logged in`)
}
c.Type("html")
return renderer.Render(c, "profilo.html", util.H{
"User": c.Locals("user"),
})
})
app.Get("/logout", func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: "",
Expires: time.Now(),
})
return c.Redirect("/")
})
app.Get("/news/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
article, err := newsArticlesRegistry.GetArticle(articleID)
if err != nil {
return err
}
html, err := article.Render()
if err != nil {
return err
}
c.Type("html")
return renderer.Render(c, "news-base.html", util.H{
"User": c.Locals("user"),
"Article": article,
"ContentHTML": template.HTML(html),
})
})
log.Printf("Starting server on host %q", config.Host)
err := app.Listen(config.Host)
if err != nil {
log.Fatal(err)
}
}
+7
View File
@@ -1,5 +1,12 @@
package model
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"`
-342
View File
@@ -1,342 +0,0 @@
function pointInInterval(a, b, x) {
return a <= x && x <= b;
}
function intervalIntersectsInterval(a, b, c, d) {
if (c <= a) {
return a <= d;
}
return pointInInterval(a, b, c);
}
function rectIntersectRect(x1, y1, x2, y2, x3, y3, x4, y4) {
return intervalIntersectsInterval(x1, x2, x3, x4) && intervalIntersectsInterval(y1, y2, y3, y4);
}
function intersects(x1, y1, x2, y2, x3, y3, x4, y4) {
const a_minx = Math.min(x1, x2);
const a_maxx = Math.max(x1, x2);
const a_miny = Math.min(y1, y2);
const a_maxy = Math.max(y1, y2);
const b_minx = Math.min(x3, x4);
const b_maxx = Math.max(x3, x4);
const b_miny = Math.min(y3, y4);
const b_maxy = Math.max(y3, y4);
if (!rectIntersectRect(a_minx, a_miny, a_maxx, a_maxy, b_minx, b_miny, b_maxx, b_maxy))
return false;
const a_dx = x2 - x1;
const a_dy = y2 - y1;
const b_dx = x4 - x3;
const b_dy = y4 - y3;
const det = -b_dx * a_dy + a_dx * b_dy;
if (Math.abs(det) === 0 && -a_dy * (x3 - x1) + a_dx * (y3 - y1) === 0) {
return y1 <= y3 <= y2 || y1 <= y4 <= y2;
}
const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / det;
const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / det;
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
}
const $canvas = document.querySelector('#circuit-pattern');
let g = $canvas.getContext('2d');
let WIDTH = $canvas.offsetWidth;
let HEIGHT = $canvas.offsetHeight;
const TS = 20;
let ROWS = HEIGHT / TS;
let COLS = WIDTH / TS;
function updateCanvasDimensions() {
g = $canvas.getContext('2d');
WIDTH = $canvas.offsetWidth;
HEIGHT = $canvas.offsetHeight;
ROWS = HEIGHT / TS;
COLS = WIDTH / TS;
$canvas.width = WIDTH * devicePixelRatio;
$canvas.height = HEIGHT * devicePixelRatio;
g.scale(devicePixelRatio, devicePixelRatio);
}
window.addEventListener('resize', () => updateCanvasDimensions());
updateCanvasDimensions();
const state = {
wires: [], // :: [(Int, Int)]
nextWire: null,
nextWireIndex: 0,
nextSegmentLen: 0,
t: 0,
};
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomChoice(list) {
return list[randomInt(0, list.length - 1)];
}
const DIRS = {
left: [-1, 1],
center: [0, 1],
right: [+1, 1],
};
const NEXT_DIRS = {
left: ['center'],
center: ['left', 'right'],
right: ['center'],
};
function canPlaceSegment([[p1x, p1y], [p2x, p2y]]) {
return state.wires.every(wire => {
return everyWireSegment(wire, ([[sx, sy], [lx, ly]]) => {
return !intersects(sx, sy, lx, ly, p1x, p1y, p2x, p2y);
});
})
}
function everyWireSegment(wire, segmentFn) {
let [start, ...rest] = wire;
return rest.every((pt) => {
const r = segmentFn([start, pt]);
start = pt;
return r;
});
}
function generateWire() {
const pieceCount = Math.pow(randomFloat(1, 2.5), 2) | 0;
const startPoint = [
randomInt(0, COLS),
Math.pow(randomFloat(0, ROWS / 12), 2) | 0,
];
const sizes = Array(pieceCount).fill(0).map(() => randomInt(2, 4) | 0);
const dirs = [randomChoice(Object.keys(DIRS))];
for (let i = 0; i < pieceCount - 1; i++) {
const last = dirs[dirs.length - 1];
dirs.push(randomChoice(NEXT_DIRS[last]));
}
const wire = [startPoint];
for (let i = 0; i < pieceCount; i++) {
const [lpx, lpy] = wire[wire.length - 1];
const [dx, dy] = DIRS[dirs[i]];
const s = sizes[i];
const s_bonus = (dirs[i] === 'center' ? s * 2 : s) | 0;
wire.push([lpx + dx * s_bonus, lpy + dy * s_bonus]);
}
const ok = everyWireSegment(wire, segment => {
return canPlaceSegment(segment);
});
if (!ok)
return null;
return wire;
}
// // LinearintERPolation
function lerp(a, b, t) {
return t * (b - a) + a;
}
function lerpPoint([x1, y1], [x2, y2], t) {
return [lerp(x1, x2, t), lerp(y1, y2, t)];
}
const CELLS_PER_SECOND = 15;
let lastTime = new Date().getTime();
function update() {
const now = new Date().getTime();
const delta = (now - lastTime) / 1000;
lastTime = now;
if (state.nextWire) {
state.t += CELLS_PER_SECOND * delta;
if (state.t >= 1.0) {
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
state.nextWireIndex++;
}
if (state.nextWireIndex > state.nextWire.length) {
state.wires.push(state.nextWire);
state.nextWire = null;
}
} else {
const wire = generateWire();
if (wire) {
state.nextWire = wire;
state.nextWireIndex = 2;
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
state.t = 0;
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
}
}
}
function getColors() {
if (document.body.classList.contains('dark-mode')) {
return {
BACKGROUND: '#282828',
CIRCUIT: '#38302e',
}
} else {
return {
BACKGROUND: '#eaeaea',
CIRCUIT: '#d4d4d4',
};
}
}
function render() {
const { BACKGROUND, CIRCUIT } = getColors();
g.clearRect(0, 0, WIDTH, HEIGHT);
g.strokeStyle = CIRCUIT;
g.lineWidth = 3;
state.wires.forEach(wire => {
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
});
if (state.nextWire) {
const wirePart = state.nextWire.slice(0, state.nextWireIndex);
const wire = [
...wirePart.slice(0, wirePart.length - 1),
lerpPoint(
wirePart[wirePart.length - 2],
wirePart[wirePart.length - 1],
state.t
)
];
const [[sx, sy], ...rest] = wire;
function renderDot(x, y) {
if (y === 0) {
return;
}
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
g.stroke();
}
renderDot(sx, sy);
g.beginPath();
g.moveTo(sx * TS, sy * TS);
rest.forEach(([x, y]) => {
g.lineTo(x * TS, y * TS);
});
g.stroke();
const [lx, ly] = wire[wire.length - 1];
renderDot(lx, ly);
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
if (y === 0) {
return;
}
g.fillStyle = BACKGROUND;
g.beginPath();
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
g.fill();
});
}
requestAnimationFrame(render);
}
// g.translate(0.5, 0.5);
window.addEventListener('DOMContentLoaded', () => {
// $canvas.classList.remove('hide');
render();
let i = 20;
while (i > 0) {
const wire = generateWire();
if (wire) {
state.wires.push(wire);
i--;
}
}
setInterval(() => {
update();
}, 1000 / 30);
})
+34
View File
@@ -0,0 +1,34 @@
package rss
import (
"git.phc.dm.unipi.it/phc/website/articles"
"git.phc.dm.unipi.it/phc/website/config"
"github.com/gorilla/feeds"
)
func GenerateRssFeed(entries []*articles.Article, sectionName string, title string, link string, description string) *feeds.Feed {
// Initialize RSS Feed
feed := &feeds.Feed{
Title: title,
Link: &feeds.Link{Href: link},
Description: description,
}
var feedItems []*feeds.Item
// Add items to RSS feeds
for _, entry := range entries {
feedItems = append(feedItems,
&feeds.Item{
Id: entry.Id,
Title: entry.Title,
Link: &feeds.Link{Href: config.BaseUrl + "/" + sectionName + "/" + entry.Id},
Description: entry.Description,
Created: entry.PublishDate,
})
}
feed.Items = feedItems
return feed
}
+202
View File
@@ -0,0 +1,202 @@
package server
import (
"time"
"git.phc.dm.unipi.it/phc/website/handler"
"git.phc.dm.unipi.it/phc/website/model"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/redirect/v2"
)
func UserMiddleware(h handler.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Cookies("session-token")
user := h.HandleUser(token)
c.Locals("user", user)
return c.Next()
}
}
func CreateContext(ctx *fiber.Ctx) handler.Context {
// the "_" here is required because Go cannot cast <nil> of type interface{} to *model.User, in other words <nil> of type *model.User and <nil> of type interface{} are different type. In this case the "_" returns a boolean that tells whether the cast succeeded or not, if it is false the user variable gets assigned its default zero value that is <nil> of type *model.User
user, _ := ctx.Locals("user").(*model.User)
context := handler.Context{}
handler.SetContextValue(context, handler.UserKey, user)
return context
}
func NewFiberServer(h handler.Service) *fiber.App {
app := fiber.New()
routes(h, app)
return app
}
// routes defines routes under "/"
func routes(h handler.Service, r fiber.Router) {
//
// Initial setup
//
r.Use(logger.New())
r.Use(recover.New())
// Remove trailing slash from URLs
r.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/*/": "/$1",
},
}))
// Serve generated css and js files and the static "./_public" folder
r.Static("/public/", "./_frontend/out")
r.Static("/public/", "./_public")
// Process all request and add user to the request context if there is a session cookie
r.Use(UserMiddleware(h))
//
// Pages
//
r.Get("/", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "home.html", CreateContext(c))
})
r.Get("/link", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "link.html", CreateContext(c))
})
r.Get("/login", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "login.html", CreateContext(c))
})
r.Get("/utenti", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStaticPage(c, "utenti.html", CreateContext(c))
})
r.Get("/storia", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleStoriaPage(c, CreateContext(c))
})
r.Get("/appunti", func(c *fiber.Ctx) error {
query := c.Query("q", "")
c.Type("html")
return h.HandleQueryAppunti(c, query, CreateContext(c))
})
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleAppuntiCondivisiPage(c, CreateContext(c))
})
r.Get("/news", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleNewsPage(c, CreateContext(c))
})
r.Get("/guide", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleGuidePage(c, CreateContext(c))
})
r.Get("/news/rss", func(c *fiber.Ctx) error {
c.Type("xml")
return h.HandleNewsFeedPage(c)
})
r.Get("/guide/rss", func(c *fiber.Ctx) error {
c.Type("xml")
return h.HandleGuideFeedPage(c)
})
r.Post("/login", func(c *fiber.Ctx) error {
var loginForm struct {
Provider string `form:"provider"`
Username string `form:"username"`
Password string `form:"password"`
}
if err := c.BodyParser(&loginForm); err != nil {
return err
}
session, err := h.HandleLogin(loginForm.Username, loginForm.Password)
if err != nil {
return err
}
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: session.Token,
Expires: inThreeDays,
})
return c.Redirect("/profilo")
})
r.Get("/profilo", func(c *fiber.Ctx) error {
c.Type("html")
return h.HandleProfilePage(c, CreateContext(c))
})
r.Get("/logout", func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "session-token",
Path: "/",
Value: "",
Expires: time.Now(),
})
return c.Redirect("/")
})
r.Get("/news/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
c.Type("html")
return h.HandleNewsArticlePage(c, articleID, CreateContext(c))
})
r.Get("/guide/:article", func(c *fiber.Ctx) error {
articleID := c.Params("article")
c.Type("html")
return h.HandleGuideArticlePage(c, articleID, CreateContext(c))
})
routesApi(h, r.Group("/api"))
}
// routesApi defines routes under "/api"
func routesApi(h handler.Service, r fiber.Router) {
r.Get("/utenti", func(c *fiber.Ctx) error {
utenti, err := h.HandleListaUtenti()
if err != nil {
return err
}
return c.JSON(utenti)
})
r.Get("/profilo", func(c *fiber.Ctx) error {
user, err := h.HandleRequiredUser(CreateContext(c))
if err != nil {
return err
}
return c.JSON(user)
})
}
-44
View File
@@ -1,44 +0,0 @@
{
"macchinisti": [
{
"uid": "minnocci",
"fullName": "Francesco Minnocci",
"entryDate": "2022/02"
},
{
"uid": "manicastri",
"fullName": "Francesco Manicastri",
"entryDate": "2022/05"
},
{
"uid": "delucreziis",
"fullName": "Antonio De Lucreziis",
"entryDate": "2019/??"
},
{
"uid": "caporali",
"fullName": "Francesco Caporali",
"entryDate": "2018/??",
"exitDate": "2022/03"
},
{
"uid": "dachille",
"fullName": "Letizia D'Achille",
"entryDate": "2018/??",
"exitDate": "2022/03"
}
],
"eventi": [
{
"type": "simple",
"title": "Nuovo sito del PHC",
"description": "TBA",
"date": "2022/??"
},
{
"type": "simple",
"title": "Apertura del PHC",
"date": "1994"
}
]
}
+103 -79
View File
@@ -1,16 +1,58 @@
package main
package storia
import (
"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
}
+12 -5
View File
@@ -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"),
}
-41
View File
@@ -1,41 +0,0 @@
package main
import (
"encoding/json"
"io/ioutil"
)
type UserInfo struct {
Uid string `json:"uid"`
Nome string `json:"nome"`
Cognome string `json:"cognome"`
Tags []string `json:"tags"`
IsMacchinista bool `json:"macchinista,omitempty"`
}
func GetUtenti() ([]UserInfo, error) {
history, err := GetRawHistory()
if err != nil {
return nil, err
}
// Creo un set per velocizzare l'utilizzo dopo
macchinisti := map[string]bool{}
for _, m := range history.Macchinisti {
macchinisti[m.Uid] = true
}
var users []UserInfo
usersJsonData, err := ioutil.ReadFile("./utenti-poisson-2022.local.json")
if err != nil {
return nil, err
}
if err := json.Unmarshal(usersJsonData, &users); err != nil {
return nil, err
}
return users, nil
}
-1
View File
@@ -1 +0,0 @@
package util
+3 -3
View File
@@ -1,10 +1,10 @@
package util
// 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
+1
View File
@@ -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)
+26
View File
@@ -0,0 +1,26 @@
package util
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](elements ...T) Set[T] {
set := Set[T]{}
for _, e := range elements {
set[e] = struct{}{}
}
return set
}
func (set Set[T]) Add(value T) {
set[value] = struct{}{}
}
func (set Set[T]) Remove(value T) {
delete(set, value)
}
func (set Set[T]) Has(value T) bool {
_, found := set[value]
return found
}
-78
View File
@@ -1,78 +0,0 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ template "title" . }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
crossorigin="anonymous"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body, {
delimiters: [
{ left: "$", right: "$", display: false },
{ left: "$$", right: "$$", display: true },
{ left: "\\(", right: "\\)", display: false },
{ left: "\\[", right: "\\]", display: true }
]
});
});
document.addEventListener('DOMContentLoaded', () => {
const $toggle = document.querySelector('#toggle-dark-mode');
const $toggleIcon = document.querySelector('#toggle-dark-mode i');
// Loads prefered dark from from localStorage or defaults to media query.
let prefersDarkMode = localStorage.getItem('theme') !== undefined ? localStorage.getItem('theme') === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
function storePrefersDarkMode(mode) {
prefersDarkMode = mode;
localStorage.setItem('theme', mode ? 'dark' : 'light');
}
function displayToggle() {
document.body.classList.toggle('dark-mode', prefersDarkMode);
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode);
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode);
}
$toggle.addEventListener('click', () => {
storePrefersDarkMode(!prefersDarkMode);
displayToggle();
});
displayToggle();
});
</script>
<link rel="stylesheet" href="/public/style.css">
<link rel="stylesheet" href="/public/theme-dark.css">
</head>
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
{{ template "navbar" . }}
<div class="main">
{{ template "body" . }}
</div>
<footer>
<div class="footer-item">
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
</div>
</footer>
</body>
</html>
{{end}}