Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5897ac50f | ||
|
|
01982942a8 | ||
|
|
be8c35229b | ||
|
|
e36fddb2b3
|
||
|
|
e34f91fce6
|
||
|
|
064f42fb9f
|
||
|
|
a541740a29 | ||
|
|
d1fa37524d | ||
|
|
762d350898 | ||
|
|
15bc406494 |
@@ -1,3 +0,0 @@
|
||||
Dockerfile
|
||||
node_modules
|
||||
.git
|
||||
@@ -1,22 +0,0 @@
|
||||
# Server config
|
||||
MODE=development
|
||||
HOST=:8080
|
||||
|
||||
# Separate services
|
||||
GIT_URL=https://git.phc.dm.unipi.it
|
||||
CHAT_URL=https://chat.phc.dm.unipi.it
|
||||
|
||||
# Other
|
||||
EMAIL=macchinisti@lists.dm.unipi.it
|
||||
|
||||
# Base URL
|
||||
BASE_URL=localhost:8080
|
||||
|
||||
# Lista Utenti
|
||||
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
|
||||
|
||||
# AuthService
|
||||
AUTH_SERVICE_HOST=:memory:
|
||||
|
||||
# Origine per Lista Utenti
|
||||
LISTA_UTENTI=utenti-poisson-2022.local.json
|
||||
@@ -1,17 +1,8 @@
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Local files
|
||||
.vscode/
|
||||
*.local*
|
||||
bin/
|
||||
|
||||
# Generated output
|
||||
.out/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# NodeJS
|
||||
node_modules/
|
||||
|
||||
# Executables
|
||||
phc-website-server
|
||||
!phc-website-server/
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /_frontend
|
||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
COPY ./_frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm fetch --prod
|
||||
COPY ./_frontend ./
|
||||
RUN pnpm install -r --offline --prod
|
||||
RUN make build
|
||||
|
||||
FROM golang:1.19-alpine AS backend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /backend
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
COPY ./ ./
|
||||
RUN make backend
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=frontend-builder /_frontend/out ./_frontend/out
|
||||
COPY --from=backend-builder /backend/phc-website-server ./phc-website-server
|
||||
COPY ./_views ./_views
|
||||
COPY ./_content/news ./_content/news
|
||||
COPY ./_content/guide ./_content/guide
|
||||
COPY ./_public ./_public
|
||||
COPY ./.env ./.env
|
||||
EXPOSE 8000
|
||||
CMD ["./website-server"]
|
||||
@@ -1,35 +1,11 @@
|
||||
|
||||
GO_SOURCES = $(shell find . -name '*.go')
|
||||
GO_EXECUTABLE = phc-website-server
|
||||
|
||||
.PHONY: all
|
||||
all: frontend backend
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: frontend
|
||||
frontend:
|
||||
$(MAKE) -C _frontend/
|
||||
@echo "Built Frontend"
|
||||
|
||||
#
|
||||
# Build Backend
|
||||
#
|
||||
$(NPM) run build
|
||||
|
||||
.PHONY: backend
|
||||
backend: $(GO_EXECUTABLE)
|
||||
@echo "Built Backend"
|
||||
|
||||
$(GO_EXECUTABLE): $(GO_SOURCES)
|
||||
go build -o $(GO_EXECUTABLE) ./cmd/phc-website-server
|
||||
|
||||
#
|
||||
# Debug
|
||||
#
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
@echo "GO_SOURCES = $(GO_SOURCES)"
|
||||
@echo "GO_EXECUTABLE = $(GO_EXECUTABLE)"
|
||||
backend:
|
||||
go build -v -o ./out/server ./cmd/server
|
||||
|
||||
@@ -1,180 +1,30 @@
|
||||
# phc/website
|
||||
# PHC Website
|
||||
|
||||
Backend e frontend del nuovo sito per il PHC.
|
||||
Sito web del PHC
|
||||
|
||||
## Usage
|
||||
|
||||
To setup the project
|
||||
Come usare questo progetto
|
||||
|
||||
```bash shell
|
||||
# Clone the repo
|
||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
||||
$ cd _frontend
|
||||
$ make setup
|
||||
### Development
|
||||
|
||||
```sh
|
||||
# Start the backend server and the frontend development server
|
||||
$ go run -v ./cmd/dev-server
|
||||
|
||||
# Or using entr lo get live reload for go files
|
||||
$ fd -e go | entr -r go run -v ./cmd/dev-server
|
||||
```
|
||||
|
||||
To just do a full build and start the project run
|
||||
### Production
|
||||
|
||||
```bash shell
|
||||
# Setup local development env file
|
||||
$ cp .env.dev .env
|
||||
```sh
|
||||
$ go run ./cmd/server
|
||||
```
|
||||
|
||||
# Setup and do a full build of frontend and backend
|
||||
### Deploy
|
||||
|
||||
```sh
|
||||
$ make all
|
||||
|
||||
# Compile and start the server
|
||||
$ go run -v ./cmd/phc-website-server
|
||||
```
|
||||
|
||||
Alternativamente se si sta modificando in live il codice è comodo usare [`entr`](https://github.com/eradman/entr) e `fd` (un'alternativa a `find`)
|
||||
|
||||
```bash shell
|
||||
# Restart server when go files change
|
||||
$ printf '%s\n' $(echo **/*.go) | entr -r go run ./cmd/phc-website-server
|
||||
```
|
||||
|
||||
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
|
||||
# Recompile files inside "_frontend/src" on change
|
||||
_frontend/ $ pnpm run watch
|
||||
```
|
||||
|
||||
## Backend
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `github.com/gofiber/fiber/v2`
|
||||
|
||||
Backend server framework.
|
||||
|
||||
- `github.com/joho/godotenv`
|
||||
|
||||
Library used to load `.env` config files.
|
||||
|
||||
- `github.com/yuin/goldmark`
|
||||
|
||||
Along with `github.com/yuin/goldmark-highlighting`, `github.com/alecthomas/chroma` and `github.com/litao91/goldmark-mathjax` are used to render Markdown articles and pages with latex expression and syntax highlighting.
|
||||
|
||||
- `gopkg.in/yaml.v3`
|
||||
|
||||
Used to load YAML frontmatter in Markdown documents.
|
||||
|
||||
- `github.com/alecthomas/repr`
|
||||
|
||||
Utility to pretty print Go values.
|
||||
|
||||
### Architecture
|
||||
|
||||
The go project is organized as a library and the main server executable is inside `cmd/phc-website-server` that starts the server provided by the `server/` package and injects all concrete service instances.
|
||||
|
||||
After a recent refactor all code that interacts with services is inside the `handler/` package that abstracts away the HTTP server dependency to test more easily each route.
|
||||
|
||||
The `handler/` has a `Context` type that for now is just used to pass around the user if the HTTP request contained a session token.
|
||||
|
||||
The `model/` package provides some common types used in the whole application.
|
||||
|
||||
The `config/` package is just a singleton loaded when the application is started providing config values loaded from a `.env` file.
|
||||
|
||||
#### Services
|
||||
|
||||
All other Go packages are services used by the `handler/` packages that provide authentication (`auth/`), template rendering (`templates/`), articles loading (`articles/`).
|
||||
|
||||
Some very small services for now just have a single file and are `storia.go` and `utenti.go` (each provides a `-Service` interface and a default concrete implementation).
|
||||
|
||||
## Frontend
|
||||
|
||||
**Warning.** Forse a breve ci sarà un lieve refactor da AlpineJS a Preact perché è più comodo.
|
||||
|
||||
All frontend code is stored inside the `_frontend/` directory. (This will be implied in paths from now on)
|
||||
|
||||
This is a NodeJS project that uses <https://pnpm.io/> as a package manager.
|
||||
|
||||
This project compiles javascript files using _RollupJS_ (a tree shaking js bundler) and scss files using _sass_.
|
||||
|
||||
### Javascript
|
||||
|
||||
These files a processed by RollupJS into `out/`
|
||||
|
||||
- `base.js`
|
||||
|
||||
Script che avvia KaTeX e il _theme switcher_.
|
||||
|
||||
- `utenti.js`
|
||||
|
||||
Script che si occupa di mostrare la lista degli utenti con fuzzy search.
|
||||
|
||||
This script is imported by `_views/utenti.html` along side its dependencies loaded from common CDNs
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
- `profilo.js`
|
||||
|
||||
Script che aggiunge un minimo di interattività alla pagina renderizzata da `_views/profilo.html`
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="/public/profilo.min.js"></script>
|
||||
```
|
||||
|
||||
- `homepage-art.ts`
|
||||
|
||||
Script che renderizza l'animazione della homepage.
|
||||
|
||||
|
||||
- `appunti-condivisi.jsx`
|
||||
|
||||
Script che aggiunte l'interattività alla pagina `/appunti/condivisi` utilizzando Preact come libreria di UI.
|
||||
|
||||
```html
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODE`
|
||||
|
||||
Può essere `production` (default) o `development`.
|
||||
|
||||
- `HOST`
|
||||
|
||||
Indirizzo sul quale servire il sito, di default è `:8080`.
|
||||
|
||||
- `EMAIL`
|
||||
|
||||
Indirizzo di posta elettronica per contattare gli amministratori del sito,
|
||||
che compare nel footer di ogni pagina.
|
||||
|
||||
|
||||
- `<SERVIZIO>_URL`
|
||||
|
||||
Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale).
|
||||
|
||||
Per ora ci sono solo i seguenti
|
||||
|
||||
- `GIT_URL`
|
||||
- `CHAT_URL`
|
||||
|
||||
- `BASE_URL`
|
||||
|
||||
Base dell'url per i link nel sito, per adesso usato per il feed RSS. In locale conviene impostarlo come `HOST` magari premettendo il protocollo (eg. `http://localhost:8080`)
|
||||
|
||||
- `USER_PAGES_BASE_URL`
|
||||
|
||||
Base dell'url per le pagine utente di Poisson, di default `https://poisson.phc.dm.unipi.it/~`
|
||||
|
||||
- `AUTH_SERVICE_HOST`
|
||||
|
||||
Indirizzo del servizio generico di autenticazione.
|
||||
|
||||
## Altri Servizi
|
||||
|
||||
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 `http://localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
id: guida-terminale
|
||||
title: "Guida al Terminale"
|
||||
tags: shell, zsh, vim, ssh
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
|
||||
---
|
||||
|
||||
## 1. Shell & Terminale: differenze?
|
||||
|
||||
- introduzione storica + bash
|
||||
|
||||
### Le Basi
|
||||
|
||||
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
|
||||
- copiare/incollare testo
|
||||
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
|
||||
- chiedere aiuto: man, info
|
||||
|
||||
### Zsh: una shell moderna e potente
|
||||
|
||||
https://wiki.archlinux.org/title/Zsh
|
||||
|
||||
## 2. Vim: l'Editor intramontabile
|
||||
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
|
||||
|
||||
## 3. E la TTY?
|
||||
Come utilizzarla, quando è utile, limitazioni(no scrollback)
|
||||
|
||||
## SSH: la magia della remote shell
|
||||
@@ -1 +0,0 @@
|
||||
["delucreziis", "serdyuk", "minnocci", "manicastri"]
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
id: 2021-12-22-notizia-1
|
||||
title: "Notizia 1"
|
||||
tags: important, prova, test, foo, bar
|
||||
publish_date: 2021/12/22 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur "adipisicing" elit. Repudiandae -- optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora $1 + 1$ ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
$$
|
||||
\int_0^1 x^2 \mathrm d x
|
||||
$$
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
```
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
- bar with some `code`
|
||||
```go
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
|
||||
#### Tables
|
||||
|
||||
<https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#tables>
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
|
||||
| Expression | Derivative | Integral |
|
||||
| :---: | :---: | :---: |
|
||||
| $x^a$ | $a x^{a-1}$ | $\displaystyle \frac{1}{a+1} x^{a+1} + c$ se $a \neq -1$ |
|
||||
| $\sin x$ | $\cos x$ | $\displaystyle -\cos x + c$ |
|
||||
| $e^x$ | $e^x$ | $\displaystyle e^x + c$ |
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 2021-12-23-notizia-2
|
||||
title: "Notizia 2"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/23 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 2021-12-24-notizia-3
|
||||
title: "Notizia 3"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/24 18:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
|
||||
@@ -1,38 +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/09
|
||||
- uid: serdyuk
|
||||
fullName: Illya Serdyuk
|
||||
entryDate: 2019/12
|
||||
- uid: caporali
|
||||
fullName: Francesco Caporali
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
- uid: dachille
|
||||
fullName: Letizia D'Achille
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
eventi:
|
||||
- type: simple
|
||||
title: Nuovo sito del PHC
|
||||
description: |
|
||||
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
|
||||
date: 2022/09
|
||||
icon: fa-solid fa-star
|
||||
- type: simple
|
||||
title: Data di pubblicazione del sito Poisson originale
|
||||
date: 1996/06/17
|
||||
description: |
|
||||
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
|
||||
icon: fa-solid fa-upload
|
||||
- type: simple
|
||||
title: Apertura del PHC
|
||||
date: 1994
|
||||
icon: fa-solid fa-flag-checkered
|
||||
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,23 +0,0 @@
|
||||
|
||||
# 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/
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { defineConfig } from 'rollup'
|
||||
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import esbuild from 'rollup-plugin-esbuild'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'src/base.js',
|
||||
output: {
|
||||
file: 'out/base.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/utenti.js',
|
||||
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
|
||||
output: {
|
||||
file: 'out/utenti.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
// map library names to global constants
|
||||
alpinejs: 'Alpine',
|
||||
'fuse.js': 'Fuse',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/profilo.js',
|
||||
external: ['alpinejs'],
|
||||
output: {
|
||||
file: 'out/profilo.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
alpinejs: 'Alpine',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/homepage-art.ts',
|
||||
output: {
|
||||
file: 'out/homepage-art.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [esbuild({ minify: true })],
|
||||
},
|
||||
{
|
||||
input: 'src/appunti-condivisi.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -1,24 +0,0 @@
|
||||
import { render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
const Counter = ({}) => {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
const increment = () => {
|
||||
setValue(value => value + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
Counter: {value} <button onClick={increment}>+</button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
<>
|
||||
<h1>Preact is Working!</h1>
|
||||
<Counter />
|
||||
</>,
|
||||
document.querySelector('#app')
|
||||
)
|
||||
@@ -1,41 +0,0 @@
|
||||
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()
|
||||
})
|
||||
@@ -1,378 +0,0 @@
|
||||
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)
|
||||
@@ -1,16 +0,0 @@
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
Alpine.data('profilo', () => ({
|
||||
init() {
|
||||
console.log('Profilo!')
|
||||
},
|
||||
}))
|
||||
|
||||
Alpine.data('passwordForm', () => ({
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
passwordSame: true,
|
||||
onUpdate() {
|
||||
this.passwordSame = this.password === this.passwordAgain
|
||||
},
|
||||
}))
|
||||
@@ -1,69 +0,0 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--fg-300: #777;
|
||||
--fg-400: #666;
|
||||
--fg-500: #333;
|
||||
--fg-600: #222;
|
||||
|
||||
--bg-000: #f0f0f0;
|
||||
--bg-100: #f0f0f0;
|
||||
--bg-500: #eaeaea;
|
||||
--bg-550: #ecedee;
|
||||
--bg-600: #e4e5e7;
|
||||
--bg-700: #d5d5d5;
|
||||
--bg-750: #c8c8c8;
|
||||
--bg-800: #c0c0c0;
|
||||
--bg-850: #b8b8b8;
|
||||
|
||||
--accent-300: #5cc969;
|
||||
--accent-400: #4eaa59;
|
||||
--accent-500: #278542;
|
||||
--accent-600: #2e974c;
|
||||
--accent-700: #154d24;
|
||||
--accent-800: #002d0d;
|
||||
|
||||
--ft-ss: 'Inter', sans-serif;
|
||||
--ft-ss-wt-light: 300;
|
||||
--ft-ss-wt-normal: 400;
|
||||
--ft-ss-wt-medium: 500;
|
||||
--ft-ss-wt-bold: 700;
|
||||
|
||||
--shadow-500: 0 0 16px 0 #00000018;
|
||||
|
||||
// Components
|
||||
--text-input-bg: var(--bg-000);
|
||||
--text-input-readonly-bg: var(--bg-600);
|
||||
--text-input-readonly-fg: var(--fg-300);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-500);
|
||||
color: var(--fg-500);
|
||||
|
||||
font-family: var(--ft-ss);
|
||||
font-size: 17px;
|
||||
font-weight: var(--ft-ss-wt-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@import './typography.scss';
|
||||
@@ -1,78 +0,0 @@
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import Alpine from 'alpinejs'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
const SHOW_MORE_INCREMENT = 15
|
||||
const FUSE_OPTIONS = {
|
||||
includeScore: true,
|
||||
keys: [
|
||||
'nome',
|
||||
'cognome',
|
||||
'tags',
|
||||
{ name: 'nomeCompleto', getFn: user => `${user.nome} ${user.cognome}` },
|
||||
],
|
||||
}
|
||||
|
||||
const SORT_MODES = {
|
||||
chronological: () => 0,
|
||||
name: (a, b) => (a.nome < b.nome ? -1 : 1),
|
||||
surname: (a, b) => (a.cognome < b.cognome ? -1 : 1),
|
||||
}
|
||||
|
||||
function getSortedUserList(original, mode) {
|
||||
return [...original].sort(SORT_MODES[mode])
|
||||
}
|
||||
|
||||
Alpine.data('utenti', () => ({
|
||||
searchField: '', // two-way binding for the search input field
|
||||
sortMode: 'chronological', // two-way binding for the sorting mode
|
||||
fetchedUsers: [], // hold complete user list
|
||||
sortedUserBuffer: [], // Yet another buffer of the user list for the sort mode
|
||||
fuse: new Fuse([], FUSE_OPTIONS), // current fuse instance, used to filter the list above
|
||||
searchResultsBuffer: [], // stores the full current search
|
||||
searchResults: [], // list to renderer on screen with a subset of the whole search results buffer
|
||||
async init() {
|
||||
// Get user list from server
|
||||
const response = await fetch('/api/utenti')
|
||||
this.fetchedUsers = await response.json()
|
||||
|
||||
// This will call the function "showMore()" when the user is near the end of the list
|
||||
new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
console.log('Near the bottom of the page')
|
||||
this.showMore()
|
||||
}
|
||||
})
|
||||
}).observe(this.$refs.spinner)
|
||||
|
||||
// Initialize with an empty query
|
||||
this.updateSortMode()
|
||||
this.updateSearch()
|
||||
},
|
||||
showMore() {
|
||||
// setTimeout(() => {
|
||||
// Updates the final "searchResults" list with more items from the previous buffer
|
||||
const newCount = this.searchResults.length + SHOW_MORE_INCREMENT
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, newCount)
|
||||
// }, 250) // For fun
|
||||
},
|
||||
setResults(list) {
|
||||
this.searchResultsBuffer = list.filter(
|
||||
entry => entry.score === undefined || entry.score <= 0.25
|
||||
)
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, SHOW_MORE_INCREMENT)
|
||||
},
|
||||
updateSortMode() {
|
||||
this.sortedUserBuffer = getSortedUserList(this.fetchedUsers, this.sortMode)
|
||||
this.fuse.setCollection(this.sortedUserBuffer)
|
||||
this.updateSearch()
|
||||
},
|
||||
updateSearch() {
|
||||
console.time('search')
|
||||
if (this.searchField.trim().length === 0) {
|
||||
// Reset the result list
|
||||
this.setResults(this.sortedUserBuffer.map(user => ({ item: user })))
|
||||
} else {
|
||||
// Update the result list with the new results
|
||||
this.setResults(this.fuse.search(this.searchField))
|
||||
}
|
||||
console.timeEnd('search')
|
||||
},
|
||||
}))
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"noEmitOnError": true
|
||||
},
|
||||
"filesGlob": ["./src/**/*.ts"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 790 B |
@@ -1,945 +0,0 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #eaeaea;
|
||||
--fg: #333;
|
||||
|
||||
--bg-lighter: #f0f0f0;
|
||||
|
||||
--bg-dark: hsl(220, 5%, 93%);
|
||||
--bg-darker: hsl(220, 5%, 90%);
|
||||
--bg-darker-2: #d5d5d5;
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--bg-darker-3: #c0c0c0;
|
||||
--bg-darker-4: #b8b8b8;
|
||||
|
||||
--accent-1: #278542;
|
||||
--accent-1-fg: #154d24;
|
||||
|
||||
--card-date: #666;
|
||||
--card-content: #222;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #00000018;
|
||||
|
||||
--text-input-bg: #fff;
|
||||
--text-input-readonly-bg: #e4e4e4;
|
||||
--text-input-readonly-fg: #777;
|
||||
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 70ch;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 8rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
color: var(--accent-1-fg);
|
||||
}
|
||||
|
||||
/* Tutti i link dentro la navbar sono speciali e non sembrano link */
|
||||
nav .nav-element {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--accent-1-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav .nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
transition: transform 150ms ease-in-out;
|
||||
}
|
||||
|
||||
nav .nav-logo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
nav .nav-logo img {
|
||||
width: 150px;
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
nav .nav-main {
|
||||
display: flex;
|
||||
/* grid-template-columns: repeat(5, 1fr) 1fr auto 1fr; */
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-item.filler {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element {
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-button {
|
||||
margin: 0 0.5rem;
|
||||
/* padding: 0.5rem; */
|
||||
|
||||
aspect-ratio: 1;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
/* background: var(--bg-darker-2); */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-radius: 1rem;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
user-select: none;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element:hover,
|
||||
nav .nav-main .nav-button:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown {
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items .nav-item:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: none;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
/*
|
||||
nav .nav-main .nav-item.dropdown .nav-items::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: -2px;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
|
||||
background: var(--bg-darker);
|
||||
} */
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover + .nav-items,
|
||||
nav .nav-main .nav-item.dropdown .name + .nav-items:hover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* .nav-main borders */
|
||||
.nav-main > .nav-item:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.nav-main > .nav-item:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 3rem;
|
||||
|
||||
width: 80ch;
|
||||
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-bottom: none;
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
max-width: 60ch;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card .title {
|
||||
font-size: 22px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
font-weight: var(--font-weight-light);
|
||||
color: var(--card-content);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0 0.5rem;
|
||||
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tags .tag {
|
||||
height: 1.5rem;
|
||||
border-radius: calc(1.5rem / 2);
|
||||
padding: 0 calc(1.5rem / 2);
|
||||
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--card-date);
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
a:not(.button) {
|
||||
color: var(--accent-1-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not(.button):hover {
|
||||
color: var(--accent-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 17px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 1rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
background: var(--bg-darker-2-1);
|
||||
box-shadow: 0 4px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border: 1px solid var(--accent-2-darker);
|
||||
background: var(--accent-2);
|
||||
color: var(--accent-2-darkest);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--accent-2-lighter);
|
||||
}
|
||||
|
||||
button.icon {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
select {
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 16px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 0.25rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Text Fields */
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'] {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
background: var(--text-input-bg);
|
||||
color: var(--fg);
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
input[type='password'] {
|
||||
font-family: caption;
|
||||
}
|
||||
|
||||
input[type='text']:read-only,
|
||||
input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
input[type='text'].error,
|
||||
input[type='password'].error {
|
||||
background: #faa;
|
||||
color: #311;
|
||||
}
|
||||
|
||||
/* Compound Controls */
|
||||
|
||||
.compound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 8px 0 #00000022;
|
||||
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.compound > .divider {
|
||||
height: 2rem;
|
||||
width: 1px;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.compound .icon {
|
||||
width: 2.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.compound > select {
|
||||
background: none;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.compound > button,
|
||||
.compound > .button,
|
||||
.compound > input,
|
||||
.compound > select {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.compound > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.compound > :not(:last-child) {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
form .field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form .field-set {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-auto-rows: auto;
|
||||
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
form .field-set .fill {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
form .field-set label {
|
||||
grid-column: 1 / 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
form .field-set input {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.page-home {
|
||||
/* TODO: Sarebbe meglio se si riuscisse a capire come farlo senza */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-home .nav-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-home .super {
|
||||
position: absolute;
|
||||
left: 50vw;
|
||||
top: 50vh;
|
||||
|
||||
width: 90vw;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 2vmin;
|
||||
}
|
||||
|
||||
.page-home canvas {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
|
||||
transition: opacity 1000ms ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.page-home canvas.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-home .super .block.text {
|
||||
max-width: 40ch;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.page-home .super .block.text h1 {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-home .super .block.text p {
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.page-home .super .block.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-home .super .block.image img {
|
||||
max-width: 80ch;
|
||||
max-height: 50vh;
|
||||
filter: drop-shadow(0 0 64px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 8px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.page-home .main {
|
||||
padding-top: calc(100vh - 6rem);
|
||||
}
|
||||
|
||||
.page-utenti .user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item .icon {
|
||||
width: 1.75rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.page-utenti .spinner {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 5rem;
|
||||
|
||||
color: var(--fg);
|
||||
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.page-storia .history-container {
|
||||
--bar-size: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
margin: 5rem 0;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -2rem;
|
||||
width: var(--bar-size);
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
top: -3rem;
|
||||
bottom: -3rem;
|
||||
left: 0;
|
||||
border-left: var(--bar-size) dashed var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event::before {
|
||||
--size: 1rem;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(-2rem + var(--bar-size) / 2 - var(--size) / 2);
|
||||
transform: translate(0, 1rem);
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
border-radius: 100%;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .title {
|
||||
font-size: 22px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .date {
|
||||
color: var(--card-date);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .spacer {
|
||||
height: calc(var(--size) * 1rem);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 2rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search input[type='text'] {
|
||||
width: 50ch;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
/* Rendered Markdown */
|
||||
|
||||
.news-content p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-content h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.news-content img {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 2px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
.news-content .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content .tags,
|
||||
.news-content .date {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content table {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
/* Math */
|
||||
|
||||
.katex-display {
|
||||
margin: 1rem 0;
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
table td:not(:first-child),
|
||||
table th:not(:first-child) {
|
||||
border-left: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table td {
|
||||
border-top: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table tbody tr:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
body.dark-mode {
|
||||
--bg: #282828;
|
||||
--fg: #a6cc92;
|
||||
/* --fg: #6eac4d; */
|
||||
/* Magari questo: */
|
||||
/* --fg: #a3b09c; */
|
||||
|
||||
--bg-dark: hsl(10, 10%, 20%);
|
||||
--bg-darker: hsl(10, 10%, 17%);
|
||||
--bg-darker-2: #1d2021;
|
||||
--bg-darker-2-1: #1d5021;
|
||||
--bg-darker-3: #101111;
|
||||
|
||||
--accent-1: #154d24;
|
||||
--accent-1-fg: #278542;
|
||||
|
||||
--card-date: #928374;
|
||||
--card-content: #d4be98;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #16182077;
|
||||
|
||||
--text-input-bg: var(--bg-darker);
|
||||
--text-input-readonly-bg: hsl(10, 10%, 22%);
|
||||
--text-input-readonly-fg: hsl(10, 10%, 40%);
|
||||
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
.dark-mode button {
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
background: var(--bg-darker);
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
.dark-mode button:hover {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.dark-mode button.primary {
|
||||
border: 1px solid #113a1c;
|
||||
background: #1e6732;
|
||||
color: #b7e3c3;
|
||||
}
|
||||
|
||||
.dark-mode button.primary:hover {
|
||||
background: #23773a;
|
||||
}
|
||||
|
||||
.dark-mode nav .nav-logo img {
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.dark-mode input[type] {
|
||||
background: #4b4342;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text']:read-only,
|
||||
.dark-mode input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text'].error,
|
||||
.dark-mode input[type='password'].error {
|
||||
background: #633;
|
||||
color: #faa;
|
||||
}
|
||||
|
||||
.dark-mode pre {
|
||||
background: var(--bg-darker);
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
/* Trick molto molto malvagio per non dover rendere il colorscheme dei blocchi di codice dinamici rispetto alla dark mode */
|
||||
.dark-mode pre > code > span {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.dark-mode table td,
|
||||
.dark-mode table th {
|
||||
border-color: var(--fg);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
</section>
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
{{end}}
|
||||
@@ -1,43 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Raccolta degli Appunti
|
||||
</h1>
|
||||
<p>
|
||||
Questa è la raccolta degli appunti presenti su Poisson. Cerca il titolo della dispensa, il nome e cognome o l'username dell'autore oppure scrivi il nome del corso rispetto a cui filtrare. Altrimenti in cima compariranno gli appunti più "gettonati".
|
||||
</p>
|
||||
<p>
|
||||
Puoi aggiungere le tue dispense dalla <a href="/appunti/condivisi">pagina personale di condivisione</a>.
|
||||
</p>
|
||||
<div class="search">
|
||||
<div class="compound">
|
||||
<input type="text" id="search-field" placeholder="Prova con "Geometria 1" o "Mezzedimi"..." autocomplete="off" {{ if .Query }}value="{{ .Query }}"{{ end }}>
|
||||
<button class="icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appunti-list">
|
||||
<code>TODO: Lista work in progress</code>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Aggiorna automaticamente la barra dell'URL con la query corrente
|
||||
document.querySelector('#search-field').addEventListener('input', function() {
|
||||
const q = this.value.trim();
|
||||
history.replaceState({}, 'Appunti',
|
||||
q.length === 0 ? '/appunti' : '/appunti?q=' + encodeURIComponent(q)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
{{end}}
|
||||
@@ -1,44 +0,0 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ template "title" . }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
|
||||
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
|
||||
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
|
||||
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script 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}}
|
||||
@@ -1,18 +0,0 @@
|
||||
{{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}}
|
||||
@@ -1,37 +0,0 @@
|
||||
{{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}}
|
||||
@@ -1,118 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Home • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<canvas id="wires-animation"></canvas>
|
||||
|
||||
<!-- Non avevo molta fantasia per il nome di questa classe -->
|
||||
<section class="super">
|
||||
<div class="block image">
|
||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
||||
</div>
|
||||
<div class="block text">
|
||||
<h1>
|
||||
Cos'è il PHC?
|
||||
</h1>
|
||||
<p>
|
||||
Il PHC è un progetto di sperimentazione tecnologica di alcuni studenti di Matematica di Pisa che va avanti fin dal 1994.
|
||||
</p>
|
||||
<p>
|
||||
Fisicamente, occupa la stanza 106 del Dipartimento, mentre questo sito ne è la sede virtuale. Qui si trovano vari servizi offerti agli studenti, e ne si documenta lo sviluppo nella pagina <a href="/news">Notizie</a>.
|
||||
</p>
|
||||
<p>
|
||||
Nella sezione <b>Progetti</b> si trovano alcune delle attività e dei servizi del PHC, mentre <a href="/utenti">Utenti</a> contiene l'indice delle pagine personali degli studenti, che sono storicamente uno dei progetti più importanti del PHC.
|
||||
</p>
|
||||
<p>
|
||||
Per più informazioni sul PHC, si consulti la sezione <a href="/storia">Storia</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Notizie Importanti</h2>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-1">News 1</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
much doge, ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-2">News 2</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-3">News 3</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<!-- TODO: Progetti/Servizi/Cose fornite -->
|
||||
<h2>Progetti</h2>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
Vuoi diventare un macchinista?
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
|
||||
Stai spesso in dipartimento? Allora fai pure un salto in PHC a parlare con noi, per diventare apprendista (e magari un giorno macchinista) o semplicemente per imparare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/public/homepage-art.min.js"></script>
|
||||
{{end}}
|
||||
@@ -1,89 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Link Utili • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-link"></i>
|
||||
Link Utili
|
||||
</h1>
|
||||
<p class="center">
|
||||
Questo è un elenco di alcuni indirizzi potenzialmente utili
|
||||
</p>
|
||||
<ul>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://linktr.ee/aulastudenti">Aula Studenti</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://credenziali.phc.dm.unipi.it/">Credenziali Poisson</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="http://detexify.kirelabs.org/classify.html">Non trovi un simbolo Latex?</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://git.phc.dm.unipi.it/phc/dm-scripts/src/branch/main/printa4">Script per stampare in dipartimento dal proprio PC</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://github.com/SuzanneSoy/WiTeX">Wikipedia+LaTex??</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i <a href="https://en.wikipedia.org/wiki/Abstract_nonsense">tuoi</a> <a href="https://en.wikipedia.org/wiki/New_Math">articoli</a> <a href="https://en.wikipedia.org/wiki/%C3%89variste_Galois#Final_days">preferiti</a> di Wikipedia con un typesetting in stile LaTex!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://www.dm.unipi.it">CdS Matematica</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</section>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
{{end}}
|
||||
@@ -1,55 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Accedi • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-user"></i>
|
||||
Account di Poisson
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
<form class="card" action="/login" method="POST">
|
||||
<div class="title">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Accedi
|
||||
</div>
|
||||
<p>
|
||||
Inserisci le tue credenziali di Poisson per accedere
|
||||
</p>
|
||||
|
||||
<div class="field-set">
|
||||
<!-- <label for="login-provider">Provider:</label> -->
|
||||
<input type="hidden" name="provider" id="login-provider" value="poisson-ldap">
|
||||
|
||||
<label for="login-username">Username:</label>
|
||||
<input type="text" name="username" id="login-username">
|
||||
|
||||
<label for="login-password">Password:</label>
|
||||
<input type="password" name="password" id="login-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Accedi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Ottenere un account</h2>
|
||||
<p>
|
||||
Dal 2022 in nuovi utenti hanno bisogno di compilare un modulo se vogliono ottenere un account<sup>1</sup>, scarica il <a href="#">modulo di richiesta</a> e portacelo in PHC o inviacelo via email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>.
|
||||
</p>
|
||||
<p>
|
||||
1: In realtà il modulo ancora non esiste
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Recupero credenziali</h2>
|
||||
<p>
|
||||
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una
|
||||
email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,18 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Article.Title }} • News • 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}}
|
||||
@@ -1,67 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}News • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h2>
|
||||
Feed RSS
|
||||
<a href="/news/rss"> <i class="fa-solid fa-rss"></i></a>
|
||||
</h2>
|
||||
<h1>
|
||||
<i class="far fa-newspaper"></i>
|
||||
Notizie Importanti
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
{{ if .HasTag "important" }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<i class="fas fa-history"></i>
|
||||
Archivio notizie
|
||||
</h2>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
{{ if not (.HasTag "important") }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,88 +0,0 @@
|
||||
{{ define "navbar" }}
|
||||
<nav>
|
||||
<!-- Site -->
|
||||
<div class="nav-logo">
|
||||
<a class="nav-element" href="/">
|
||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-main">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/utenti">Utenti</a>
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Progetti
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="{{ .Config.GitUrl }}">Gitea</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="{{ .Config.ChatUrl }}">Zulip</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/seminari">Seminari</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Risorse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/news">News</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/appunti">Appunti</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/guide">Guide</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/link">Link Utili</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/storia">Storia</a>
|
||||
</div>
|
||||
<!-- <div class="nav-item">
|
||||
<a class="nav-element" href="/about">About</a>
|
||||
</div> -->
|
||||
|
||||
<div class="nav-item filler"></div>
|
||||
|
||||
<!-- User Related -->
|
||||
<div class="nav-item">
|
||||
<div id="toggle-dark-mode" class="nav-button">
|
||||
<i class="fas fa-moon"></i>
|
||||
</div>
|
||||
</div>
|
||||
{{if .User}}
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/profilo">@{{ .User.Username }}</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/login">Accedi</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
||||
@@ -1,105 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{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/profilo.min.js"></script>
|
||||
<section x-data="profilo">
|
||||
<h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
|
||||
<p class="center">
|
||||
<a class="button" href="/logout">Logout</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Appunti e Dispense</h2>
|
||||
<p>
|
||||
Per gestire i tuoi appunti e le tue dispense caricate nella sezione <a href="/appunti">Appunti</a> del sito vai
|
||||
<a href="/appunti/condivisi">alla pagina appunti condivisi</a>.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Impostazioni</h2>
|
||||
<div class="card-list">
|
||||
<form class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Sito PHC
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna i campi relativi al sito del PHC, l'immagine del profilo verrà visualizzata sulla tua pagina utente <a href="#">phc.dm.unipi.it/u/{{ .User.Username }}</a> e nella lista di <a href="/utenti">tutti gli utenti</a>.
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="website-nickname">Nickname</label>
|
||||
<input type="text" name="nickname" id="website-nickname" placeholder="{{ .User.Username }}">
|
||||
<label for="website-profile-picture">Immagine Profilo</label>
|
||||
<input type="file" name="picture" id="website-profile-picture">
|
||||
</div>
|
||||
<p>
|
||||
Puoi anche aggiungere link ad altri tuoi profili su siti esterni (quando riempi l'ultimo campo se ne crea uno nuovo sotto)
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="website-link-1">Link 1</label>
|
||||
<input type="text" name="link" id="website-link-1"
|
||||
placeholder="https://github.com/...">
|
||||
<label for="website-link-2">Link 2</label>
|
||||
<input type="text" name="link" id="website-link-2"
|
||||
placeholder="https://twitter.com/...">
|
||||
<label for="website-link-3">Link 3</label>
|
||||
<input type="text" name="link" id="website-link-3"
|
||||
placeholder="https://news.ycombinator.com/user?id=...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Informazioni Utente
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna i campi modificabili dell'account Poisson
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="user-username">Username</label>
|
||||
<input type="text" readonly name="username" id="user-username" value="{{ .User.Username }}">
|
||||
<label for="user-name">Nome</label>
|
||||
<input type="text" readonly name="name" id="user-name" value="{{ .User.Name }}">
|
||||
<label for="user-surname">Cognome</label>
|
||||
<input type="text" readonly name="surname" id="user-surname" value="{{ .User.Surname }}">
|
||||
<label for="user-email">Email</label>
|
||||
<input type="text" readonly name="email" id="user-email" value="{{ .User.Email }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna</button>
|
||||
</div>
|
||||
</form>
|
||||
<form x-data="passwordForm" class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Modifica Password
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna la password di Poisson
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="pass-username">Username</label>
|
||||
<input type="text" readonly name="username" id="pass-username" value="{{ .User.Username }}">
|
||||
<label for="pass-password">Password</label>
|
||||
<input type="password" name="password" id="pass-password" :class="!passwordSame ? 'error' : ''"
|
||||
x-model="password" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
||||
<label for="pass-password-2">Ripeti Password</label>
|
||||
<input type="password" name="password-2" id="pass-password-2" :class="!passwordSame ? 'error' : ''"
|
||||
x-model="passwordAgain" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Recupero Credenziali Poisson</h2>
|
||||
<p>
|
||||
Per il recupero credenziali, puoi venire direttamente in PHC a parlarne con noi, oppure inviarci una email
|
||||
all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul
|
||||
sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,61 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Storia • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-clock"></i>
|
||||
Storia del PHC
|
||||
</h1>
|
||||
<p>
|
||||
Qui è annoverata la storia del PHC tramite una linea temporale di alcuni fra gli eventi principali. Al momento è un <i>work in progress</i>, ma aggiungeremo almeno le date di ingresso di tutti i macchinisti.
|
||||
</p>
|
||||
<p>
|
||||
Per delle note storiche un po' più dettagliate, si legga l'ottima pagine sul sito del dipartimento:
|
||||
<a href="http://betti.dm.unipi.it/servizi/PHC.html">http://betti.dm.unipi.it/servizi/PHC.html</a>.
|
||||
</p>
|
||||
<div class="history-container">
|
||||
<div class="timeline-bar"></div>
|
||||
<div class="events">
|
||||
{{ range .Storia }}
|
||||
{{ if eq .Type "simple" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
{{ if .Icon }}
|
||||
<i class="{{ .Icon }}"></i>
|
||||
{{ end }}
|
||||
{{ .Title }}
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
<div class="description">
|
||||
{{ raw .Description }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "entry-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-sign-in"></i>
|
||||
Ingresso di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "exit-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-sign-out"></i>
|
||||
Uscita di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "spacer" }}
|
||||
<div class="spacer" style="--size: {{ .Size }}"></div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,64 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Utenti • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
<section x-data="utenti">
|
||||
<h1>
|
||||
<i class="fas fa-users"></i>
|
||||
Lista degli Utenti
|
||||
</h1>
|
||||
<p>
|
||||
Questa è la lista di tutti gli utenti con un account su Poisson. Scrivi nome, cognome o
|
||||
username di un utente per filtrare la lista in tempo reale. Altrimenti di base in cima
|
||||
compariranno gli utenti con più "follower".
|
||||
</p>
|
||||
<div class="search">
|
||||
<div class="compound padded">
|
||||
<div class="icon" title="Ordina per">
|
||||
<i class="fas fa-sort"></i>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<select x-model="sortMode" @click="updateSortMode()">
|
||||
<option value="chronological">Cronologico</option>
|
||||
<option value="name">Nome</option>
|
||||
<option value="surname">Cognome</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="compound">
|
||||
<input type="text" x-model="searchField" @input="updateSearch()" placeholder="Cerca..." autocomplete="off" />
|
||||
<button class="icon" title="In realtà questo tasto è finto">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-list card-list">
|
||||
<template x-for="entry in searchResults" :key="entry.item.uid">
|
||||
<div class="user-item card">
|
||||
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
|
||||
sortMode === 'surname' ?
|
||||
entry.item.cognome + ' ' + entry.item.nome :
|
||||
entry.item.nome + ' ' + entry.item.cognome
|
||||
"></a>
|
||||
<template x-if="entry.item.tags?.includes('macchinista')">
|
||||
<div class="icon" title="Macchinista">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="entry.item.tags?.includes('rappresentante')">
|
||||
<div class="icon" title="Rappresentante">
|
||||
<i class="fas fa-landmark"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template x-if="entry.score">
|
||||
<span x-text="entry.score"></span>
|
||||
</template> -->
|
||||
</div>
|
||||
</template>
|
||||
<div class="spinner" x-ref="spinner" x-show="searchResults.length < searchResultsBuffer.length">
|
||||
<i class="fas fa-hourglass"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,113 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
ArticlePath string
|
||||
markdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
|
||||
func (article *Article) HasTag(tag string) bool {
|
||||
for _, t := range article.Tags {
|
||||
if t == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewArticle(articlePath string) (*Article, error) {
|
||||
article := &Article{
|
||||
ArticlePath: articlePath,
|
||||
}
|
||||
|
||||
err := article.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func trimAll(vs []string) []string {
|
||||
r := []string{}
|
||||
for _, v := range vs {
|
||||
r = append(r, strings.TrimSpace(v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (article *Article) load() error {
|
||||
fileBytes, err := os.ReadFile(article.ArticlePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source := string(fileBytes)
|
||||
|
||||
// TODO: Ehm bugia pare che esista "https://github.com/yuin/goldmark-meta" però non penso valga la pena aggiungerlo
|
||||
parts := strings.SplitN(source, "---", 3)[1:]
|
||||
|
||||
frontMatterSource := parts[0]
|
||||
markdownSource := parts[1]
|
||||
|
||||
var frontMatter struct {
|
||||
Id string `yaml:"id"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Tags string `yaml:"tags"`
|
||||
PublishDate string `yaml:"publish_date"`
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
article.Id = frontMatter.Id
|
||||
article.Title = frontMatter.Title
|
||||
article.Description = frontMatter.Description
|
||||
article.Tags = trimAll(strings.Split(frontMatter.Tags, ","))
|
||||
article.PublishDate = publishDate
|
||||
|
||||
article.markdownSource = markdownSource
|
||||
article.renderedHTML = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (article *Article) Render() (string, error) {
|
||||
if config.Mode == "development" {
|
||||
article.load()
|
||||
}
|
||||
|
||||
if article.renderedHTML == "" {
|
||||
var buf bytes.Buffer
|
||||
if err := Markdown.Convert([]byte(article.markdownSource), &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
article.renderedHTML = buf.String()
|
||||
}
|
||||
|
||||
return article.renderedHTML, nil
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
RootPath string
|
||||
ArticleCache map[string]*Article
|
||||
}
|
||||
|
||||
func NewRegistry(rootPath string) *Registry {
|
||||
return &Registry{
|
||||
rootPath,
|
||||
map[string]*Article{},
|
||||
}
|
||||
}
|
||||
|
||||
func (registry *Registry) loadArticles() error {
|
||||
entries, err := os.ReadDir(registry.RootPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
article, err := NewArticle(path.Join(registry.RootPath, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry.ArticleCache[article.Id] = article
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticle(id string) (*Article, error) {
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
return nil, fmt.Errorf(`no article with id "%s"`, id)
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticles() ([]*Article, error) {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
articles := []*Article{}
|
||||
for _, article := range registry.ArticleCache {
|
||||
articles = append(articles, article)
|
||||
}
|
||||
|
||||
sort.Slice(articles, func(i, j int) bool {
|
||||
return articles[i].PublishDate.After(articles[j].PublishDate)
|
||||
})
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/formatters/html"
|
||||
mathjax "github.com/litao91/goldmark-mathjax"
|
||||
highlighting "github.com/yuin/goldmark-highlighting"
|
||||
)
|
||||
|
||||
var Markdown goldmark.Markdown
|
||||
|
||||
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
|
||||
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
lang, ok := c.Language()
|
||||
if entering {
|
||||
if ok {
|
||||
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
|
||||
return
|
||||
}
|
||||
w.WriteString("<pre><code>")
|
||||
} else {
|
||||
if ok {
|
||||
w.WriteString("</pre></code>")
|
||||
return
|
||||
}
|
||||
w.WriteString("</pre></code>")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Markdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
extension.Typographer,
|
||||
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
|
||||
mathjax.NewMathJax(),
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("github"),
|
||||
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
publicDir: './client/public',
|
||||
srcDir: './client',
|
||||
outDir: './out/client',
|
||||
vite: {
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:4000',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ErrInvalidSession is thrown when an AuthenticatorService is given a missing
|
||||
// or invalid session token
|
||||
var ErrInvalidSession = fmt.Errorf(`invalid session token`)
|
||||
|
||||
// Service is an authentication service abstraction. When a user is logged in a
|
||||
// new session token is returned, this can be used to read and modify user
|
||||
// properties without having to re-send the user password. (TODO: implement
|
||||
// token renewal)
|
||||
type Service interface {
|
||||
// GetUser retrieves the user info given the username
|
||||
GetUser(username string) (*model.User, error)
|
||||
// GetUsers retrieves the full user list from the authentication service
|
||||
GetUsers() ([]*model.User, error)
|
||||
// GetSession retrieves the user session associated to a session token
|
||||
GetSession(token string) (*model.Session, error)
|
||||
// Login tries to log in a user given username and password and if successful returns a new user session
|
||||
Login(username, password string) (*model.Session, error)
|
||||
}
|
||||
|
||||
// UserForSession returns the user linked to the given session token, this is just a shortcut for calling [AuthenticatorService.GetSession] and then [AuthenticatorService.GetUser]
|
||||
func UserForSession(as Service, token string) (*model.User, error) {
|
||||
session, err := as.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := as.GetUser(session.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// NewDefaultService create an AuthenticatorService given an "host" string,
|
||||
// If host is ":memory:" then this uses the [auth.Memory] implementation,
|
||||
// otherwise for now this defaults to [auth.LDAPAuthService]
|
||||
func NewDefaultService(host string) Service {
|
||||
if host == ":memory:" {
|
||||
return exampleMemoryUsers
|
||||
}
|
||||
|
||||
return newLDAPAuthService(host)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
urlpkg "net/url"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ldapUser represents an LDAP User, most fields are inherited from [auth.User]
|
||||
type ldapUser struct {
|
||||
Uid string `json:"username"`
|
||||
NumericId int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Gecos string `json:"gecos"`
|
||||
}
|
||||
|
||||
// AsUser converts an [ldapUser] to an instance of [auth.User]
|
||||
func (u ldapUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Uid,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
FullName: u.Gecos,
|
||||
}
|
||||
}
|
||||
|
||||
// ldapAuthService just holds the remote host of the HTTP LDAP service to make requests to
|
||||
type ldapAuthService struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func newLDAPAuthService(host string) Service {
|
||||
return &ldapAuthService{host}
|
||||
}
|
||||
|
||||
// doGetRequest is a utility to make HTTP GET requests
|
||||
func (a *ldapAuthService) doGetRequest(url string, response interface{}) error {
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", u, bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doPostRequest is a utility to make HTTP POST requests
|
||||
func (a *ldapAuthService) doPostRequest(url string, request interface{}, response interface{}) error {
|
||||
jsonStr, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(response)
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetUser(username string) (*model.User, error) {
|
||||
var user ldapUser
|
||||
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetUsers() ([]*model.User, error) {
|
||||
ldapUsers := []*ldapUser{}
|
||||
if err := a.doGetRequest("/users", &ldapUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.User, len(ldapUsers))
|
||||
for i, u := range ldapUsers {
|
||||
users[i] = u.AsUser()
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetSession(token string) (*model.Session, error) {
|
||||
var response model.Session
|
||||
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) Login(username, password string) (*model.Session, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
var response model.Session
|
||||
if err := a.doPostRequest("/login", reqBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
var exampleMemoryUsers = &Memory{
|
||||
Users: map[string]*memoryUser{
|
||||
"aziis98": {
|
||||
User: model.User{
|
||||
Username: "aziis98",
|
||||
Name: "Antonio",
|
||||
Surname: "De Lucreziis",
|
||||
Email: "aziis98@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "123",
|
||||
},
|
||||
"bachoseven": {
|
||||
User: model.User{
|
||||
Username: "bachoseven",
|
||||
Name: "Francesco",
|
||||
Surname: "Minnocci",
|
||||
Email: "bachoseven@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "234",
|
||||
},
|
||||
},
|
||||
Sessions: map[string]*memorySession{},
|
||||
}
|
||||
|
||||
type memoryUser struct {
|
||||
model.User
|
||||
Password string `json:"-"`
|
||||
}
|
||||
|
||||
func (u memoryUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
FullName: u.FullName,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
type memorySession struct {
|
||||
Username string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (s memorySession) AsSession() *model.Session {
|
||||
return &model.Session{
|
||||
Username: s.Username,
|
||||
Token: s.Token,
|
||||
}
|
||||
}
|
||||
|
||||
type Memory struct {
|
||||
Users map[string]*memoryUser
|
||||
Sessions map[string]*memorySession
|
||||
}
|
||||
|
||||
func (m *Memory) GetUser(username string) (*model.User, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetUsers() ([]*model.User, error) {
|
||||
users := make([]*model.User, len(m.Users))
|
||||
i := 0
|
||||
for _, u := range m.Users {
|
||||
users[i] = u.AsUser()
|
||||
i++
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetSession(token string) (*model.Session, error) {
|
||||
session, ok := m.Sessions[token]
|
||||
if !ok {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) Login(username string, password string) (*model.Session, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
if user.Password != password {
|
||||
return nil, fmt.Errorf(`invalid credentials`)
|
||||
}
|
||||
|
||||
session := &memorySession{username, util.GenerateRandomString(15)}
|
||||
m.Sessions[session.Token] = session
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import PageLayout from './PageLayout.astro'
|
||||
|
||||
const { frontmatter } = Astro.props
|
||||
---
|
||||
|
||||
<PageLayout {...frontmatter} />
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import '@fontsource/open-sans/latin.css'
|
||||
import '@fontsource/source-sans-pro/latin.css'
|
||||
import '@fontsource/source-code-pro/latin.css'
|
||||
|
||||
import '../styles/main.scss'
|
||||
|
||||
const { title, description, thumbnail, pageName } = Astro.props
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta property="og:title" content={title ?? 'PHC'} />
|
||||
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
||||
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
||||
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
|
||||
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class:list={['page-' + (pageName ?? 'unknown')]}>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout {...Astro.props}>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||
</div>
|
||||
<div class="links">
|
||||
<a role="button" href="#">Utenti</a>
|
||||
<a role="button" href="#">Notizie</a>
|
||||
<a role="button" href="#">Progetti</a>
|
||||
<a role="button" href="#">About</a>
|
||||
<a class="primary" role="button" href="#">Accedi</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<footer>© PHC 2023</footer>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
import PageLayout from '../layouts/PageLayout.astro'
|
||||
---
|
||||
|
||||
<PageLayout pageName="homepage">
|
||||
<section class="principal">
|
||||
<div class="circuit-layer">
|
||||
<canvas id="circuits-art"></canvas>
|
||||
<script src="../scripts/circuits-art.ts"></script>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||
</div>
|
||||
<div class="whats-phc">
|
||||
<div class="title">Cos'è il PHC?</div>
|
||||
<div class="content">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Totam vero deserunt tempore reprehenderit atque, voluptate
|
||||
dolorum excepturi libero pariatur sequi?
|
||||
</p>
|
||||
<p>
|
||||
Laboriosam soluta ab a illum mollitia quaerat quia, veniam consequatur expedita dolore autem reiciendis quae rem
|
||||
excepturi optio? Maiores, hic?
|
||||
</p>
|
||||
<p>
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Exercitationem error atque amet. Tempora earum nemo eveniet
|
||||
aspernatur quam quas, doloribus expedita quisquam dignissimos cupiditate inventore a modi optio harum veritatis,
|
||||
adipisci ab ullam distinctio odio quod delectus ipsum, rerum animi.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="news">
|
||||
<div class="zig-zag">
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
<pattern id="zig-zag-1" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||
<path fill="#C2A8EB" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag-1)" x="0" y="0" width="1000" height="1"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="title">Ultime Notizie</div>
|
||||
|
||||
<div class="news-list">
|
||||
<div class="news">
|
||||
<div class="title">Lorem ipsum dolor sit.</div>
|
||||
<div class="abstract">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||
voluptatem pariatur eius!
|
||||
</p>
|
||||
<p>
|
||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="news">
|
||||
<div class="title">Tempore provident impedit libero?</div>
|
||||
<div class="abstract">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||
voluptatem pariatur eius!
|
||||
</p>
|
||||
<p>
|
||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||
</p>
|
||||
<p>
|
||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||
</p>
|
||||
<p>
|
||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="news">
|
||||
<div class="title">Alias molestias consectetur quam</div>
|
||||
<div class="abstract">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||
voluptatem pariatur eius!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="news">
|
||||
<div class="title">Inventore dignissimos sapiente nulla</div>
|
||||
<div class="abstract">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||
voluptatem pariatur eius!
|
||||
</p>
|
||||
<p>
|
||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="primary" href="#" role="button">Vai all'Archivio</a>
|
||||
</section>
|
||||
<section class="projects">
|
||||
<div class="zig-zag">
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="title">Progetti</div>
|
||||
|
||||
<div class="project-list">
|
||||
<a href="#">
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
<div class="title">Gitea</div>
|
||||
<div class="description">
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||
accusantium labore explicabo quaerat incidunt.
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
<div class="title">Zulip</div>
|
||||
<div class="description">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellendus, veritatis.</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
<div class="title">Orario</div>
|
||||
<div class="description">
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||
accusantium labore explicabo quaerat incidunt.
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
<div class="title">Problemi</div>
|
||||
<div class="description">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non, hic libero beatae voluptatem incidunt.
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#">
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
<div class="title">Cluster "Steffè"</div>
|
||||
<div class="description">
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||
accusantium labore explicabo quaerat incidunt.
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="wanna-be-macchinista">
|
||||
<div class="zig-zag">
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
<pattern id="zig-zag-3" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||
<path fill="#888" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag-3)" x="0" y="0" width="1000" height="1"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="title">Vuoi diventare macchinista?</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
||||
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
@@ -0,0 +1,316 @@
|
||||
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
|
||||
|
||||
interface Grid<T> extends Iterable<[number, number, T]> {
|
||||
has(point: [number, number]): boolean
|
||||
get(point: [number, number]): T
|
||||
set(point: [number, number], value: T): void
|
||||
}
|
||||
|
||||
function createGrid<T>(): Grid<T> {
|
||||
const cells: Record<string, T> = {}
|
||||
|
||||
return {
|
||||
has([x, y]) {
|
||||
return cells[`${x},${y}`] !== undefined
|
||||
},
|
||||
get([x, y]) {
|
||||
return cells[`${x},${y}`]
|
||||
},
|
||||
set([x, y], value) {
|
||||
cells[`${x},${y}`] = value
|
||||
},
|
||||
*[Symbol.iterator]() {
|
||||
for (const [coord, value] of Object.entries(cells)) {
|
||||
const [x, y] = coord.split(',').map(s => parseInt(s))
|
||||
yield [x, y, value]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
|
||||
|
||||
type WireDirection = 'down' | 'down-left' | 'down-right'
|
||||
|
||||
type WireSteps = { position: Point2; direction: WireCell }[]
|
||||
|
||||
type Point2 = [number, number]
|
||||
|
||||
type State = {
|
||||
grid: Grid<WireCell>
|
||||
queuedWire: {
|
||||
index: number
|
||||
steps: WireSteps
|
||||
} | null
|
||||
badTries: 0
|
||||
}
|
||||
|
||||
type Renderer = {
|
||||
timer: number
|
||||
}
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
|
||||
const RENDERER_FPS = 30
|
||||
|
||||
function setup() {
|
||||
console.log('Setting up circuits art...')
|
||||
|
||||
$canvas.width = $canvas.clientWidth
|
||||
$canvas.height = $canvas.clientHeight
|
||||
|
||||
const g = $canvas.getContext('2d')!
|
||||
const state: State = {
|
||||
grid: createGrid(),
|
||||
queuedWire: null,
|
||||
badTries: 0,
|
||||
}
|
||||
|
||||
const startTime = new Date()
|
||||
|
||||
const handle = setInterval(() => {
|
||||
const time = new Date().getTime() - startTime.getTime()
|
||||
|
||||
update(state, g.canvas.width, g.canvas.height, time)
|
||||
render(g, state, time)
|
||||
}, 1000 / RENDERER_FPS)
|
||||
|
||||
renderer = { timer: handle }
|
||||
}
|
||||
|
||||
function update(state: State, width: number, height: number, time: number) {
|
||||
const w = (width / CELL_SIZE) | 0
|
||||
// const h = (height / CELL_SIZE) | 0
|
||||
|
||||
if (state.badTries > 1000) {
|
||||
console.log('finished')
|
||||
clearInterval(renderer!.timer)
|
||||
}
|
||||
|
||||
if (!state.queuedWire) {
|
||||
const sx = randomInt(0, w)
|
||||
if (!state.grid.has([sx, 0])) {
|
||||
const steps = generateWire(state.grid, [sx, 0])
|
||||
if (steps.length < 7) {
|
||||
state.badTries++
|
||||
return
|
||||
}
|
||||
|
||||
state.queuedWire = {
|
||||
index: 0,
|
||||
steps,
|
||||
}
|
||||
|
||||
state.grid.set(state.queuedWire.steps[0].position, 'dot')
|
||||
return
|
||||
}
|
||||
|
||||
state.badTries++
|
||||
} else {
|
||||
const wire = state.queuedWire
|
||||
|
||||
const step = wire.steps[wire.index]
|
||||
state.grid.set(step.position, step.direction)
|
||||
|
||||
if (wire.index + 1 < wire.steps.length) {
|
||||
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
|
||||
}
|
||||
|
||||
wire.index++
|
||||
|
||||
if (wire.index >= wire.steps.length) {
|
||||
state.queuedWire = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CELL_SIZE = 27
|
||||
|
||||
const BACKGROUND_COLOR = '#ecffe3'
|
||||
const WIRE_COLOR = '#a6ce94'
|
||||
|
||||
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
|
||||
'down': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(0, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'down-left': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(-1, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'down-right': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(+1, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'dot': g => {
|
||||
g.fillStyle = BACKGROUND_COLOR
|
||||
g.beginPath()
|
||||
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||
g.fill()
|
||||
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||
g.stroke()
|
||||
},
|
||||
}
|
||||
|
||||
function render(g: CanvasRenderingContext2D, state: State, time: number) {
|
||||
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
|
||||
g.resetTransform()
|
||||
g.scale(CELL_SIZE, CELL_SIZE)
|
||||
g.lineWidth = 3 / CELL_SIZE
|
||||
|
||||
const w = (g.canvas.width / CELL_SIZE) | 0
|
||||
const h = (g.canvas.height / CELL_SIZE) | 0
|
||||
for (let y = 0; y <= h + 1; y++) {
|
||||
for (let x = 0; x <= w + 1; x++) {
|
||||
if (!state.grid.has([x, y])) continue
|
||||
|
||||
const cell = state.grid.get([x, y])
|
||||
|
||||
g.save()
|
||||
g.translate(x, y)
|
||||
|
||||
// g.fillStyle = '#f008'
|
||||
// g.beginPath()
|
||||
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||
// g.fill()
|
||||
|
||||
RENDER_CELL[cell](g)
|
||||
g.restore()
|
||||
}
|
||||
}
|
||||
|
||||
// if (state.queuedWire) {
|
||||
// for (const step of state.queuedWire.steps) {
|
||||
// const [x, y] = step.position
|
||||
|
||||
// g.fillStyle = '#00f8'
|
||||
// g.save()
|
||||
// g.translate(x, y)
|
||||
|
||||
// g.beginPath()
|
||||
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||
// g.fill()
|
||||
// g.restore()
|
||||
// }
|
||||
// }
|
||||
|
||||
// const [mx, my] = state.mouse
|
||||
// g.save()
|
||||
// g.fillStyle = '#0008'
|
||||
// g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
|
||||
// g.beginPath()
|
||||
// g.rect(0, 0, 1, 1)
|
||||
// g.fill()
|
||||
// g.restore()
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (renderer) {
|
||||
clearInterval(renderer.timer)
|
||||
}
|
||||
setup()
|
||||
})
|
||||
|
||||
function randomInt(from: number, to: number) {
|
||||
return Math.floor(Math.random() * (to - from + 1))
|
||||
}
|
||||
|
||||
function randomChoice<T>(choices: T[]): T {
|
||||
return choices[randomInt(0, choices.length - 1)]
|
||||
}
|
||||
|
||||
function randomWeightedChoice<T>(choices: [T, number][]) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3 + 4 + 5 + 6
|
||||
randomWeightedChoice([
|
||||
['a', 3],
|
||||
['b', 4],
|
||||
['c', 5],
|
||||
['d', 6],
|
||||
])
|
||||
|
||||
const DIR_TO_VEC: Record<WireDirection, Point2> = {
|
||||
['down']: [0, 1],
|
||||
['down-left']: [-1, 1],
|
||||
['down-right']: [+1, 1],
|
||||
}
|
||||
|
||||
type ShortCircuitBoolean = boolean | (() => boolean)
|
||||
|
||||
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
|
||||
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
|
||||
|
||||
/**
|
||||
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
|
||||
*/
|
||||
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
|
||||
['down']: ([x, y], grid) =>
|
||||
!grid.has([x, y + 1]) &&
|
||||
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
|
||||
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
|
||||
['down-left']: ([x, y], grid) => !grid.has([x - 1, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
|
||||
['down-right']: ([x, y], grid) =>
|
||||
!grid.has([x + 1, y + 1]) && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
|
||||
}
|
||||
|
||||
function pruneDirections(grid: Grid<WireCell>, position: Point2, directions: WireDirection[]): WireDirection[] {
|
||||
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
|
||||
}
|
||||
|
||||
function generateWire(grid: Grid<WireCell>, startingPoint: Point2): { position: Point2; direction: WireCell }[] {
|
||||
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
|
||||
let currentPosition = startingPoint
|
||||
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
|
||||
|
||||
const steps: { position: Point2; direction: WireCell }[] = []
|
||||
|
||||
for (let i = 0; i < segmentLength; i++) {
|
||||
const availableDirections = pruneDirections(grid, currentPosition, ['down', 'down-left', 'down-right'])
|
||||
if (availableDirections.length === 0) {
|
||||
break
|
||||
} else {
|
||||
const dir =
|
||||
availableDirections.includes(currentDirection) && Math.random() < 0.25
|
||||
? currentDirection
|
||||
: randomChoice(availableDirections)
|
||||
|
||||
if ((currentDirection === 'down-left' && dir === 'down-right') || (currentDirection === 'down-right' && dir === 'down-left')) {
|
||||
break
|
||||
}
|
||||
|
||||
const [x, y] = currentPosition
|
||||
const [dx, dy] = DIR_TO_VEC[dir]
|
||||
|
||||
steps.push({
|
||||
position: [x, y],
|
||||
direction: dir,
|
||||
})
|
||||
|
||||
currentPosition = [x + dx, y + dy]
|
||||
currentDirection = dir
|
||||
}
|
||||
}
|
||||
|
||||
const last = steps.at(-1)
|
||||
if (last) {
|
||||
last.direction = 'dot'
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 18px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-snap-type: y proximity;
|
||||
scroll-padding-top: 4rem;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
//
|
||||
// Typography
|
||||
//
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
p + p {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Controls
|
||||
//
|
||||
|
||||
button,
|
||||
.button,
|
||||
[role='button'] {
|
||||
appearance: none;
|
||||
|
||||
background: #fff;
|
||||
|
||||
border: 3px solid #222;
|
||||
border-radius: 6px;
|
||||
box-shadow: 4px 4px 0 0 #222;
|
||||
|
||||
transition: all 64ms linear;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 0 #222;
|
||||
}
|
||||
|
||||
padding: 0.25rem 1.5rem;
|
||||
|
||||
text-decoration: none;
|
||||
color: #222;
|
||||
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-weight: 600;
|
||||
|
||||
&.primary {
|
||||
background: #1e6733;
|
||||
color: #f4fef7;
|
||||
|
||||
&:hover {
|
||||
background: #2b8b47;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
|
||||
header {
|
||||
z-index: 10;
|
||||
|
||||
height: 6rem;
|
||||
border-bottom: 4px solid #222;
|
||||
background: #fff;
|
||||
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.logo {
|
||||
padding-left: 0.5rem;
|
||||
|
||||
img {
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
|
||||
padding: 0 1.5rem;
|
||||
|
||||
a {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
min-height: 6rem;
|
||||
border-top: 4px solid #222;
|
||||
background: #444;
|
||||
color: #fdfdfd;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-homepage {
|
||||
header .logo {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
section {
|
||||
position: relative;
|
||||
|
||||
min-height: calc(100vh - 6rem);
|
||||
|
||||
&:last-of-type {
|
||||
min-height: calc(100vh - 10rem);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 2rem;
|
||||
|
||||
scroll-snap-align: start;
|
||||
|
||||
& > .title {
|
||||
font-weight: 700;
|
||||
font-size: 60px;
|
||||
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
|
||||
padding-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.zig-zag {
|
||||
z-index: 5;
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
|
||||
&:first-child {
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
section.principal {
|
||||
z-index: -2;
|
||||
|
||||
min-height: calc(100vh - 2rem);
|
||||
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rem;
|
||||
|
||||
padding: 6rem 0;
|
||||
|
||||
background: #ecffe3;
|
||||
// circuit color
|
||||
// background: #a6ce94;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
position: relative;
|
||||
|
||||
.circuit-layer {
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding-top: 6rem;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 640px;
|
||||
position: relative;
|
||||
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
filter: drop-shadow(6px 6px 0 #222) drop-shadow(4px 0 0 #222) drop-shadow(0 4px 0 #222) drop-shadow(-4px 0 0 #222)
|
||||
drop-shadow(0 -4px 0 #222);
|
||||
}
|
||||
}
|
||||
|
||||
.whats-phc {
|
||||
background: #e4c5ff;
|
||||
|
||||
border: 4px solid #222;
|
||||
border-radius: 8px;
|
||||
box-shadow: 6px 6px 0 0 #222;
|
||||
|
||||
padding: 2rem;
|
||||
|
||||
max-width: 37rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
.title {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@extend .text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.news {
|
||||
background: #c2a8eb;
|
||||
|
||||
gap: 3rem;
|
||||
|
||||
padding-bottom: 6rem;
|
||||
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 3rem;
|
||||
|
||||
padding: 0 3rem;
|
||||
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.news {
|
||||
background: #fffbeb;
|
||||
|
||||
border: 3px solid #222;
|
||||
border-radius: 9px;
|
||||
box-shadow: 9px 9px 0 0 #222;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 22rem;
|
||||
max-height: 27rem;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #c67e14;
|
||||
border: 2px solid #222;
|
||||
|
||||
&:hover {
|
||||
background-color: #e69419;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: #c67e14;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline solid 2px;
|
||||
}
|
||||
}
|
||||
|
||||
& > .title {
|
||||
// border-bottom: 2px solid #222;
|
||||
|
||||
border-top-left-radius: 9px;
|
||||
border-top-right-radius: 9px;
|
||||
|
||||
padding: 1rem;
|
||||
background: #f8e8b1;
|
||||
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
}
|
||||
& > .abstract {
|
||||
flex-grow: 1;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
@extend .text;
|
||||
}
|
||||
& > .continue {
|
||||
padding: 1rem;
|
||||
|
||||
display: grid;
|
||||
align-items: end;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[role='button'] {
|
||||
padding: 0.5rem 2rem;
|
||||
|
||||
&.primary {
|
||||
// background: #824ed4;
|
||||
// color: #f0e6ff;
|
||||
background: #f8e8b1;
|
||||
color: #000d;
|
||||
|
||||
&:hover {
|
||||
// background: #8e5ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.projects {
|
||||
background: #f5f2cc;
|
||||
|
||||
padding-bottom: 6rem;
|
||||
|
||||
.project-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||
gap: 3rem;
|
||||
|
||||
padding: 0 6rem;
|
||||
|
||||
align-items: start;
|
||||
|
||||
.project {
|
||||
// background: #fcddff;
|
||||
// background: #ffa89c;
|
||||
background: #a2d4f3;
|
||||
color: #000e;
|
||||
|
||||
border: 3px solid #222;
|
||||
border-radius: 9px;
|
||||
box-shadow: 9px 9px 0 0 #222;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
|
||||
gap: 0.25rem 1rem;
|
||||
|
||||
.title {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.image {
|
||||
grid-row: span 2;
|
||||
// place-self: center;
|
||||
|
||||
.box {
|
||||
background: #0003;
|
||||
border: 3px solid #0006;
|
||||
border-radius: 6px;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
transition: all 128ms ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: translate(0, -8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.wanna-be-macchinista {
|
||||
background: #888;
|
||||
color: #fdfdfd;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Misc
|
||||
//
|
||||
|
||||
::-webkit-scrollbar-track:vertical {
|
||||
background-color: #f0f0f0;
|
||||
border-left: 2px solid #222;
|
||||
border-top: 2px solid #222;
|
||||
border-bottom: 2px solid #222;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:horizontal {
|
||||
background-color: #f0f0f0;
|
||||
border-top: 2px solid #222;
|
||||
border-left: 2px solid #222;
|
||||
border-right: 2px solid #222;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #1e6733;
|
||||
border: 2px solid #222;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #2b8b47;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: #f0f0f0;
|
||||
// border-left: 2px solid #222;
|
||||
// border-top: 2px solid #222;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
l := sl.New()
|
||||
|
||||
//
|
||||
// Setup the application
|
||||
//
|
||||
|
||||
// Config
|
||||
sl.ProvideFunc(l, config.Slot, config.Configure)
|
||||
|
||||
// Database
|
||||
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
||||
Users: []model.User{
|
||||
{
|
||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||
Username: "claire-doe",
|
||||
FullName: db.NewOption("Claire Doe"),
|
||||
Email: db.NewOption("claire.doe@example.org"),
|
||||
},
|
||||
{
|
||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||
Username: "john-smith",
|
||||
FullName: db.NewOption("John Smith"),
|
||||
Email: db.NewOption("john.smith@example.org"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Server
|
||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||
listautenti.MountApiRoutesHook,
|
||||
)
|
||||
|
||||
//
|
||||
// Start the application
|
||||
//
|
||||
|
||||
sl.MustInvoke(l, server.Slot)
|
||||
|
||||
r, w := io.Pipe()
|
||||
|
||||
cfg := sl.MustUse(l, config.Slot)
|
||||
|
||||
cmd := exec.Command(cfg.NpmCommand, "run", "dev")
|
||||
cmd.Stdout = w
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
authService := auth.NewDefaultService(config.AuthServiceHost)
|
||||
|
||||
listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
h := &handler.DefaultHandler{
|
||||
AuthService: authService,
|
||||
Renderer: templates.NewRenderer(
|
||||
"./_views/",
|
||||
"./_views/base.html",
|
||||
"./_views/partials/*.html",
|
||||
),
|
||||
NewsArticlesRegistry: articles.NewRegistry("./_content/news"),
|
||||
GuideArticlesRegistry: articles.NewRegistry("./_content/guide"),
|
||||
Storia: &storia.JsonFileStoria{
|
||||
Path: "./_content/storia.yaml",
|
||||
},
|
||||
ListaUtenti: listaUtentiService,
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(h)
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
if err := app.Listen(config.Host); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||
)
|
||||
|
||||
func main() {
|
||||
l := sl.New()
|
||||
|
||||
//
|
||||
// Setup the application
|
||||
//
|
||||
|
||||
// Config
|
||||
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
||||
|
||||
// Database
|
||||
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
||||
Users: []model.User{
|
||||
{
|
||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||
Username: "claire-doe",
|
||||
FullName: db.NewOption("Claire Doe"),
|
||||
Email: db.NewOption("claire.doe@example.org"),
|
||||
},
|
||||
{
|
||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||
Username: "john-smith",
|
||||
FullName: db.NewOption("John Smith"),
|
||||
Email: db.NewOption("john.smith@example.org"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Server
|
||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||
listautenti.MountApiRoutesHook,
|
||||
)
|
||||
|
||||
//
|
||||
// Start the application
|
||||
//
|
||||
|
||||
err := sl.Invoke(l, server.Slot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var Mode string
|
||||
var Host string
|
||||
|
||||
var GitUrl string
|
||||
var ChatUrl string
|
||||
var Email string
|
||||
|
||||
var BaseUrl string
|
||||
|
||||
var UserPagesBaseUrl string
|
||||
|
||||
var AuthServiceHost string
|
||||
|
||||
// ListaUtenti è un file json da cui leggere la lista degli utenti oppure ":auth:" per utilizzare il servizio di autenticazione
|
||||
var ListaUtenti string
|
||||
|
||||
func loadEnv(target *string, name, defaultValue string) {
|
||||
value := os.Getenv(name)
|
||||
if len(strings.TrimSpace(value)) == 0 {
|
||||
*target = defaultValue
|
||||
} else {
|
||||
*target = value
|
||||
}
|
||||
|
||||
log.Printf("%s = %v", name, *target)
|
||||
}
|
||||
|
||||
func Load() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
||||
// Production
|
||||
loadEnv(&Mode, "MODE", "production")
|
||||
loadEnv(&Host, "HOST", ":8080")
|
||||
loadEnv(&BaseUrl, "BASE_URL", "localhost:8080")
|
||||
|
||||
// Services
|
||||
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
|
||||
loadEnv(&ChatUrl, "CHAT_URL", "https://chat.example.org")
|
||||
loadEnv(&Email, "EMAIL", "mail@example.org")
|
||||
|
||||
// Poisson
|
||||
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
|
||||
|
||||
// Auth
|
||||
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
|
||||
|
||||
// Altro
|
||||
loadEnv(&ListaUtenti, "LISTA_UTENTI", ":auth:")
|
||||
}
|
||||
|
||||
func Object() util.Map {
|
||||
return util.Map{
|
||||
"Mode": Mode,
|
||||
"Host": Host,
|
||||
|
||||
"GitUrl": GitUrl,
|
||||
"ChatUrl": ChatUrl,
|
||||
"Email": Email,
|
||||
|
||||
"BaseUrl": BaseUrl,
|
||||
|
||||
"UserPagesBaseUrl": UserPagesBaseUrl,
|
||||
|
||||
"AuthServiceHost": AuthServiceHost,
|
||||
|
||||
"ListaUtenti": ListaUtenti,
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,28 @@
|
||||
module git.phc.dm.unipi.it/phc/website
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.9.4
|
||||
github.com/gofiber/fiber/v2 v2.34.0
|
||||
github.com/gofiber/redirect/v2 v2.1.23
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f
|
||||
github.com/yuin/goldmark v1.4.4
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/gorilla/feeds v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/alecthomas/repr v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.44.0 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
github.com/valyala/fasthttp v1.46.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
)
|
||||
|
||||
@@ -1,78 +1,93 @@
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
|
||||
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
|
||||
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
|
||||
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
|
||||
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/fasthttp v1.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
|
||||
github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/rss"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
}
|
||||
|
||||
//
|
||||
// Typed context
|
||||
//
|
||||
|
||||
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
|
||||
const UserKey ContextKey[*model.User] = "user"
|
||||
|
||||
func (ctx Context) getUser() *model.User {
|
||||
return GetContextValue(ctx, UserKey)
|
||||
}
|
||||
|
||||
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
|
||||
type DefaultHandler struct {
|
||||
AuthService auth.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
ListaUtenti lista_utenti.Service
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
|
||||
return h.Renderer.Render(w, view, util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
|
||||
utenti, err := h.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
|
||||
utenti, err := h.ListaUtenti.GetUtenti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
|
||||
storia, err := h.Storia.GetStoria()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "storia.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Storia": storia,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Query": query,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
|
||||
session, err := h.AuthService.Login(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUser(token string) *model.User {
|
||||
user, _ := auth.UserForSession(h.AuthService, token)
|
||||
return user
|
||||
}
|
||||
|
||||
var ErrNoUser = fmt.Errorf(`user not logged in`)
|
||||
|
||||
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return nil, ErrNoUser
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "profilo.html", util.Map{
|
||||
"User": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
|
||||
registry, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
|
||||
|
||||
return newsFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
|
||||
registry, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
|
||||
|
||||
return guideFeed.WriteRss(w)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// This package provides a small framework to work with SQL databases using
|
||||
// generics. The most important part for now is the "Ref[T]" type that lets
|
||||
// us pass around typed IDs to table rows in a type safe manner.
|
||||
//
|
||||
// For now this library uses both generics for keeping the interface type safe
|
||||
// and reflection to extract automatically some metedata from structs that
|
||||
// represent tables.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Option è un valore che nel database può potenzialmente essere "NULL"
|
||||
//
|
||||
// NOTE/TODO: Magari si può fare una cosa fatta bene e nascondere
|
||||
// l'implementazione al Go, ad esempio Option[string] può essere NULL o testo
|
||||
// nel db mentre Option[time.Time] può essere codificato in json come
|
||||
//
|
||||
// "{ present: false }"
|
||||
//
|
||||
// oppure
|
||||
//
|
||||
// "{ present: true, value: '2023/04/...' }"
|
||||
//
|
||||
// in modo da semplificare leggermente la processazione in Go. Altrimenti si
|
||||
// può separare in Option[T] e Nullable[T].
|
||||
type Option[T any] struct {
|
||||
present bool
|
||||
value T
|
||||
}
|
||||
|
||||
func (o Option[T]) MarshalJSON() ([]byte, error) {
|
||||
if !o.present {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
return json.Marshal(o.value)
|
||||
}
|
||||
|
||||
func (o Option[T]) UnmarshalJSON(v []byte) error {
|
||||
if slices.Equal(v, []byte("null")) {
|
||||
var zero T
|
||||
o.present = false
|
||||
o.value = zero
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(v, &o.value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewOption[T any](value T) Option[T] {
|
||||
return Option[T]{true, value}
|
||||
}
|
||||
|
||||
func NewEmptyOption[T any]() Option[T] {
|
||||
return Option[T]{present: false}
|
||||
}
|
||||
|
||||
func (Option[T]) SqlType() string {
|
||||
return "BLOB"
|
||||
}
|
||||
|
||||
// func (v *Option[T]) Scan(a any) error {
|
||||
// data, ok := a.([]byte)
|
||||
// if !ok {
|
||||
// return fmt.Errorf(`scan expected []byte`)
|
||||
// }
|
||||
|
||||
// m := map[string]any{}
|
||||
// if err := json.Unmarshal(data, m); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// if present, _ := m["present"].(bool); present {
|
||||
|
||||
// m["value"]
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Ref è un id tipato da un'entità presente nel database. È il tipo fondamentale
|
||||
// esportato da questo package. Non dovrebbe essere troppo importante ma
|
||||
// internamente è una stringa, inoltre è consigliato che una struct
|
||||
// con _primary key_ abbia un campo di tipo Ref a se stessa.
|
||||
//
|
||||
// type User struct {
|
||||
// Id db.Ref[User] // meglio se unico e immutabile
|
||||
// ...
|
||||
// }
|
||||
type Ref[T any] string
|
||||
|
||||
func (Ref[T]) SqlType() string {
|
||||
return "TEXT"
|
||||
}
|
||||
|
||||
func (v *Ref[T]) Scan(a any) error {
|
||||
s, ok := a.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf(`scan expected string`)
|
||||
}
|
||||
|
||||
*v = Ref[T](s)
|
||||
return nil
|
||||
}
|
||||
|
||||
type sqlValue interface {
|
||||
SqlType() string
|
||||
sql.Scanner
|
||||
}
|
||||
|
||||
type Table[T any] struct {
|
||||
Name string
|
||||
PrimaryKey func(value *T, pk ...string) Ref[T]
|
||||
Columns [][2]string
|
||||
}
|
||||
|
||||
var commonTypes = map[string]string{
|
||||
"string": "TEXT",
|
||||
"int": "INTEGER",
|
||||
"int32": "INTEGER",
|
||||
"int64": "INTEGER",
|
||||
"float32": "REAL",
|
||||
"float64": "REAL",
|
||||
}
|
||||
|
||||
func toSqlType(typ reflect.Type) string {
|
||||
st, ok := reflect.New(typ).Interface().(sqlValue)
|
||||
if ok {
|
||||
return st.SqlType()
|
||||
}
|
||||
|
||||
return commonTypes[typ.Name()]
|
||||
}
|
||||
|
||||
func AutoTable[T any](name string) Table[T] {
|
||||
columns := [][2]string{}
|
||||
pkIndex := -1
|
||||
|
||||
var zero T
|
||||
typ := reflect.TypeOf(zero)
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
fieldTyp := typ.Field(i)
|
||||
isPK, _, typ, col := fieldInfo(fieldTyp)
|
||||
if isPK {
|
||||
pkIndex = i
|
||||
}
|
||||
columns = append(columns, [2]string{col, toSqlType(typ)})
|
||||
}
|
||||
|
||||
if pkIndex == -1 {
|
||||
panic(fmt.Sprintf("struct %T has no primary key field", zero))
|
||||
}
|
||||
|
||||
// debug logging
|
||||
// log.Printf("[auto table] struct %T has primary key %q", zero, typ.Field(pkIndex).Name)
|
||||
|
||||
return Table[T]{
|
||||
Name: name,
|
||||
PrimaryKey: func(value *T, pk ...string) Ref[T] {
|
||||
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||
// they are the same type so this should be safe
|
||||
ptr := reflect.ValueOf(value).Elem().Field(pkIndex).Addr().UnsafePointer()
|
||||
pkPtr := (*string)(ptr)
|
||||
|
||||
if len(pk) > 0 {
|
||||
*pkPtr = pk[0]
|
||||
}
|
||||
|
||||
return Ref[T](*pkPtr)
|
||||
},
|
||||
Columns: columns,
|
||||
}
|
||||
}
|
||||
|
||||
func (table Table[T]) CreateIfNotExists(dbConn *sql.DB) error {
|
||||
columns := make([]string, len(table.Columns))
|
||||
|
||||
for i, col := range table.Columns {
|
||||
columns[i] = fmt.Sprintf("%s %s", col[0], col[1])
|
||||
}
|
||||
|
||||
stmt := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s(%s)`,
|
||||
table.Name,
|
||||
strings.Join(columns, ", "),
|
||||
)
|
||||
|
||||
_, err := dbConn.Exec(stmt)
|
||||
return err
|
||||
}
|
||||
|
||||
func Create[T any](dbConn *sql.DB, table Table[T], value T) (Ref[T], error) {
|
||||
id := randomHex(15)
|
||||
|
||||
table.PrimaryKey(&value, id)
|
||||
|
||||
columns, values := structDatabaseColumnValues(&value)
|
||||
|
||||
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||
table.Name,
|
||||
strings.Join(columns, ", "),
|
||||
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||
)
|
||||
|
||||
if _, err := dbConn.Exec(stmt, values...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Ref[T](id), nil
|
||||
}
|
||||
|
||||
func Insert[T any](dbConn *sql.DB, table Table[T], value T) error {
|
||||
columns, values := structDatabaseColumnValues(&value)
|
||||
|
||||
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||
table.Name,
|
||||
strings.Join(columns, ", "),
|
||||
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||
)
|
||||
|
||||
_, err := dbConn.Exec(stmt, values...)
|
||||
return err
|
||||
}
|
||||
|
||||
func Read[T any](dbConn *sql.DB, table Table[T], id Ref[T]) (T, error) {
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func ReadAll[T any](dbConn *sql.DB, table Table[T]) ([]T, error) {
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func Update[T any](dbConn *sql.DB, table Table[T], id Ref[T], value T) error {
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func Delete[T any](dbConn *sql.DB, table Table[T], id Ref[T]) error {
|
||||
panic("todo")
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"testing"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
type mockDB struct {
|
||||
*testing.T
|
||||
|
||||
LastQuery string
|
||||
}
|
||||
|
||||
func (db *mockDB) Prepare(query string) (driver.Stmt, error) { return nil, nil }
|
||||
func (db *mockDB) Close() error { return nil }
|
||||
func (db *mockDB) Begin() (driver.Tx, error) { return nil, nil }
|
||||
|
||||
func (db *mockDB) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||
db.LastQuery = query
|
||||
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *mockDB) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||
db.LastQuery = query
|
||||
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *mockDB) Open(name string) (driver.Conn, error) {
|
||||
return &mockDB{T: db.T}, nil
|
||||
}
|
||||
|
||||
func (db *mockDB) Connect(context.Context) (driver.Conn, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *mockDB) Driver() driver.Driver {
|
||||
return db
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
func TestDb1(t *testing.T) {
|
||||
type Product struct {
|
||||
Id db.Ref[Product] `db:"id*"`
|
||||
Title string `db:"title"`
|
||||
Quantity int `db:"quantity"`
|
||||
}
|
||||
|
||||
products := db.AutoTable[Product]("products")
|
||||
|
||||
mock := &mockDB{T: t}
|
||||
conn := sql.OpenDB(mock)
|
||||
|
||||
err := products.CreateIfNotExists(conn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, mock.LastQuery,
|
||||
`CREATE TABLE IF NOT EXISTS products(id TEXT, title TEXT, quantity INTEGER)`,
|
||||
)
|
||||
|
||||
ref1, err := db.Create(conn, products, Product{Title: "Foo", Quantity: 27})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Assert(t, string(ref1) != "")
|
||||
assert.Equal(t, mock.LastQuery,
|
||||
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||
)
|
||||
|
||||
if err := db.Insert(conn, products, Product{Id: "123", Title: "Bar", Quantity: 42}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, mock.LastQuery,
|
||||
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package db
|
||||
@@ -0,0 +1,118 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func randomHex(len int) string {
|
||||
buff := make([]byte, len/2+1)
|
||||
rand.Read(buff)
|
||||
str := hex.EncodeToString(buff)
|
||||
return str[:len]
|
||||
}
|
||||
|
||||
func fieldInfo(fieldTyp reflect.StructField) (bool, string, reflect.Type, string) {
|
||||
fieldName := fieldTyp.Name
|
||||
columnName, ok := fieldTyp.Tag.Lookup("db")
|
||||
if !ok {
|
||||
columnName = strings.ToLower(fieldName)
|
||||
}
|
||||
if ok && columnName[len(columnName)-1] == '*' {
|
||||
return true, fieldName, fieldTyp.Type, columnName[:len(columnName)-1]
|
||||
}
|
||||
|
||||
return false, fieldName, fieldTyp.Type, columnName
|
||||
}
|
||||
|
||||
func fieldPrimaryKey(fieldTyp reflect.StructField) bool {
|
||||
isPK, _, _, _ := fieldInfo(fieldTyp)
|
||||
return isPK
|
||||
}
|
||||
|
||||
// structDatabaseColumnValues takes a pointer to a struct and returns a pair
|
||||
// of slices, one with the column names and the other with pointers to all
|
||||
// its exported fields.
|
||||
//
|
||||
// The column name can be changed using the "db" tag attribute on that field
|
||||
func structDatabaseColumnValues(s any) ([]string, []any) {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
|
||||
numFields := v.NumField()
|
||||
|
||||
names := []string{}
|
||||
values := []any{}
|
||||
|
||||
for i := 0; i < numFields; i++ {
|
||||
fieldTyp := v.Type().Field(i)
|
||||
fieldVal := v.Field(i)
|
||||
|
||||
if fieldTyp.IsExported() {
|
||||
key := strings.ToLower(fieldTyp.Name)
|
||||
|
||||
if name, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||
key = name
|
||||
|
||||
// primary key has a "*" at the end, exclude from column name
|
||||
if key[len(key)-1] == '*' {
|
||||
key = key[:len(key)-1]
|
||||
}
|
||||
}
|
||||
|
||||
names = append(names, key)
|
||||
values = append(values, fieldVal.Addr().Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return names, values
|
||||
}
|
||||
|
||||
func structPrimaryKeyPtr(s any) (*string, bool) {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldTyp := v.Type().Field(i)
|
||||
fieldVal := v.Field(i)
|
||||
|
||||
if fieldTyp.IsExported() {
|
||||
if fieldPrimaryKey(fieldTyp) {
|
||||
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||
// they are the same type so this should be safe
|
||||
ptr := fieldVal.Addr().UnsafePointer()
|
||||
return (*string)(ptr), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// structDatabasePrimaryKeyColumn takes a pointer to a struct and returns the
|
||||
// name of the field with the primary key
|
||||
func structDatabasePrimaryKeyColumn(s any) (field, column string, ok bool) {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldTyp := v.Type().Field(i)
|
||||
if fieldTyp.IsExported() {
|
||||
key := fieldTyp.Name
|
||||
if value, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||
if value[len(value)-1] == '*' {
|
||||
return key, value[:len(value)-1], true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func repeatSlice[T any](value T, count int) []T {
|
||||
s := make([]T, count)
|
||||
for i := 0; i < count; i++ {
|
||||
s[i] = value
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// The [sl] package has two main concepts, the [ServiceLocator] itself is the
|
||||
// main object that one should pass around through the application. A
|
||||
// [ServiceLocator] has a list of slots that can be filled with
|
||||
// [ProvideFunc] and [Provide] and retrieved with [Use]. Slots should
|
||||
// be unique by type and they can only be created with the [NewSlot] function.
|
||||
//
|
||||
// The usual way to use this module is to make slots for Go interfaces and
|
||||
// then pass implementations using the [Provide] and
|
||||
// [ProvideFunc] functions.
|
||||
//
|
||||
// Services can be of various types:
|
||||
// - a service with no dependencies can be directly injected inside a
|
||||
// ServiceLocator using [Provide].
|
||||
// - a service with dependencies on other service should use
|
||||
// [ProvideFunc]. This lets the service configure itself when needed
|
||||
// and makes the developer not think about correctly ordering the
|
||||
// recursive configuration of its dependencies.
|
||||
// - a service can also be private, in this case the slot for a service
|
||||
// should be a private field in the service package. This kind of
|
||||
// services should also provide a way to inject them into a
|
||||
// ServiceLocator.
|
||||
// - a package can also just provide a slot with some value. This is useful
|
||||
// for using the ServiceLocator to easily pass around values, effectively
|
||||
// threating slots just as dynamically scoped variables.
|
||||
package sl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Logger is the debug logger
|
||||
//
|
||||
// TODO: in the future this will be disabled and discard by default.
|
||||
//
|
||||
// As this is the service locator module it was meaning less to pass this
|
||||
// through the ServiceLocator itself (without making the whole module more
|
||||
// complex)
|
||||
var Logger *log.Logger = log.New(os.Stderr, "[service locator] ", log.Lmsgprefix)
|
||||
|
||||
// slot is just a "typed" unique "symbol".
|
||||
//
|
||||
// This must be defined like so and not for example "struct{ typeName string }"
|
||||
// because we might want to have more slots for the same type.
|
||||
type slot[T any] *struct{}
|
||||
|
||||
type Hook[T any] func(*ServiceLocator, T) error
|
||||
|
||||
// hook is just a typed unique symbol
|
||||
type hook[T any] *struct{}
|
||||
|
||||
// NewSlot is the only way to create instances of the slot type. Each instance
|
||||
// is unique.
|
||||
//
|
||||
// This then lets you attach a service instance of type "T" to a
|
||||
// [ServiceLocator] object.
|
||||
func NewSlot[T any]() slot[T] {
|
||||
return slot[T](new(struct{}))
|
||||
}
|
||||
|
||||
// NewHook is the only way to create instances of the hook type. Each instance
|
||||
// is unique.
|
||||
//
|
||||
// This lets you have a service dispatch an hook
|
||||
func NewHook[T any]() hook[T] {
|
||||
return hook[T](new(struct{}))
|
||||
}
|
||||
|
||||
// slotEntry represents a service that can lazily configured
|
||||
// (using "configureFunc"). Once configured the instance is kept in the "value"
|
||||
// field and "created" will always be "true". The field "typeName" just for
|
||||
// debugging purposes.
|
||||
type slotEntry struct {
|
||||
// typeName is just used for debugging purposes
|
||||
typeName string
|
||||
|
||||
// configureFunc is used by lazily provided slot values to tell how to
|
||||
// configure them self when required
|
||||
configureFunc func(*ServiceLocator) (any, error)
|
||||
|
||||
// configured tells if this slot is already configured
|
||||
configured bool
|
||||
|
||||
// value for this slot
|
||||
value any
|
||||
}
|
||||
|
||||
// checkConfigured tries to call configure on this slot entry if not already configured
|
||||
func (s *slotEntry) checkConfigured(l *ServiceLocator) error {
|
||||
if !s.configured {
|
||||
v, err := s.configureFunc(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Logger.Printf(`[slot: %s] configured service of type %T`, s.typeName, v)
|
||||
|
||||
s.configured = true
|
||||
s.value = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type hookEntry struct {
|
||||
// typeName is just used for debugging purposes
|
||||
typeName string
|
||||
|
||||
// listeners is a list of functions to call when this hook is called
|
||||
listeners []func(*ServiceLocator, any) error
|
||||
}
|
||||
|
||||
// ServiceLocator is the main context passed around to retrive service
|
||||
// instances, the interface uses generics so to inject and retrive service
|
||||
// instances you should use the functions [Provide], [ProvideFunc] and [Use].
|
||||
// This is essentially a dictionary indexed by slots that are them self just
|
||||
// typed unique symbols
|
||||
type ServiceLocator struct {
|
||||
providers map[any]*slotEntry
|
||||
hooks map[any]*hookEntry
|
||||
}
|
||||
|
||||
// New creates a new [ServiceLocator] context to pass around in the application.
|
||||
func New() *ServiceLocator {
|
||||
return &ServiceLocator{
|
||||
providers: map[any]*slotEntry{},
|
||||
hooks: map[any]*hookEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
// Provide will inject a concrete instance inside the ServiceLocator "l" for
|
||||
// the given "slotKey". This should be used for injecting "static" services, for
|
||||
// instances whose construction depend on other services you should use the
|
||||
// [ProvideFunc] function.
|
||||
//
|
||||
// This is generic over "T" to check that instances for the given slot type
|
||||
// check as "T" can also be an interface.
|
||||
func Provide[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
||||
typeName := getTypeName[T]()
|
||||
|
||||
Logger.Printf(`[slot: %s] provided value of type %T`, typeName, value)
|
||||
|
||||
l.providers[slotKey] = &slotEntry{
|
||||
typeName: typeName,
|
||||
configured: true,
|
||||
value: value,
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// ProvideFunc will inject an instance inside the given ServiceLocator
|
||||
// and "slotKey" that is created only when requested with a call to the
|
||||
// [Use] function.
|
||||
//
|
||||
// This is generic over "T" to check that instances for the given slot type
|
||||
// check as "T" can also be an interface.
|
||||
func ProvideFunc[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
||||
typeName := getTypeName[T]()
|
||||
Logger.Printf(`[slot: %s] inject lazy provider`, typeName)
|
||||
|
||||
l.providers[slotKey] = &slotEntry{
|
||||
typeName: typeName,
|
||||
configureFunc: func(l *ServiceLocator) (any, error) { return createFunc(l) },
|
||||
configured: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Use retrieves the value of type T associated with the given slot key from
|
||||
// the provided ServiceLocator instance.
|
||||
//
|
||||
// If the ServiceLocator does not have a value for the slot key, or if the
|
||||
// value wasn't correctly configured (in the case of a lazy slot), an error
|
||||
// is returned.
|
||||
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
||||
var zero T
|
||||
|
||||
slot, ok := l.providers[slotKey]
|
||||
if !ok {
|
||||
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||||
}
|
||||
|
||||
err := slot.checkConfigured(l)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
v := slot.value.(T)
|
||||
|
||||
Logger.Printf(`[slot: %s] using slot with value of type %T`, getTypeName[T](), v)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func MustUse[T any](l *ServiceLocator, slotKey slot[T]) T {
|
||||
v, err := Use(l, slotKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func Invoke[T any](l *ServiceLocator, slotKey slot[T]) error {
|
||||
slot, ok := l.providers[slotKey]
|
||||
if !ok {
|
||||
return fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||||
}
|
||||
|
||||
err := slot.checkConfigured(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := slot.value.(T)
|
||||
|
||||
Logger.Printf(`[slot: %s] invoked slot with value of type %T`, getTypeName[T](), v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func MustInvoke[T any](l *ServiceLocator, slotKey slot[T]) {
|
||||
if err := Invoke(l, slotKey); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ProvideHook[T any](l *ServiceLocator, hookKey hook[T], listeners ...Hook[T]) {
|
||||
typeName := getTypeName[T]()
|
||||
Logger.Printf(`[hook: %s] injecting hooks`, typeName)
|
||||
|
||||
// cast type safe listeners to internal untyped version to put inside the hook map
|
||||
anyListeners := make([]func(*ServiceLocator, any) error, len(listeners))
|
||||
for i, l := range listeners {
|
||||
ll := l
|
||||
anyListeners[i] = func(l *ServiceLocator, a any) error {
|
||||
t, ok := a.(T)
|
||||
if !ok {
|
||||
panic(`illegal state`)
|
||||
}
|
||||
|
||||
return ll(l, t)
|
||||
}
|
||||
}
|
||||
|
||||
l.hooks[hookKey] = &hookEntry{
|
||||
typeName: typeName,
|
||||
listeners: anyListeners,
|
||||
}
|
||||
}
|
||||
|
||||
func UseHook[T any](l *ServiceLocator, hookKey hook[T], value T) error {
|
||||
hookEntry, ok := l.hooks[hookKey]
|
||||
if !ok {
|
||||
return fmt.Errorf(`no injected hooks for hook of type %s`, hookEntry.typeName)
|
||||
}
|
||||
|
||||
Logger.Printf(`[hook: %s] calling hook with value of type %T`, hookEntry.typeName, value)
|
||||
for _, hookFunc := range hookEntry.listeners {
|
||||
if err := hookFunc(l, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MustUseHook[T any](l *ServiceLocator, hookKey hook[T], value T) {
|
||||
if err := UseHook(l, hookKey, value); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// getTypeName is a trick to get the name of a type (even if it is an
|
||||
// interface type)
|
||||
func getTypeName[T any]() string {
|
||||
var zero T
|
||||
return fmt.Sprintf(`%T`, &zero)[1:]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// CompactIndentedLines removes all indentation from a string and also removes all newlines, usefull in tests
|
||||
func CompactIndentedLines(s string) string {
|
||||
return regexp.MustCompile(`(?m)\n\s+`).ReplaceAllString(s, "")
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type authListaUtenti struct {
|
||||
AuthService auth.Service
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
authUsers, err := a.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.ListUser, 0, len(authUsers))
|
||||
for _, u := range authUsers {
|
||||
users = append(users, &model.ListUser{
|
||||
Uid: u.Username,
|
||||
Nome: u.Name,
|
||||
Cognome: u.Surname,
|
||||
Tags: []string{},
|
||||
})
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, a.Macchinisti)
|
||||
mergeRappresentanti(users, a.Rappresentanti)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type jsonListaUtenti struct {
|
||||
Path string
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newJsonListaUtenti(path string) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
var users []*model.ListUser
|
||||
|
||||
f, err := os.Open(j.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, j.Macchinisti)
|
||||
mergeRappresentanti(users, j.Rappresentanti)
|
||||
|
||||
log.Printf("Caricata lista di %d utenti", len(users))
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetUtenti() ([]*model.ListUser, error)
|
||||
}
|
||||
|
||||
// New crea un nuovo servizio che lista gli utenti utilizzando AuthService se config è ":auth:" oppure un file json
|
||||
func New(authService auth.Service, config string) (Service, error) {
|
||||
if config == ":auth:" {
|
||||
return newAuthListaUtenti(authService)
|
||||
}
|
||||
|
||||
return newJsonListaUtenti(config)
|
||||
}
|
||||
|
||||
func loadMacchinisti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/macchinisti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macchinisti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&macchinisti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(macchinisti...), nil
|
||||
}
|
||||
|
||||
func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := macchinisti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "macchinista")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRappresentanti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/rappresentanti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(rappresentanti...), nil
|
||||
}
|
||||
|
||||
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := rappresentanti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "rappresentante")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package model
|
||||
|
||||
type ListUser struct {
|
||||
Uid string `json:"uid"`
|
||||
Nome string `json:"nome"`
|
||||
Cognome string `json:"cognome"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user returned from AuthenticatorService
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
// FullName is a separate field from Name and Surname because for example
|
||||
// ldap stores them all as separate fields.
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// WithDefaultFullName is a utility that returns a copy of the given user with the full name set to the concatenation of the name and surname of the user.
|
||||
func (u User) WithDefaultFullName() User {
|
||||
return User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
|
||||
FullName: u.Username + " " + u.Surname,
|
||||
}
|
||||
}
|
||||
|
||||
// Session represents a session returned from AuthenticatorService
|
||||
type Session struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "website-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend for the PHC website",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/open-sans": "^4.5.14",
|
||||
"@fontsource/source-code-pro": "^4.5.14",
|
||||
"@fontsource/source-sans-pro": "^4.5.11",
|
||||
"astro": "^2.3.1",
|
||||
"sass": "^1.62.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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,54 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mode string
|
||||
Host string
|
||||
|
||||
NpmCommand string
|
||||
}
|
||||
|
||||
var Slot = sl.NewSlot[Config]()
|
||||
|
||||
func setFromEnvOrDefault(target *string, m map[string]string, key string, defaultValue string) {
|
||||
v, ok := m[key]
|
||||
if ok {
|
||||
*target = v
|
||||
} else {
|
||||
*target = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func Configure(l *sl.ServiceLocator) (Config, error) {
|
||||
env, err := godotenv.Read(".env")
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
setFromEnvOrDefault(&cfg.Mode, env, "MODE", "production")
|
||||
setFromEnvOrDefault(&cfg.Host, env, "HOST", ":4000")
|
||||
setFromEnvOrDefault(&cfg.NpmCommand, env, "NPM_COMMAND", "npm")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
var ExampleProductionConfig = Config{
|
||||
Mode: "production",
|
||||
Host: ":4000",
|
||||
|
||||
NpmCommand: "npm",
|
||||
}
|
||||
|
||||
var ExampleDevelopmentConfig = Config{
|
||||
Mode: "development",
|
||||
Host: ":4000",
|
||||
|
||||
NpmCommand: "npm",
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||
)
|
||||
|
||||
var Slot = sl.NewSlot[Database]()
|
||||
|
||||
// Database is the main database service interface
|
||||
type Database interface {
|
||||
// User
|
||||
|
||||
CreateUser(user model.User) (db.Ref[model.User], error)
|
||||
ReadUser(id db.Ref[model.User]) (model.User, error)
|
||||
ReadUsers() ([]model.User, error)
|
||||
UpdateUser(user model.User) error
|
||||
DeleteUser(id db.Ref[model.User]) error
|
||||
|
||||
// Accounts
|
||||
|
||||
CreateAccount(account model.Account) (db.Ref[model.Account], error)
|
||||
ReadAccount(id db.Ref[model.Account]) (model.Account, error)
|
||||
UpdateAccount(user model.Account) error
|
||||
DeleteAccount(id db.Ref[model.Account]) error
|
||||
|
||||
// Users & Accounts
|
||||
|
||||
ReadUserAccounts(user db.Ref[model.User]) ([]model.Account, error)
|
||||
}
|
||||
|
||||
// Memory is an in-memory implementation of [Database] that can be used for testing
|
||||
type Memory struct {
|
||||
Database // TODO: Per ora almeno così facciamo compilare il codice
|
||||
|
||||
Users []model.User
|
||||
}
|
||||
|
||||
func (m *Memory) CreateUser(user model.User) (db.Ref[model.User], error) {
|
||||
m.Users = append(m.Users, user)
|
||||
return db.Ref[model.User](user.Id), nil
|
||||
}
|
||||
|
||||
func (m *Memory) ReadUser(id db.Ref[model.User]) (model.User, error) {
|
||||
for _, u := range m.Users {
|
||||
if u.Id == id {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
|
||||
}
|
||||
|
||||
func (m *Memory) ReadUsers() ([]model.User, error) {
|
||||
return m.Users, nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdateUser(user model.User) error {
|
||||
for i, u := range m.Users {
|
||||
if u.Id == user.Id {
|
||||
m.Users[i] = user
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(`no user with id "%s"`, user.Id)
|
||||
}
|
||||
|
||||
func (m *Memory) DeleteUser(id db.Ref[model.User]) error {
|
||||
for i, u := range m.Users {
|
||||
if u.Id == id {
|
||||
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(`no user with id "%s"`, id)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||
)
|
||||
|
||||
var Users = db.AutoTable[model.User]("users")
|
||||
var Accounts = db.AutoTable[model.Account]("accounts")
|
||||
@@ -1,202 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/redirect/v2"
|
||||
)
|
||||
|
||||
func UserMiddleware(h handler.Service) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Cookies("session-token")
|
||||
user := h.HandleUser(token)
|
||||
c.Locals("user", user)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContext(ctx *fiber.Ctx) handler.Context {
|
||||
// the "_" here is required because Go cannot cast <nil> of type interface{} to *model.User, in other words <nil> of type *model.User and <nil> of type interface{} are different type. In this case the "_" returns a boolean that tells whether the cast succeeded or not, if it is false the user variable gets assigned its default zero value that is <nil> of type *model.User
|
||||
user, _ := ctx.Locals("user").(*model.User)
|
||||
|
||||
context := handler.Context{}
|
||||
handler.SetContextValue(context, handler.UserKey, user)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
func NewFiberServer(h handler.Service) *fiber.App {
|
||||
app := fiber.New()
|
||||
routes(h, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// routes defines routes under "/"
|
||||
func routes(h handler.Service, r fiber.Router) {
|
||||
//
|
||||
// Initial setup
|
||||
//
|
||||
|
||||
r.Use(logger.New())
|
||||
r.Use(recover.New())
|
||||
|
||||
// Remove trailing slash from URLs
|
||||
r.Use(redirect.New(redirect.Config{
|
||||
Rules: map[string]string{
|
||||
"/*/": "/$1",
|
||||
},
|
||||
}))
|
||||
|
||||
// Serve generated css and js files and the static "./_public" folder
|
||||
r.Static("/public/", "./_frontend/out")
|
||||
r.Static("/public/", "./_public")
|
||||
|
||||
// Process all request and add user to the request context if there is a session cookie
|
||||
r.Use(UserMiddleware(h))
|
||||
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
|
||||
r.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "home.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/link", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "link.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/login", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "login.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "utenti.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/storia", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStoriaPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti", func(c *fiber.Ctx) error {
|
||||
query := c.Query("q", "")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleQueryAppunti(c, query, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleAppuntiCondivisiPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleNewsPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleGuidePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleNewsFeedPage(c)
|
||||
})
|
||||
|
||||
r.Get("/guide/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleGuideFeedPage(c)
|
||||
})
|
||||
|
||||
r.Post("/login", func(c *fiber.Ctx) error {
|
||||
var loginForm struct {
|
||||
Provider string `form:"provider"`
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&loginForm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := h.HandleLogin(loginForm.Username, loginForm.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: session.Token,
|
||||
Expires: inThreeDays,
|
||||
})
|
||||
|
||||
return c.Redirect("/profilo")
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleProfilePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/logout", func(c *fiber.Ctx) error {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
})
|
||||
|
||||
return c.Redirect("/")
|
||||
})
|
||||
|
||||
r.Get("/news/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleNewsArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleGuideArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
routesApi(h, r.Group("/api"))
|
||||
}
|
||||
|
||||
// routesApi defines routes under "/api"
|
||||
func routesApi(h handler.Service, r fiber.Router) {
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
utenti, err := h.HandleListaUtenti()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(utenti)
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
user, err := h.HandleRequiredUser(CreateContext(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package listautenti
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||
)
|
||||
|
||||
func MountApiRoutesHook(l *sl.ServiceLocator, api fiber.Router) error {
|
||||
db, err := sl.Use(l, database.Slot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.Get("/lista-utenti", func(c *fiber.Ctx) error {
|
||||
users, err := db.ReadUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Pre-process data to strip server only fields...
|
||||
return c.JSON(users)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package listautenti_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/valyala/fasthttp/fasthttputil"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||
"git.phc.dm.unipi.it/phc/website/libs/util"
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||
)
|
||||
|
||||
func TestApiListaUtenti(t *testing.T) {
|
||||
|
||||
memDB := &database.Memory{
|
||||
Users: []model.User{
|
||||
{
|
||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||
Username: "claire-doe",
|
||||
FullName: db.NewOption("Claire Doe"),
|
||||
Email: db.NewOption("claire.doe@example.org"),
|
||||
},
|
||||
{
|
||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||
Username: "john-smith",
|
||||
FullName: db.NewOption("John Smith"),
|
||||
Email: db.NewOption("john.smith@example.org"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
l := sl.New()
|
||||
|
||||
// Config
|
||||
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
||||
|
||||
// Database
|
||||
sl.Provide[database.Database](l, database.Slot, memDB)
|
||||
|
||||
// Server
|
||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||
listautenti.MountApiRoutesHook,
|
||||
)
|
||||
|
||||
// Initialize server instance
|
||||
srv, err := sl.Use(l, server.Slot)
|
||||
assert.NilError(t, err)
|
||||
|
||||
//
|
||||
// Try doing the request
|
||||
//
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
ln := fasthttputil.NewInmemoryListener()
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
err := fasthttp.Serve(ln, srv.Router.Handler())
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to serve: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return ln.Dial()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
assert.NilError(t, err)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, string(body), util.CompactIndentedLines(`
|
||||
[
|
||||
{
|
||||
"Id":"e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||
"Username":"claire-doe",
|
||||
"FullName":"Claire Doe",
|
||||
"Email":"claire.doe@example.org"
|
||||
},
|
||||
{
|
||||
"Id":"9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||
"Username":"john-smith",
|
||||
"FullName":"John Smith",
|
||||
"Email":"john.smith@example.org"
|
||||
}
|
||||
]
|
||||
`))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package model
|
||||
|
||||
import "git.phc.dm.unipi.it/phc/website/libs/db"
|
||||
|
||||
type User struct {
|
||||
// Id è l'id unico di questo utente, non è modificabile una volta creato
|
||||
// l'utente.
|
||||
Id db.Ref[User] `db:"id"`
|
||||
|
||||
// Username è il nome leggibile di questo utente utilizzato anche per le
|
||||
// route per singolo utente, deve essere unico nel sito.
|
||||
//
|
||||
// NOTE: Quando un utente accede per la prima volta di default gli viene
|
||||
// chiesto se usare quello dell'account che sta usando o se cambiarlo
|
||||
// (in teoria non dovrebbe essere un problema poterlo modificare
|
||||
// successivamente).
|
||||
Username string `db:"username"`
|
||||
|
||||
// FullName da mostrare in giro per il sito
|
||||
FullName db.Option[string] `db:"full_name"`
|
||||
|
||||
// Email per eventuale contatto
|
||||
Email db.Option[string] `db:"email"`
|
||||
}
|
||||
|
||||
type ProviderType string
|
||||
|
||||
const AteneoProvider ProviderType = "ateneo"
|
||||
const PoissonProvider ProviderType = "poisson"
|
||||
|
||||
type Account struct {
|
||||
Id db.Ref[Account] `db:"id"` // Id of this entity
|
||||
UserId db.Ref[User] `db:"user_id"` // UserId tells the owner of this account binding
|
||||
Provider ProviderType `db:"provider"` // Provider is the name of the authentication method
|
||||
|
||||
Token string `db:"token"` // Token to use to make requests on behalf of this user
|
||||
}
|
||||