Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
807a4c54ac | ||
|
|
c7e33c8e72 | ||
|
|
9df45cf83a | ||
|
|
2485133661 | ||
|
|
2ce1ac9bf1 | ||
|
|
03f976a8f9 | ||
|
|
64200cd654 | ||
|
|
8c4ed0b70e | ||
|
|
63ec0d559b | ||
|
|
346459f822
|
||
|
|
6cccf78e45 | ||
|
|
192d8ea944 | ||
|
|
805d9a7ba5 | ||
|
|
85a0a21d15
|
||
|
|
4be4a68912
|
||
|
|
5decb85552
|
||
|
|
3d0bd18aef
|
||
|
|
489d7f161d | ||
|
|
6e90f8c32f | ||
|
|
5337849bc7 | ||
|
|
f0a9a17fc0
|
||
|
|
1ddc71f64b
|
||
|
|
684859c197
|
||
|
|
c1a701353a
|
||
|
|
9c10e1b8fc
|
||
|
|
9553dbe252
|
||
|
|
03bae38ebe
|
||
|
|
f875c9c6bf
|
||
|
|
cfb62677db
|
||
|
|
67f8421516 | ||
|
|
a5039610e1 | ||
|
|
1f90dacbdd | ||
|
|
c9d02d22dd | ||
|
|
0f7bde6232 | ||
|
|
860cc76503 | ||
|
|
227619e5eb | ||
|
|
92cee16cc8 | ||
|
|
0520e7641f | ||
|
|
45d41d714b | ||
|
|
cb76d0cdf2 | ||
|
|
dc53ee66cb | ||
|
|
41b08b86e0 | ||
|
|
1d61efeda0 | ||
|
|
39ad130e81 | ||
|
|
6a692fdf5f | ||
|
|
b0f4eb069e | ||
|
|
0d7d24f1b9 | ||
|
|
e6042c4e99
|
||
|
|
751298f4c6 | ||
|
|
6cf6e54bbb | ||
|
|
b2a9355d1d | ||
|
|
e3b71962e5
|
||
|
|
2e2f15874b
|
||
|
|
5d3f082d62 | ||
|
|
80ef1f11c2 | ||
|
|
cdb5d2ef07 | ||
|
|
9393fa73b9 | ||
|
|
4c87ade7a0
|
||
|
|
9aea9bc3a8 | ||
|
|
c8e9ee964f | ||
|
|
e6c46d75dc | ||
|
|
58db3e3901 | ||
|
|
91998e8924 | ||
|
|
1ccb71d3a1 | ||
|
|
a53ee26e78 | ||
|
|
5b0fb7fa8a
|
||
|
|
f775634fd3
|
||
|
|
ce2ec3e5ad | ||
|
|
163a6f8a2c
|
||
|
|
7aed41dd0c
|
||
|
|
57f0a97c68
|
||
|
|
349fb9bcf1 | ||
|
|
57f5764608 | ||
|
|
8bd5f83fc0 | ||
|
|
280c3aaef4 | ||
|
|
447e87b691 | ||
|
|
99371a1688 | ||
|
|
695720e44c | ||
|
|
1a2e2341d3 | ||
|
|
783bf20e21 | ||
|
|
650278828c | ||
|
|
b79eb41438 | ||
|
|
847f588e87 | ||
|
|
bfb5a61e14 | ||
|
|
106531603c | ||
|
|
1a27b2d91e | ||
|
|
b9444b4687 | ||
|
|
a2582c884b | ||
|
|
e727b2f71f | ||
|
|
54f5d798e8 | ||
|
|
e7cf338a8c | ||
|
|
cf21b9a143 | ||
|
|
c04a161ccc | ||
|
|
8bbfc57ea8 | ||
|
|
bdff6aadd0 | ||
|
|
41c654daea | ||
|
|
9a868120b8 | ||
|
|
b0f4a58221 | ||
|
|
4c0a6ed1c0 | ||
|
|
79c7cd9642 | ||
|
|
97266879e0 | ||
|
|
b9ac8a61f7 | ||
|
|
4344c397f6 | ||
|
|
1f099d4d53 | ||
|
|
b442af689c | ||
|
|
9ddcc9b0f5 | ||
|
|
7a7fef0580 | ||
|
|
9890bd2c58 | ||
|
|
8a5e1a54c0 | ||
|
|
8e141af5df | ||
|
|
5d6d4bd4d4 | ||
|
|
514973852c | ||
|
|
647d192ee7 | ||
|
|
219a966860 | ||
|
|
a5f724d167 | ||
|
|
d98dc0bc3e | ||
|
|
6c36b803e9 | ||
|
|
26268329d9 | ||
|
|
5043cc2593 | ||
|
|
ef1183b35e | ||
|
|
9db584aab6 | ||
|
|
f3c45b469c | ||
|
|
5f570cca7b | ||
|
|
eea1291887 | ||
|
|
646a078131 | ||
|
|
c7a3c2a153 | ||
|
|
77c73713d4 | ||
|
|
168ef97460 | ||
|
|
4f1e72a39d | ||
|
|
0e8bee0520 | ||
|
|
5b088cdb8b | ||
|
|
c61287f412 | ||
|
|
246b82c5b0 | ||
|
|
33c15ffaad | ||
|
|
d0c9fdc3ef | ||
|
|
95b7939960 | ||
|
|
8885f40c43 | ||
|
|
935278357b | ||
|
|
1c3040ee79 | ||
|
|
ae8b2b2d51 | ||
|
|
764b569e31 | ||
|
|
b7d65edbc9 | ||
|
|
3a81824019 | ||
|
|
6f0b1467e8 | ||
|
|
b19400f3ee | ||
|
|
1510163d7b | ||
|
|
aa21c64880 | ||
|
|
bffe3056f7 | ||
|
|
744d7e0860 | ||
|
|
f7cba43f7b | ||
|
|
55711c1f0e | ||
|
|
a7c5bee55b |
@@ -0,0 +1,3 @@
|
||||
Dockerfile
|
||||
node_modules
|
||||
.git
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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,8 +1,17 @@
|
||||
# Environment files
|
||||
.env
|
||||
*.local*
|
||||
bin/
|
||||
|
||||
.out/
|
||||
# Local files
|
||||
.vscode/
|
||||
*.local*
|
||||
|
||||
# Generated output
|
||||
out/
|
||||
dist/
|
||||
|
||||
# NodeJS
|
||||
node_modules/
|
||||
|
||||
# Executables
|
||||
phc-website-server
|
||||
!phc-website-server/
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Architecture
|
||||
|
||||
## Server, Database and File Uploads
|
||||
|
||||
- **server**
|
||||
|
||||
The server module defines all routes and extracts data from HTTP request and passes data to the **handler** module.
|
||||
|
||||
- **handler**
|
||||
|
||||
This is the main controller module that dispatches requests to various services and constructs back responses to send to the client.
|
||||
|
||||
### Services
|
||||
|
||||
...
|
||||
|
||||
- **appunti**
|
||||
|
||||
This module manages all things relating the _appunti condivisi_ pages, it talks with the **database** and **auth** modules.
|
||||
|
||||
- **database**
|
||||
|
||||
This is one of the "leaf" modules that doesn't depend on anything, its purpose is to talk to the database and return "low level data" stored there. For example instances of `time.Time` are represented as `string`s at this layer.
|
||||
@@ -0,0 +1,29 @@
|
||||
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,11 +1,35 @@
|
||||
|
||||
GO_SOURCES = $(shell find . -name '*.go')
|
||||
GO_EXECUTABLE = phc-website-server
|
||||
|
||||
.PHONY: all
|
||||
all: frontend backend
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: frontend
|
||||
frontend:
|
||||
$(NPM) run build
|
||||
$(MAKE) -C _frontend/
|
||||
@echo "Built Frontend"
|
||||
|
||||
#
|
||||
# Build Backend
|
||||
#
|
||||
|
||||
.PHONY: backend
|
||||
backend:
|
||||
go build -v -o ./out/server ./cmd/server
|
||||
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)"
|
||||
|
||||
@@ -1,30 +1,180 @@
|
||||
# PHC Website
|
||||
# phc/website
|
||||
|
||||
Sito web del PHC
|
||||
Backend e frontend del nuovo sito per il PHC.
|
||||
|
||||
## Usage
|
||||
|
||||
Come usare questo progetto
|
||||
To setup the project
|
||||
|
||||
### 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
|
||||
```bash shell
|
||||
# Clone the repo
|
||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
||||
$ cd _frontend
|
||||
$ make setup
|
||||
```
|
||||
|
||||
### Production
|
||||
To just do a full build and start the project run
|
||||
|
||||
```sh
|
||||
$ go run ./cmd/server
|
||||
```
|
||||
```bash shell
|
||||
# Setup local development env file
|
||||
$ cp .env.dev .env
|
||||
|
||||
### Deploy
|
||||
|
||||
```sh
|
||||
# Setup and do a full build of frontend and backend
|
||||
$ 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 è `localhost:8000`.
|
||||
|
||||
- `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 `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
id: guida-git
|
||||
title: "Guida a Git"
|
||||
tags: git
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Vedremo come creare una repository Git per tracciare i cambiamenti di una serie di file nel tempo, e molto altro.
|
||||
---
|
||||
|
||||
## A cosa serve
|
||||
|
||||
## Creiamo una repository git
|
||||
|
||||
Nonostante per grossi progetti sia comodo usare server Git come github.com, che essendo popolare attirerà l'attenzione di più potenziali __contributors__, per progetti personali o
|
||||
di piccola scala va benissimo utilizzare un server **self-hostato**: in questo caso noi useremo il server del PHC.
|
||||
|
||||
Per prima cosa, visitiamo git.phc.dm.unipi.it ed accediamo con le credenziali di
|
||||
|
||||
## Comandi di base
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
id: guida-terminale
|
||||
title: "Guida al Terminale"
|
||||
tags: shell, zsh, vim, ssh
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
|
||||
---
|
||||
|
||||
## 1. Shell & Terminale: differenze?
|
||||
|
||||
- introduzione storica + bash
|
||||
|
||||
### Le Basi
|
||||
|
||||
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
|
||||
- copiare/incollare testo
|
||||
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
|
||||
- chiedere aiuto: man, info
|
||||
|
||||
### Zsh: una shell moderna e potente
|
||||
|
||||
https://wiki.archlinux.org/title/Zsh
|
||||
|
||||
## 2. Vim: l'Editor intramontabile
|
||||
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
|
||||
|
||||
## 3. E la TTY?
|
||||
Come utilizzarla, quando è utile, limitazioni(no scrollback)
|
||||
|
||||
## SSH: la magia della remote shell
|
||||
@@ -0,0 +1 @@
|
||||
["delucreziis", "serdyuk", "minnocci", "manicastri"]
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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$ |
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
macchinisti:
|
||||
- uid: minnocci
|
||||
fullName: Francesco Minnocci
|
||||
entryDate: 2022/02
|
||||
- uid: manicastri
|
||||
fullName: Francesco Manicastri
|
||||
entryDate: 2022/05
|
||||
- uid: delucreziis
|
||||
fullName: Antonio De Lucreziis
|
||||
entryDate: 2019/09
|
||||
- uid: caporali
|
||||
fullName: Francesco Caporali
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
- uid: dachille
|
||||
fullName: Letizia D'Achille
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
eventi:
|
||||
- type: simple
|
||||
title: Nuovo sito del PHC
|
||||
description: |
|
||||
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
|
||||
date: 2022/09
|
||||
icon: fa-solid fa-star
|
||||
- type: simple
|
||||
title: Data di pubblicazione del sito Poisson originale
|
||||
date: 1996/06/17
|
||||
description: |
|
||||
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
|
||||
icon: fa-solid fa-upload
|
||||
- type: simple
|
||||
title: Apertura del PHC
|
||||
date: 1994
|
||||
icon: fa-solid fa-flag-checkered
|
||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,23 @@
|
||||
|
||||
# https://pnpm.io/ is better at storing packages than npm, if you still want to use npm just run "make NPM=npm <target>"
|
||||
NPM = pnpm
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: all
|
||||
all: setup build
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
$(NPM) install
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
mkdir -p out/
|
||||
$(NPM) run build
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf out/
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Package to generate frontend files for phc/website",
|
||||
"scripts": {
|
||||
"build": "npm-run-all -p build:*",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"build:js": "rollup -c",
|
||||
"watch:js": "rollup -c -w",
|
||||
"build:css": "sass -s compressed src/styles/main.scss out/next.css",
|
||||
"watch:css": "sass -w src/styles/main.scss out/next.css"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "aziis98 <antonio.delucreziis@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-typescript": "^8.3.4",
|
||||
"esbuild": "^0.15.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^2.75.3",
|
||||
"rollup-plugin-esbuild": "^4.9.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.52.3",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.10.2",
|
||||
"fuse.js": "^6.6.2",
|
||||
"preact": "^10.10.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { defineConfig } from 'rollup'
|
||||
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import esbuild from 'rollup-plugin-esbuild'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'src/base.js',
|
||||
output: {
|
||||
file: 'out/base.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/utenti.js',
|
||||
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
|
||||
output: {
|
||||
file: 'out/utenti.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
// map library names to global constants
|
||||
alpinejs: 'Alpine',
|
||||
'fuse.js': 'Fuse',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/profilo.js',
|
||||
external: ['alpinejs'],
|
||||
output: {
|
||||
file: 'out/profilo.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
alpinejs: 'Alpine',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/homepage-art.ts',
|
||||
output: {
|
||||
file: 'out/homepage-art.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [esbuild({ minify: true })],
|
||||
},
|
||||
{
|
||||
input: 'src/appunti-condivisi/main.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
// https://rollupjs.org/guide/en/#-w--watch
|
||||
!process.env.ROLLUP_WATCH && terser(),
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,524 @@
|
||||
import { render } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { formatFileSize, intersperse } from './util.js'
|
||||
|
||||
function randomHex(length = 16) {
|
||||
return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista di tag "standard" disponibili nel completamento per i tags
|
||||
*/
|
||||
const availableTags = [
|
||||
// Genera una ventina di coppie come "2022/2023"
|
||||
...Array.from({ length: 20 }, (_, i) => {
|
||||
const year = new Date().getFullYear() - i
|
||||
|
||||
return { id: `${year}/${year + 1}`, label: `Anno Accademico ${year}/${year + 1}` }
|
||||
}),
|
||||
// List di tag "standard", l'id è la vera stringa del tag, mentre la label
|
||||
{ id: 'geometria-1', label: 'Geometria 1' },
|
||||
{ id: 'geometria-2', label: 'Geometria 2' },
|
||||
{ id: 'eti', label: 'Elementi di Teoria degli Insiemi' },
|
||||
{ id: 'eta', label: 'Elementi di Topologia Algebrica' },
|
||||
{ id: 'ega', label: 'Elementi di Geometria Algebrica' },
|
||||
{ id: 'gtd', label: 'Geometria e Topologia Differenziale' },
|
||||
{ id: 'ist-anal', label: 'Istituzioni di Analisi' },
|
||||
{ id: 'ist-geom', label: 'Istituzioni di Geometria' },
|
||||
{ id: 'ist-fis', label: 'Istituzioni di Fisica' },
|
||||
{ id: 'ist-prob', label: 'Istituzioni di Probabilità' },
|
||||
{ id: 'ist-alg', label: 'Istituzioni di Algebra' },
|
||||
{ id: 'ist-num', label: 'Istituzioni di Analisi Numerica' },
|
||||
{ id: 'analisi-1', label: 'Analisi 1' },
|
||||
{ id: 'analisi-2', label: 'Analisi 2' },
|
||||
{ id: 'aritmetica', label: 'Aritmetica' },
|
||||
{ id: 'programmazione', label: 'Programmazione' },
|
||||
{ id: 'fisica-1', label: 'Fisica 1' },
|
||||
{ id: 'steffe-1', label: 'Laboratorio di Comunicazione Mediante Calcolatore' },
|
||||
]
|
||||
|
||||
const useAutosizeTextarea = ({ minRows } = {}) => {
|
||||
minRows ??= 1
|
||||
|
||||
const textareaRef = useRef(null)
|
||||
|
||||
const updateTextareaHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
updateTextareaHeight()
|
||||
})
|
||||
|
||||
return {
|
||||
rows: minRows,
|
||||
ref: textareaRef,
|
||||
onInput: updateTextareaHeight,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTag = tag => {
|
||||
return tag
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\p{L}0-9\/\-]/gu, '')
|
||||
}
|
||||
|
||||
const InputTags = ({ tags, setTags, availableTags }) => {
|
||||
availableTags ??= []
|
||||
|
||||
const [id] = useState('tags-' + randomHex())
|
||||
|
||||
const nextRef = useRef()
|
||||
const [nextText, setNextText] = useState('')
|
||||
|
||||
const onFocus = e => {
|
||||
if (!e.target.closest('.tags .tag .remove')) {
|
||||
nextRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = tag => {
|
||||
setTags(tags => tags.filter(t => t !== tag))
|
||||
}
|
||||
|
||||
const addTag = tag => {
|
||||
setTags(tags => [...tags, tag])
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (e.key === 'Backspace' && nextText.length === 0) {
|
||||
removeTag(tags.at(-1))
|
||||
}
|
||||
|
||||
const trimmed = nextText.trim()
|
||||
if (e.key === 'Enter' && trimmed.length > 0) {
|
||||
addTag(normalizeTag(trimmed))
|
||||
setNextText('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-tags" onClick={onFocus}>
|
||||
{tags.map(tag => (
|
||||
<div class="tag">
|
||||
<span>{tag}</span>
|
||||
<span class="remove" onClick={() => removeTag(tag)}>
|
||||
<i class="fa-solid fa-remove"></i>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
ref={nextRef}
|
||||
list={id}
|
||||
value={nextText}
|
||||
onInput={e => setNextText(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<datalist id={id}>
|
||||
{availableTags.map(({ id, label }) => (
|
||||
<option value={id} label={label} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const withClasses = a => {
|
||||
if (Array.isArray(a)) {
|
||||
return { class: a.filter(className => !!className).join(' ') }
|
||||
} else if (typeof a === 'object') {
|
||||
return {
|
||||
class: Object.entries(a)
|
||||
.flatMap(([className, active]) => (active ? [className] : []))
|
||||
.join(' '),
|
||||
}
|
||||
} else if (typeof a === 'string') {
|
||||
return { class: a }
|
||||
} else {
|
||||
throw new Error(`Invalid class format`)
|
||||
}
|
||||
}
|
||||
|
||||
const UploadRegion = ({}) => {
|
||||
const [draggingOver, setDraggingOver] = useState(false)
|
||||
|
||||
const onDragOver = e => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
setDraggingOver(true)
|
||||
}
|
||||
|
||||
const onDragLeave = () => {
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
const onDropFiles = files => {
|
||||
if (files.length !== 1) {
|
||||
throw new Error('Must drop one file')
|
||||
}
|
||||
|
||||
const [file] = files
|
||||
console.dir(file)
|
||||
|
||||
if (file.type !== 'application/pdf') {
|
||||
console.error('The file must be a PDF')
|
||||
}
|
||||
|
||||
console.log(formatFileSize(file.size))
|
||||
}
|
||||
|
||||
const onDrop = e => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
onDropFiles(
|
||||
[...e.dataTransfer.items].flatMap(item =>
|
||||
item.kind === 'file' ? [item.getAsFile()] : []
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onDropFiles([...e.dataTransfer.files])
|
||||
}
|
||||
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...withClasses(['upload-region', draggingOver && 'dragging-over'])}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{!draggingOver ? (
|
||||
<>
|
||||
<span>Trascina qui un PDF oppure usa il tasto sottostante</span>
|
||||
<InputFile accept="application/pdf" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div class="release-text">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
<span>Rilascia per iniziare il caricamento</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Progress = ({ value, max }) => {
|
||||
return (
|
||||
<div class="progress-bar">
|
||||
<div class="indicator" style={{ width: Math.floor((value / max) * 100) + '%' }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CancellableUpload = ({ uploadedSize, totalSize, onCancel }) => {
|
||||
return (
|
||||
<>
|
||||
<div class="progress-bytes">
|
||||
{formatFileSize(uploadedSize)} / {formatFileSize(totalSize)}
|
||||
</div>
|
||||
<div class="progress-and-action">
|
||||
<Progress value={uploadedSize} max={totalSize} />
|
||||
<button onClick={onCancel}>Annulla</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadPopup = ({}) => {
|
||||
const [shown, setShown] = useState(false)
|
||||
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
const [doneUploading, setDoneUploading] = useState(false)
|
||||
|
||||
const hash = '59e514dd50c63051'
|
||||
|
||||
return (
|
||||
<>
|
||||
{shown && (
|
||||
<div class="upload-popup">
|
||||
<div class="popup">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
Carica la dispensa "<code>file.pdf</code>"
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>
|
||||
{/* TODO: */}
|
||||
Inserisci titolo, descrizione e tag per questa dispensa, una volta
|
||||
premuto <strong>Salva</strong> la dispensa verrà aggiunta
|
||||
inizialmente non sarà visibile nell'elenco degli appunti finché non
|
||||
verrà approvata da un moderatore
|
||||
</p>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea placeholder="Descrizione..." {...descriptionTextareaProps}>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
|
||||
{!doneUploading ? (
|
||||
<CancellableUpload
|
||||
uploadedSize={34.2 * 1024 ** 2}
|
||||
totalSize={45.6 * 1024 ** 2}
|
||||
/>
|
||||
) : (
|
||||
<div class="right">
|
||||
<div class="upload-message">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
<span title={hash}>File caricato</span>
|
||||
</div>
|
||||
<button>Annulla</button>
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const InputFile = ({ accept, onFile }) => {
|
||||
const inputFileRef = useRef()
|
||||
|
||||
const [file, setFile] = useState(null)
|
||||
|
||||
const onButtonClick = e => {
|
||||
inputFileRef.current.click()
|
||||
}
|
||||
|
||||
const onInputFile = e => {
|
||||
if (e.target.files.length === 1) {
|
||||
const f = e.target.files[0]
|
||||
|
||||
setFile(f)
|
||||
onFile?.(f)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-file">
|
||||
<input type="file" ref={inputFileRef} accept={accept} onInput={onInputFile} />
|
||||
<button onClick={onButtonClick}>Carica File</button>
|
||||
{file && <div class="file-name">{file.name}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TabellaApprovazioni = ({ pendingApprovazioni }) => {
|
||||
return (
|
||||
<div class="table approvazioni">
|
||||
<div class="download header"></div>
|
||||
<div class="hash header">PDF</div>
|
||||
<div class="title header">Dispensa</div>
|
||||
<div class="owner header">Proprietario</div>
|
||||
<div class="actions header">Azioni</div>
|
||||
|
||||
{intersperse(
|
||||
pendingApprovazioni.map(({ id, title, owner, hash }) => (
|
||||
<>
|
||||
<div class="download">
|
||||
<a class="button icon" href={`/appunti/files/${hash}`}>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hash">
|
||||
<code>{hash}</code>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href={`/appunti/${id}`} title={id}>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
<div class="owner">
|
||||
<a href={`/u/${owner}`}>@{owner}</a>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)),
|
||||
<div class="separator"></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const App = ({}) => {
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadRegion />
|
||||
<UploadPopup />
|
||||
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>Le tue dispense</h1>
|
||||
<div class="table dispense">
|
||||
<div class="edit header"></div>
|
||||
<div class="name header">Nome</div>
|
||||
<div class="tags header">Tags</div>
|
||||
<div class="status header">Stato</div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name">Appunti di Geometria 1</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-1</div>
|
||||
<div class="tag">prof-1</div>
|
||||
<div class="tag">2016/2017</div>
|
||||
</div>
|
||||
<div class="status approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="expanded">
|
||||
<div class="edit-close">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edit-container">
|
||||
<div class="header">
|
||||
<a href="/appunti/6f82dca3d83b475c">
|
||||
<span class="title">Mezzedimi</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea
|
||||
placeholder="Descrizione..."
|
||||
{...descriptionTextareaProps}
|
||||
>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
</div>
|
||||
<p>
|
||||
Puoi anche caricare una nuova versione del PDF ma ricorda che se
|
||||
carichi un file non precedentemente approvato inizialmente
|
||||
scomparirà dall'elenco principale in attesa di apporvazione da parte
|
||||
di un moderatore.
|
||||
</p>
|
||||
<div class="form">
|
||||
<div class="label">Stato</div>
|
||||
<div class="stato-approvazione approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
Approvata
|
||||
</div>
|
||||
|
||||
<div class="label">Cambia PDF</div>
|
||||
<InputFile accept="application/pdf" />
|
||||
|
||||
<div class="right">
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name pending">Appunti di Geometria 2</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-2</div>
|
||||
<div class="tag">prof-2</div>
|
||||
<div class="tag">2017/2018</div>
|
||||
<div class="tag">tanti-tag-1</div>
|
||||
<div class="tag">tanti-tag-2</div>
|
||||
<div class="tag">tanti-tag-3</div>
|
||||
<div class="tag">tanti-tag-4</div>
|
||||
<div class="tag">tanti-tag-5</div>
|
||||
<div class="tag">tanti-tag-6</div>
|
||||
</div>
|
||||
<div class="status pending" title="In attesa di approvazione...">
|
||||
<i class="fas fa-hourglass"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name rejected">F1Le SuP3R LeGaLe</div>
|
||||
<div class="tags">
|
||||
<div class="tag">foo</div>
|
||||
<div class="tag">bar</div>
|
||||
</div>
|
||||
<div class="status rejected">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>PDF da Approvare</h1>
|
||||
<TabellaApprovazioni
|
||||
pendingApprovazioni={[
|
||||
{
|
||||
id: '59e514dd50c63051',
|
||||
title: 'GAAL',
|
||||
owner: 'mezzedimi',
|
||||
hash: '2c8d593c6be289ab',
|
||||
},
|
||||
{
|
||||
id: '4c52d9726c438f3d',
|
||||
title: 'Dispensa 1',
|
||||
owner: 'persona-1',
|
||||
hash: '346ba392a3a1eb86',
|
||||
},
|
||||
{
|
||||
id: 'eecb96f04e319c4c',
|
||||
title: 'Dispensa 2',
|
||||
owner: 'persona-1',
|
||||
hash: '74f9652c28f82e7f',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render(<App />, document.querySelector('#app'))
|
||||
@@ -0,0 +1,17 @@
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`
|
||||
}
|
||||
if (bytes < 1024 ** 2) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`
|
||||
}
|
||||
if (bytes < 1024 ** 3) {
|
||||
return `${(bytes / 1024 ** 2).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
export function intersperse(list, separator) {
|
||||
return list.flatMap((el, i) => (i === 0 ? [el] : [separator, el]))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $toggle = document.querySelector('#toggle-dark-mode')
|
||||
const $toggleIcon = document.querySelector('#toggle-dark-mode i')
|
||||
|
||||
// Loads preferred dark from from localStorage or defaults to media query.
|
||||
let prefersDarkMode =
|
||||
localStorage.getItem('theme') !== undefined
|
||||
? localStorage.getItem('theme') === 'dark'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
function storePrefersDarkMode(mode) {
|
||||
prefersDarkMode = mode
|
||||
localStorage.setItem('theme', mode ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
function displayToggle() {
|
||||
document.body.classList.toggle('dark-mode', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode)
|
||||
|
||||
document.dispatchEvent(new CustomEvent('theme:change'))
|
||||
}
|
||||
|
||||
$toggle.addEventListener('click', () => {
|
||||
storePrefersDarkMode(!prefersDarkMode)
|
||||
displayToggle()
|
||||
})
|
||||
|
||||
displayToggle()
|
||||
})
|
||||
@@ -0,0 +1,378 @@
|
||||
type Point2i = [number, number]
|
||||
|
||||
type WireDirection = 'down-left' | 'down' | 'down-right'
|
||||
|
||||
type TipPosition = false | 'begin' | 'end' | 'begin-end'
|
||||
|
||||
type WirePiece = {
|
||||
direction: WireDirection
|
||||
lerp: number
|
||||
tipPosition: TipPosition
|
||||
}
|
||||
|
||||
type LatticePoint = string
|
||||
|
||||
function toLatticePoint(x: number, y: number): LatticePoint {
|
||||
return `${x | 0},${y | 0}`
|
||||
}
|
||||
|
||||
function fromLatticePoint(p: LatticePoint): Point2i {
|
||||
const [x, y] = p.split(',').map(s => parseInt(s))
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
type WireNode = { point: Point2i; direction: WireDirection }
|
||||
|
||||
type Wire = WireNode[]
|
||||
|
||||
type World = {
|
||||
dimensions: Point2i
|
||||
|
||||
wirePieces: { [point: LatticePoint]: WirePiece }
|
||||
wiresQueue: { wire: Wire; cursor: number }[]
|
||||
}
|
||||
|
||||
function randomChoice<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right'])
|
||||
|
||||
const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => {
|
||||
if (direction === 'down') return [x, y + 1]
|
||||
if (direction === 'down-left') return [x - 1, y + 1]
|
||||
if (direction === 'down-right') return [x + 1, y + 1]
|
||||
throw 'invalid'
|
||||
}
|
||||
|
||||
function checkPoint(world: World, [x, y]: [number, number]): boolean {
|
||||
return !!world.wirePieces[toLatticePoint(x, y)]
|
||||
}
|
||||
|
||||
function wireIntersects(world: World, wire: Wire): boolean {
|
||||
return wire.some(({ point: [x, y], direction }) => {
|
||||
// TODO: The point check actually "doubly" depends on direction
|
||||
if (direction === 'down') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1])
|
||||
}
|
||||
if (direction === 'down-left') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y])
|
||||
}
|
||||
if (direction === 'down-right') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y])
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function generateWire(world: World): Wire {
|
||||
const [w, h] = world.dimensions
|
||||
|
||||
const randomPoint = (): [number, number] => [
|
||||
Math.floor(Math.random() * w),
|
||||
Math.floor(Math.pow(Math.random(), 2) * h * 0.5),
|
||||
]
|
||||
|
||||
const wireLength = 3 + Math.floor(Math.random() * 10)
|
||||
const wire: Wire = [
|
||||
{
|
||||
point: randomPoint(),
|
||||
direction: randomDirection(),
|
||||
},
|
||||
]
|
||||
|
||||
let prev = wire[0]
|
||||
let dir = prev.direction
|
||||
|
||||
for (let i = 0; i < wireLength; i++) {
|
||||
const p = nextPoint(prev.point, dir)
|
||||
|
||||
if (Math.random() < 0.325) {
|
||||
// change direction
|
||||
if (dir === 'down') {
|
||||
dir = randomChoice(['down-left', 'down-right'])
|
||||
} else {
|
||||
dir = 'down'
|
||||
}
|
||||
}
|
||||
|
||||
wire.push({
|
||||
point: p,
|
||||
direction: dir,
|
||||
})
|
||||
prev = wire[wire.length - 1]
|
||||
}
|
||||
|
||||
return wire
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
return {
|
||||
backgroundColor: '#282828',
|
||||
circuitColor: '#38302e',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: '#eaeaea',
|
||||
circuitColor: '#d4d4d4',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Art {
|
||||
static CELL_SIZE = 28
|
||||
static TIP_RADIUS = 4
|
||||
static WIRE_LERP_SPEED = 25 // units / seconds
|
||||
|
||||
renewGraphicsContext: boolean = true
|
||||
dirty: boolean
|
||||
|
||||
world: World
|
||||
|
||||
constructor($canvas: HTMLCanvasElement) {
|
||||
let g: CanvasRenderingContext2D
|
||||
|
||||
let unMount = this.setup($canvas)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.renewGraphicsContext = true
|
||||
unMount()
|
||||
unMount = this.setup($canvas)
|
||||
})
|
||||
|
||||
document.addEventListener('theme:change', () => {
|
||||
this.dirty = true
|
||||
})
|
||||
|
||||
const renderFn = () => {
|
||||
if (this.renewGraphicsContext) {
|
||||
$canvas.width = $canvas.offsetWidth * devicePixelRatio
|
||||
$canvas.height = $canvas.offsetHeight * devicePixelRatio
|
||||
|
||||
g = $canvas.getContext('2d')!
|
||||
g.scale(devicePixelRatio, devicePixelRatio)
|
||||
}
|
||||
|
||||
if (this.dirty || this.renewGraphicsContext) {
|
||||
console.log('Rendering')
|
||||
this.render(g, $canvas.offsetWidth, $canvas.offsetHeight)
|
||||
}
|
||||
|
||||
this.dirty = false
|
||||
this.renewGraphicsContext = false
|
||||
requestAnimationFrame(renderFn)
|
||||
}
|
||||
|
||||
renderFn()
|
||||
}
|
||||
|
||||
setup($canvas: HTMLCanvasElement) {
|
||||
this.world = {
|
||||
dimensions: [
|
||||
Math.ceil($canvas.offsetWidth / Art.CELL_SIZE),
|
||||
Math.ceil($canvas.offsetHeight / Art.CELL_SIZE),
|
||||
],
|
||||
wirePieces: {},
|
||||
wiresQueue: [],
|
||||
}
|
||||
|
||||
let failedTries = 0
|
||||
|
||||
const wireGeneratorTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('Trying to generate wire')
|
||||
if (failedTries > 400) {
|
||||
console.log('Stopped generating wires')
|
||||
clearInterval(wireGeneratorTimer)
|
||||
return
|
||||
}
|
||||
|
||||
const wire = generateWire(this.world)
|
||||
if (!wireIntersects(this.world, wire)) {
|
||||
failedTries = 0
|
||||
this.world.wiresQueue.push({ wire, cursor: 0 })
|
||||
} else {
|
||||
failedTries++
|
||||
}
|
||||
}, 10)
|
||||
|
||||
let pieceLerpBeginTime = new Date().getTime()
|
||||
const wireQueueTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
// console.log('Interpolating queued wire')
|
||||
|
||||
// get top wire to add
|
||||
const wireInterp = this.world.wiresQueue[0]
|
||||
if (wireInterp.cursor < wireInterp.wire.length) {
|
||||
const currentNode = wireInterp.wire[wireInterp.cursor]
|
||||
const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED
|
||||
|
||||
const now = new Date().getTime()
|
||||
if (now > pieceLerpEndTime) {
|
||||
wireInterp.cursor++
|
||||
pieceLerpBeginTime = new Date().getTime()
|
||||
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
direction: currentNode.direction,
|
||||
lerp: 1,
|
||||
tipPosition:
|
||||
wireInterp.cursor === 1
|
||||
? 'begin'
|
||||
: wireInterp.cursor === wireInterp.wire.length
|
||||
? 'end'
|
||||
: false,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
return
|
||||
}
|
||||
|
||||
const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
...currentNode,
|
||||
tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end',
|
||||
lerp,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
} else {
|
||||
this.world.wiresQueue.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}, 1000 / 60)
|
||||
|
||||
const unMount = () => {
|
||||
clearInterval(wireGeneratorTimer)
|
||||
clearInterval(wireQueueTimer)
|
||||
}
|
||||
|
||||
// document.addEventListener('keypress', e => {
|
||||
// if (e.key === 'r') {
|
||||
// unMount()
|
||||
// this.setup($canvas)
|
||||
// }
|
||||
// })
|
||||
|
||||
return unMount
|
||||
}
|
||||
|
||||
render(g: CanvasRenderingContext2D, width: number, height: number) {
|
||||
g.clearRect(0, 0, width, height)
|
||||
|
||||
const { backgroundColor, circuitColor } = getTheme()
|
||||
|
||||
// Grid
|
||||
// g.lineWidth = 1
|
||||
// g.strokeStyle = '#ddd'
|
||||
// g.beginPath()
|
||||
// for (let i = 0; i < height / Art.CELL_SIZE; i++) {
|
||||
// g.moveTo(0, i * Art.CELL_SIZE)
|
||||
// g.lineTo(width, i * Art.CELL_SIZE)
|
||||
// }
|
||||
// for (let j = 0; j < width / Art.CELL_SIZE; j++) {
|
||||
// g.moveTo(j * Art.CELL_SIZE, 0)
|
||||
// g.lineTo(j * Art.CELL_SIZE, height)
|
||||
// }
|
||||
// g.stroke()
|
||||
|
||||
g.lineWidth = 3
|
||||
g.strokeStyle = circuitColor
|
||||
g.lineCap = 'round'
|
||||
g.lineJoin = 'round'
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
g.beginPath()
|
||||
g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE)
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down':
|
||||
g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down-right':
|
||||
g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
}
|
||||
g.stroke()
|
||||
}
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
const drawTip = () => {
|
||||
if (
|
||||
y !== 0 &&
|
||||
(piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end')
|
||||
) {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = (x - piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = (x + piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (piece.tipPosition) {
|
||||
g.fillStyle = backgroundColor
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.fill()
|
||||
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement
|
||||
new Art($canvas)
|
||||
@@ -0,0 +1,16 @@
|
||||
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
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,69 @@
|
||||
/* 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';
|
||||
@@ -0,0 +1,78 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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')
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"noEmitOnError": true
|
||||
},
|
||||
"filesGlob": ["./src/**/*.ts"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
@@ -0,0 +1,93 @@
|
||||
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-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%);
|
||||
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--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],
|
||||
.dark-mode textarea {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,44 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ template "title" . }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
|
||||
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
|
||||
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
|
||||
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/public/base.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/public/style.css">
|
||||
<link rel="stylesheet" href="/public/theme-dark.css">
|
||||
<link rel="icon" type="image/png" href="/public/icons/icons8-electronics-100.png" />
|
||||
</head>
|
||||
|
||||
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
|
||||
{{ template "navbar" . }}
|
||||
|
||||
<div class="main">
|
||||
{{ template "body" . }}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="footer-item">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Dispensa.Title }} • Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
{{ .Dispensa.Title }}
|
||||
</h1>
|
||||
<p>
|
||||
{{ .Dispensa.Description }}
|
||||
</p>
|
||||
{{end}}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Article.Title }} • Guide • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section class="news-content">
|
||||
<h1>{{ .Article.Title }}</h1>
|
||||
<div class="date">
|
||||
{{ .Article.PublishDate.Format "2006/01/02" }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Article.Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ .ContentHTML }}
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Guide • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h2>
|
||||
Feed RSS
|
||||
<a href="/guide/rss"> <i class="fa-solid fa-rss"></i></a>
|
||||
</h2>
|
||||
<h1>
|
||||
<i class="fa-solid fa-person-chalkboard"></i>
|
||||
Articoli
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/guide/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{end}}
|
||||
@@ -0,0 +1,107 @@
|
||||
{{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>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum vel maiores dolorum dicta, repellat nulla repellendus amet fuga voluptas molestiae, magni</p>
|
||||
<p>Voluptatibus sequi praesentium similique, quos ex illo autem quidem!</p>
|
||||
</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>
|
||||
<!-- <h2>Altro...</h2> -->
|
||||
<div class="card">
|
||||
<div class="title">Vuoi diventare un macchinista?</div>
|
||||
<!-- <h3 style="font-weight: 500;">Vuoi diventare un macchinista?</h3> -->
|
||||
<div class="description">
|
||||
<p>
|
||||
Ti interessa (o interesserebbe) smanettare al PC, montare e smontare i cose? Stai spesso in dipartimento? Allora
|
||||
puoi venire a parlare con noi per diventare un apprendista macchinista per poi entrare nel PHC.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/public/homepage-art.min.js"></script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,89 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,55 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,67 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,88 @@
|
||||
{{ 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">
|
||||
<a class="nav-element" href="/progetti">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Progetti
|
||||
</div>
|
||||
</a>
|
||||
</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">
|
||||
<a class="nav-element" href="/risorse">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Risorse
|
||||
</div>
|
||||
</a>
|
||||
</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 }}
|
||||
@@ -0,0 +1,105 @@
|
||||
{{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.Username }}</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}}
|
||||
@@ -0,0 +1,61 @@
|
||||
{{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}}
|
||||
@@ -0,0 +1,59 @@
|
||||
{{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.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}}
|
||||
@@ -0,0 +1,55 @@
|
||||
package appunti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// Service isola l'handler dal modulo del database e si occupa solo delle interazioni riguardanti gli appunti e le dispense
|
||||
type Service interface {
|
||||
GetDispensa(id string) (*model.Dispensa, error)
|
||||
CreateDispensa(template model.Dispensa) (string, error)
|
||||
}
|
||||
|
||||
type DefaultService struct {
|
||||
DB *database.DB
|
||||
}
|
||||
|
||||
var _ Service = &DefaultService{}
|
||||
|
||||
func (s *DefaultService) GetDispensa(id string) (*model.Dispensa, error) {
|
||||
dispensa, err := s.DB.Dispense.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := s.DB.DispensaTags.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Dispensa{
|
||||
Id: dispensa.Id,
|
||||
OwnerId: dispensa.OwnerId,
|
||||
Title: dispensa.Title,
|
||||
Description: dispensa.Description,
|
||||
Tags: tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DefaultService) CreateDispensa(template model.Dispensa) (string, error) {
|
||||
dispensaId, err := s.DB.Dispense.Create(database.Dispensa{
|
||||
OwnerId: template.OwnerId,
|
||||
Title: template.Title,
|
||||
Description: template.Description,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := s.DB.DispensaTags.Set(dispensaId, template.Tags); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"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 {
|
||||
req, err := http.NewRequest(
|
||||
"GET", path.Join(a.Host, "poisson-ldap", url), 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
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), 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
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
import PageLayout from './PageLayout.astro'
|
||||
|
||||
const { frontmatter } = Astro.props
|
||||
---
|
||||
|
||||
<PageLayout {...frontmatter} />
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,199 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 344 B |
@@ -1,316 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
*,
|
||||
*::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;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"git.phc.dm.unipi.it/phc/website/database/sqlite"
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
auth := auth.NewDefaultService(config.AuthServiceHost)
|
||||
|
||||
// Create database connection and apply pending migrations
|
||||
db := util.Must(sqlite.New("phc-server.local.db"))
|
||||
if err := db.Migrate("./database/migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(&handler.DefaultHandler{
|
||||
AuthService: auth,
|
||||
Renderer: templates.NewRenderer(
|
||||
"./_views/",
|
||||
"./_views/base.html",
|
||||
"./_views/partials/*.html",
|
||||
),
|
||||
NewsArticlesRegistry: articles.NewRegistry("./_content/news"),
|
||||
GuideArticlesRegistry: articles.NewRegistry("./_content/guide"),
|
||||
Storia: &storia.JsonFileStoria{
|
||||
Path: "./_content/storia.yaml",
|
||||
},
|
||||
ListaUtenti: util.Must(lista_utenti.New(auth, config.ListaUtenti)),
|
||||
Appunti: &appunti.DefaultService{
|
||||
DB: db,
|
||||
},
|
||||
})
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
if err := app.Listen(config.Host); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sqlite.New("phc-server.local.db")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Migrate("database/migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package database
|
||||
|
||||
type DBMigrate interface {
|
||||
Migrate(migrationDir string) error
|
||||
}
|
||||
|
||||
type DBQueryAppunti interface {
|
||||
AllDispensaFile() ([]DispensaFile, error)
|
||||
}
|
||||
|
||||
type DBDispense interface {
|
||||
Create(template Dispensa) (string, error)
|
||||
Get(id string) (Dispensa, error)
|
||||
All() ([]Dispensa, error)
|
||||
Update(d Dispensa) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBUploads interface {
|
||||
Create(template Upload) (string, error)
|
||||
Get(id string) (Upload, error)
|
||||
}
|
||||
|
||||
type DBFileApprovals interface {
|
||||
Create(template FileApproval) (string, error)
|
||||
Get(id string) (FileApproval, error)
|
||||
All() ([]FileApproval, error)
|
||||
Update(d FileApproval) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBDispensaTags interface {
|
||||
Set(dispensaId string, tags []string) error
|
||||
Get(dispensaId string) ([]string, error)
|
||||
}
|
||||
|
||||
type DBDownloads interface {
|
||||
Create(template Download) error
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
DBMigrate
|
||||
DBQueryAppunti
|
||||
|
||||
Dispense DBDispense
|
||||
Uploads DBUploads
|
||||
FileApprovals DBFileApprovals
|
||||
DispensaTags DBDispensaTags
|
||||
Downloads DBDownloads
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
-- Dispense
|
||||
CREATE TABLE IF NOT EXISTS "dispense"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Contenuto caricato
|
||||
CREATE TABLE IF NOT EXISTS "uploads"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"file" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Approvazioni contenuti caricati
|
||||
CREATE TABLE IF NOT EXISTS "file_approvals"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL, -- Moderatore che ha creato questa approvazione
|
||||
"upload_id" TEXT NOT NULL, -- Upload a cui si riferisce questa approvazione
|
||||
"status" TEXT NOT NULL, -- Risultato dell'approvazione: "approved" | "rejected"
|
||||
FOREIGN KEY (upload_id) REFERENCES uploads(id)
|
||||
);
|
||||
|
||||
-- Download counter
|
||||
CREATE TABLE IF NOT EXISTS "downloads"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Tags per le dispense
|
||||
CREATE TABLE IF NOT EXISTS "tags"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
package database
|
||||
|
||||
//
|
||||
// Tables
|
||||
//
|
||||
|
||||
type Dispensa struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
|
||||
type FileApproval struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
Status string `db:"status"` // "approved" | "rejected"
|
||||
}
|
||||
|
||||
type Download struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Timestamp string `db:"timestamp"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Tag string `db:"tag"`
|
||||
}
|
||||
|
||||
//
|
||||
// Common Joins
|
||||
//
|
||||
|
||||
// DispensaFile is a join of dispensa with the most recent upload for that "dispensa_id"
|
||||
type DispensaFile struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
Timestamp string
|
||||
Filename string
|
||||
}
|
||||
|
||||
func New(filename string) (*database.DB, error) {
|
||||
db, err := sqlx.Open("sqlite3", filename+"?_fk=1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &database.DB{
|
||||
DBMigrate: &sqliteDBMigrate{db},
|
||||
// DBQueryAppunti: ...,
|
||||
|
||||
Dispense: &sqliteDBDispense{db},
|
||||
Uploads: &sqliteDBUploads{db},
|
||||
FileApprovals: &sqliteDBFileApprovals{db},
|
||||
Downloads: &sqliteDBDownloads{db},
|
||||
DispensaTags: &sqliteDBTags{db},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBMigrate struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBMigrate = sqliteDBMigrate{}
|
||||
|
||||
func (db sqliteDBMigrate) Migrate(migrationFolder string) error {
|
||||
log.Printf(`Creating migrations table`)
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(`Loading applied migrations`)
|
||||
appliedMigrations := []migration{}
|
||||
if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(migrationFolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return fmt.Errorf("no dirs in migrations folder")
|
||||
}
|
||||
|
||||
if i < len(appliedMigrations) {
|
||||
if appliedMigrations[i].Filename != entry.Name() {
|
||||
return fmt.Errorf("misapplied migration %q with %q", appliedMigrations[i].Filename, entry.Name())
|
||||
}
|
||||
|
||||
log.Printf("Found applied migration %q", entry.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Applying new migration %q", entry.Name())
|
||||
|
||||
migrationPath := path.Join(migrationFolder, entry.Name())
|
||||
|
||||
sqlStmts, err := os.ReadFile(migrationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(sqlStmts)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO migrations VALUES (?, ?)`,
|
||||
time.Now().Format(time.RFC3339),
|
||||
entry.Name(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("All migrations applied successfully, database up to date")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDispense struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispense = sqliteDBDispense{}
|
||||
|
||||
func (d sqliteDBDispense) Create(template database.Dispensa) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
dispense(id, created_at, owner_id, title, description)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :title, :description)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Get(id string) (database.Dispensa, error) {
|
||||
var dispensa database.Dispensa
|
||||
if err := d.DB.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil {
|
||||
return database.Dispensa{}, err
|
||||
}
|
||||
|
||||
return dispensa, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) All() ([]database.Dispensa, error) {
|
||||
var dispense []database.Dispensa
|
||||
if err := d.DB.Select(&dispense, `SELECT * FROM dispense`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dispense, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Update(dispensa database.Dispensa) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
UPDATE
|
||||
dispense
|
||||
SET
|
||||
owner_id = :owner_id
|
||||
title = :title,
|
||||
description = :description
|
||||
WHERE
|
||||
id = :id
|
||||
`, &dispensa); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Delete(id string) error {
|
||||
panic("TODO: Not implemented")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBUploads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBUploads = sqliteDBUploads{}
|
||||
|
||||
func (u sqliteDBUploads) Create(template database.Upload) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
|
||||
if _, err := u.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
uploads(id, created_at, owner_id, dispensa_id, file)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :dispensa_id, :file)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (u sqliteDBUploads) Get(id string) (database.Upload, error) {
|
||||
var upload database.Upload
|
||||
if err := u.DB.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil {
|
||||
return database.Upload{}, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBFileApprovals struct{ *sqlx.DB }
|
||||
|
||||
func (f sqliteDBFileApprovals) Create(template database.FileApproval) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := f.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
file_approvals(id, created_at, owner_id, upload_id, status)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :upload_id, :status)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Get(id string) (database.FileApproval, error) {
|
||||
var fileApproval database.FileApproval
|
||||
if err := f.DB.Get(&fileApproval, `SELECT * FROM file_approvals WHERE id = ?`, id); err != nil {
|
||||
return database.FileApproval{}, err
|
||||
}
|
||||
|
||||
return fileApproval, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) All() ([]database.FileApproval, error) {
|
||||
var fileApprovals []database.FileApproval
|
||||
if err := f.DB.Select(&fileApprovals, `SELECT * FROM file_approvals`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileApprovals, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Update(fileApproval database.FileApproval) error {
|
||||
if _, err := f.DB.NamedExec(`
|
||||
UPDATE
|
||||
file_approvals
|
||||
SET
|
||||
owner_id = :owner_id,
|
||||
upload_id = :upload_id,
|
||||
status = :status
|
||||
WHERE
|
||||
id = :id
|
||||
`, &fileApproval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Delete(id string) error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDownloads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDownloads = sqliteDBDownloads{}
|
||||
|
||||
func (d sqliteDBDownloads) Create(template database.Download) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
downloads(dispensa_id, timestamp)
|
||||
VALUES
|
||||
(:dispensa_id, :timestamp)
|
||||
`, &template); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBTags struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispensaTags = sqliteDBTags{}
|
||||
|
||||
func (t sqliteDBTags) Set(dispensaId string, tags []string) error {
|
||||
tagsRows := []map[string]any{}
|
||||
for _, t := range tags {
|
||||
tagsRows = append(tagsRows, map[string]any{
|
||||
"dispensa_id": dispensaId,
|
||||
"name": t,
|
||||
})
|
||||
}
|
||||
|
||||
tx, err := t.DB.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
DELETE FROM tags WHERE dispensa_id = ?
|
||||
`, dispensaId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(`
|
||||
INSERT INTO tags(dispensa_id, name) VALUES (:dispensa_id, :name)
|
||||
`, tagsRows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t sqliteDBTags) Get(dispensaId string) ([]string, error) {
|
||||
var tags []string
|
||||
if err := t.DB.Select(&tags, `SELECT name FROM tags WHERE dispensa_id = ?`, dispensaId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
module git.phc.dm.unipi.it/phc/website
|
||||
|
||||
go 1.20
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
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.46.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // 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
|
||||
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/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
)
|
||||
|
||||
@@ -1,93 +1,85 @@
|
||||
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/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/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/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.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
|
||||
github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
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/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.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=
|
||||
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=
|
||||
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/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.6/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=
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package handler
|
||||
|
||||
type ContextKey[T any] string
|
||||
|
||||
type Context map[string]any
|
||||
|
||||
func GetContextValue[T any](ctx Context, key ContextKey[T]) T {
|
||||
value, present := ctx[string(key)]
|
||||
if !present {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
typedValue, _ := value.(T)
|
||||
|
||||
return typedValue
|
||||
}
|
||||
|
||||
func SetContextValue[T any](ctx Context, key ContextKey[T], value T) {
|
||||
ctx[string(key)] = value
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/rss"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
|
||||
// Storia
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Appunti
|
||||
HandleAppuntiPage(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleDispensaPage(w io.Writer, id string, ctx Context) error
|
||||
|
||||
// News
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Guide
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
|
||||
// User
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
|
||||
// Article Pages
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
|
||||
// RSS
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
// User
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
|
||||
// User Listing
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
|
||||
// Appunti
|
||||
HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error)
|
||||
HandleGetDispensa(dispensaId string) (*model.Dispensa, error)
|
||||
}
|
||||
|
||||
//
|
||||
// Typed context
|
||||
//
|
||||
|
||||
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
|
||||
const UserKey ContextKey[*model.User] = "user"
|
||||
|
||||
func (ctx Context) getUser() *model.User {
|
||||
return GetContextValue(ctx, UserKey)
|
||||
}
|
||||
|
||||
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
|
||||
type DefaultHandler struct {
|
||||
AuthService auth.Service
|
||||
Appunti appunti.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
ListaUtenti lista_utenti.Service
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
var _ Service = &DefaultHandler{}
|
||||
|
||||
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
|
||||
return h.Renderer.Render(w, view, util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
|
||||
utenti, err := h.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
|
||||
utenti, err := h.ListaUtenti.GetUtenti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
|
||||
storia, err := h.Storia.GetStoria()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "storia.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Storia": storia,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiPage(w io.Writer, query string, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Query": query,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleDispensaPage(w io.Writer, id string, ctx Context) error {
|
||||
dispensa, err := h.Appunti.GetDispensa(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "dispensa.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Dispensa": dispensa,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
|
||||
session, err := h.AuthService.Login(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUser(token string) *model.User {
|
||||
user, _ := auth.UserForSession(h.AuthService, token)
|
||||
return user
|
||||
}
|
||||
|
||||
var ErrNoUser = fmt.Errorf(`user not logged in`)
|
||||
|
||||
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return nil, ErrNoUser
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "profilo.html", util.Map{
|
||||
"User": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
|
||||
registry, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
|
||||
|
||||
return newsFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
|
||||
registry, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
|
||||
|
||||
return guideFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
func (h *DefaultHandler) HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return "", ErrNoUser
|
||||
}
|
||||
|
||||
template.OwnerId = user.Username
|
||||
|
||||
dispensaId, err := h.Appunti.CreateDispensa(template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGetDispensa(id string) (*model.Dispensa, error) {
|
||||
return h.Appunti.GetDispensa(id)
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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 (?, ?, ?)`,
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package db
|
||||
@@ -1,118 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// 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:]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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, "")
|
||||
}
|
||||