Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
807a4c54ac | ||
|
|
c7e33c8e72 | ||
|
|
9df45cf83a | ||
|
|
2485133661 | ||
|
|
2ce1ac9bf1 | ||
|
|
03f976a8f9 | ||
|
|
64200cd654 | ||
|
|
8c4ed0b70e | ||
|
|
63ec0d559b | ||
|
|
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/
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Architecture
|
||||
|
||||
## Server, Database and File Uploads
|
||||
|
||||
- **server**
|
||||
|
||||
The server module defines all routes and extracts data from HTTP request and passes data to the **handler** module.
|
||||
|
||||
- **handler**
|
||||
|
||||
This is the main controller module that dispatches requests to various services and constructs back responses to send to the client.
|
||||
|
||||
### Services
|
||||
|
||||
...
|
||||
|
||||
- **appunti**
|
||||
|
||||
This module manages all things relating the _appunti condivisi_ pages, it talks with the **database** and **auth** modules.
|
||||
|
||||
- **database**
|
||||
|
||||
This is one of the "leaf" modules that doesn't depend on anything, its purpose is to talk to the database and return "low level data" stored there. For example instances of `time.Time` are represented as `string`s at this layer.
|
||||
@@ -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 è `localhost:8000`.
|
||||
|
||||
- `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/)
|
||||
|
||||
@@ -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,35 @@
|
||||
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: 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,75 @@
|
||||
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/main.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
// https://rollupjs.org/guide/en/#-w--watch
|
||||
!process.env.ROLLUP_WATCH && terser(),
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,524 @@
|
||||
import { render } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { formatFileSize, intersperse } from './util.js'
|
||||
|
||||
function randomHex(length = 16) {
|
||||
return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista di tag "standard" disponibili nel completamento per i tags
|
||||
*/
|
||||
const availableTags = [
|
||||
// Genera una ventina di coppie come "2022/2023"
|
||||
...Array.from({ length: 20 }, (_, i) => {
|
||||
const year = new Date().getFullYear() - i
|
||||
|
||||
return { id: `${year}/${year + 1}`, label: `Anno Accademico ${year}/${year + 1}` }
|
||||
}),
|
||||
// List di tag "standard", l'id è la vera stringa del tag, mentre la label
|
||||
{ id: 'geometria-1', label: 'Geometria 1' },
|
||||
{ id: 'geometria-2', label: 'Geometria 2' },
|
||||
{ id: 'eti', label: 'Elementi di Teoria degli Insiemi' },
|
||||
{ id: 'eta', label: 'Elementi di Topologia Algebrica' },
|
||||
{ id: 'ega', label: 'Elementi di Geometria Algebrica' },
|
||||
{ id: 'gtd', label: 'Geometria e Topologia Differenziale' },
|
||||
{ id: 'ist-anal', label: 'Istituzioni di Analisi' },
|
||||
{ id: 'ist-geom', label: 'Istituzioni di Geometria' },
|
||||
{ id: 'ist-fis', label: 'Istituzioni di Fisica' },
|
||||
{ id: 'ist-prob', label: 'Istituzioni di Probabilità' },
|
||||
{ id: 'ist-alg', label: 'Istituzioni di Algebra' },
|
||||
{ id: 'ist-num', label: 'Istituzioni di Analisi Numerica' },
|
||||
{ id: 'analisi-1', label: 'Analisi 1' },
|
||||
{ id: 'analisi-2', label: 'Analisi 2' },
|
||||
{ id: 'aritmetica', label: 'Aritmetica' },
|
||||
{ id: 'programmazione', label: 'Programmazione' },
|
||||
{ id: 'fisica-1', label: 'Fisica 1' },
|
||||
{ id: 'steffe-1', label: 'Laboratorio di Comunicazione Mediante Calcolatore' },
|
||||
]
|
||||
|
||||
const useAutosizeTextarea = ({ minRows } = {}) => {
|
||||
minRows ??= 1
|
||||
|
||||
const textareaRef = useRef(null)
|
||||
|
||||
const updateTextareaHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
updateTextareaHeight()
|
||||
})
|
||||
|
||||
return {
|
||||
rows: minRows,
|
||||
ref: textareaRef,
|
||||
onInput: updateTextareaHeight,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTag = tag => {
|
||||
return tag
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\p{L}0-9\/\-]/gu, '')
|
||||
}
|
||||
|
||||
const InputTags = ({ tags, setTags, availableTags }) => {
|
||||
availableTags ??= []
|
||||
|
||||
const [id] = useState('tags-' + randomHex())
|
||||
|
||||
const nextRef = useRef()
|
||||
const [nextText, setNextText] = useState('')
|
||||
|
||||
const onFocus = e => {
|
||||
if (!e.target.closest('.tags .tag .remove')) {
|
||||
nextRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = tag => {
|
||||
setTags(tags => tags.filter(t => t !== tag))
|
||||
}
|
||||
|
||||
const addTag = tag => {
|
||||
setTags(tags => [...tags, tag])
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (e.key === 'Backspace' && nextText.length === 0) {
|
||||
removeTag(tags.at(-1))
|
||||
}
|
||||
|
||||
const trimmed = nextText.trim()
|
||||
if (e.key === 'Enter' && trimmed.length > 0) {
|
||||
addTag(normalizeTag(trimmed))
|
||||
setNextText('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-tags" onClick={onFocus}>
|
||||
{tags.map(tag => (
|
||||
<div class="tag">
|
||||
<span>{tag}</span>
|
||||
<span class="remove" onClick={() => removeTag(tag)}>
|
||||
<i class="fa-solid fa-remove"></i>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
ref={nextRef}
|
||||
list={id}
|
||||
value={nextText}
|
||||
onInput={e => setNextText(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<datalist id={id}>
|
||||
{availableTags.map(({ id, label }) => (
|
||||
<option value={id} label={label} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const withClasses = a => {
|
||||
if (Array.isArray(a)) {
|
||||
return { class: a.filter(className => !!className).join(' ') }
|
||||
} else if (typeof a === 'object') {
|
||||
return {
|
||||
class: Object.entries(a)
|
||||
.flatMap(([className, active]) => (active ? [className] : []))
|
||||
.join(' '),
|
||||
}
|
||||
} else if (typeof a === 'string') {
|
||||
return { class: a }
|
||||
} else {
|
||||
throw new Error(`Invalid class format`)
|
||||
}
|
||||
}
|
||||
|
||||
const UploadRegion = ({}) => {
|
||||
const [draggingOver, setDraggingOver] = useState(false)
|
||||
|
||||
const onDragOver = e => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
setDraggingOver(true)
|
||||
}
|
||||
|
||||
const onDragLeave = () => {
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
const onDropFiles = files => {
|
||||
if (files.length !== 1) {
|
||||
throw new Error('Must drop one file')
|
||||
}
|
||||
|
||||
const [file] = files
|
||||
console.dir(file)
|
||||
|
||||
if (file.type !== 'application/pdf') {
|
||||
console.error('The file must be a PDF')
|
||||
}
|
||||
|
||||
console.log(formatFileSize(file.size))
|
||||
}
|
||||
|
||||
const onDrop = e => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
onDropFiles(
|
||||
[...e.dataTransfer.items].flatMap(item =>
|
||||
item.kind === 'file' ? [item.getAsFile()] : []
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onDropFiles([...e.dataTransfer.files])
|
||||
}
|
||||
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...withClasses(['upload-region', draggingOver && 'dragging-over'])}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{!draggingOver ? (
|
||||
<>
|
||||
<span>Trascina qui un PDF oppure usa il tasto sottostante</span>
|
||||
<InputFile accept="application/pdf" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div class="release-text">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
<span>Rilascia per iniziare il caricamento</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Progress = ({ value, max }) => {
|
||||
return (
|
||||
<div class="progress-bar">
|
||||
<div class="indicator" style={{ width: Math.floor((value / max) * 100) + '%' }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CancellableUpload = ({ uploadedSize, totalSize, onCancel }) => {
|
||||
return (
|
||||
<>
|
||||
<div class="progress-bytes">
|
||||
{formatFileSize(uploadedSize)} / {formatFileSize(totalSize)}
|
||||
</div>
|
||||
<div class="progress-and-action">
|
||||
<Progress value={uploadedSize} max={totalSize} />
|
||||
<button onClick={onCancel}>Annulla</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadPopup = ({}) => {
|
||||
const [shown, setShown] = useState(false)
|
||||
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
const [doneUploading, setDoneUploading] = useState(false)
|
||||
|
||||
const hash = '59e514dd50c63051'
|
||||
|
||||
return (
|
||||
<>
|
||||
{shown && (
|
||||
<div class="upload-popup">
|
||||
<div class="popup">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
Carica la dispensa "<code>file.pdf</code>"
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>
|
||||
{/* TODO: */}
|
||||
Inserisci titolo, descrizione e tag per questa dispensa, una volta
|
||||
premuto <strong>Salva</strong> la dispensa verrà aggiunta
|
||||
inizialmente non sarà visibile nell'elenco degli appunti finché non
|
||||
verrà approvata da un moderatore
|
||||
</p>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea placeholder="Descrizione..." {...descriptionTextareaProps}>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
|
||||
{!doneUploading ? (
|
||||
<CancellableUpload
|
||||
uploadedSize={34.2 * 1024 ** 2}
|
||||
totalSize={45.6 * 1024 ** 2}
|
||||
/>
|
||||
) : (
|
||||
<div class="right">
|
||||
<div class="upload-message">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
<span title={hash}>File caricato</span>
|
||||
</div>
|
||||
<button>Annulla</button>
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const InputFile = ({ accept, onFile }) => {
|
||||
const inputFileRef = useRef()
|
||||
|
||||
const [file, setFile] = useState(null)
|
||||
|
||||
const onButtonClick = e => {
|
||||
inputFileRef.current.click()
|
||||
}
|
||||
|
||||
const onInputFile = e => {
|
||||
if (e.target.files.length === 1) {
|
||||
const f = e.target.files[0]
|
||||
|
||||
setFile(f)
|
||||
onFile?.(f)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-file">
|
||||
<input type="file" ref={inputFileRef} accept={accept} onInput={onInputFile} />
|
||||
<button onClick={onButtonClick}>Carica File</button>
|
||||
{file && <div class="file-name">{file.name}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TabellaApprovazioni = ({ pendingApprovazioni }) => {
|
||||
return (
|
||||
<div class="table approvazioni">
|
||||
<div class="download header"></div>
|
||||
<div class="hash header">PDF</div>
|
||||
<div class="title header">Dispensa</div>
|
||||
<div class="owner header">Proprietario</div>
|
||||
<div class="actions header">Azioni</div>
|
||||
|
||||
{intersperse(
|
||||
pendingApprovazioni.map(({ id, title, owner, hash }) => (
|
||||
<>
|
||||
<div class="download">
|
||||
<a class="button icon" href={`/appunti/files/${hash}`}>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hash">
|
||||
<code>{hash}</code>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href={`/appunti/${id}`} title={id}>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
<div class="owner">
|
||||
<a href={`/u/${owner}`}>@{owner}</a>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)),
|
||||
<div class="separator"></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const App = ({}) => {
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadRegion />
|
||||
<UploadPopup />
|
||||
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>Le tue dispense</h1>
|
||||
<div class="table dispense">
|
||||
<div class="edit header"></div>
|
||||
<div class="name header">Nome</div>
|
||||
<div class="tags header">Tags</div>
|
||||
<div class="status header">Stato</div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name">Appunti di Geometria 1</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-1</div>
|
||||
<div class="tag">prof-1</div>
|
||||
<div class="tag">2016/2017</div>
|
||||
</div>
|
||||
<div class="status approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="expanded">
|
||||
<div class="edit-close">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edit-container">
|
||||
<div class="header">
|
||||
<a href="/appunti/6f82dca3d83b475c">
|
||||
<span class="title">Mezzedimi</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea
|
||||
placeholder="Descrizione..."
|
||||
{...descriptionTextareaProps}
|
||||
>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
</div>
|
||||
<p>
|
||||
Puoi anche caricare una nuova versione del PDF ma ricorda che se
|
||||
carichi un file non precedentemente approvato inizialmente
|
||||
scomparirà dall'elenco principale in attesa di apporvazione da parte
|
||||
di un moderatore.
|
||||
</p>
|
||||
<div class="form">
|
||||
<div class="label">Stato</div>
|
||||
<div class="stato-approvazione approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
Approvata
|
||||
</div>
|
||||
|
||||
<div class="label">Cambia PDF</div>
|
||||
<InputFile accept="application/pdf" />
|
||||
|
||||
<div class="right">
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name pending">Appunti di Geometria 2</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-2</div>
|
||||
<div class="tag">prof-2</div>
|
||||
<div class="tag">2017/2018</div>
|
||||
<div class="tag">tanti-tag-1</div>
|
||||
<div class="tag">tanti-tag-2</div>
|
||||
<div class="tag">tanti-tag-3</div>
|
||||
<div class="tag">tanti-tag-4</div>
|
||||
<div class="tag">tanti-tag-5</div>
|
||||
<div class="tag">tanti-tag-6</div>
|
||||
</div>
|
||||
<div class="status pending" title="In attesa di approvazione...">
|
||||
<i class="fas fa-hourglass"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name rejected">F1Le SuP3R LeGaLe</div>
|
||||
<div class="tags">
|
||||
<div class="tag">foo</div>
|
||||
<div class="tag">bar</div>
|
||||
</div>
|
||||
<div class="status rejected">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>PDF da Approvare</h1>
|
||||
<TabellaApprovazioni
|
||||
pendingApprovazioni={[
|
||||
{
|
||||
id: '59e514dd50c63051',
|
||||
title: 'GAAL',
|
||||
owner: 'mezzedimi',
|
||||
hash: '2c8d593c6be289ab',
|
||||
},
|
||||
{
|
||||
id: '4c52d9726c438f3d',
|
||||
title: 'Dispensa 1',
|
||||
owner: 'persona-1',
|
||||
hash: '346ba392a3a1eb86',
|
||||
},
|
||||
{
|
||||
id: 'eecb96f04e319c4c',
|
||||
title: 'Dispensa 2',
|
||||
owner: 'persona-1',
|
||||
hash: '74f9652c28f82e7f',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render(<App />, document.querySelector('#app'))
|
||||
@@ -0,0 +1,17 @@
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`
|
||||
}
|
||||
if (bytes < 1024 ** 2) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`
|
||||
}
|
||||
if (bytes < 1024 ** 3) {
|
||||
return `${(bytes / 1024 ** 2).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
export function intersperse(list, separator) {
|
||||
return list.flatMap((el, i) => (i === 0 ? [el] : [separator, el]))
|
||||
}
|
||||
@@ -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 |
@@ -5,10 +5,12 @@
|
||||
--bg: #eaeaea;
|
||||
--fg: #333;
|
||||
|
||||
--fg-lighter: #444;
|
||||
|
||||
--bg-lighter: #f0f0f0;
|
||||
|
||||
--bg-dark: hsl(220, 5%, 93%);
|
||||
--bg-darker: hsl(220, 5%, 90%);
|
||||
--bg-dark: #ecedee;
|
||||
--bg-darker: #e4e5e7;
|
||||
--bg-darker-2: #d5d5d5;
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--bg-darker-3: #c0c0c0;
|
||||
@@ -36,6 +38,8 @@
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
|
||||
--accent-mix-bg: #dae4db;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -420,6 +424,11 @@ hr {
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
@@ -445,6 +454,74 @@ p.center {
|
||||
|
||||
/* Controls */
|
||||
|
||||
.progress-bar {
|
||||
background: var(--bg-darker-2-1);
|
||||
color: var(--fg);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020, inset 0 0 8px 0 #00000030;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.progress-bar .indicator {
|
||||
border-radius: 4px;
|
||||
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, var(--accent-1), var(--accent-2));
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
animation: test-progressbar 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes test-progressbar {
|
||||
0% {
|
||||
width: 0%;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar .indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* background-size: 50px 50px;
|
||||
|
||||
--highlight: rgba(255, 255, 255, 0.1);
|
||||
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
transparent 0%,
|
||||
var(--highlight) 5%,
|
||||
var(--highlight) 25%,
|
||||
transparent 30%,
|
||||
transparent 50%,
|
||||
var(--highlight) 55%,
|
||||
var(--highlight) 75%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
animation: move 4s linear infinite; */
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 50px 50px;
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.button) {
|
||||
color: var(--accent-1-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -464,7 +541,7 @@ a.button {
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -488,6 +565,9 @@ button,
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
@@ -496,21 +576,35 @@ button:hover,
|
||||
box-shadow: 0 4px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
button.primary,
|
||||
.button.primary {
|
||||
border: 1px solid var(--accent-2-darker);
|
||||
background: var(--accent-2);
|
||||
color: var(--accent-2-darkest);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
button.primary:hover,
|
||||
.button.primary:hover {
|
||||
background: var(--accent-2-lighter);
|
||||
}
|
||||
|
||||
button.icon {
|
||||
button.icon,
|
||||
.button.icon {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
button.flat {
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.flat:hover {
|
||||
background: var(--bg-darker-2);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
select {
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -539,12 +633,14 @@ select {
|
||||
/* Text Fields */
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'] {
|
||||
input[type='file'],
|
||||
input[type='password'],
|
||||
textarea {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
height: 2.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
@@ -560,6 +656,17 @@ input[type='password'] {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
min-height: 2rem;
|
||||
padding: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
input[type='password'] {
|
||||
font-family: caption;
|
||||
}
|
||||
@@ -577,6 +684,21 @@ input[type='password'].error {
|
||||
color: #311;
|
||||
}
|
||||
|
||||
/* Better File Input */
|
||||
|
||||
.input-file {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-file > input[type='file'] {
|
||||
position: fixed;
|
||||
top: -100px;
|
||||
}
|
||||
|
||||
/* Compound Controls */
|
||||
|
||||
.compound {
|
||||
@@ -845,6 +967,458 @@ form .field-set input {
|
||||
height: calc(var(--size) * 1rem);
|
||||
}
|
||||
|
||||
/* Appunti Condivisi */
|
||||
|
||||
.page-appunti-condivisi .main {
|
||||
padding: 0 1rem inherit;
|
||||
max-width: 95ch;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region {
|
||||
padding: 2rem;
|
||||
|
||||
width: 30rem;
|
||||
height: 30vh;
|
||||
|
||||
border: 3px dashed var(--bg-darker-4);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
transition: border-color 150ms ease-in-out, background-color 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region.dragging-over {
|
||||
border-color: var(--accent-1);
|
||||
color: var(--accent-1-fg);
|
||||
|
||||
background: var(--accent-mix-bg);
|
||||
}
|
||||
|
||||
/* Serve altrimenti l'evento dropleave viene emesso anche quando si passa da ".upload-region" ad uno dei figli, così invece rendiamo tutti i figli trasparenti agli eventi del mouse */
|
||||
.page-appunti-condivisi .upload-region.dragging-over * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region.dragging-over .release-text {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-size: 22px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup {
|
||||
z-index: 100;
|
||||
background-color: #00000066;
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
/* backdrop-filter: blur(0.5rem); */
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 32px #00000033;
|
||||
|
||||
min-width: 40ch;
|
||||
max-width: 60ch;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .block {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .form {
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .popup .form .label {
|
||||
min-width: 10ch;
|
||||
}
|
||||
|
||||
.form .progress-bytes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
|
||||
user-select: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form .progress-and-action {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup > .header {
|
||||
background: var(--bg-darker);
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
|
||||
color: var(--fg-lighter);
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .title {
|
||||
font-size: 20px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .title code {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi #app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 3rem;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table {
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense {
|
||||
grid-template-columns: auto 1fr minmax(auto, 50ch) auto;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .pending {
|
||||
color: royalblue;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .rejected {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table > .header {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--bg-darker);
|
||||
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table .separator {
|
||||
grid-column: 1 / -1;
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit,
|
||||
.page-appunti-condivisi .table.dispense .name,
|
||||
.page-appunti-condivisi .table.dispense .tags,
|
||||
.page-appunti-condivisi .table.dispense .status {
|
||||
padding: 1rem 0 1rem 1rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .tags {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .status {
|
||||
place-self: center;
|
||||
|
||||
padding: 1rem 1.25rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .expanded {
|
||||
grid-column: span 4;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .expanded p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header code {
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header .title {
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container .stato-approvazione {
|
||||
font-weight: var(--font-weight-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container .stato-approvazione.approved {
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-close {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni {
|
||||
grid-template-columns: auto auto 1fr auto auto;
|
||||
|
||||
width: 80ch;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .download,
|
||||
.page-appunti-condivisi .table.approvazioni .hash,
|
||||
.page-appunti-condivisi .table.approvazioni .title,
|
||||
.page-appunti-condivisi .table.approvazioni .owner,
|
||||
.page-appunti-condivisi .table.approvazioni .actions {
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .hash {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem 2rem;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form button:not(.icon) {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .label {
|
||||
height: 2rem;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > textarea {
|
||||
resize: vertical;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .fill {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .right {
|
||||
grid-column: span 2;
|
||||
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags {
|
||||
padding: 0.5rem;
|
||||
min-height: 2rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
background: var(--text-input-bg);
|
||||
color: var(--fg);
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag {
|
||||
user-select: none;
|
||||
|
||||
height: 1.5rem;
|
||||
border-radius: calc(1.5rem / 2);
|
||||
padding: 0 calc(1.5rem / 2);
|
||||
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--card-date);
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag .remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag .remove:hover {
|
||||
color: var(--card-content);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags input[type='text'] {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 1.5rem;
|
||||
|
||||
min-height: unset;
|
||||
max-width: 10rem;
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Animazioni */
|
||||
|
||||
/* Pending hourglass with a bit of overshoot */
|
||||
/* @keyframes pending-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
45% {
|
||||
transform: rotate(190deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
60% {
|
||||
transform: rotate(175deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(185deg);
|
||||
}
|
||||
95% {
|
||||
transform: rotate(370deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
} */
|
||||
|
||||
@keyframes pending-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -943,3 +1517,22 @@ table td {
|
||||
table tbody tr:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
/* Utils */
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex.col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.fill-h {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -59,7 +59,8 @@ body.dark-mode {
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.dark-mode input[type] {
|
||||
.dark-mode input[type],
|
||||
.dark-mode textarea {
|
||||
background: #4b4342;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
<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,14 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Dispensa.Title }} • Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
{{ .Dispensa.Title }}
|
||||
</h1>
|
||||
<p>
|
||||
{{ .Dispensa.Description }}
|
||||
</p>
|
||||
{{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">
|
||||
@@ -103,5 +103,5 @@
|
||||
</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}}
|
||||
@@ -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.Username }}</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>
|
||||
@@ -3,7 +3,7 @@
|
||||
{{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>
|
||||
@@ -41,7 +41,7 @@
|
||||
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>
|
||||
@@ -0,0 +1,55 @@
|
||||
package appunti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// Service isola l'handler dal modulo del database e si occupa solo delle interazioni riguardanti gli appunti e le dispense
|
||||
type Service interface {
|
||||
GetDispensa(id string) (*model.Dispensa, error)
|
||||
CreateDispensa(template model.Dispensa) (string, error)
|
||||
}
|
||||
|
||||
type DefaultService struct {
|
||||
DB *database.DB
|
||||
}
|
||||
|
||||
var _ Service = &DefaultService{}
|
||||
|
||||
func (s *DefaultService) GetDispensa(id string) (*model.Dispensa, error) {
|
||||
dispensa, err := s.DB.Dispense.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := s.DB.DispensaTags.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Dispensa{
|
||||
Id: dispensa.Id,
|
||||
OwnerId: dispensa.OwnerId,
|
||||
Title: dispensa.Title,
|
||||
Description: dispensa.Description,
|
||||
Tags: tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DefaultService) CreateDispensa(template model.Dispensa) (string, error) {
|
||||
dispensaId, err := s.DB.Dispense.Create(database.Dispensa{
|
||||
OwnerId: template.OwnerId,
|
||||
Title: template.Title,
|
||||
Description: template.Description,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := s.DB.DispensaTags.Set(dispensaId, template.Tags); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -33,17 +33,17 @@ 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 {
|
||||
req, err := http.NewRequest(
|
||||
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
@@ -68,7 +68,7 @@ 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
|
||||
@@ -89,7 +89,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 +98,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 +112,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 +121,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,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"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/database/sqlite"
|
||||
"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"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
auth := auth.NewDefaultService(config.AuthServiceHost)
|
||||
|
||||
// Create database connection and apply pending migrations
|
||||
db := util.Must(sqlite.New("phc-server.local.db"))
|
||||
if err := db.Migrate("./database/migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(&handler.DefaultHandler{
|
||||
AuthService: auth,
|
||||
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: util.Must(lista_utenti.New(auth, config.ListaUtenti)),
|
||||
Appunti: &appunti.DefaultService{
|
||||
DB: db,
|
||||
},
|
||||
})
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
if err := app.Listen(config.Host); err != nil {
|
||||
panic(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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sqlite.New("phc-server.local.db")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Migrate("database/migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package database
|
||||
|
||||
type DBMigrate interface {
|
||||
Migrate(migrationDir string) error
|
||||
}
|
||||
|
||||
type DBQueryAppunti interface {
|
||||
AllDispensaFile() ([]DispensaFile, error)
|
||||
}
|
||||
|
||||
type DBDispense interface {
|
||||
Create(template Dispensa) (string, error)
|
||||
Get(id string) (Dispensa, error)
|
||||
All() ([]Dispensa, error)
|
||||
Update(d Dispensa) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBUploads interface {
|
||||
Create(template Upload) (string, error)
|
||||
Get(id string) (Upload, error)
|
||||
}
|
||||
|
||||
type DBFileApprovals interface {
|
||||
Create(template FileApproval) (string, error)
|
||||
Get(id string) (FileApproval, error)
|
||||
All() ([]FileApproval, error)
|
||||
Update(d FileApproval) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBDispensaTags interface {
|
||||
Set(dispensaId string, tags []string) error
|
||||
Get(dispensaId string) ([]string, error)
|
||||
}
|
||||
|
||||
type DBDownloads interface {
|
||||
Create(template Download) error
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
DBMigrate
|
||||
DBQueryAppunti
|
||||
|
||||
Dispense DBDispense
|
||||
Uploads DBUploads
|
||||
FileApprovals DBFileApprovals
|
||||
DispensaTags DBDispensaTags
|
||||
Downloads DBDownloads
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
-- Dispense
|
||||
CREATE TABLE IF NOT EXISTS "dispense"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Contenuto caricato
|
||||
CREATE TABLE IF NOT EXISTS "uploads"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"file" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Approvazioni contenuti caricati
|
||||
CREATE TABLE IF NOT EXISTS "file_approvals"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL, -- Moderatore che ha creato questa approvazione
|
||||
"upload_id" TEXT NOT NULL, -- Upload a cui si riferisce questa approvazione
|
||||
"status" TEXT NOT NULL, -- Risultato dell'approvazione: "approved" | "rejected"
|
||||
FOREIGN KEY (upload_id) REFERENCES uploads(id)
|
||||
);
|
||||
|
||||
-- Download counter
|
||||
CREATE TABLE IF NOT EXISTS "downloads"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Tags per le dispense
|
||||
CREATE TABLE IF NOT EXISTS "tags"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
package database
|
||||
|
||||
//
|
||||
// Tables
|
||||
//
|
||||
|
||||
type Dispensa struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
|
||||
type FileApproval struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
Status string `db:"status"` // "approved" | "rejected"
|
||||
}
|
||||
|
||||
type Download struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Timestamp string `db:"timestamp"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Tag string `db:"tag"`
|
||||
}
|
||||
|
||||
//
|
||||
// Common Joins
|
||||
//
|
||||
|
||||
// DispensaFile is a join of dispensa with the most recent upload for that "dispensa_id"
|
||||
type DispensaFile struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
Timestamp string
|
||||
Filename string
|
||||
}
|
||||
|
||||
func New(filename string) (*database.DB, error) {
|
||||
db, err := sqlx.Open("sqlite3", filename+"?_fk=1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &database.DB{
|
||||
DBMigrate: &sqliteDBMigrate{db},
|
||||
// DBQueryAppunti: ...,
|
||||
|
||||
Dispense: &sqliteDBDispense{db},
|
||||
Uploads: &sqliteDBUploads{db},
|
||||
FileApprovals: &sqliteDBFileApprovals{db},
|
||||
Downloads: &sqliteDBDownloads{db},
|
||||
DispensaTags: &sqliteDBTags{db},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBMigrate struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBMigrate = sqliteDBMigrate{}
|
||||
|
||||
func (db sqliteDBMigrate) Migrate(migrationFolder string) error {
|
||||
log.Printf(`Creating migrations table`)
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(`Loading applied migrations`)
|
||||
appliedMigrations := []migration{}
|
||||
if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(migrationFolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return fmt.Errorf("no dirs in migrations folder")
|
||||
}
|
||||
|
||||
if i < len(appliedMigrations) {
|
||||
if appliedMigrations[i].Filename != entry.Name() {
|
||||
return fmt.Errorf("misapplied migration %q with %q", appliedMigrations[i].Filename, entry.Name())
|
||||
}
|
||||
|
||||
log.Printf("Found applied migration %q", entry.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Applying new migration %q", entry.Name())
|
||||
|
||||
migrationPath := path.Join(migrationFolder, entry.Name())
|
||||
|
||||
sqlStmts, err := os.ReadFile(migrationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(sqlStmts)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO migrations VALUES (?, ?)`,
|
||||
time.Now().Format(time.RFC3339),
|
||||
entry.Name(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("All migrations applied successfully, database up to date")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDispense struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispense = sqliteDBDispense{}
|
||||
|
||||
func (d sqliteDBDispense) Create(template database.Dispensa) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
dispense(id, created_at, owner_id, title, description)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :title, :description)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Get(id string) (database.Dispensa, error) {
|
||||
var dispensa database.Dispensa
|
||||
if err := d.DB.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil {
|
||||
return database.Dispensa{}, err
|
||||
}
|
||||
|
||||
return dispensa, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) All() ([]database.Dispensa, error) {
|
||||
var dispense []database.Dispensa
|
||||
if err := d.DB.Select(&dispense, `SELECT * FROM dispense`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dispense, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Update(dispensa database.Dispensa) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
UPDATE
|
||||
dispense
|
||||
SET
|
||||
owner_id = :owner_id
|
||||
title = :title,
|
||||
description = :description
|
||||
WHERE
|
||||
id = :id
|
||||
`, &dispensa); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Delete(id string) error {
|
||||
panic("TODO: Not implemented")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBUploads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBUploads = sqliteDBUploads{}
|
||||
|
||||
func (u sqliteDBUploads) Create(template database.Upload) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
|
||||
if _, err := u.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
uploads(id, created_at, owner_id, dispensa_id, file)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :dispensa_id, :file)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (u sqliteDBUploads) Get(id string) (database.Upload, error) {
|
||||
var upload database.Upload
|
||||
if err := u.DB.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil {
|
||||
return database.Upload{}, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBFileApprovals struct{ *sqlx.DB }
|
||||
|
||||
func (f sqliteDBFileApprovals) Create(template database.FileApproval) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := f.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
file_approvals(id, created_at, owner_id, upload_id, status)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :upload_id, :status)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Get(id string) (database.FileApproval, error) {
|
||||
var fileApproval database.FileApproval
|
||||
if err := f.DB.Get(&fileApproval, `SELECT * FROM file_approvals WHERE id = ?`, id); err != nil {
|
||||
return database.FileApproval{}, err
|
||||
}
|
||||
|
||||
return fileApproval, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) All() ([]database.FileApproval, error) {
|
||||
var fileApprovals []database.FileApproval
|
||||
if err := f.DB.Select(&fileApprovals, `SELECT * FROM file_approvals`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileApprovals, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Update(fileApproval database.FileApproval) error {
|
||||
if _, err := f.DB.NamedExec(`
|
||||
UPDATE
|
||||
file_approvals
|
||||
SET
|
||||
owner_id = :owner_id,
|
||||
upload_id = :upload_id,
|
||||
status = :status
|
||||
WHERE
|
||||
id = :id
|
||||
`, &fileApproval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Delete(id string) error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDownloads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDownloads = sqliteDBDownloads{}
|
||||
|
||||
func (d sqliteDBDownloads) Create(template database.Download) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
downloads(dispensa_id, timestamp)
|
||||
VALUES
|
||||
(:dispensa_id, :timestamp)
|
||||
`, &template); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBTags struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispensaTags = sqliteDBTags{}
|
||||
|
||||
func (t sqliteDBTags) Set(dispensaId string, tags []string) error {
|
||||
tagsRows := []map[string]any{}
|
||||
for _, t := range tags {
|
||||
tagsRows = append(tagsRows, map[string]any{
|
||||
"dispensa_id": dispensaId,
|
||||
"name": t,
|
||||
})
|
||||
}
|
||||
|
||||
tx, err := t.DB.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
DELETE FROM tags WHERE dispensa_id = ?
|
||||
`, dispensaId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(`
|
||||
INSERT INTO tags(dispensa_id, name) VALUES (:dispensa_id, :name)
|
||||
`, tagsRows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t sqliteDBTags) Get(dispensaId string) ([]string, error) {
|
||||
var tags []string
|
||||
if err := t.DB.Select(&tags, `SELECT name FROM tags WHERE dispensa_id = ?`, dispensaId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -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,7 +17,10 @@ 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/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
|
||||
@@ -15,19 +15,28 @@ github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -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,293 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"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 {
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
|
||||
// Storia
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Appunti
|
||||
HandleAppuntiPage(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleDispensaPage(w io.Writer, id string, ctx Context) error
|
||||
|
||||
// News
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Guide
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
|
||||
// User
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
|
||||
// Article Pages
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
|
||||
// RSS
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
// User
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
|
||||
// User Listing
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
|
||||
// Appunti
|
||||
HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error)
|
||||
HandleGetDispensa(dispensaId string) (*model.Dispensa, 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
|
||||
Appunti appunti.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
ListaUtenti lista_utenti.Service
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
var _ Service = &DefaultHandler{}
|
||||
|
||||
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) HandleAppuntiPage(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) HandleDispensaPage(w io.Writer, id string, ctx Context) error {
|
||||
dispensa, err := h.Appunti.GetDispensa(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "dispensa.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Dispensa": dispensa,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
func (h *DefaultHandler) HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return "", ErrNoUser
|
||||
}
|
||||
|
||||
template.OwnerId = user.Username
|
||||
|
||||
dispensaId, err := h.Appunti.CreateDispensa(template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGetDispensa(id string) (*model.Dispensa, error) {
|
||||
return h.Appunti.GetDispensa(id)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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]
|
||||
}
|
||||
|
||||
func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti}, 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)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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]
|
||||
}
|
||||
|
||||
func newJsonListaUtenti(path string) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti}, 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)
|
||||
|
||||
log.Printf("Caricata lista di %d utenti", len(users))
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type Dispensa struct {
|
||||
Id string `json:"id"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
@@ -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,248 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"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/etag"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"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",
|
||||
},
|
||||
}))
|
||||
|
||||
// Better static file serving setup
|
||||
r.Route("/public", func(r fiber.Router) {
|
||||
// Cache client side files based on checksum
|
||||
r.Use(etag.New())
|
||||
|
||||
staticConfig := fiber.Static{}
|
||||
if config.Mode == "development" {
|
||||
log.Printf("Disabling Cache-Control in development mode")
|
||||
|
||||
// if no "Cache-Control" is present the browser will cache heuristically (and we don't want that)
|
||||
r.Use(func(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "no-cache")
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
staticConfig.CacheDuration = 1 * time.Millisecond
|
||||
}
|
||||
|
||||
r.Static("/", "./_frontend/out", staticConfig)
|
||||
r.Static("/", "./_public", staticConfig)
|
||||
})
|
||||
|
||||
// Serve generated css and js files and the static "./_public" folder
|
||||
|
||||
// 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.HandleAppuntiPage(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)
|
||||
})
|
||||
|
||||
r.Post("/appunti/dispense", func(c *fiber.Ctx) error {
|
||||
var dispensaTemplate model.Dispensa
|
||||
if err := c.BodyParser(&dispensaTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dispensaId, err := h.HandleCreateDispensa(dispensaTemplate, CreateContext(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(dispensaId)
|
||||
})
|
||||
r.Get("/appunti/dispense", func(c *fiber.Ctx) error {
|
||||
dispensaId := c.Query("id")
|
||||
|
||||
dispensa, err := h.HandleGetDispensa(dispensaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(dispensa)
|
||||
})
|
||||
}
|
||||
@@ -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.json" è 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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package util
|
||||
|
||||
import "log"
|
||||
|
||||
func Must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -2,10 +2,11 @@ package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
)
|
||||
|
||||
// GenerateRandomString generates a random hex string from a random array of "n" bytes
|
||||
func GenerateRandomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
|
||||
@@ -14,5 +15,5 @@ func GenerateRandomString(n int) string {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||