Compare commits
No commits in common. 'dev-old' and 'main' have entirely different histories.
@ -1,3 +0,0 @@
|
||||
Dockerfile
|
||||
node_modules
|
||||
.git
|
@ -0,0 +1,50 @@
|
||||
# This file defines a Drone pipeline that builds a static website with "npm run build". This
|
||||
# pipeline must be marked as "Trusted" in the Drone project settings.
|
||||
#
|
||||
# We mount the target directory of the project at "/var/www/{project}" to the container
|
||||
# "dist/" directory and the run the build. A caveat is that the container builds files
|
||||
# with "root" permissions, so we need to fix those after each build with a second pipeline.
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: deploy
|
||||
image: node:latest
|
||||
volumes:
|
||||
- name: host-website-dist
|
||||
path: /mnt/website
|
||||
commands:
|
||||
- npm install
|
||||
- npm run build
|
||||
- cp -rT ./dist /mnt/website
|
||||
|
||||
volumes:
|
||||
- name: host-website-dist
|
||||
host: # this volume is mounted on the host machine
|
||||
path: /var/www/website
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: exec # this job is executed on the host machine
|
||||
name: caddy-permissions
|
||||
|
||||
depends_on:
|
||||
- default
|
||||
|
||||
steps:
|
||||
- name: chown
|
||||
commands:
|
||||
- chown -R caddy:caddy /var/www/website
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
@ -1,22 +0,0 @@
|
||||
# Server config
|
||||
MODE=development
|
||||
HOST=:8080
|
||||
|
||||
# Separate services
|
||||
GIT_URL=https://git.phc.dm.unipi.it
|
||||
CHAT_URL=https://chat.phc.dm.unipi.it
|
||||
|
||||
# Other
|
||||
EMAIL=macchinisti@lists.dm.unipi.it
|
||||
|
||||
# Base URL
|
||||
BASE_URL=localhost:8080
|
||||
|
||||
# Lista Utenti
|
||||
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
|
||||
|
||||
# AuthService
|
||||
AUTH_SERVICE_HOST=:memory:
|
||||
|
||||
# Origine per Lista Utenti
|
||||
LISTA_UTENTI=utenti-poisson-2022.local.json
|
@ -1,17 +1,23 @@
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Local files
|
||||
.vscode/
|
||||
*.local*
|
||||
|
||||
# Generated output
|
||||
out/
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# NodeJS
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Executables
|
||||
phc-website-server
|
||||
!phc-website-server/
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local data
|
||||
*.local*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 110,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"npm.packageManager": "bun"
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /_frontend
|
||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
COPY ./_frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm fetch --prod
|
||||
COPY ./_frontend ./
|
||||
RUN pnpm install -r --offline --prod
|
||||
RUN make build
|
||||
|
||||
FROM golang:1.19-alpine AS backend-builder
|
||||
RUN apk add make
|
||||
WORKDIR /backend
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
COPY ./ ./
|
||||
RUN make backend
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=frontend-builder /_frontend/out ./_frontend/out
|
||||
COPY --from=backend-builder /backend/phc-website-server ./phc-website-server
|
||||
COPY ./_views ./_views
|
||||
COPY ./_content/news ./_content/news
|
||||
COPY ./_content/guide ./_content/guide
|
||||
COPY ./_public ./_public
|
||||
COPY ./.env ./.env
|
||||
EXPOSE 8000
|
||||
CMD ["./website-server"]
|
@ -1,35 +0,0 @@
|
||||
|
||||
GO_SOURCES = $(shell find . -name '*.go')
|
||||
GO_EXECUTABLE = phc-website-server
|
||||
|
||||
.PHONY: all
|
||||
all: frontend backend
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: frontend
|
||||
frontend:
|
||||
$(MAKE) -C _frontend/
|
||||
@echo "Built Frontend"
|
||||
|
||||
#
|
||||
# Build Backend
|
||||
#
|
||||
|
||||
.PHONY: backend
|
||||
backend: $(GO_EXECUTABLE)
|
||||
@echo "Built Backend"
|
||||
|
||||
$(GO_EXECUTABLE): $(GO_SOURCES)
|
||||
go build -o $(GO_EXECUTABLE) ./cmd/phc-website-server
|
||||
|
||||
#
|
||||
# Debug
|
||||
#
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
@echo "GO_SOURCES = $(GO_SOURCES)"
|
||||
@echo "GO_EXECUTABLE = $(GO_EXECUTABLE)"
|
@ -1,180 +1,42 @@
|
||||
# phc/website
|
||||
# PHC Website
|
||||
|
||||
Backend e frontend del nuovo sito per il PHC.
|
||||
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||
|
||||
## Usage
|
||||
## Installazione
|
||||
|
||||
To setup the project
|
||||
|
||||
```bash shell
|
||||
# Clone the repo
|
||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
||||
$ cd _frontend
|
||||
$ make setup
|
||||
```
|
||||
|
||||
To just do a full build and start the project run
|
||||
|
||||
```bash shell
|
||||
# Setup local development env file
|
||||
$ cp .env.dev .env
|
||||
|
||||
# Setup and do a full build of frontend and backend
|
||||
$ make all
|
||||
|
||||
# Compile and start the server
|
||||
$ go run -v ./cmd/phc-website-server
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
Alternativamente se si sta modificando in live il codice è comodo usare [`entr`](https://github.com/eradman/entr) e `fd` (un'alternativa a `find`)
|
||||
## Sviluppo
|
||||
|
||||
```bash shell
|
||||
# Restart server when go files change
|
||||
$ printf '%s\n' $(echo **/*.go) | entr -r go run ./cmd/phc-website-server
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
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
|
||||
## Build
|
||||
|
||||
```bash shell
|
||||
# Recompile files inside "_frontend/src" on change
|
||||
_frontend/ $ pnpm run watch
|
||||
```bash
|
||||
bun build
|
||||
```
|
||||
|
||||
## 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_.
|
||||
## Deploy [TODO]
|
||||
|
||||
### Javascript
|
||||
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro).
|
||||
|
||||
These files a processed by RollupJS into `out/`
|
||||
|
||||
- `base.js`
|
||||
|
||||
Script che avvia KaTeX e il _theme switcher_.
|
||||
|
||||
- `utenti.js`
|
||||
|
||||
Script che si occupa di mostrare la lista degli utenti con fuzzy search.
|
||||
|
||||
This script is imported by `_views/utenti.html` along side its dependencies loaded from common CDNs
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
- `profilo.js`
|
||||
|
||||
Script che aggiunge un minimo di interattività alla pagina renderizzata da `_views/profilo.html`
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="/public/profilo.min.js"></script>
|
||||
```
|
||||
|
||||
- `homepage-art.ts`
|
||||
|
||||
Script che renderizza l'animazione della homepage.
|
||||
|
||||
|
||||
- `appunti-condivisi.jsx`
|
||||
|
||||
Script che aggiunte l'interattività alla pagina `/appunti/condivisi` utilizzando Preact come libreria di UI.
|
||||
|
||||
```html
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODE`
|
||||
|
||||
Può essere `production` (default) o `development`.
|
||||
|
||||
- `HOST`
|
||||
|
||||
Indirizzo sul quale servire il sito, di default è `:8080`.
|
||||
|
||||
- `EMAIL`
|
||||
|
||||
Indirizzo di posta elettronica per contattare gli amministratori del sito,
|
||||
che compare nel footer di ogni pagina.
|
||||
|
||||
|
||||
- `<SERVIZIO>_URL`
|
||||
|
||||
Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale).
|
||||
|
||||
Per ora ci sono solo i seguenti
|
||||
|
||||
- `GIT_URL`
|
||||
- `CHAT_URL`
|
||||
|
||||
- `BASE_URL`
|
||||
|
||||
Base dell'url per i link nel sito, per adesso usato per il feed RSS. In locale conviene impostarlo come `HOST` magari premettendo il protocollo (eg. `http://localhost:8080`)
|
||||
|
||||
- `USER_PAGES_BASE_URL`
|
||||
```bash
|
||||
docker build -t phc-website .
|
||||
docker run -p 3000:3000 phc-website
|
||||
```
|
||||
|
||||
Base dell'url per le pagine utente di Poisson, di default `https://poisson.phc.dm.unipi.it/~`
|
||||
C'è anche un `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD.
|
||||
|
||||
- `AUTH_SERVICE_HOST`
|
||||
## Come Contribuire
|
||||
|
||||
Indirizzo del servizio generico di autenticazione.
|
||||
**Nota per Sviluppatori**: SE il branch `dev` non esiste o è indietro rispetto a `main`, bisogna portarlo avanti a `main` e poi continuare con le modifiche dal lì.
|
||||
|
||||
## Altri Servizi
|
||||
Sentitevi liberi di aprire una PR per qualsiasi modifica o aggiunta al sito web. Il branch `main` è protetto e corrisponde alla versione di produzione del sito web, le modifiche devono prima essere accettate su `dev` e poi mergeate su `main` da un amministratore.
|
||||
|
||||
Questo progetto dipende dal servizio `phc/auth-service` che permettere agli utenti di autenticarsi usando vari meccanismi.
|
||||
### Cose da fare
|
||||
|
||||
Il servizio di autenticazione di default girerà su `http://localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
||||
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
id: guida-git
|
||||
title: "Guida a Git"
|
||||
tags: git
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Vedremo come creare una repository Git per tracciare i cambiamenti di una serie di file nel tempo, e molto altro.
|
||||
---
|
||||
|
||||
## A cosa serve
|
||||
|
||||
## Creiamo una repository git
|
||||
|
||||
Nonostante per grossi progetti sia comodo usare server Git come github.com, che essendo popolare attirerà l'attenzione di più potenziali __contributors__, per progetti personali o
|
||||
di piccola scala va benissimo utilizzare un server **self-hostato**: in questo caso noi useremo il server del PHC.
|
||||
|
||||
Per prima cosa, visitiamo git.phc.dm.unipi.it ed accediamo con le credenziali di
|
||||
|
||||
## Comandi di base
|
@ -1,31 +0,0 @@
|
||||
---
|
||||
id: guida-terminale
|
||||
title: "Guida al Terminale"
|
||||
tags: shell, zsh, vim, ssh
|
||||
publish_date: 2022/08/27 22:00
|
||||
description: |
|
||||
Questo articolo introduce all'uso dei comandi di base di un terminale su sistemi Unix come Linux e MacOS; parleremo inoltre di Vim, Zsh e SSH.
|
||||
---
|
||||
|
||||
## 1. Shell & Terminale: differenze?
|
||||
|
||||
- introduzione storica + bash
|
||||
|
||||
### Le Basi
|
||||
|
||||
- keybinding utili: ctrl+a/e, ctrl+c, ctrl+d
|
||||
- copiare/incollare testo
|
||||
- comandi di base: navigazione(cd, ls), modifica(cp, mv, rm, mkdir)
|
||||
- chiedere aiuto: man, info
|
||||
|
||||
### Zsh: una shell moderna e potente
|
||||
|
||||
https://wiki.archlinux.org/title/Zsh
|
||||
|
||||
## 2. Vim: l'Editor intramontabile
|
||||
Parentesi storica, neovim, VimTutor e comandi di base(come si esce?)
|
||||
|
||||
## 3. E la TTY?
|
||||
Come utilizzarla, quando è utile, limitazioni(no scrollback)
|
||||
|
||||
## SSH: la magia della remote shell
|
@ -1 +0,0 @@
|
||||
["delucreziis", "serdyuk", "minnocci", "manicastri"]
|
@ -1,91 +0,0 @@
|
||||
---
|
||||
id: 2021-12-22-notizia-1
|
||||
title: "Notizia 1"
|
||||
tags: important, prova, test, foo, bar
|
||||
publish_date: 2021/12/22 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur "adipisicing" elit. Repudiandae -- optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora $1 + 1$ ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
$$
|
||||
\int_0^1 x^2 \mathrm d x
|
||||
$$
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||
![testing](https://picsum.photos/400/300)
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
```
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
- bar with some `code`
|
||||
```go
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
|
||||
#### Tables
|
||||
|
||||
<https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#tables>
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
|
||||
| Expression | Derivative | Integral |
|
||||
| :---: | :---: | :---: |
|
||||
| $x^a$ | $a x^{a-1}$ | $\displaystyle \frac{1}{a+1} x^{a+1} + c$ se $a \neq -1$ |
|
||||
| $\sin x$ | $\cos x$ | $\displaystyle -\cos x + c$ |
|
||||
| $e^x$ | $e^x$ | $\displaystyle e^x + c$ |
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 2021-12-23-notizia-2
|
||||
title: "Notizia 2"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/23 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||
![testing](https://picsum.photos/200/300)
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 2021-12-24-notizia-3
|
||||
title: "Notizia 3"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/24 18:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||
![testing](https://picsum.photos/200/300)
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
@ -1 +0,0 @@
|
||||
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
|
@ -1,38 +0,0 @@
|
||||
macchinisti:
|
||||
- uid: minnocci
|
||||
fullName: Francesco Minnocci
|
||||
entryDate: 2022/02
|
||||
- uid: manicastri
|
||||
fullName: Francesco Manicastri
|
||||
entryDate: 2022/05
|
||||
- uid: delucreziis
|
||||
fullName: Antonio De Lucreziis
|
||||
entryDate: 2019/09
|
||||
- uid: serdyuk
|
||||
fullName: Illya Serdyuk
|
||||
entryDate: 2019/12
|
||||
- uid: caporali
|
||||
fullName: Francesco Caporali
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
- uid: dachille
|
||||
fullName: Letizia D'Achille
|
||||
entryDate: 2018/09
|
||||
exitDate: 2022/03
|
||||
eventi:
|
||||
- type: simple
|
||||
title: Nuovo sito del PHC
|
||||
description: |
|
||||
Featuring: ricerca utenti, dark mode, nuovo logo e molto altro!
|
||||
date: 2022/09
|
||||
icon: fa-solid fa-star
|
||||
- type: simple
|
||||
title: Data di pubblicazione del sito Poisson originale
|
||||
date: 1996/06/17
|
||||
description: |
|
||||
Visitalo sul [Web Archive](https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it)!
|
||||
icon: fa-solid fa-upload
|
||||
- type: simple
|
||||
title: Apertura del PHC
|
||||
date: 1994
|
||||
icon: fa-solid fa-flag-checkered
|
Before Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 620 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.2 MiB |
@ -1,23 +0,0 @@
|
||||
|
||||
# https://pnpm.io/ is better at storing packages than npm, if you still want to use npm just run "make NPM=npm <target>"
|
||||
NPM = pnpm
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: all
|
||||
all: setup build
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
$(NPM) install
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
mkdir -p out/
|
||||
$(NPM) run build
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf out/
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Package to generate frontend files for phc/website",
|
||||
"scripts": {
|
||||
"build": "npm-run-all -p build:*",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"build:js": "rollup -c",
|
||||
"watch:js": "rollup -c -w",
|
||||
"build:css": "sass -s compressed src/styles/main.scss out/next.css",
|
||||
"watch:css": "sass -w src/styles/main.scss out/next.css"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "aziis98 <antonio.delucreziis@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-typescript": "^8.3.4",
|
||||
"esbuild": "^0.15.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^2.75.3",
|
||||
"rollup-plugin-esbuild": "^4.9.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.52.3",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.10.2",
|
||||
"fuse.js": "^6.6.2",
|
||||
"preact": "^10.10.6"
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import { defineConfig } from 'rollup'
|
||||
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import esbuild from 'rollup-plugin-esbuild'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'src/base.js',
|
||||
output: {
|
||||
file: 'out/base.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/utenti.js',
|
||||
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
|
||||
output: {
|
||||
file: 'out/utenti.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
// map library names to global constants
|
||||
alpinejs: 'Alpine',
|
||||
'fuse.js': 'Fuse',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/profilo.js',
|
||||
external: ['alpinejs'],
|
||||
output: {
|
||||
file: 'out/profilo.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
alpinejs: 'Alpine',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/homepage-art.ts',
|
||||
output: {
|
||||
file: 'out/homepage-art.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [esbuild({ minify: true })],
|
||||
},
|
||||
{
|
||||
input: 'src/appunti-condivisi.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
},
|
||||
])
|
@ -1,24 +0,0 @@
|
||||
import { render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
const Counter = ({}) => {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
const increment = () => {
|
||||
setValue(value => value + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
Counter: {value} <button onClick={increment}>+</button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
<>
|
||||
<h1>Preact is Working!</h1>
|
||||
<Counter />
|
||||
</>,
|
||||
document.querySelector('#app')
|
||||
)
|
@ -1,41 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $toggle = document.querySelector('#toggle-dark-mode')
|
||||
const $toggleIcon = document.querySelector('#toggle-dark-mode i')
|
||||
|
||||
// Loads preferred dark from from localStorage or defaults to media query.
|
||||
let prefersDarkMode =
|
||||
localStorage.getItem('theme') !== undefined
|
||||
? localStorage.getItem('theme') === 'dark'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
function storePrefersDarkMode(mode) {
|
||||
prefersDarkMode = mode
|
||||
localStorage.setItem('theme', mode ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
function displayToggle() {
|
||||
document.body.classList.toggle('dark-mode', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode)
|
||||
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode)
|
||||
|
||||
document.dispatchEvent(new CustomEvent('theme:change'))
|
||||
}
|
||||
|
||||
$toggle.addEventListener('click', () => {
|
||||
storePrefersDarkMode(!prefersDarkMode)
|
||||
displayToggle()
|
||||
})
|
||||
|
||||
displayToggle()
|
||||
})
|
@ -1,378 +0,0 @@
|
||||
type Point2i = [number, number]
|
||||
|
||||
type WireDirection = 'down-left' | 'down' | 'down-right'
|
||||
|
||||
type TipPosition = false | 'begin' | 'end' | 'begin-end'
|
||||
|
||||
type WirePiece = {
|
||||
direction: WireDirection
|
||||
lerp: number
|
||||
tipPosition: TipPosition
|
||||
}
|
||||
|
||||
type LatticePoint = string
|
||||
|
||||
function toLatticePoint(x: number, y: number): LatticePoint {
|
||||
return `${x | 0},${y | 0}`
|
||||
}
|
||||
|
||||
function fromLatticePoint(p: LatticePoint): Point2i {
|
||||
const [x, y] = p.split(',').map(s => parseInt(s))
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
type WireNode = { point: Point2i; direction: WireDirection }
|
||||
|
||||
type Wire = WireNode[]
|
||||
|
||||
type World = {
|
||||
dimensions: Point2i
|
||||
|
||||
wirePieces: { [point: LatticePoint]: WirePiece }
|
||||
wiresQueue: { wire: Wire; cursor: number }[]
|
||||
}
|
||||
|
||||
function randomChoice<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right'])
|
||||
|
||||
const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => {
|
||||
if (direction === 'down') return [x, y + 1]
|
||||
if (direction === 'down-left') return [x - 1, y + 1]
|
||||
if (direction === 'down-right') return [x + 1, y + 1]
|
||||
throw 'invalid'
|
||||
}
|
||||
|
||||
function checkPoint(world: World, [x, y]: [number, number]): boolean {
|
||||
return !!world.wirePieces[toLatticePoint(x, y)]
|
||||
}
|
||||
|
||||
function wireIntersects(world: World, wire: Wire): boolean {
|
||||
return wire.some(({ point: [x, y], direction }) => {
|
||||
// TODO: The point check actually "doubly" depends on direction
|
||||
if (direction === 'down') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1])
|
||||
}
|
||||
if (direction === 'down-left') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y])
|
||||
}
|
||||
if (direction === 'down-right') {
|
||||
return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y])
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function generateWire(world: World): Wire {
|
||||
const [w, h] = world.dimensions
|
||||
|
||||
const randomPoint = (): [number, number] => [
|
||||
Math.floor(Math.random() * w),
|
||||
Math.floor(Math.pow(Math.random(), 2) * h * 0.5),
|
||||
]
|
||||
|
||||
const wireLength = 3 + Math.floor(Math.random() * 10)
|
||||
const wire: Wire = [
|
||||
{
|
||||
point: randomPoint(),
|
||||
direction: randomDirection(),
|
||||
},
|
||||
]
|
||||
|
||||
let prev = wire[0]
|
||||
let dir = prev.direction
|
||||
|
||||
for (let i = 0; i < wireLength; i++) {
|
||||
const p = nextPoint(prev.point, dir)
|
||||
|
||||
if (Math.random() < 0.325) {
|
||||
// change direction
|
||||
if (dir === 'down') {
|
||||
dir = randomChoice(['down-left', 'down-right'])
|
||||
} else {
|
||||
dir = 'down'
|
||||
}
|
||||
}
|
||||
|
||||
wire.push({
|
||||
point: p,
|
||||
direction: dir,
|
||||
})
|
||||
prev = wire[wire.length - 1]
|
||||
}
|
||||
|
||||
return wire
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
return {
|
||||
backgroundColor: '#282828',
|
||||
circuitColor: '#38302e',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: '#eaeaea',
|
||||
circuitColor: '#d4d4d4',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Art {
|
||||
static CELL_SIZE = 28
|
||||
static TIP_RADIUS = 4
|
||||
static WIRE_LERP_SPEED = 25 // units / seconds
|
||||
|
||||
renewGraphicsContext: boolean = true
|
||||
dirty: boolean
|
||||
|
||||
world: World
|
||||
|
||||
constructor($canvas: HTMLCanvasElement) {
|
||||
let g: CanvasRenderingContext2D
|
||||
|
||||
let unMount = this.setup($canvas)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.renewGraphicsContext = true
|
||||
unMount()
|
||||
unMount = this.setup($canvas)
|
||||
})
|
||||
|
||||
document.addEventListener('theme:change', () => {
|
||||
this.dirty = true
|
||||
})
|
||||
|
||||
const renderFn = () => {
|
||||
if (this.renewGraphicsContext) {
|
||||
$canvas.width = $canvas.offsetWidth * devicePixelRatio
|
||||
$canvas.height = $canvas.offsetHeight * devicePixelRatio
|
||||
|
||||
g = $canvas.getContext('2d')!
|
||||
g.scale(devicePixelRatio, devicePixelRatio)
|
||||
}
|
||||
|
||||
if (this.dirty || this.renewGraphicsContext) {
|
||||
console.log('Rendering')
|
||||
this.render(g, $canvas.offsetWidth, $canvas.offsetHeight)
|
||||
}
|
||||
|
||||
this.dirty = false
|
||||
this.renewGraphicsContext = false
|
||||
requestAnimationFrame(renderFn)
|
||||
}
|
||||
|
||||
renderFn()
|
||||
}
|
||||
|
||||
setup($canvas: HTMLCanvasElement) {
|
||||
this.world = {
|
||||
dimensions: [
|
||||
Math.ceil($canvas.offsetWidth / Art.CELL_SIZE),
|
||||
Math.ceil($canvas.offsetHeight / Art.CELL_SIZE),
|
||||
],
|
||||
wirePieces: {},
|
||||
wiresQueue: [],
|
||||
}
|
||||
|
||||
let failedTries = 0
|
||||
|
||||
const wireGeneratorTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('Trying to generate wire')
|
||||
if (failedTries > 400) {
|
||||
console.log('Stopped generating wires')
|
||||
clearInterval(wireGeneratorTimer)
|
||||
return
|
||||
}
|
||||
|
||||
const wire = generateWire(this.world)
|
||||
if (!wireIntersects(this.world, wire)) {
|
||||
failedTries = 0
|
||||
this.world.wiresQueue.push({ wire, cursor: 0 })
|
||||
} else {
|
||||
failedTries++
|
||||
}
|
||||
}, 10)
|
||||
|
||||
let pieceLerpBeginTime = new Date().getTime()
|
||||
const wireQueueTimer = setInterval(() => {
|
||||
if (this.world.wiresQueue.length > 0) {
|
||||
// console.log('Interpolating queued wire')
|
||||
|
||||
// get top wire to add
|
||||
const wireInterp = this.world.wiresQueue[0]
|
||||
if (wireInterp.cursor < wireInterp.wire.length) {
|
||||
const currentNode = wireInterp.wire[wireInterp.cursor]
|
||||
const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED
|
||||
|
||||
const now = new Date().getTime()
|
||||
if (now > pieceLerpEndTime) {
|
||||
wireInterp.cursor++
|
||||
pieceLerpBeginTime = new Date().getTime()
|
||||
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
direction: currentNode.direction,
|
||||
lerp: 1,
|
||||
tipPosition:
|
||||
wireInterp.cursor === 1
|
||||
? 'begin'
|
||||
: wireInterp.cursor === wireInterp.wire.length
|
||||
? 'end'
|
||||
: false,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
return
|
||||
}
|
||||
|
||||
const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED
|
||||
this.world.wirePieces[toLatticePoint(...currentNode.point)] = {
|
||||
...currentNode,
|
||||
tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end',
|
||||
lerp,
|
||||
}
|
||||
|
||||
this.dirty = true
|
||||
} else {
|
||||
this.world.wiresQueue.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}, 1000 / 60)
|
||||
|
||||
const unMount = () => {
|
||||
clearInterval(wireGeneratorTimer)
|
||||
clearInterval(wireQueueTimer)
|
||||
}
|
||||
|
||||
// document.addEventListener('keypress', e => {
|
||||
// if (e.key === 'r') {
|
||||
// unMount()
|
||||
// this.setup($canvas)
|
||||
// }
|
||||
// })
|
||||
|
||||
return unMount
|
||||
}
|
||||
|
||||
render(g: CanvasRenderingContext2D, width: number, height: number) {
|
||||
g.clearRect(0, 0, width, height)
|
||||
|
||||
const { backgroundColor, circuitColor } = getTheme()
|
||||
|
||||
// Grid
|
||||
// g.lineWidth = 1
|
||||
// g.strokeStyle = '#ddd'
|
||||
// g.beginPath()
|
||||
// for (let i = 0; i < height / Art.CELL_SIZE; i++) {
|
||||
// g.moveTo(0, i * Art.CELL_SIZE)
|
||||
// g.lineTo(width, i * Art.CELL_SIZE)
|
||||
// }
|
||||
// for (let j = 0; j < width / Art.CELL_SIZE; j++) {
|
||||
// g.moveTo(j * Art.CELL_SIZE, 0)
|
||||
// g.lineTo(j * Art.CELL_SIZE, height)
|
||||
// }
|
||||
// g.stroke()
|
||||
|
||||
g.lineWidth = 3
|
||||
g.strokeStyle = circuitColor
|
||||
g.lineCap = 'round'
|
||||
g.lineJoin = 'round'
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
g.beginPath()
|
||||
g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE)
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down':
|
||||
g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
case 'down-right':
|
||||
g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE)
|
||||
break
|
||||
}
|
||||
g.stroke()
|
||||
}
|
||||
|
||||
for (const [lp, piece] of Object.entries(this.world.wirePieces)) {
|
||||
const [x, y] = fromLatticePoint(lp)
|
||||
const drawTip = () => {
|
||||
if (
|
||||
y !== 0 &&
|
||||
(piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end')
|
||||
) {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = y * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') {
|
||||
switch (piece.direction) {
|
||||
case 'down-left':
|
||||
{
|
||||
const cx = (x - piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down':
|
||||
{
|
||||
const cx = x * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
case 'down-right':
|
||||
{
|
||||
const cx = (x + piece.lerp) * Art.CELL_SIZE
|
||||
const cy = (y + piece.lerp) * Art.CELL_SIZE
|
||||
g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (piece.tipPosition) {
|
||||
g.fillStyle = backgroundColor
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.fill()
|
||||
|
||||
g.beginPath()
|
||||
drawTip()
|
||||
g.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement
|
||||
new Art($canvas)
|
@ -1,16 +0,0 @@
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
Alpine.data('profilo', () => ({
|
||||
init() {
|
||||
console.log('Profilo!')
|
||||
},
|
||||
}))
|
||||
|
||||
Alpine.data('passwordForm', () => ({
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
passwordSame: true,
|
||||
onUpdate() {
|
||||
this.passwordSame = this.password === this.passwordAgain
|
||||
},
|
||||
}))
|
@ -1,69 +0,0 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--fg-300: #777;
|
||||
--fg-400: #666;
|
||||
--fg-500: #333;
|
||||
--fg-600: #222;
|
||||
|
||||
--bg-000: #f0f0f0;
|
||||
--bg-100: #f0f0f0;
|
||||
--bg-500: #eaeaea;
|
||||
--bg-550: #ecedee;
|
||||
--bg-600: #e4e5e7;
|
||||
--bg-700: #d5d5d5;
|
||||
--bg-750: #c8c8c8;
|
||||
--bg-800: #c0c0c0;
|
||||
--bg-850: #b8b8b8;
|
||||
|
||||
--accent-300: #5cc969;
|
||||
--accent-400: #4eaa59;
|
||||
--accent-500: #278542;
|
||||
--accent-600: #2e974c;
|
||||
--accent-700: #154d24;
|
||||
--accent-800: #002d0d;
|
||||
|
||||
--ft-ss: 'Inter', sans-serif;
|
||||
--ft-ss-wt-light: 300;
|
||||
--ft-ss-wt-normal: 400;
|
||||
--ft-ss-wt-medium: 500;
|
||||
--ft-ss-wt-bold: 700;
|
||||
|
||||
--shadow-500: 0 0 16px 0 #00000018;
|
||||
|
||||
// Components
|
||||
--text-input-bg: var(--bg-000);
|
||||
--text-input-readonly-bg: var(--bg-600);
|
||||
--text-input-readonly-fg: var(--fg-300);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-500);
|
||||
color: var(--fg-500);
|
||||
|
||||
font-family: var(--ft-ss);
|
||||
font-size: 17px;
|
||||
font-weight: var(--ft-ss-wt-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@import './typography.scss';
|
@ -1,78 +0,0 @@
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
import Alpine from 'alpinejs'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
const SHOW_MORE_INCREMENT = 15
|
||||
const FUSE_OPTIONS = {
|
||||
includeScore: true,
|
||||
keys: [
|
||||
'nome',
|
||||
'cognome',
|
||||
'tags',
|
||||
{ name: 'nomeCompleto', getFn: user => `${user.nome} ${user.cognome}` },
|
||||
],
|
||||
}
|
||||
|
||||
const SORT_MODES = {
|
||||
chronological: () => 0,
|
||||
name: (a, b) => (a.nome < b.nome ? -1 : 1),
|
||||
surname: (a, b) => (a.cognome < b.cognome ? -1 : 1),
|
||||
}
|
||||
|
||||
function getSortedUserList(original, mode) {
|
||||
return [...original].sort(SORT_MODES[mode])
|
||||
}
|
||||
|
||||
Alpine.data('utenti', () => ({
|
||||
searchField: '', // two-way binding for the search input field
|
||||
sortMode: 'chronological', // two-way binding for the sorting mode
|
||||
fetchedUsers: [], // hold complete user list
|
||||
sortedUserBuffer: [], // Yet another buffer of the user list for the sort mode
|
||||
fuse: new Fuse([], FUSE_OPTIONS), // current fuse instance, used to filter the list above
|
||||
searchResultsBuffer: [], // stores the full current search
|
||||
searchResults: [], // list to renderer on screen with a subset of the whole search results buffer
|
||||
async init() {
|
||||
// Get user list from server
|
||||
const response = await fetch('/api/utenti')
|
||||
this.fetchedUsers = await response.json()
|
||||
|
||||
// This will call the function "showMore()" when the user is near the end of the list
|
||||
new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
console.log('Near the bottom of the page')
|
||||
this.showMore()
|
||||
}
|
||||
})
|
||||
}).observe(this.$refs.spinner)
|
||||
|
||||
// Initialize with an empty query
|
||||
this.updateSortMode()
|
||||
this.updateSearch()
|
||||
},
|
||||
showMore() {
|
||||
// setTimeout(() => {
|
||||
// Updates the final "searchResults" list with more items from the previous buffer
|
||||
const newCount = this.searchResults.length + SHOW_MORE_INCREMENT
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, newCount)
|
||||
// }, 250) // For fun
|
||||
},
|
||||
setResults(list) {
|
||||
this.searchResultsBuffer = list.filter(
|
||||
entry => entry.score === undefined || entry.score <= 0.25
|
||||
)
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, SHOW_MORE_INCREMENT)
|
||||
},
|
||||
updateSortMode() {
|
||||
this.sortedUserBuffer = getSortedUserList(this.fetchedUsers, this.sortMode)
|
||||
this.fuse.setCollection(this.sortedUserBuffer)
|
||||
this.updateSearch()
|
||||
},
|
||||
updateSearch() {
|
||||
console.time('search')
|
||||
if (this.searchField.trim().length === 0) {
|
||||
// Reset the result list
|
||||
this.setResults(this.sortedUserBuffer.map(user => ({ item: user })))
|
||||
} else {
|
||||
// Update the result list with the new results
|
||||
this.setResults(this.fuse.search(this.searchField))
|
||||
}
|
||||
console.timeEnd('search')
|
||||
},
|
||||
}))
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"noEmitOnError": true
|
||||
},
|
||||
"filesGlob": ["./src/**/*.ts"]
|
||||
}
|
Before Width: | Height: | Size: 790 B |
@ -1,945 +0,0 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #eaeaea;
|
||||
--fg: #333;
|
||||
|
||||
--bg-lighter: #f0f0f0;
|
||||
|
||||
--bg-dark: hsl(220, 5%, 93%);
|
||||
--bg-darker: hsl(220, 5%, 90%);
|
||||
--bg-darker-2: #d5d5d5;
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--bg-darker-3: #c0c0c0;
|
||||
--bg-darker-4: #b8b8b8;
|
||||
|
||||
--accent-1: #278542;
|
||||
--accent-1-fg: #154d24;
|
||||
|
||||
--card-date: #666;
|
||||
--card-content: #222;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #00000018;
|
||||
|
||||
--text-input-bg: #fff;
|
||||
--text-input-readonly-bg: #e4e4e4;
|
||||
--text-input-readonly-fg: #777;
|
||||
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 70ch;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 8rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
color: var(--accent-1-fg);
|
||||
}
|
||||
|
||||
/* Tutti i link dentro la navbar sono speciali e non sembrano link */
|
||||
nav .nav-element {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--accent-1-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav .nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
transition: transform 150ms ease-in-out;
|
||||
}
|
||||
|
||||
nav .nav-logo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
nav .nav-logo img {
|
||||
width: 150px;
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
nav .nav-main {
|
||||
display: flex;
|
||||
/* grid-template-columns: repeat(5, 1fr) 1fr auto 1fr; */
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-item.filler {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element {
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-button {
|
||||
margin: 0 0.5rem;
|
||||
/* padding: 0.5rem; */
|
||||
|
||||
aspect-ratio: 1;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
/* background: var(--bg-darker-2); */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-radius: 1rem;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
user-select: none;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element:hover,
|
||||
nav .nav-main .nav-button:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown {
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items .nav-item:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: none;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
/*
|
||||
nav .nav-main .nav-item.dropdown .nav-items::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: -2px;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
|
||||
background: var(--bg-darker);
|
||||
} */
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover + .nav-items,
|
||||
nav .nav-main .nav-item.dropdown .name + .nav-items:hover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* .nav-main borders */
|
||||
.nav-main > .nav-item:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.nav-main > .nav-item:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 3rem;
|
||||
|
||||
width: 80ch;
|
||||
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-bottom: none;
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
max-width: 60ch;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card .title {
|
||||
font-size: 22px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
font-weight: var(--font-weight-light);
|
||||
color: var(--card-content);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0 0.5rem;
|
||||
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tags .tag {
|
||||
height: 1.5rem;
|
||||
border-radius: calc(1.5rem / 2);
|
||||
padding: 0 calc(1.5rem / 2);
|
||||
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--card-date);
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
a:not(.button) {
|
||||
color: var(--accent-1-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not(.button):hover {
|
||||
color: var(--accent-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 17px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 1rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
background: var(--bg-darker-2-1);
|
||||
box-shadow: 0 4px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border: 1px solid var(--accent-2-darker);
|
||||
background: var(--accent-2);
|
||||
color: var(--accent-2-darkest);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--accent-2-lighter);
|
||||
}
|
||||
|
||||
button.icon {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
select {
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 16px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 0.25rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Text Fields */
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'] {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
background: var(--text-input-bg);
|
||||
color: var(--fg);
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
input[type='password'] {
|
||||
font-family: caption;
|
||||
}
|
||||
|
||||
input[type='text']:read-only,
|
||||
input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
input[type='text'].error,
|
||||
input[type='password'].error {
|
||||
background: #faa;
|
||||
color: #311;
|
||||
}
|
||||
|
||||
/* Compound Controls */
|
||||
|
||||
.compound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 8px 0 #00000022;
|
||||
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.compound > .divider {
|
||||
height: 2rem;
|
||||
width: 1px;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.compound .icon {
|
||||
width: 2.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.compound > select {
|
||||
background: none;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.compound > button,
|
||||
.compound > .button,
|
||||
.compound > input,
|
||||
.compound > select {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.compound > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.compound > :not(:last-child) {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
form .field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form .field-set {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-auto-rows: auto;
|
||||
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
form .field-set .fill {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
form .field-set label {
|
||||
grid-column: 1 / 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
form .field-set input {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.page-home {
|
||||
/* TODO: Sarebbe meglio se si riuscisse a capire come farlo senza */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-home .nav-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-home .super {
|
||||
position: absolute;
|
||||
left: 50vw;
|
||||
top: 50vh;
|
||||
|
||||
width: 90vw;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 2vmin;
|
||||
}
|
||||
|
||||
.page-home canvas {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
|
||||
transition: opacity 1000ms ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.page-home canvas.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-home .super .block.text {
|
||||
max-width: 40ch;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.page-home .super .block.text h1 {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-home .super .block.text p {
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.page-home .super .block.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-home .super .block.image img {
|
||||
max-width: 80ch;
|
||||
max-height: 50vh;
|
||||
filter: drop-shadow(0 0 64px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 8px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.page-home .main {
|
||||
padding-top: calc(100vh - 6rem);
|
||||
}
|
||||
|
||||
.page-utenti .user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item .icon {
|
||||
width: 1.75rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.page-utenti .spinner {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 5rem;
|
||||
|
||||
color: var(--fg);
|
||||
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.page-storia .history-container {
|
||||
--bar-size: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
margin: 5rem 0;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -2rem;
|
||||
width: var(--bar-size);
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
top: -3rem;
|
||||
bottom: -3rem;
|
||||
left: 0;
|
||||
border-left: var(--bar-size) dashed var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event::before {
|
||||
--size: 1rem;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(-2rem + var(--bar-size) / 2 - var(--size) / 2);
|
||||
transform: translate(0, 1rem);
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
border-radius: 100%;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .title {
|
||||
font-size: 22px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .date {
|
||||
color: var(--card-date);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .spacer {
|
||||
height: calc(var(--size) * 1rem);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 2rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search input[type='text'] {
|
||||
width: 50ch;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
/* Rendered Markdown */
|
||||
|
||||
.news-content p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-content h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.news-content img {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 2px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
.news-content .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content .tags,
|
||||
.news-content .date {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content table {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
/* Math */
|
||||
|
||||
.katex-display {
|
||||
margin: 1rem 0;
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
table td:not(:first-child),
|
||||
table th:not(:first-child) {
|
||||
border-left: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table td {
|
||||
border-top: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table tbody tr:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
body.dark-mode {
|
||||
--bg: #282828;
|
||||
--fg: #a6cc92;
|
||||
/* --fg: #6eac4d; */
|
||||
/* Magari questo: */
|
||||
/* --fg: #a3b09c; */
|
||||
|
||||
--bg-dark: hsl(10, 10%, 20%);
|
||||
--bg-darker: hsl(10, 10%, 17%);
|
||||
--bg-darker-2: #1d2021;
|
||||
--bg-darker-2-1: #1d5021;
|
||||
--bg-darker-3: #101111;
|
||||
|
||||
--accent-1: #154d24;
|
||||
--accent-1-fg: #278542;
|
||||
|
||||
--card-date: #928374;
|
||||
--card-content: #d4be98;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #16182077;
|
||||
|
||||
--text-input-bg: var(--bg-darker);
|
||||
--text-input-readonly-bg: hsl(10, 10%, 22%);
|
||||
--text-input-readonly-fg: hsl(10, 10%, 40%);
|
||||
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
.dark-mode button {
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
background: var(--bg-darker);
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
.dark-mode button:hover {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.dark-mode button.primary {
|
||||
border: 1px solid #113a1c;
|
||||
background: #1e6732;
|
||||
color: #b7e3c3;
|
||||
}
|
||||
|
||||
.dark-mode button.primary:hover {
|
||||
background: #23773a;
|
||||
}
|
||||
|
||||
.dark-mode nav .nav-logo img {
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.dark-mode input[type] {
|
||||
background: #4b4342;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text']:read-only,
|
||||
.dark-mode input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text'].error,
|
||||
.dark-mode input[type='password'].error {
|
||||
background: #633;
|
||||
color: #faa;
|
||||
}
|
||||
|
||||
.dark-mode pre {
|
||||
background: var(--bg-darker);
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
/* Trick molto molto malvagio per non dover rendere il colorscheme dei blocchi di codice dinamici rispetto alla dark mode */
|
||||
.dark-mode pre > code > span {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.dark-mode table td,
|
||||
.dark-mode table th {
|
||||
border-color: var(--fg);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
</section>
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
{{end}}
|
@ -1,43 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Raccolta degli Appunti
|
||||
</h1>
|
||||
<p>
|
||||
Questa è la raccolta degli appunti presenti su Poisson. Cerca il titolo della dispensa, il nome e cognome o l'username dell'autore oppure scrivi il nome del corso rispetto a cui filtrare. Altrimenti in cima compariranno gli appunti più "gettonati".
|
||||
</p>
|
||||
<p>
|
||||
Puoi aggiungere le tue dispense dalla <a href="/appunti/condivisi">pagina personale di condivisione</a>.
|
||||
</p>
|
||||
<div class="search">
|
||||
<div class="compound">
|
||||
<input type="text" id="search-field" placeholder="Prova con "Geometria 1" o "Mezzedimi"..." autocomplete="off" {{ if .Query }}value="{{ .Query }}"{{ end }}>
|
||||
<button class="icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appunti-list">
|
||||
<code>TODO: Lista work in progress</code>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Aggiorna automaticamente la barra dell'URL con la query corrente
|
||||
document.querySelector('#search-field').addEventListener('input', function() {
|
||||
const q = this.value.trim();
|
||||
history.replaceState({}, 'Appunti',
|
||||
q.length === 0 ? '/appunti' : '/appunti?q=' + encodeURIComponent(q)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
{{end}}
|
@ -1,44 +0,0 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ template "title" . }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
|
||||
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
|
||||
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
|
||||
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/public/base.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/public/style.css">
|
||||
<link rel="stylesheet" href="/public/theme-dark.css">
|
||||
<link rel="icon" type="image/png" href="/public/icons/icons8-electronics-100.png" />
|
||||
</head>
|
||||
|
||||
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
|
||||
{{ template "navbar" . }}
|
||||
|
||||
<div class="main">
|
||||
{{ template "body" . }}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="footer-item">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
@ -1,18 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Article.Title }} • Guide • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section class="news-content">
|
||||
<h1>{{ .Article.Title }}</h1>
|
||||
<div class="date">
|
||||
{{ .Article.PublishDate.Format "2006/01/02" }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Article.Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ .ContentHTML }}
|
||||
</section>
|
||||
{{end}}
|
@ -1,37 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Guide • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h2>
|
||||
Feed RSS
|
||||
<a href="/guide/rss"> <i class="fa-solid fa-rss"></i></a>
|
||||
</h2>
|
||||
<h1>
|
||||
<i class="fa-solid fa-person-chalkboard"></i>
|
||||
Articoli
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/guide/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{end}}
|
@ -1,118 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Home • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<canvas id="wires-animation"></canvas>
|
||||
|
||||
<!-- Non avevo molta fantasia per il nome di questa classe -->
|
||||
<section class="super">
|
||||
<div class="block image">
|
||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
||||
</div>
|
||||
<div class="block text">
|
||||
<h1>
|
||||
Cos'è il PHC?
|
||||
</h1>
|
||||
<p>
|
||||
Il PHC è un progetto di sperimentazione tecnologica di alcuni studenti di Matematica di Pisa che va avanti fin dal 1994.
|
||||
</p>
|
||||
<p>
|
||||
Fisicamente, occupa la stanza 106 del Dipartimento, mentre questo sito ne è la sede virtuale. Qui si trovano vari servizi offerti agli studenti, e ne si documenta lo sviluppo nella pagina <a href="/news">Notizie</a>.
|
||||
</p>
|
||||
<p>
|
||||
Nella sezione <b>Progetti</b> si trovano alcune delle attività e dei servizi del PHC, mentre <a href="/utenti">Utenti</a> contiene l'indice delle pagine personali degli studenti, che sono storicamente uno dei progetti più importanti del PHC.
|
||||
</p>
|
||||
<p>
|
||||
Per più informazioni sul PHC, si consulti la sezione <a href="/storia">Storia</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Notizie Importanti</h2>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-1">News 1</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
much doge, ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-2">News 2</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/notizia-super-wow-3">News 3</a>
|
||||
</div>
|
||||
<div class="date">yyyy-mm-dd</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<!-- TODO: Progetti/Servizi/Cose fornite -->
|
||||
<h2>Progetti</h2>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">Project title</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
Vuoi diventare un macchinista?
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
|
||||
Stai spesso in dipartimento? Allora fai pure un salto in PHC a parlare con noi, per diventare apprendista (e magari un giorno macchinista) o semplicemente per imparare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/public/homepage-art.min.js"></script>
|
||||
{{end}}
|
@ -1,89 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Link Utili • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-link"></i>
|
||||
Link Utili
|
||||
</h1>
|
||||
<p class="center">
|
||||
Questo è un elenco di alcuni indirizzi potenzialmente utili
|
||||
</p>
|
||||
<ul>
|
||||
<div class="card-list">
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://linktr.ee/aulastudenti">Aula Studenti</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://credenziali.phc.dm.unipi.it/">Credenziali Poisson</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="http://detexify.kirelabs.org/classify.html">Non trovi un simbolo Latex?</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://git.phc.dm.unipi.it/phc/dm-scripts/src/branch/main/printa4">Script per stampare in dipartimento dal proprio PC</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://github.com/SuzanneSoy/WiTeX">Wikipedia+LaTex??</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i <a href="https://en.wikipedia.org/wiki/Abstract_nonsense">tuoi</a> <a href="https://en.wikipedia.org/wiki/New_Math">articoli</a> <a href="https://en.wikipedia.org/wiki/%C3%89variste_Galois#Final_days">preferiti</a> di Wikipedia con un typesetting in stile LaTex!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="https://www.dm.unipi.it">CdS Matematica</a>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</section>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
{{end}}
|
@ -1,55 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Accedi • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-user"></i>
|
||||
Account di Poisson
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
<form class="card" action="/login" method="POST">
|
||||
<div class="title">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Accedi
|
||||
</div>
|
||||
<p>
|
||||
Inserisci le tue credenziali di Poisson per accedere
|
||||
</p>
|
||||
|
||||
<div class="field-set">
|
||||
<!-- <label for="login-provider">Provider:</label> -->
|
||||
<input type="hidden" name="provider" id="login-provider" value="poisson-ldap">
|
||||
|
||||
<label for="login-username">Username:</label>
|
||||
<input type="text" name="username" id="login-username">
|
||||
|
||||
<label for="login-password">Password:</label>
|
||||
<input type="password" name="password" id="login-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Accedi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Ottenere un account</h2>
|
||||
<p>
|
||||
Dal 2022 in nuovi utenti hanno bisogno di compilare un modulo se vogliono ottenere un account<sup>1</sup>, scarica il <a href="#">modulo di richiesta</a> e portacelo in PHC o inviacelo via email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>.
|
||||
</p>
|
||||
<p>
|
||||
1: In realtà il modulo ancora non esiste
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Recupero credenziali</h2>
|
||||
<p>
|
||||
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una
|
||||
email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
@ -1,18 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Article.Title }} • News • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section class="news-content">
|
||||
<h1>{{ .Article.Title }}</h1>
|
||||
<div class="date">
|
||||
{{ .Article.PublishDate.Format "2006/01/02" }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Article.Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ .ContentHTML }}
|
||||
</section>
|
||||
{{end}}
|
@ -1,67 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}News • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h2>
|
||||
Feed RSS
|
||||
<a href="/news/rss"> <i class="fa-solid fa-rss"></i></a>
|
||||
</h2>
|
||||
<h1>
|
||||
<i class="far fa-newspaper"></i>
|
||||
Notizie Importanti
|
||||
</h1>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
{{ if .HasTag "important" }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<i class="fas fa-history"></i>
|
||||
Archivio notizie
|
||||
</h2>
|
||||
<div class="card-list">
|
||||
{{ range .Articles }}
|
||||
{{ if not (.HasTag "important") }}
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
<a href="/news/{{ .Id }}">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
||||
<div class="description">
|
||||
<p>{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="tags">
|
||||
{{ range .Tags }}
|
||||
<span class="tag">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
@ -1,88 +0,0 @@
|
||||
{{ define "navbar" }}
|
||||
<nav>
|
||||
<!-- Site -->
|
||||
<div class="nav-logo">
|
||||
<a class="nav-element" href="/">
|
||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-main">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/utenti">Utenti</a>
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Progetti
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="{{ .Config.GitUrl }}">Gitea</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="{{ .Config.ChatUrl }}">Zulip</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/seminari">Seminari</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Risorse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/news">News</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/appunti">Appunti</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/guide">Guide</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/link">Link Utili</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/storia">Storia</a>
|
||||
</div>
|
||||
<!-- <div class="nav-item">
|
||||
<a class="nav-element" href="/about">About</a>
|
||||
</div> -->
|
||||
|
||||
<div class="nav-item filler"></div>
|
||||
|
||||
<!-- User Related -->
|
||||
<div class="nav-item">
|
||||
<div id="toggle-dark-mode" class="nav-button">
|
||||
<i class="fas fa-moon"></i>
|
||||
</div>
|
||||
</div>
|
||||
{{if .User}}
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/profilo">@{{ .User.Username }}</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="nav-item">
|
||||
<a class="nav-element" href="/login">Accedi</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
@ -1,105 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Profilo di @{{ .User.Username }} • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="/public/profilo.min.js"></script>
|
||||
<section x-data="profilo">
|
||||
<h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
|
||||
<p class="center">
|
||||
<a class="button" href="/logout">Logout</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Appunti e Dispense</h2>
|
||||
<p>
|
||||
Per gestire i tuoi appunti e le tue dispense caricate nella sezione <a href="/appunti">Appunti</a> del sito vai
|
||||
<a href="/appunti/condivisi">alla pagina appunti condivisi</a>.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Impostazioni</h2>
|
||||
<div class="card-list">
|
||||
<form class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Sito PHC
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna i campi relativi al sito del PHC, l'immagine del profilo verrà visualizzata sulla tua pagina utente <a href="#">phc.dm.unipi.it/u/{{ .User.Username }}</a> e nella lista di <a href="/utenti">tutti gli utenti</a>.
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="website-nickname">Nickname</label>
|
||||
<input type="text" name="nickname" id="website-nickname" placeholder="{{ .User.Username }}">
|
||||
<label for="website-profile-picture">Immagine Profilo</label>
|
||||
<input type="file" name="picture" id="website-profile-picture">
|
||||
</div>
|
||||
<p>
|
||||
Puoi anche aggiungere link ad altri tuoi profili su siti esterni (quando riempi l'ultimo campo se ne crea uno nuovo sotto)
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="website-link-1">Link 1</label>
|
||||
<input type="text" name="link" id="website-link-1"
|
||||
placeholder="https://github.com/...">
|
||||
<label for="website-link-2">Link 2</label>
|
||||
<input type="text" name="link" id="website-link-2"
|
||||
placeholder="https://twitter.com/...">
|
||||
<label for="website-link-3">Link 3</label>
|
||||
<input type="text" name="link" id="website-link-3"
|
||||
placeholder="https://news.ycombinator.com/user?id=...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Informazioni Utente
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna i campi modificabili dell'account Poisson
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="user-username">Username</label>
|
||||
<input type="text" readonly name="username" id="user-username" value="{{ .User.Username }}">
|
||||
<label for="user-name">Nome</label>
|
||||
<input type="text" readonly name="name" id="user-name" value="{{ .User.Name }}">
|
||||
<label for="user-surname">Cognome</label>
|
||||
<input type="text" readonly name="surname" id="user-surname" value="{{ .User.Surname }}">
|
||||
<label for="user-email">Email</label>
|
||||
<input type="text" readonly name="email" id="user-email" value="{{ .User.Email }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna</button>
|
||||
</div>
|
||||
</form>
|
||||
<form x-data="passwordForm" class="card" action="/profile/impostazioni" method="POST">
|
||||
<div class="title">
|
||||
Modifica Password
|
||||
</div>
|
||||
<p>
|
||||
Aggiorna la password di Poisson
|
||||
</p>
|
||||
<div class="field-set">
|
||||
<label for="pass-username">Username</label>
|
||||
<input type="text" readonly name="username" id="pass-username" value="{{ .User.Username }}">
|
||||
<label for="pass-password">Password</label>
|
||||
<input type="password" name="password" id="pass-password" :class="!passwordSame ? 'error' : ''"
|
||||
x-model="password" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
||||
<label for="pass-password-2">Ripeti Password</label>
|
||||
<input type="password" name="password-2" id="pass-password-2" :class="!passwordSame ? 'error' : ''"
|
||||
x-model="passwordAgain" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="primary">Aggiorna Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Recupero Credenziali Poisson</h2>
|
||||
<p>
|
||||
Per il recupero credenziali, puoi venire direttamente in PHC a parlarne con noi, oppure inviarci una email
|
||||
all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul
|
||||
sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
@ -1,61 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Storia • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-clock"></i>
|
||||
Storia del PHC
|
||||
</h1>
|
||||
<p>
|
||||
Qui è annoverata la storia del PHC tramite una linea temporale di alcuni fra gli eventi principali. Al momento è un <i>work in progress</i>, ma aggiungeremo almeno le date di ingresso di tutti i macchinisti.
|
||||
</p>
|
||||
<p>
|
||||
Per delle note storiche un po' più dettagliate, si legga l'ottima pagine sul sito del dipartimento:
|
||||
<a href="http://betti.dm.unipi.it/servizi/PHC.html">http://betti.dm.unipi.it/servizi/PHC.html</a>.
|
||||
</p>
|
||||
<div class="history-container">
|
||||
<div class="timeline-bar"></div>
|
||||
<div class="events">
|
||||
{{ range .Storia }}
|
||||
{{ if eq .Type "simple" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
{{ if .Icon }}
|
||||
<i class="{{ .Icon }}"></i>
|
||||
{{ end }}
|
||||
{{ .Title }}
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
<div class="description">
|
||||
{{ raw .Description }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "entry-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-sign-in"></i>
|
||||
Ingresso di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "exit-macchinista" }}
|
||||
<div class="event">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-sign-out"></i>
|
||||
Uscita di
|
||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
||||
</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if eq .Type "spacer" }}
|
||||
<div class="spacer" style="--size: {{ .Size }}"></div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
@ -1,64 +0,0 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Utenti • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
||||
<script src="/public/utenti.min.js"></script>
|
||||
<section x-data="utenti">
|
||||
<h1>
|
||||
<i class="fas fa-users"></i>
|
||||
Lista degli Utenti
|
||||
</h1>
|
||||
<p>
|
||||
Questa è la lista di tutti gli utenti con un account su Poisson. Scrivi nome, cognome o
|
||||
username di un utente per filtrare la lista in tempo reale. Altrimenti di base in cima
|
||||
compariranno gli utenti con più "follower".
|
||||
</p>
|
||||
<div class="search">
|
||||
<div class="compound padded">
|
||||
<div class="icon" title="Ordina per">
|
||||
<i class="fas fa-sort"></i>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<select x-model="sortMode" @click="updateSortMode()">
|
||||
<option value="chronological">Cronologico</option>
|
||||
<option value="name">Nome</option>
|
||||
<option value="surname">Cognome</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="compound">
|
||||
<input type="text" x-model="searchField" @input="updateSearch()" placeholder="Cerca..." autocomplete="off" />
|
||||
<button class="icon" title="In realtà questo tasto è finto">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-list card-list">
|
||||
<template x-for="entry in searchResults" :key="entry.item.uid">
|
||||
<div class="user-item card">
|
||||
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
|
||||
sortMode === 'surname' ?
|
||||
entry.item.cognome + ' ' + entry.item.nome :
|
||||
entry.item.nome + ' ' + entry.item.cognome
|
||||
"></a>
|
||||
<template x-if="entry.item.tags?.includes('macchinista')">
|
||||
<div class="icon" title="Macchinista">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="entry.item.tags?.includes('rappresentante')">
|
||||
<div class="icon" title="Rappresentante">
|
||||
<i class="fas fa-landmark"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template x-if="entry.score">
|
||||
<span x-text="entry.score"></span>
|
||||
</template> -->
|
||||
</div>
|
||||
</template>
|
||||
<div class="spinner" x-ref="spinner" x-show="searchResults.length < searchResultsBuffer.length">
|
||||
<i class="fas fa-hourglass"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
@ -1,113 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
ArticlePath string
|
||||
markdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
|
||||
func (article *Article) HasTag(tag string) bool {
|
||||
for _, t := range article.Tags {
|
||||
if t == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewArticle(articlePath string) (*Article, error) {
|
||||
article := &Article{
|
||||
ArticlePath: articlePath,
|
||||
}
|
||||
|
||||
err := article.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func trimAll(vs []string) []string {
|
||||
r := []string{}
|
||||
for _, v := range vs {
|
||||
r = append(r, strings.TrimSpace(v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (article *Article) load() error {
|
||||
fileBytes, err := os.ReadFile(article.ArticlePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source := string(fileBytes)
|
||||
|
||||
// TODO: Ehm bugia pare che esista "https://github.com/yuin/goldmark-meta" però non penso valga la pena aggiungerlo
|
||||
parts := strings.SplitN(source, "---", 3)[1:]
|
||||
|
||||
frontMatterSource := parts[0]
|
||||
markdownSource := parts[1]
|
||||
|
||||
var frontMatter struct {
|
||||
Id string `yaml:"id"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Tags string `yaml:"tags"`
|
||||
PublishDate string `yaml:"publish_date"`
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
article.Id = frontMatter.Id
|
||||
article.Title = frontMatter.Title
|
||||
article.Description = frontMatter.Description
|
||||
article.Tags = trimAll(strings.Split(frontMatter.Tags, ","))
|
||||
article.PublishDate = publishDate
|
||||
|
||||
article.markdownSource = markdownSource
|
||||
article.renderedHTML = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (article *Article) Render() (string, error) {
|
||||
if config.Mode == "development" {
|
||||
article.load()
|
||||
}
|
||||
|
||||
if article.renderedHTML == "" {
|
||||
var buf bytes.Buffer
|
||||
if err := Markdown.Convert([]byte(article.markdownSource), &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
article.renderedHTML = buf.String()
|
||||
}
|
||||
|
||||
return article.renderedHTML, nil
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
RootPath string
|
||||
ArticleCache map[string]*Article
|
||||
}
|
||||
|
||||
func NewRegistry(rootPath string) *Registry {
|
||||
return &Registry{
|
||||
rootPath,
|
||||
map[string]*Article{},
|
||||
}
|
||||
}
|
||||
|
||||
func (registry *Registry) loadArticles() error {
|
||||
entries, err := os.ReadDir(registry.RootPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
article, err := NewArticle(path.Join(registry.RootPath, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry.ArticleCache[article.Id] = article
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticle(id string) (*Article, error) {
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
return nil, fmt.Errorf(`no article with id "%s"`, id)
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticles() ([]*Article, error) {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
articles := []*Article{}
|
||||
for _, article := range registry.ArticleCache {
|
||||
articles = append(articles, article)
|
||||
}
|
||||
|
||||
sort.Slice(articles, func(i, j int) bool {
|
||||
return articles[i].PublishDate.After(articles[j].PublishDate)
|
||||
})
|
||||
|
||||
return articles, nil
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/formatters/html"
|
||||
mathjax "github.com/litao91/goldmark-mathjax"
|
||||
highlighting "github.com/yuin/goldmark-highlighting"
|
||||
)
|
||||
|
||||
var Markdown goldmark.Markdown
|
||||
|
||||
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
|
||||
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
lang, ok := c.Language()
|
||||
if entering {
|
||||
if ok {
|
||||
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
|
||||
return
|
||||
}
|
||||
w.WriteString("<pre><code>")
|
||||
} else {
|
||||
if ok {
|
||||
w.WriteString("</pre></code>")
|
||||
return
|
||||
}
|
||||
w.WriteString("</pre></code>")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Markdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
extension.Typographer,
|
||||
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
|
||||
mathjax.NewMathJax(),
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("github"),
|
||||
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import preact from '@astrojs/preact'
|
||||
|
||||
import mdx from '@astrojs/mdx'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'github-light',
|
||||
},
|
||||
},
|
||||
integrations: [preact(), mdx()],
|
||||
output: 'static'
|
||||
})
|
@ -1,52 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ErrInvalidSession is thrown when an AuthenticatorService is given a missing
|
||||
// or invalid session token
|
||||
var ErrInvalidSession = fmt.Errorf(`invalid session token`)
|
||||
|
||||
// Service is an authentication service abstraction. When a user is logged in a
|
||||
// new session token is returned, this can be used to read and modify user
|
||||
// properties without having to re-send the user password. (TODO: implement
|
||||
// token renewal)
|
||||
type Service interface {
|
||||
// GetUser retrieves the user info given the username
|
||||
GetUser(username string) (*model.User, error)
|
||||
// GetUsers retrieves the full user list from the authentication service
|
||||
GetUsers() ([]*model.User, error)
|
||||
// GetSession retrieves the user session associated to a session token
|
||||
GetSession(token string) (*model.Session, error)
|
||||
// Login tries to log in a user given username and password and if successful returns a new user session
|
||||
Login(username, password string) (*model.Session, error)
|
||||
}
|
||||
|
||||
// UserForSession returns the user linked to the given session token, this is just a shortcut for calling [AuthenticatorService.GetSession] and then [AuthenticatorService.GetUser]
|
||||
func UserForSession(as Service, token string) (*model.User, error) {
|
||||
session, err := as.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := as.GetUser(session.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// NewDefaultService create an AuthenticatorService given an "host" string,
|
||||
// If host is ":memory:" then this uses the [auth.Memory] implementation,
|
||||
// otherwise for now this defaults to [auth.LDAPAuthService]
|
||||
func NewDefaultService(host string) Service {
|
||||
if host == ":memory:" {
|
||||
return exampleMemoryUsers
|
||||
}
|
||||
|
||||
return newLDAPAuthService(host)
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
urlpkg "net/url"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ldapUser represents an LDAP User, most fields are inherited from [auth.User]
|
||||
type ldapUser struct {
|
||||
Uid string `json:"username"`
|
||||
NumericId int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Gecos string `json:"gecos"`
|
||||
}
|
||||
|
||||
// AsUser converts an [ldapUser] to an instance of [auth.User]
|
||||
func (u ldapUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Uid,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
FullName: u.Gecos,
|
||||
}
|
||||
}
|
||||
|
||||
// ldapAuthService just holds the remote host of the HTTP LDAP service to make requests to
|
||||
type ldapAuthService struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func newLDAPAuthService(host string) Service {
|
||||
return &ldapAuthService{host}
|
||||
}
|
||||
|
||||
// doGetRequest is a utility to make HTTP GET requests
|
||||
func (a *ldapAuthService) doGetRequest(url string, response interface{}) error {
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", u, bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doPostRequest is a utility to make HTTP POST requests
|
||||
func (a *ldapAuthService) doPostRequest(url string, request interface{}, response interface{}) error {
|
||||
jsonStr, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(response)
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetUser(username string) (*model.User, error) {
|
||||
var user ldapUser
|
||||
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetUsers() ([]*model.User, error) {
|
||||
ldapUsers := []*ldapUser{}
|
||||
if err := a.doGetRequest("/users", &ldapUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.User, len(ldapUsers))
|
||||
for i, u := range ldapUsers {
|
||||
users[i] = u.AsUser()
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) GetSession(token string) (*model.Session, error) {
|
||||
var response model.Session
|
||||
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuthService) Login(username, password string) (*model.Session, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
var response model.Session
|
||||
if err := a.doPostRequest("/login", reqBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
var exampleMemoryUsers = &Memory{
|
||||
Users: map[string]*memoryUser{
|
||||
"aziis98": {
|
||||
User: model.User{
|
||||
Username: "aziis98",
|
||||
Name: "Antonio",
|
||||
Surname: "De Lucreziis",
|
||||
Email: "aziis98@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "123",
|
||||
},
|
||||
"bachoseven": {
|
||||
User: model.User{
|
||||
Username: "bachoseven",
|
||||
Name: "Francesco",
|
||||
Surname: "Minnocci",
|
||||
Email: "bachoseven@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "234",
|
||||
},
|
||||
},
|
||||
Sessions: map[string]*memorySession{},
|
||||
}
|
||||
|
||||
type memoryUser struct {
|
||||
model.User
|
||||
Password string `json:"-"`
|
||||
}
|
||||
|
||||
func (u memoryUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
FullName: u.FullName,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
type memorySession struct {
|
||||
Username string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (s memorySession) AsSession() *model.Session {
|
||||
return &model.Session{
|
||||
Username: s.Username,
|
||||
Token: s.Token,
|
||||
}
|
||||
}
|
||||
|
||||
type Memory struct {
|
||||
Users map[string]*memoryUser
|
||||
Sessions map[string]*memorySession
|
||||
}
|
||||
|
||||
func (m *Memory) GetUser(username string) (*model.User, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetUsers() ([]*model.User, error) {
|
||||
users := make([]*model.User, len(m.Users))
|
||||
i := 0
|
||||
for _, u := range m.Users {
|
||||
users[i] = u.AsUser()
|
||||
i++
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetSession(token string) (*model.Session, error) {
|
||||
session, ok := m.Sessions[token]
|
||||
if !ok {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) Login(username string, password string) (*model.Session, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
if user.Password != password {
|
||||
return nil, fmt.Errorf(`invalid credentials`)
|
||||
}
|
||||
|
||||
session := &memorySession{username, util.GenerateRandomString(15)}
|
||||
m.Sessions[session.Token] = session
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
authService := auth.NewDefaultService(config.AuthServiceHost)
|
||||
|
||||
listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
h := &handler.DefaultHandler{
|
||||
AuthService: authService,
|
||||
Renderer: templates.NewRenderer(
|
||||
"./_views/",
|
||||
"./_views/base.html",
|
||||
"./_views/partials/*.html",
|
||||
),
|
||||
NewsArticlesRegistry: articles.NewRegistry("./_content/news"),
|
||||
GuideArticlesRegistry: articles.NewRegistry("./_content/guide"),
|
||||
Storia: &storia.JsonFileStoria{
|
||||
Path: "./_content/storia.yaml",
|
||||
},
|
||||
ListaUtenti: listaUtentiService,
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(h)
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
if err := app.Listen(config.Host); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var Mode string
|
||||
var Host string
|
||||
|
||||
var GitUrl string
|
||||
var ChatUrl string
|
||||
var Email string
|
||||
|
||||
var BaseUrl string
|
||||
|
||||
var UserPagesBaseUrl string
|
||||
|
||||
var AuthServiceHost string
|
||||
|
||||
// ListaUtenti è un file json da cui leggere la lista degli utenti oppure ":auth:" per utilizzare il servizio di autenticazione
|
||||
var ListaUtenti string
|
||||
|
||||
func loadEnv(target *string, name, defaultValue string) {
|
||||
value := os.Getenv(name)
|
||||
if len(strings.TrimSpace(value)) == 0 {
|
||||
*target = defaultValue
|
||||
} else {
|
||||
*target = value
|
||||
}
|
||||
|
||||
log.Printf("%s = %v", name, *target)
|
||||
}
|
||||
|
||||
func Load() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
||||
// Production
|
||||
loadEnv(&Mode, "MODE", "production")
|
||||
loadEnv(&Host, "HOST", ":8080")
|
||||
loadEnv(&BaseUrl, "BASE_URL", "localhost:8080")
|
||||
|
||||
// Services
|
||||
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
|
||||
loadEnv(&ChatUrl, "CHAT_URL", "https://chat.example.org")
|
||||
loadEnv(&Email, "EMAIL", "mail@example.org")
|
||||
|
||||
// Poisson
|
||||
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
|
||||
|
||||
// Auth
|
||||
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
|
||||
|
||||
// Altro
|
||||
loadEnv(&ListaUtenti, "LISTA_UTENTI", ":auth:")
|
||||
}
|
||||
|
||||
func Object() util.Map {
|
||||
return util.Map{
|
||||
"Mode": Mode,
|
||||
"Host": Host,
|
||||
|
||||
"GitUrl": GitUrl,
|
||||
"ChatUrl": ChatUrl,
|
||||
"Email": Email,
|
||||
|
||||
"BaseUrl": BaseUrl,
|
||||
|
||||
"UserPagesBaseUrl": UserPagesBaseUrl,
|
||||
|
||||
"AuthServiceHost": AuthServiceHost,
|
||||
|
||||
"ListaUtenti": ListaUtenti,
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
module git.phc.dm.unipi.it/phc/website
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.9.4
|
||||
github.com/gofiber/fiber/v2 v2.34.0
|
||||
github.com/gofiber/redirect/v2 v2.1.23
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f
|
||||
github.com/yuin/goldmark v1.4.4
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/gorilla/feeds v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/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,78 +0,0 @@
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
|
||||
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
|
||||
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
|
||||
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,21 +0,0 @@
|
||||
package handler
|
||||
|
||||
type ContextKey[T any] string
|
||||
|
||||
type Context map[string]any
|
||||
|
||||
func GetContextValue[T any](ctx Context, key ContextKey[T]) T {
|
||||
value, present := ctx[string(key)]
|
||||
if !present {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
typedValue, _ := value.(T)
|
||||
|
||||
return typedValue
|
||||
}
|
||||
|
||||
func SetContextValue[T any](ctx Context, key ContextKey[T], value T) {
|
||||
ctx[string(key)] = value
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/rss"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
}
|
||||
|
||||
//
|
||||
// Typed context
|
||||
//
|
||||
|
||||
// UserKey is a typed type for *model.User used to extract a user form a [handler.Context]
|
||||
const UserKey ContextKey[*model.User] = "user"
|
||||
|
||||
func (ctx Context) getUser() *model.User {
|
||||
return GetContextValue(ctx, UserKey)
|
||||
}
|
||||
|
||||
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
|
||||
type DefaultHandler struct {
|
||||
AuthService auth.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
ListaUtenti lista_utenti.Service
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
|
||||
return h.Renderer.Render(w, view, util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUtenti() ([]*model.User, error) {
|
||||
utenti, err := h.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleListaUtenti() ([]*model.ListUser, error) {
|
||||
utenti, err := h.ListaUtenti.GetUtenti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return utenti, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
|
||||
storia, err := h.Storia.GetStoria()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "storia.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Storia": storia,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Query": query,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti-condivisi.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuidePage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Articles": articles,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleLogin(username, password string) (*model.Session, error) {
|
||||
session, err := h.AuthService.Login(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleUser(token string) *model.User {
|
||||
user, _ := auth.UserForSession(h.AuthService, token)
|
||||
return user
|
||||
}
|
||||
|
||||
var ErrNoUser = fmt.Errorf(`user not logged in`)
|
||||
|
||||
func (h *DefaultHandler) HandleRequiredUser(ctx Context) (*model.User, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return nil, ErrNoUser
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleProfilePage(w io.Writer, ctx Context) error {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "profilo.html", util.Map{
|
||||
"User": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.NewsArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "news-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error {
|
||||
article, err := h.GuideArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "guide-base.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsFeedPage(w io.Writer) error {
|
||||
registry, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newsFeed := rss.GenerateRssFeed(registry, "news", "Feed Notizie PHC", "https://phc.dm.unipi.it/news", "Le ultime nuove sul PHC.")
|
||||
|
||||
return newsFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
|
||||
registry, err := h.GuideArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guideFeed := rss.GenerateRssFeed(registry, "guide", "Feed Guide PHC", "https://phc.dm.unipi.it/guide", "Le più recenti guide a carattere informatico a cura dei macchinisti del PHC.")
|
||||
|
||||
return guideFeed.WriteRss(w)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type authListaUtenti struct {
|
||||
AuthService auth.Service
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
authUsers, err := a.AuthService.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.ListUser, 0, len(authUsers))
|
||||
for _, u := range authUsers {
|
||||
users = append(users, &model.ListUser{
|
||||
Uid: u.Username,
|
||||
Nome: u.Name,
|
||||
Cognome: u.Surname,
|
||||
Tags: []string{},
|
||||
})
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, a.Macchinisti)
|
||||
mergeRappresentanti(users, a.Rappresentanti)
|
||||
|
||||
return users, nil
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type jsonListaUtenti struct {
|
||||
Path string
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newJsonListaUtenti(path string) (Service, error) {
|
||||
macchinisti, err := loadMacchinisti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
var users []*model.ListUser
|
||||
|
||||
f, err := os.Open(j.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, j.Macchinisti)
|
||||
mergeRappresentanti(users, j.Rappresentanti)
|
||||
|
||||
log.Printf("Caricata lista di %d utenti", len(users))
|
||||
|
||||
return users, nil
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package lista_utenti
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetUtenti() ([]*model.ListUser, error)
|
||||
}
|
||||
|
||||
// New crea un nuovo servizio che lista gli utenti utilizzando AuthService se config è ":auth:" oppure un file json
|
||||
func New(authService auth.Service, config string) (Service, error) {
|
||||
if config == ":auth:" {
|
||||
return newAuthListaUtenti(authService)
|
||||
}
|
||||
|
||||
return newJsonListaUtenti(config)
|
||||
}
|
||||
|
||||
func loadMacchinisti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/macchinisti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macchinisti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&macchinisti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(macchinisti...), nil
|
||||
}
|
||||
|
||||
func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := macchinisti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "macchinista")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRappresentanti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/rappresentanti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(rappresentanti...), nil
|
||||
}
|
||||
|
||||
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := rappresentanti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "rappresentante")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package model
|
||||
|
||||
type ListUser struct {
|
||||
Uid string `json:"uid"`
|
||||
Nome string `json:"nome"`
|
||||
Cognome string `json:"cognome"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user returned from AuthenticatorService
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
// FullName is a separate field from Name and Surname because for example
|
||||
// ldap stores them all as separate fields.
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// WithDefaultFullName is a utility that returns a copy of the given user with the full name set to the concatenation of the name and surname of the user.
|
||||
func (u User) WithDefaultFullName() User {
|
||||
return User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
|
||||
FullName: u.Username + " " + u.Surname,
|
||||
}
|
||||
}
|
||||
|
||||
// Session represents a session returned from AuthenticatorService
|
||||
type Session struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "website",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "run-s astro:sync astro:dev",
|
||||
"build": "run-s astro:build",
|
||||
"astro:sync": "astro sync",
|
||||
"astro:dev": "astro dev",
|
||||
"astro:build": "astro check && astro build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/node": "^8.3.4",
|
||||
"@astrojs/preact": "^3.5.3",
|
||||
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||
"@fontsource/iosevka": "^5.0.11",
|
||||
"@fontsource/mononoki": "^5.0.11",
|
||||
"@fontsource/open-sans": "^5.0.24",
|
||||
"@fontsource/source-code-pro": "^5.0.16",
|
||||
"@fontsource/source-sans-pro": "^5.0.8",
|
||||
"@fontsource/space-mono": "^5.0.20",
|
||||
"@preact/signals": "^1.3.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"astro": "^4.15.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"katex": "^0.16.9",
|
||||
"preact": "^10.19.6",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/mdx": "^3.1.7",
|
||||
"@types/katex": "^0.16.7",
|
||||
"jsdom": "^24.1.1",
|
||||
"linkedom": "^0.18.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"sass": "^1.71.1",
|
||||
"tsx": "^4.7.1"
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="80" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="160" y="50" width="150" height="60" fill="#1E6733" />
|
||||
<rect x="140" y="90" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="140" y="200" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="140" y="410" width="10" height="20" fill="#ECC333" />
|
||||
<rect x="140" y="350" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="240" y="110" width="70" height="10" fill="#ECC333" />
|
||||
<rect x="250" y="130" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="340" y="50" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="340" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="590" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="690" y="180" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="690" y="310" width="60" height="140" fill="#1E6733" />
|
||||
<rect x="690" y="50" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="590" y="320" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="590" y="50" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="420" y="240" width="150" height="60" fill="#1E6733" />
|
||||
<rect x="340" y="320" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="240" y="140" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="350" y="170" width="40" height="10" fill="#ECC333" />
|
||||
<rect x="330" y="330" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="160" y="200" width="80" height="60" fill="#1E6733" />
|
||||
<rect x="650" y="200" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="750" y="330" width="10" height="60" fill="#ECC333" />
|
||||
<rect x="800" y="450" width="40" height="10" fill="#ECC333" />
|
||||
<rect x="850" y="450" width="30" height="10" fill="#ECC333" />
|
||||
<rect x="750" y="90" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="810" y="110" width="60" height="10" fill="#ECC333" />
|
||||
<rect x="580" y="330" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="580" y="60" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4" />
|
||||
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4" />
|
||||
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="350" y="430" width="40" height="10" fill="#303030" />
|
||||
<rect x="350" y="410" width="40" height="10" fill="#303030" />
|
||||
<rect x="350" y="390" width="40" height="10" fill="#303030" />
|
||||
<rect x="700" y="70" width="20" height="40" fill="#303030" />
|
||||
<rect x="700" y="120" width="20" height="40" fill="#303030" />
|
||||
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4" />
|
||||
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4" />
|
||||
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="370" y="70" width="20" height="30" fill="#303030" />
|
||||
<rect x="370" y="110" width="20" height="30" fill="#303030" />
|
||||
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030" />
|
||||
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030" />
|
||||
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030" />
|
||||
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030" />
|
||||
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030" />
|
||||
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030" />
|
||||
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030" />
|
||||
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z"
|
||||
fill="#1E6733" />
|
||||
<rect x="750" y="190" width="10" height="40" fill="#ECC333" />
|
||||
<rect x="750" y="240" width="10" height="20" fill="#ECC333" />
|
||||
<rect x="400" y="200" width="10" height="40" fill="#ECC333" />
|
||||
<rect x="400" y="250" width="10" height="20" fill="#ECC333" />
|
||||
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
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 |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
@ -0,0 +1,8 @@
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
||||
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
@ -1,34 +0,0 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
func GenerateRssFeed(entries []*articles.Article, sectionName string, title string, link string, description string) *feeds.Feed {
|
||||
|
||||
// Initialize RSS Feed
|
||||
feed := &feeds.Feed{
|
||||
Title: title,
|
||||
Link: &feeds.Link{Href: link},
|
||||
Description: description,
|
||||
}
|
||||
|
||||
var feedItems []*feeds.Item
|
||||
|
||||
// Add items to RSS feeds
|
||||
for _, entry := range entries {
|
||||
feedItems = append(feedItems,
|
||||
&feeds.Item{
|
||||
Id: entry.Id,
|
||||
Title: entry.Title,
|
||||
Link: &feeds.Link{Href: config.BaseUrl + "/" + sectionName + "/" + entry.Id},
|
||||
Description: entry.Description,
|
||||
Created: entry.PublishDate,
|
||||
})
|
||||
}
|
||||
feed.Items = feedItems
|
||||
|
||||
return feed
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/redirect/v2"
|
||||
)
|
||||
|
||||
func UserMiddleware(h handler.Service) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Cookies("session-token")
|
||||
user := h.HandleUser(token)
|
||||
c.Locals("user", user)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContext(ctx *fiber.Ctx) handler.Context {
|
||||
// the "_" here is required because Go cannot cast <nil> of type interface{} to *model.User, in other words <nil> of type *model.User and <nil> of type interface{} are different type. In this case the "_" returns a boolean that tells whether the cast succeeded or not, if it is false the user variable gets assigned its default zero value that is <nil> of type *model.User
|
||||
user, _ := ctx.Locals("user").(*model.User)
|
||||
|
||||
context := handler.Context{}
|
||||
handler.SetContextValue(context, handler.UserKey, user)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
func NewFiberServer(h handler.Service) *fiber.App {
|
||||
app := fiber.New()
|
||||
routes(h, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// routes defines routes under "/"
|
||||
func routes(h handler.Service, r fiber.Router) {
|
||||
//
|
||||
// Initial setup
|
||||
//
|
||||
|
||||
r.Use(logger.New())
|
||||
r.Use(recover.New())
|
||||
|
||||
// Remove trailing slash from URLs
|
||||
r.Use(redirect.New(redirect.Config{
|
||||
Rules: map[string]string{
|
||||
"/*/": "/$1",
|
||||
},
|
||||
}))
|
||||
|
||||
// Serve generated css and js files and the static "./_public" folder
|
||||
r.Static("/public/", "./_frontend/out")
|
||||
r.Static("/public/", "./_public")
|
||||
|
||||
// Process all request and add user to the request context if there is a session cookie
|
||||
r.Use(UserMiddleware(h))
|
||||
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
|
||||
r.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "home.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/link", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "link.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/login", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "login.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStaticPage(c, "utenti.html", CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/storia", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleStoriaPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti", func(c *fiber.Ctx) error {
|
||||
query := c.Query("q", "")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleQueryAppunti(c, query, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleAppuntiCondivisiPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleNewsPage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleGuidePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/news/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleNewsFeedPage(c)
|
||||
})
|
||||
|
||||
r.Get("/guide/rss", func(c *fiber.Ctx) error {
|
||||
c.Type("xml")
|
||||
return h.HandleGuideFeedPage(c)
|
||||
})
|
||||
|
||||
r.Post("/login", func(c *fiber.Ctx) error {
|
||||
var loginForm struct {
|
||||
Provider string `form:"provider"`
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&loginForm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := h.HandleLogin(loginForm.Username, loginForm.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: session.Token,
|
||||
Expires: inThreeDays,
|
||||
})
|
||||
|
||||
return c.Redirect("/profilo")
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return h.HandleProfilePage(c, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/logout", func(c *fiber.Ctx) error {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
})
|
||||
|
||||
return c.Redirect("/")
|
||||
})
|
||||
|
||||
r.Get("/news/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleNewsArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/guide/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleGuideArticlePage(c, articleID, CreateContext(c))
|
||||
})
|
||||
|
||||
routesApi(h, r.Group("/api"))
|
||||
}
|
||||
|
||||
// routesApi defines routes under "/api"
|
||||
func routesApi(h handler.Service, r fiber.Router) {
|
||||
r.Get("/utenti", func(c *fiber.Ctx) error {
|
||||
utenti, err := h.HandleListaUtenti()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(utenti)
|
||||
})
|
||||
|
||||
r.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
user, err := h.HandleRequiredUser(CreateContext(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
})
|
||||
}
|
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* image?: string,
|
||||
* course?: string,
|
||||
* title?: string,
|
||||
* author: string,
|
||||
* courseYear: string
|
||||
* }} AppuntiCardProps
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AppuntiCardProps} param0
|
||||
* @returns
|
||||
*/
|
||||
export const AppuntiCard = ({ course, title, author, courseYear }) => {
|
||||
return (
|
||||
<div class="appunti-item">
|
||||
<div class="thumbnail"></div>
|
||||
{title && <div class="title">{title}</div>}
|
||||
{course && <div class="course">{course}</div>}
|
||||
<div class="author">@{author}</div>
|
||||
<div class="course-year">{courseYear}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const AppuntiList = ({ children }) => {
|
||||
return <div class="appunti-list">{children}</div>
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { type ComponentChildren } from 'preact'
|
||||
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||
import { clsx, isMobile } from './lib/util'
|
||||
|
||||
export const ComboBox = ({
|
||||
value,
|
||||
setValue,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
setValue: (s: string) => void
|
||||
children: Record<string, ComponentChildren>
|
||||
}) => {
|
||||
const [cloak, setCloak] = useState(true)
|
||||
const [open, setOpen] = useState(true)
|
||||
const comboRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
const [itemWidth, setItemWidth] = useState<number>(200)
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false)
|
||||
setCloak(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
class="combobox"
|
||||
ref={comboRef}
|
||||
style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}
|
||||
>
|
||||
<div class="selected" onClick={() => setOpen(!open)}>
|
||||
<div class="content">{children[value]}</div>
|
||||
<span class="material-symbols-outlined">expand_more</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
class={clsx('dropdown', cloak && 'invisible')}
|
||||
ref={el => el && setItemWidth(el.offsetWidth)}
|
||||
>
|
||||
{Object.keys(children).map(key => (
|
||||
<div
|
||||
class="option"
|
||||
onClick={() => {
|
||||
setValue(key)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export const Counter = ({}) => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div class="counter">
|
||||
<button onClick={() => setCount(value => value - 1)}>-</button>
|
||||
<div class="value">{count}</div>
|
||||
<button onClick={() => setCount(value => value + 1)}>+</button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||
import type { JSX } from 'preact/jsx-runtime'
|
||||
|
||||
export const ShowMore = <T extends any>({
|
||||
items,
|
||||
pageSize,
|
||||
children,
|
||||
}: {
|
||||
items: ReadonlySignal<T[]>
|
||||
pageSize: number
|
||||
children: (item: T) => JSX.Element
|
||||
}) => {
|
||||
const $shownItems = useSignal(pageSize)
|
||||
|
||||
const $paginatedItems = useComputed(() => {
|
||||
return items.value.slice(0, $shownItems.value)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{$paginatedItems.value.map(children)}
|
||||
<div class="show-more">
|
||||
{$shownItems.value < items.value.length && (
|
||||
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
import { useComputed, useSignal } from '@preact/signals'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { ShowMore } from './Paginate'
|
||||
import { ComboBox } from './ComboBox'
|
||||
|
||||
type User = {
|
||||
uid: string
|
||||
gecos: string
|
||||
}
|
||||
|
||||
const FILTERS = {
|
||||
utenti: {
|
||||
icon: 'person',
|
||||
label: 'Utenti',
|
||||
},
|
||||
macchinisti: {
|
||||
icon: 'construction',
|
||||
label: 'Macchinisti',
|
||||
},
|
||||
rappstud: {
|
||||
icon: 'account_balance',
|
||||
label: 'Rappresentanti',
|
||||
},
|
||||
}
|
||||
|
||||
function applyPatches(users: User[]) {
|
||||
users.forEach(user => {
|
||||
// strip ",+" from the end of the gecos field
|
||||
user.gecos = user.gecos.replace(/,+$/, '')
|
||||
|
||||
// capitalize the first letter of each word
|
||||
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
|
||||
})
|
||||
|
||||
// reverse the order of the users
|
||||
users.reverse()
|
||||
}
|
||||
|
||||
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
|
||||
|
||||
const RAPPSTUD = [
|
||||
'smannella',
|
||||
'lotti',
|
||||
'rotolo',
|
||||
'saccani',
|
||||
'carbone',
|
||||
'mburatti',
|
||||
'ppuddu',
|
||||
'marinari',
|
||||
'evsilvestri',
|
||||
'tateo',
|
||||
'graccione',
|
||||
'dilella',
|
||||
'rocca',
|
||||
'odetti',
|
||||
'borso',
|
||||
'numero',
|
||||
]
|
||||
|
||||
export const UtentiPage = () => {
|
||||
const $utentiData = useSignal<User[]>([])
|
||||
|
||||
const $filter = useSignal('utenti')
|
||||
|
||||
const $filteredData = useComputed(() =>
|
||||
$filter.value === 'macchinisti'
|
||||
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
|
||||
: $filter.value === 'rappstud'
|
||||
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
|
||||
: $utentiData.value
|
||||
)
|
||||
|
||||
const $fuse = useComputed(
|
||||
() =>
|
||||
new Fuse($filteredData.value, {
|
||||
keys: ['gecos', 'uid'],
|
||||
})
|
||||
)
|
||||
|
||||
const $searchText = useSignal('')
|
||||
const $searchResults = useComputed(() =>
|
||||
$searchText.value.trim().length > 0
|
||||
? $fuse.value?.search($searchText.value).map(result => result.item) ?? []
|
||||
: $filteredData.value
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
applyPatches(data)
|
||||
|
||||
$utentiData.value = data
|
||||
|
||||
$fuse.value.setCollection(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="search-bar">
|
||||
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
|
||||
{Object.fromEntries(
|
||||
Object.entries(FILTERS).map(([k, v]) => [
|
||||
k,
|
||||
<>
|
||||
<span class="material-symbols-outlined">{v.icon}</span> {v.label}
|
||||
</>,
|
||||
])
|
||||
)}
|
||||
</ComboBox>
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cerca un utente Poisson..."
|
||||
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||
value={$searchText.value}
|
||||
/>
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results">
|
||||
{$searchResults.value ? (
|
||||
<ShowMore items={$searchResults} pageSize={100}>
|
||||
{poissonUser => (
|
||||
<div class="search-result">
|
||||
<div class="icon">
|
||||
<span class="material-symbols-outlined">
|
||||
{RAPPSTUD.includes(poissonUser.uid)
|
||||
? 'account_balance'
|
||||
: MACCHINISTI.includes(poissonUser.uid)
|
||||
? 'construction'
|
||||
: 'person'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text">{poissonUser.gecos}</div>
|
||||
<div class="right">
|
||||
<a
|
||||
href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}
|
||||
target="_blank"
|
||||
>
|
||||
<span class="material-symbols-outlined">open_in_new</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ShowMore>
|
||||
) : (
|
||||
<>Nessun risultato</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
export const trottleDebounce = <T extends any[], R>(
|
||||
fn: (...args: T) => R,
|
||||
delay: number,
|
||||
options: { leading?: boolean; trailing?: boolean } = {}
|
||||
): ((...args: T) => R | undefined) => {
|
||||
let lastCall = 0
|
||||
let lastResult: R | undefined
|
||||
let lastArgs: T | undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
|
||||
const leading = options.leading ?? true
|
||||
const trailing = options.trailing ?? true
|
||||
|
||||
return (...args: T): R | undefined => {
|
||||
lastArgs = args
|
||||
if (leading && Date.now() - lastCall >= delay) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...args)
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
if (trailing && lastArgs) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...lastArgs)
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
return lastResult
|
||||
}
|
||||
}
|
||||
|
||||
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
|
||||
|
||||
export function clsx(...args: ClassValue[]): string {
|
||||
return args
|
||||
.flatMap(arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return arg
|
||||
} else if (Array.isArray(arg)) {
|
||||
return clsx(...arg)
|
||||
} else if (typeof arg === 'boolean') {
|
||||
return []
|
||||
} else if (typeof arg === 'object') {
|
||||
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const isMobile = () => {
|
||||
const [windowWidth, setWindowWidth] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setWindowWidth(window.innerWidth)
|
||||
|
||||
const handleResize = () => setWindowWidth(window.innerWidth)
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
return windowWidth < 1024
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
---
|
||||
import type { AstroBuiltinProps } from 'astro'
|
||||
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
||||
|
||||
type Props = {
|
||||
large?: boolean
|
||||
style?: string
|
||||
}
|
||||
|
||||
const { large, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<div class:list={['card', large && 'large']} {...props}>
|
||||
<slot />
|
||||
</div>
|