Compare commits
No commits in common. 'next' and 'main' have entirely different histories.
@ -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,10 +1,23 @@
|
|||||||
.env
|
# build output
|
||||||
*.local*
|
|
||||||
bin/
|
|
||||||
|
|
||||||
.out/
|
|
||||||
out/
|
|
||||||
dist/
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
.vscode/
|
# 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
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
# Needed by pnpm to work with "@preact/preset-vite"
|
|
||||||
shamefully-hoist=true
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
export default {
|
||||||
|
printWidth: 120,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: 'consistent',
|
||||||
|
tabWidth: 4,
|
||||||
|
useTabs: false,
|
||||||
|
semi: false,
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
|
||||||
|
plugins: ['prettier-plugin-astro'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '*.astro',
|
||||||
|
options: {
|
||||||
|
parser: 'astro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '*.{yml,yaml,json}',
|
||||||
|
excludeFiles: 'package-lock.json',
|
||||||
|
options: {
|
||||||
|
tabWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"npm.packageManager": "bun",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# Architettura
|
|
||||||
|
|
||||||
Questo è un progetto _fullstack_ con una backend scritta in [Go](https://go.dev/) ed una frontend in JS, più precisamente utilizziamo NodeJS con [Vite](https://vitejs.dev/) per build-are la frontend.
|
|
||||||
|
|
||||||
La cosa più interessante è l'integrazione tra **Vite** e la backend in **Go** per siti con più pagine (il nostro caso). Di base Vite supporta siti con più file html ma ha bisogno che gli venga detto quali sono tutti gli _entrypoint_ HTML. Vedremo che questo progetto usa una tecnica che ci permette di indicare una volta sola le cose nel codice in Go senza stare a tenere sincronizzato il codice in Go e la configurazione di Vite.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Ci sono vari modi per lanciare la nostra applicazione in base all'_environment_, in particolare sono tutti isolati sotto forma di eseguibili in Go:
|
|
||||||
|
|
||||||
- Quando saremo in produzione l'unico server sarà quello di Go lanciato attraverso l'_entry-point_ [./cmd/server/main.go](./cmd/server/main.go)
|
|
||||||
|
|
||||||
In particolare prima di poter lanciare questo server bisogna aver eseguito [./cmd/build/main.go](./cmd/build/main.go) che esegue solo il codice relativo al router della nostra applicazione e genera un file `out/routes.json`. Poi bisogna eseguire `npm run build` che chiama Vite e genera il codice per tutte le route dentro la cartella `out/frontend/`.
|
|
||||||
|
|
||||||
- Quando siamo in development usiamo solo [./cmd/devserver/main.go](./cmd/devserver/main.go) che lancia in background il server di Vite (chiama `npm run dev` che a sua volta è un alias per `node server.js`) quindi possiamo vedere tutto in tempo reale da `localhost:3000`.
|
|
||||||
|
|
||||||
Più precisamente il server di Vite all'avvio richiede al server in Go tutte le route da montare utilizzando la route speciale `/api/development/routes` (in particolare Fiber ed ExpressJS hanno la stessa sintassi per definire le route quindi questa cosa è facile da fare).
|
|
||||||
|
|
||||||
Poi quando si prova ad andare su una pagina ci sono due casi
|
|
||||||
|
|
||||||
- Se la route era **statica** allora leggiamo il file html, lo facciamo processare a Vite e poi lo rimandiamo all'utente.
|
|
||||||
|
|
||||||
- Se invece la route era di tipo **dinamico** allora leggiamo sempre il file e lo processiamo con Vite però ora utilizziamo l'altra route speciale che esiste solo in fase di sviluppo `/api/development/render` che renderizza la pagina applicando il _templating del server_ e poi una volta finito inviamo la pagina al client.
|
|
||||||
|
|
||||||
Invece quando saremo in produzione tutte le pagina saranno già state renderizzate da Vite quindi saremo nel caso standard di _http server_ con views da renderizzare con il _template engine_ del caso prima di mandare la pagina al client.
|
|
||||||
|
|
||||||
- L'ultimo _entrypoint_ è [./cmd/build/main.go](./cmd/build/main.go) e lancia la nostra applicazione in una modalità "finta" senza server http ma vengono comunque registrate tutte le route utilizzando sempre il modulo `dev`. Questo ci permette di costruire l'albero delle route (statiche e dinamiche) che poi servirà a Vite quando faremo `npm run build`.
|
|
||||||
|
|
||||||
Ciò serve perché così ci basta definire tutte le route una volta sola nel Go e poi in automatico funzioneranno anche nel server di Vite senza dover ripetere due volte il codice. (questa è la parte più magica di _meta-programming_ di tutto il progetto)
|
|
||||||
|
|
||||||

|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
|
||||||
PROJECT_DIR="$(shell pwd)" go test -v ./...
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build:
|
|
||||||
go run -v ./cmd/build
|
|
||||||
pnpm run build
|
|
||||||
go build -o ./out/bin/server ./cmd/server
|
|
||||||
@ -1,25 +1,35 @@
|
|||||||
# Website 2
|
# PHC Website
|
||||||
|
|
||||||
Repo per il nuovo sito del PHC
|
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||||
|
|
||||||
## Docs
|
## Installazione
|
||||||
|
|
||||||
- [./ARCHITECTURE.md](./ARCHITECTURE.md)
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
Alcune note sull'architettura di questo progetto.
|
## Sviluppo
|
||||||
|
|
||||||
## Usage
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Build
|
||||||
|
|
||||||
```bash shell
|
```bash
|
||||||
# Starts the backend on port :4000 and the frontend development server on port :3000
|
bun run build
|
||||||
$ go run -v ./cmd/devserver
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
## Deploy
|
||||||
|
|
||||||
```bash shell
|
Per ora c'è un file `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD. Al momento il sito è solo statico e non ha ancora una backend.
|
||||||
# Generates "routes.json", builds all frontend artifacts and finally the main binary
|
|
||||||
$ make build
|
## Come Contribuire
|
||||||
```
|
|
||||||
|
**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ì.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Cose da fare
|
||||||
|
|
||||||
|
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import preact from '@astrojs/preact'
|
||||||
|
|
||||||
|
import mdx from '@astrojs/mdx'
|
||||||
|
|
||||||
|
import yaml from '@rollup/plugin-yaml'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [yaml()],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'github-light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: [
|
||||||
|
preact({
|
||||||
|
compat: true,
|
||||||
|
}),
|
||||||
|
mdx(),
|
||||||
|
],
|
||||||
|
output: 'static',
|
||||||
|
})
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
// sl.Inject[config.Interface](l, &config.EnvConfig{})
|
|
||||||
|
|
||||||
sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue(l, database.Slot, database.Database(
|
|
||||||
&database.Memory{}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := server.Configure(l); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create("out/routes.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(f)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
if err := enc.Encode(dev.UseRoutesMetadata(l)); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf(`generated "out/routes.json"`)
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
cfg := sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "development",
|
|
||||||
Host: ":4000",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
srv, err := server.Configure(l)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Fatal(srv.Router.Listen(cfg.Host))
|
|
||||||
}()
|
|
||||||
|
|
||||||
r, w := io.Pipe()
|
|
||||||
|
|
||||||
cmd := exec.Command("npm", "run", "dev")
|
|
||||||
cmd.Stdout = w
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
cfg := sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "production",
|
|
||||||
Host: ":4000",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
srv, err := server.Configure(l)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatal(srv.Router.Listen(cfg.Host))
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 251 KiB |
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Homepage</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Homepage
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<title>{{ .Title }} • Articoli • PHC</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/styles/main.scss" />
|
|
||||||
<link rel="stylesheet" href="./typography.scss" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Articolo "{{ .Example }}"</h1>
|
|
||||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Neque, quasi...</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<title>Articoli • PHC</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/styles/main.scss" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Articoli</h1>
|
|
||||||
{{ .Example }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
h1 {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>ListaUtenti</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="page-lista-utenti"></main>
|
|
||||||
<script type="module" src="./src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
.list {
|
|
||||||
background: red;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { render } from 'preact'
|
|
||||||
import './lista-utenti.scss'
|
|
||||||
|
|
||||||
render(
|
|
||||||
<>
|
|
||||||
<h1>Lista Utenti</h1>
|
|
||||||
<div class="list">
|
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Odit, illo molestiae. Sapiente cumque saepe maxime, temporibus ad
|
|
||||||
nulla id officiis impedit ut dolorem asperiores voluptate illo, molestiae facilis inventore. Ea.
|
|
||||||
</div>
|
|
||||||
</>,
|
|
||||||
document.querySelector('main')
|
|
||||||
)
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
module git.phc.dm.unipi.it/phc/website
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/alecthomas/repr v0.2.0
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0
|
|
||||||
github.com/joho/godotenv v1.4.0
|
|
||||||
github.com/valyala/fasthttp v1.43.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
|
||||||
github.com/klauspost/compress v1.15.9 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
|
||||||
)
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
|
||||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0 h1:YhNoUS/OTjEz+/WLYuQ01xI7RXgKEFnGBKMagAu5f0M=
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0/go.mod h1:RdebcCuCRFp4W6hr3968/XxwJVg0K+jr9/Ae0PFzZ0Q=
|
|
||||||
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.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
|
||||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
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.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
|
|
||||||
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
|
||||||
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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/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=
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import fetch from 'node-fetch'
|
|
||||||
import { readFile } from 'fs/promises'
|
|
||||||
|
|
||||||
export async function getBuildRoutesMetadata(file) {
|
|
||||||
console.log('Loading routes from disk...')
|
|
||||||
|
|
||||||
const routesRaw = await readFile(file, 'utf8')
|
|
||||||
return JSON.parse(routesRaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDevRoutesMetadata(url) {
|
|
||||||
console.log('Loading routes from go server...')
|
|
||||||
|
|
||||||
const routesReq = await fetch(url)
|
|
||||||
const routes = await routesReq.json()
|
|
||||||
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Id string
|
|
||||||
|
|
||||||
FullName string
|
|
||||||
Nickname string
|
|
||||||
|
|
||||||
AuthSources map[string]AuthSource
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthSource struct {
|
|
||||||
Provider string
|
|
||||||
AuthToken string
|
|
||||||
}
|
|
||||||
@ -1,21 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "1.0.0",
|
"type": "module",
|
||||||
"type": "module",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node server.js",
|
"dev": "run-s astro:sync astro:dev",
|
||||||
"build": "vite build --emptyOutDir"
|
"build": "run-s astro:build",
|
||||||
},
|
"astro:sync": "astro sync",
|
||||||
"devDependencies": {
|
"astro:dev": "astro dev",
|
||||||
"@preact/preset-vite": "^2.5.0",
|
"astro:build": "astro check && astro build"
|
||||||
"axios": "^1.2.6",
|
},
|
||||||
"express": "^4.18.2",
|
"dependencies": {
|
||||||
"morgan": "^1.10.0",
|
"@astrojs/check": "^0.9.4",
|
||||||
"node-fetch": "^3.3.0",
|
"@astrojs/node": "9.0.0",
|
||||||
"sass": "^1.57.1",
|
"@astrojs/preact": "4.0.0",
|
||||||
"vite": "^4.0.4"
|
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||||
},
|
"@fontsource/iosevka": "^5.0.11",
|
||||||
"dependencies": {
|
"@fontsource/mononoki": "^5.0.11",
|
||||||
"preact": "^10.11.3"
|
"@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",
|
||||||
|
"@phosphor-icons/core": "^2.1.1",
|
||||||
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"astro": "5.1.0",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
|
"lucide-static": "^0.468.0",
|
||||||
|
"marked": "^15.0.6",
|
||||||
|
"preact": "^10.19.6",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/mdx": "4.0.2",
|
||||||
|
"@rollup/plugin-yaml": "^4.1.2",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"jsdom": "^24.1.1",
|
||||||
|
"linkedom": "^0.18.4",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^3.5.0",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"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 |
@ -0,0 +1,87 @@
|
|||||||
|
<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.1 KiB |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 1.2 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,93 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { createServer as createViteServer } from 'vite'
|
|
||||||
import { getDevRoutesMetadata } from './meta/routes.js'
|
|
||||||
|
|
||||||
import fetch from 'node-fetch'
|
|
||||||
|
|
||||||
import { readFile } from 'fs/promises'
|
|
||||||
import { dirname, resolve } from 'path'
|
|
||||||
|
|
||||||
import morgan from 'morgan'
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const routes = await getDevRoutesMetadata('http://127.0.0.1:4000/api/development/routes')
|
|
||||||
|
|
||||||
console.log('Found static routes:')
|
|
||||||
for (const [route, file] of Object.entries(routes.static)) {
|
|
||||||
console.log(`- ${route} -> "${file}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found dynamic routes:')
|
|
||||||
for (const [route, file] of Object.entries(routes.dynamic)) {
|
|
||||||
console.log(`- ${route} -> "${file}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'))
|
|
||||||
|
|
||||||
const vite = await createViteServer({
|
|
||||||
server: { middlewareMode: true },
|
|
||||||
appType: 'custom',
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(vite.middlewares)
|
|
||||||
|
|
||||||
for (const [route, file] of Object.entries(routes.static)) {
|
|
||||||
app.get(route, async (req, res) => {
|
|
||||||
console.log(`Requested static route "${route}":`)
|
|
||||||
|
|
||||||
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
|
|
||||||
|
|
||||||
// Replace "./" with the absolute path of the html page
|
|
||||||
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
|
|
||||||
|
|
||||||
console.log(`- applying vite transformations for "${file}"`)
|
|
||||||
const html = await vite.transformIndexHtml(file, htmlPage)
|
|
||||||
|
|
||||||
console.log(`- sending resulting page for "${route}"`)
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [route, file] of Object.entries(routes.dynamic)) {
|
|
||||||
app.get(route, async (req, res) => {
|
|
||||||
console.log(`Requested dynamic route "${route}":`)
|
|
||||||
|
|
||||||
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
|
|
||||||
|
|
||||||
// Replace "./" with the absolute path of the html page
|
|
||||||
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
|
|
||||||
|
|
||||||
console.log(`- applying vite transformations for "${file}"`)
|
|
||||||
const html = await vite.transformIndexHtml(file, htmlPage)
|
|
||||||
|
|
||||||
console.log(`- applying server transformations for "${file}"`)
|
|
||||||
const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
route,
|
|
||||||
page: html,
|
|
||||||
request: {
|
|
||||||
params: req.params,
|
|
||||||
query: req.query,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderedHtml = await templateHtmlReq.json()
|
|
||||||
|
|
||||||
console.log(`- sending resulting page for "${route}"`)
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log(`Listening on port 3000...`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[*Config]()
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Mode string
|
|
||||||
Host string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load(l *sl.ServiceLocator) (*Config, error) {
|
|
||||||
m, err := godotenv.Read(".env")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := "production"
|
|
||||||
if v, ok := m["MODE"]; ok {
|
|
||||||
mode = v
|
|
||||||
}
|
|
||||||
host := ":4000"
|
|
||||||
if v, ok := m["HOST"]; ok {
|
|
||||||
host = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Config{
|
|
||||||
mode,
|
|
||||||
host,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[Database]()
|
|
||||||
|
|
||||||
type Database interface {
|
|
||||||
CreateUser(user model.User) error
|
|
||||||
ReadUser(id string) (model.User, error)
|
|
||||||
ReadUsers() ([]model.User, error)
|
|
||||||
UpdateUser(id string, user model.User) error
|
|
||||||
DeleteUser(id string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Memory struct {
|
|
||||||
Users []model.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) CreateUser(user model.User) error {
|
|
||||||
m.Users = append(m.Users, user)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) ReadUser(id string) (model.User, error) {
|
|
||||||
for _, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) ReadUsers() ([]model.User, error) {
|
|
||||||
return m.Users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) UpdateUser(id string, user model.User) error {
|
|
||||||
for i, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
m.Users[i] = user
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) DeleteUser(id string) error {
|
|
||||||
for i, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
package articles
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/router"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) error {
|
|
||||||
router.UseRouteTemplatedPage(l, "/articles",
|
|
||||||
"pages/articles/index.html",
|
|
||||||
func(w dev.ResponseWriter, r dev.Request) error {
|
|
||||||
tmpl := template.New("")
|
|
||||||
|
|
||||||
tmpl, err := tmpl.Parse(string(r.Page()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := map[string]any{
|
|
||||||
"Example": "Bla bla",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpl.Execute(w, ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
router.UseRouteTemplatedPage(l, "/articles/:slug",
|
|
||||||
"pages/articles/article.html",
|
|
||||||
func(w dev.ResponseWriter, r dev.Request) error {
|
|
||||||
tmpl := template.New("")
|
|
||||||
|
|
||||||
tmpl, err := tmpl.Parse(string(r.Page()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := map[string]any{
|
|
||||||
"Title": r.Param("slug"),
|
|
||||||
"Example": "Bla bla " + r.Param("slug"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpl.Execute(w, ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
package dev
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger is the debug logger, in the future this will be disabled and discard by default.
|
|
||||||
var Logger *log.Logger = log.New(os.Stderr, "[services/server/dev] ", log.Lmsgprefix)
|
|
||||||
|
|
||||||
// slot represents a private "write only" service
|
|
||||||
var slot = sl.NewSlot[*devService]()
|
|
||||||
|
|
||||||
// InjectInto a [*sl.ServiceLocator] an instance of the dev service
|
|
||||||
func InjectInto(l *sl.ServiceLocator) {
|
|
||||||
sl.InjectLazy(l, slot, Configure)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"static": dev.staticRoutes,
|
|
||||||
"dynamic": dev.dynamicRoutes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request interface {
|
|
||||||
Page() []byte
|
|
||||||
Param(key string) string
|
|
||||||
Query(key string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseWriter interface {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// devServerRequest is used when handling request from the dev server where params and queries are parsed by express
|
|
||||||
type devServerRequest struct {
|
|
||||||
page []byte
|
|
||||||
params map[string]string
|
|
||||||
query map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Page() []byte {
|
|
||||||
return r.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Param(key string) string {
|
|
||||||
return r.params[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Query(key string) string {
|
|
||||||
return r.query[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler is a custom routes handler
|
|
||||||
type Handler func(ResponseWriter, Request) error
|
|
||||||
|
|
||||||
type devService struct {
|
|
||||||
staticRoutes map[string]string
|
|
||||||
dynamicRoutes map[string]string
|
|
||||||
|
|
||||||
dynamicRoutesHandlers map[string]Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (*devService, error) {
|
|
||||||
d := &devService{
|
|
||||||
map[string]string{},
|
|
||||||
map[string]string{},
|
|
||||||
map[string]Handler{},
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config, _ := sl.Use(l, config.Slot)
|
|
||||||
if config.Mode != "development" {
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Get("/api/development/routes", func(c *fiber.Ctx) error {
|
|
||||||
return c.JSON(map[string]any{
|
|
||||||
"static": d.staticRoutes,
|
|
||||||
"dynamic": d.dynamicRoutes,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Post("/api/development/render", func(c *fiber.Ctx) error {
|
|
||||||
var data struct {
|
|
||||||
Route string `json:"route"`
|
|
||||||
HtmlPage string `json:"page"`
|
|
||||||
Request struct {
|
|
||||||
ParamsMap map[string]string `json:"params"`
|
|
||||||
QueryMap map[string]string `json:"query"`
|
|
||||||
} `json:"request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(&data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`server rendering route "%s"`, data.Route)
|
|
||||||
Logger.Printf(`- params: %s`, repr.String(data.Request.ParamsMap))
|
|
||||||
Logger.Printf(`- query: %s`, repr.String(data.Request.QueryMap))
|
|
||||||
|
|
||||||
handler, ok := d.dynamicRoutesHandlers[data.Route]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(`no handler for "%s"`, data.Route)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := handler(&buf, devServerRequest{
|
|
||||||
[]byte(data.HtmlPage),
|
|
||||||
data.Request.ParamsMap,
|
|
||||||
data.Request.QueryMap,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(buf.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page
|
|
||||||
func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.staticRoutes[mountPoint] = frontendFile
|
|
||||||
|
|
||||||
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterDynamicRoute(l *sl.ServiceLocator, mountPoint, frontendFile string, handler Handler) {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.dynamicRoutes[mountPoint] = frontendFile
|
|
||||||
dev.dynamicRoutesHandlers[mountPoint] = handler
|
|
||||||
|
|
||||||
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetArtifactPath(frontendFile string) string {
|
|
||||||
return path.Join("./out/frontend/", frontendFile)
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package listautenti
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/router"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) error {
|
|
||||||
router.UseRoutePage(l, "/utenti", "pages/lista-utenti/index.html")
|
|
||||||
|
|
||||||
db, err := sl.Use(l, database.Slot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Get("/api/lista-utenti", func(c *fiber.Ctx) error {
|
|
||||||
users, err := db.ReadUsers()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
package listautenti_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
"github.com/valyala/fasthttp/fasthttputil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test1(t *testing.T) {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
r := fiber.New()
|
|
||||||
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := fasthttp.Serve(ln, r.Handler())
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to serve: %v", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return ln.Dial()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log(string(body))
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// assert type of [ServerRequest] is [dev.Request]
|
|
||||||
var _ dev.Request = ServerRequest{}
|
|
||||||
|
|
||||||
// ServerRequest is used when the request is directly for the Go server
|
|
||||||
type ServerRequest struct {
|
|
||||||
page []byte
|
|
||||||
fiberContext *fiber.Ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ServerRequest) Page() []byte {
|
|
||||||
return r.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx ServerRequest) Param(key string) string {
|
|
||||||
return ctx.fiberContext.Params(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx ServerRequest) Query(key string) string {
|
|
||||||
return ctx.fiberContext.Query(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRoutePage(l *sl.ServiceLocator, route, frontendFile string) {
|
|
||||||
root, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.RegisterRoute(l, route, frontendFile)
|
|
||||||
|
|
||||||
root.Get(route, func(c *fiber.Ctx) error {
|
|
||||||
return c.SendFile(dev.GetArtifactPath(frontendFile))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRouteTemplatedPage(l *sl.ServiceLocator, route, frontendFile string, handler dev.Handler) {
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.RegisterDynamicRoute(l, route, frontendFile, handler)
|
|
||||||
|
|
||||||
r.Get(route, func(c *fiber.Ctx) error {
|
|
||||||
rawPage, err := os.ReadFile(dev.GetArtifactPath(frontendFile))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := handler(&buf, ServerRequest{
|
|
||||||
rawPage,
|
|
||||||
c,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Type(".html").Send(buf.Bytes())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Root = sl.NewSlot[fiber.Router]()
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/articles"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/listautenti"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct{ Router *fiber.App }
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (*Server, error) {
|
|
||||||
r := fiber.New(fiber.Config{})
|
|
||||||
r.Static("/assets", "./out/frontend/assets")
|
|
||||||
|
|
||||||
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
|
||||||
dev.InjectInto(l)
|
|
||||||
|
|
||||||
if err := listautenti.Configure(l); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := articles.Configure(l); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Server{r}, nil
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
// The [sl] package has two main concepts, the [ServiceLocator] itself is the main object that one should pass around through the application. A [ServiceLocator] has a list of slots that can be filled with [InjectLazy] and [InjectValue] and retrieved with [Use]. As slots should be unique they can only be created with the [NewSlot] function.
|
|
||||||
//
|
|
||||||
// The usual way to use this module is to make slots for go interfaces and then pass implementations using the [InjectValue] and [InjectLazy] functions.
|
|
||||||
//
|
|
||||||
// Services can be of various types:
|
|
||||||
// - a service with no dependencies can be directly injected inside a ServiceLocator using [InjectValue].
|
|
||||||
// - a service with dependencies on other service should use [InjectLazy]. This lets the service to initialize itself when required and makes the developer not think the topological sort to put onto the DAG of service dependencies.
|
|
||||||
// - a service can also be private, in this case the slot for a service should be a private field in the service package. This kind of services should also provide a way to inject them into a ServiceLocator.
|
|
||||||
// - a package also just provide a slot. This is useful for using the ServiceLocator to easily pass around values, effectively threating slots just as dynamically scoped variables.
|
|
||||||
package sl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger is the debug logger, in the future this will be disabled and discard by default.
|
|
||||||
//
|
|
||||||
// As this is the service locator module it was meaning less to pass this through the ServiceLocator itself (without making the whole module more complex)
|
|
||||||
var Logger *log.Logger = log.New(os.Stderr, "[sl] ", log.Lmsgprefix)
|
|
||||||
|
|
||||||
// slot is just a "typed" unique "symbol".
|
|
||||||
type slot[T any] *struct{}
|
|
||||||
|
|
||||||
// NewSlot is the only way to create instances of the slot type. Each instance is unique.
|
|
||||||
//
|
|
||||||
// This then lets you attach a service instance of type "T" to a [ServiceLocator] object.
|
|
||||||
func NewSlot[T any]() slot[T] {
|
|
||||||
return slot[T](new(struct{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// slotEntry represents a service that can lazily initialized (using "createFunc"). Once initialized the instance is kept in the "value" field. The field "typeName" just for debugging purposes.
|
|
||||||
type slotEntry struct {
|
|
||||||
createFunc func(*ServiceLocator) (any, error)
|
|
||||||
created bool
|
|
||||||
value any
|
|
||||||
|
|
||||||
typeName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *slotEntry) checkInitialized(l *ServiceLocator) error {
|
|
||||||
if !s.created {
|
|
||||||
v, err := s.createFunc(l)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`initialized lazy value of type %T for slot of type %s`, v, s.typeName)
|
|
||||||
|
|
||||||
s.created = true
|
|
||||||
s.value = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceLocator is the main context passed around to retrive service instances, the interface uses generics so to inject and retrive service instances you should use the functions [InjectValue], [InjectLazy] and [Use].
|
|
||||||
type ServiceLocator struct {
|
|
||||||
providers map[any]*slotEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new [ServiceLocator] context to pass around in the application.
|
|
||||||
func New() *ServiceLocator {
|
|
||||||
return &ServiceLocator{
|
|
||||||
providers: map[any]*slotEntry{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InjectValue will inject a concrete instance inside the ServiceLocator "l" for the given "slotKey". This should be used for injecting "static" services, for instances whose construction depend on other services you should use the [InjectLazy] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
|
|
||||||
func InjectValue[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
|
||||||
Logger.Printf(`injected value of type %T for slot of type %s`, value, getTypeName[T]())
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
nil,
|
|
||||||
true,
|
|
||||||
value,
|
|
||||||
getTypeName[T](),
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// InjectLazy will inject an instance inside the given ServiceLocator and "slotKey" that is created only when requested with a call to the [Use] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
|
|
||||||
func InjectLazy[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
|
||||||
Logger.Printf(`injected lazy for slot of type %s`, getTypeName[T]())
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
createFunc: func(l *ServiceLocator) (any, error) {
|
|
||||||
return createFunc(l)
|
|
||||||
},
|
|
||||||
created: false,
|
|
||||||
value: nil,
|
|
||||||
typeName: getTypeName[T](),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use retrieves the value of type T associated with the given slot key from the provided ServiceLocator instance.
|
|
||||||
//
|
|
||||||
// If the ServiceLocator does not have a value for the slot key, or if the value wasn't correctly initialized (in the case of a lazy slot), an error is returned.
|
|
||||||
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
|
||||||
var zero T
|
|
||||||
|
|
||||||
slot, ok := l.providers[slotKey]
|
|
||||||
if !ok {
|
|
||||||
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
|
||||||
}
|
|
||||||
|
|
||||||
err := slot.checkInitialized(l)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := slot.value.(T)
|
|
||||||
|
|
||||||
Logger.Printf(`using slot of type %s with value of type %T`, getTypeName[T](), v)
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTypeName is a trick to get the name of a type (even if it is an interface type)
|
|
||||||
func getTypeName[T any]() string {
|
|
||||||
var zero T
|
|
||||||
return fmt.Sprintf(`%T`, &zero)[1:]
|
|
||||||
}
|
|
||||||
|
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: 1.2 MiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 645 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 295 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,60 @@
|
|||||||
|
import { type ComponentChildren } from 'preact'
|
||||||
|
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||||
|
import { clsx, isMobile } from './lib/util'
|
||||||
|
import { PhosphorIcon } from './Icon'
|
||||||
|
|
||||||
|
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> */}
|
||||||
|
<PhosphorIcon name="caret-down" />
|
||||||
|
</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,119 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { Funnel } from '@phosphor-icons/react'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
import extendedLatex from '@/client/lib/marked-latex'
|
||||||
|
|
||||||
|
marked.use(
|
||||||
|
extendedLatex({
|
||||||
|
lazy: false,
|
||||||
|
render: (formula: string, display: boolean) => {
|
||||||
|
return display ? '$$' + formula + '$$' : '$' + formula + '$'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
import type { Database } from '@/data/domande-esami.yaml'
|
||||||
|
|
||||||
|
const useRemoteValue = <T,>(url: string): T | null => {
|
||||||
|
const [value, setValue] = useState<T | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(value => setValue(value))
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
course: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DomandeEsamiCourse = ({ course }: Props) => {
|
||||||
|
const database = useRemoteValue<Database>(`/domande-esami/api/${course}.json`)
|
||||||
|
if (!database) {
|
||||||
|
return <>Loading...</>
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
// @ts-ignore
|
||||||
|
requestIdleCallback(() => window.renderMath())
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
setTimeout(() => window.renderMath(), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseTags = [
|
||||||
|
...new Set(
|
||||||
|
database.questions.filter(question => question.course === course).flatMap(question => question.tags),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const [selectedTag, setSelectedTag] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const filteredQuestions = database.questions
|
||||||
|
.filter(question => question.course === course)
|
||||||
|
.filter(question => (selectedTag ? question.tags.includes(selectedTag) : true))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="grid-center text-center">
|
||||||
|
<h3>
|
||||||
|
<a href="/domande-esami">Domande Orali</a>
|
||||||
|
</h3>
|
||||||
|
<h1>{database.names[course]}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{courseTags.length > 1 && (
|
||||||
|
<div class="card filter">
|
||||||
|
<div class="grid-h">
|
||||||
|
<Funnel />
|
||||||
|
<strong>Filtra Tag</strong>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-wrap">
|
||||||
|
{!selectedTag
|
||||||
|
? courseTags.map(tag => (
|
||||||
|
<div class="chip clickable" onClick={() => setSelectedTag(tag)}>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: courseTags.map(tag => (
|
||||||
|
<div
|
||||||
|
class={tag === selectedTag ? 'chip clickable' : 'chip clickable disabled'}
|
||||||
|
onClick={() => setSelectedTag(tag === selectedTag ? null : tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="wide-card-list" id="questions">
|
||||||
|
{filteredQuestions.length === 0 ? (
|
||||||
|
<div class="grid-center">
|
||||||
|
<em>No questions found</em>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredQuestions.map(question => (
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
class="text"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked(question.content, { async: false }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="metadata">
|
||||||
|
{question.tags.map(tag => (
|
||||||
|
<div class="chip small">{tag}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
const icons = Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`, {
|
||||||
|
eager: true,
|
||||||
|
}),
|
||||||
|
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhosphorIcon = ({ name }: Props) => {
|
||||||
|
const icon = icons[name]
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
throw new Error(`Icon "${name}" not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img class="phosphor-icon" src={icon.default.src} alt={name} />
|
||||||
|
}
|
||||||
@ -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,166 @@
|
|||||||
|
import { useComputed, useSignal } from '@preact/signals'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
import { ShowMore } from './Paginate'
|
||||||
|
import { ComboBox } from './ComboBox'
|
||||||
|
import { PhosphorIcon } from './Icon'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
uid: string
|
||||||
|
gecos: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
utenti: {
|
||||||
|
icon: 'user',
|
||||||
|
label: 'Utenti',
|
||||||
|
},
|
||||||
|
macchinisti: {
|
||||||
|
icon: 'wrench',
|
||||||
|
label: 'Macchinisti',
|
||||||
|
},
|
||||||
|
rappstud: {
|
||||||
|
icon: 'bank',
|
||||||
|
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} */}
|
||||||
|
<PhosphorIcon name={v.icon} />
|
||||||
|
{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> */}
|
||||||
|
<PhosphorIcon name="magnifying-glass" />
|
||||||
|
</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> */}
|
||||||
|
<PhosphorIcon
|
||||||
|
name={
|
||||||
|
RAPPSTUD.includes(poissonUser.uid)
|
||||||
|
? 'bank'
|
||||||
|
: MACCHINISTI.includes(poissonUser.uid)
|
||||||
|
? 'wrench'
|
||||||
|
: 'user'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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> */}
|
||||||
|
<PhosphorIcon name="arrow-square-out" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ShowMore>
|
||||||
|
) : (
|
||||||
|
<>Nessun risultato</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
const $debugConsole = document.createElement('div')
|
||||||
|
|
||||||
|
$debugConsole.style.position = 'fixed'
|
||||||
|
$debugConsole.style.bottom = '0'
|
||||||
|
$debugConsole.style.left = '0'
|
||||||
|
$debugConsole.style.width = '100%'
|
||||||
|
$debugConsole.style.height = '25vh'
|
||||||
|
$debugConsole.style.backgroundColor = 'black'
|
||||||
|
$debugConsole.style.color = 'white'
|
||||||
|
$debugConsole.style.overflow = 'auto'
|
||||||
|
$debugConsole.style.padding = '10px'
|
||||||
|
$debugConsole.style.boxSizing = 'border-box'
|
||||||
|
$debugConsole.style.fontFamily = 'monospace'
|
||||||
|
$debugConsole.style.zIndex = '9999'
|
||||||
|
$debugConsole.style.fontSize = '15px'
|
||||||
|
$debugConsole.style.opacity = '0.8'
|
||||||
|
|
||||||
|
document.body.appendChild($debugConsole)
|
||||||
|
|
||||||
|
function logDebugConsole(...args) {
|
||||||
|
$debugConsole.innerHTML += args.join(' ') + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error = logDebugConsole
|
||||||
|
console.warn = logDebugConsole
|
||||||
|
console.log = logDebugConsole
|
||||||
|
console.debug = logDebugConsole
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
// took from: https://github.com/sxyazi/marked-extended-latex
|
||||||
|
// this has a peer dependency bug
|
||||||
|
|
||||||
|
const CLASS_NAME = 'latex-b172fea480b'
|
||||||
|
|
||||||
|
const extBlock = options => ({
|
||||||
|
name: 'latex-block',
|
||||||
|
level: 'block',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\$\$[^\$]/)?.index ?? -1
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = /^\$\$([^\$]+)\$\$/.exec(src)
|
||||||
|
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
if (!options.lazy) return options.render(token.formula, true)
|
||||||
|
return `<span class="${CLASS_NAME}" block>${token.formula}</span>`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const extInline = options => ({
|
||||||
|
name: 'latex',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\$[^\$]/)?.index ?? -1
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = /^\$([^\$]+)\$/.exec(src)
|
||||||
|
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
if (!options.lazy) return options.render(token.formula, false)
|
||||||
|
return `<span class="${CLASS_NAME}">${token.formula}</span>`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let observer
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export default (options = {}) => {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (options.lazy && options.env !== 'test') {
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries, self) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = entry.target
|
||||||
|
self.unobserve(span)
|
||||||
|
|
||||||
|
Promise.resolve(options.render(span.innerText, span.hasAttribute('block'))).then(html => {
|
||||||
|
span.innerHTML = html
|
||||||
|
})
|
||||||
|
span.classList.add('latex-rendered')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
extensions: [extBlock(options), extInline(options)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const observe = () => {
|
||||||
|
if (!observer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.disconnect()
|
||||||
|
document.querySelectorAll(`span.${CLASS_NAME}:not(.latex-rendered)`).forEach(span => {
|
||||||
|
observer.observe(span)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const disconnect = () => {
|
||||||
|
observer?.disconnect()
|
||||||
|
}
|
||||||
@ -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,50 @@
|
|||||||
|
---
|
||||||
|
import PhosphorIcon from './PhosphorIcon.astro'
|
||||||
|
|
||||||
|
const ICONS_MAP: Record<string, string> = {
|
||||||
|
github: 'github-logo',
|
||||||
|
linkedin: 'linkedin-logo',
|
||||||
|
website: 'globe',
|
||||||
|
mail: 'mailbox',
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fullName: string
|
||||||
|
description: string
|
||||||
|
|
||||||
|
image: ImageMetadata
|
||||||
|
|
||||||
|
entranceDate: number
|
||||||
|
exitDate?: number
|
||||||
|
|
||||||
|
founder?: boolean
|
||||||
|
|
||||||
|
social?: {
|
||||||
|
github?: string
|
||||||
|
linkedin?: string
|
||||||
|
website?: string
|
||||||
|
mail?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fullName, description, image, entranceDate, exitDate, founder, social } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="bubble">
|
||||||
|
<img src={image.src} alt={fullName.toLowerCase()} />
|
||||||
|
<div class="title">{fullName}</div>
|
||||||
|
<div class="date">{entranceDate}—{exitDate ?? 'Presente'}</div>
|
||||||
|
{founder && <div class="founder">Fondatore</div>}
|
||||||
|
<div class="description">{description}</div>
|
||||||
|
{
|
||||||
|
social && (
|
||||||
|
<div class="social">
|
||||||
|
{Object.entries(social).map(([key, value]) => (
|
||||||
|
<a href={value} target="_blank" rel="noopener noreferrer">
|
||||||
|
<PhosphorIcon name={ICONS_MAP[key] ?? 'question-mark'} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
large?: boolean
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { large, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['card', large && 'large']} {...props}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<footer>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
||||||
|
>macchinisti@lists.dm.unipi.it</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
const links = [
|
||||||
|
{ href: '/utenti', text: 'Utenti' },
|
||||||
|
// { href: '/macchinisti', text: 'Macchinisti' },
|
||||||
|
// { href: '/appunti', text: 'Appunti' },
|
||||||
|
{ href: '/notizie', text: 'Notizie' },
|
||||||
|
{ href: '/guide', text: 'Guide' },
|
||||||
|
{ href: '/domande-esami', text: 'Domande Orali' },
|
||||||
|
{ href: '/storia', text: 'Storia' },
|
||||||
|
// { href: '/login', text: 'Login' },
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<!-- main logo on the left -->
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- hidden checkbox for mobile js-less sidebar interaction -->
|
||||||
|
<input type="checkbox" id="header-menu-toggle" />
|
||||||
|
|
||||||
|
<!-- desktop navbar links -->
|
||||||
|
<div class="links desktop-only">
|
||||||
|
{
|
||||||
|
links.map(link => (
|
||||||
|
<a role="button" href={link.href}>
|
||||||
|
{link.text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sidebar menu for mobile -->
|
||||||
|
<div class="mobile-only">
|
||||||
|
<label id="header-menu-toggle-menu" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">menu</span>
|
||||||
|
</label>
|
||||||
|
<label id="header-menu-toggle-close" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sidebar menu only visible on mobile when #header-menu-toggle is checked -->
|
||||||
|
<div class="side-menu">
|
||||||
|
<div class="links">
|
||||||
|
{
|
||||||
|
links.map(link => (
|
||||||
|
<a role="button" href={link.href}>
|
||||||
|
{link.text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { color } = Astro.props
|
||||||
|
|
||||||
|
const patternId = 'zig-zag-' + color.slice(1)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={patternId} x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill={color} d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill={`url(#${patternId})`} x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = Astro.props
|
||||||
|
|
||||||
|
const icons = Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`),
|
||||||
|
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!icons[name]) {
|
||||||
|
throw new Error(`Icon "${name}" not found`)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Image class="phosphor-icon" src={icons[name]()} alt={name} />
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
// Card.astro
|
||||||
|
export interface Props {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
|
||||||
|
imgSrc?: string
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, imgSrc, style, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a target="_blank" href={href} style={style}>
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
|
||||||
|
</div>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<div class="description">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
const { year, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">{year} • {title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { JSDOM } from 'jsdom'
|
||||||
|
import Container from './Container.astro'
|
||||||
|
|
||||||
|
const language = Astro.props['data-language'] ?? 'text'
|
||||||
|
|
||||||
|
const html = await Astro.slots.render('default')
|
||||||
|
|
||||||
|
const rawCode = new JSDOM(html).window.document.body.textContent
|
||||||
|
---
|
||||||
|
|
||||||
|
<pre {...Astro.props}><slot /></pre>
|
||||||
|
|
||||||
|
{language === 'astro' && <Container set:html={rawCode} />}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
const { size, ...rest } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['container', size ?? 'normal']} {...rest}>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
colors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { colors } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="palette">
|
||||||
|
{
|
||||||
|
colors.map(value => (
|
||||||
|
<>
|
||||||
|
<div class="color">
|
||||||
|
<div class="region" style={{ backgroundColor: value }} />
|
||||||
|
</div>
|
||||||
|
<div class="label">{value}</div>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { z, defineCollection } from 'astro:content'
|
||||||
|
|
||||||
|
// Una notizia ha una data di pubblicazione ma non ha un autore in quanto sono
|
||||||
|
// notizie generiche sul PHC e non sono scritte da un autore specifico
|
||||||
|
const newsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
image: z
|
||||||
|
.object({
|
||||||
|
url: z.string(),
|
||||||
|
alt: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Una guida ha un autore ma non ha una data di pubblicazione in quanto è un
|
||||||
|
// contenuto statico e non è importante sapere quando è stata pubblicata
|
||||||
|
const guidesCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
series: z.string().optional(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Per ora sono su un sito a parte ma prima o poi verranno migrati qui
|
||||||
|
const seminariettiCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
eventDate: z.date(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const metaCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.any(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Export a single `collections` object to register your collection(s)
|
||||||
|
export const collections = {
|
||||||
|
news: newsCollection,
|
||||||
|
guides: guidesCollection,
|
||||||
|
seminarietti: seminariettiCollection,
|
||||||
|
meta: metaCollection,
|
||||||
|
}
|
||||||
@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
id: git-101
|
||||||
|
title: Git 101
|
||||||
|
description: Una guida 📚 introduttiva alle basi di Git
|
||||||
|
author: Luca Lombardo
|
||||||
|
tags: [git, gitea]
|
||||||
|
---
|
||||||
|
|
||||||
|
Git è un sistema di controllo di versione distribuito creato per gestire progetti di qualsiasi dimensione, mantenendo traccia delle modifiche al codice sorgente. Questa guida ci accompagnerà dai concetti di base fino alle funzionalità avanzate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Introduzione a Git**
|
||||||
|
|
||||||
|
### **Cos'è Git?**
|
||||||
|
|
||||||
|
- **Sistema di controllo di versione**: Gestisce le modifiche al codice sorgente nel tempo.
|
||||||
|
|
||||||
|
- **Distribuito**: Ogni sviluppatore ha una copia del repository.
|
||||||
|
|
||||||
|
- **Veloce e leggero**: Ottimizzato per la velocità e le prestazioni.
|
||||||
|
|
||||||
|
### **Perché usare Git?**
|
||||||
|
|
||||||
|
- **Tracciabilità**: Ogni modifica è tracciata e reversibile.
|
||||||
|
|
||||||
|
- **Collaborazione**: Più persone possono lavorare sullo stesso progetto.
|
||||||
|
|
||||||
|
- **Backup**: Repository remoto per il backup del codice.
|
||||||
|
|
||||||
|
- **Branching**: Lavoriamo su nuove funzionalità senza influenzare il codice principale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Installazione**
|
||||||
|
|
||||||
|
> Se ci troviamo al dipartimento di matematica a Pisa, è già installato su tutte le macchine dell'aula 3 ed aula 4!
|
||||||
|
|
||||||
|
### **Windows**
|
||||||
|
|
||||||
|
1. Scarichiamo [Git for Windows](https://git-scm.com/download/win).
|
||||||
|
|
||||||
|
2. Seguiamo il wizard di installazione.
|
||||||
|
|
||||||
|
3. Durante l'installazione:
|
||||||
|
|
||||||
|
- Selezioniamo "Git Bash" come terminale.
|
||||||
|
|
||||||
|
- Configuriamo un editor di testo (es. Vim o Nano).
|
||||||
|
|
||||||
|
### **macOS**
|
||||||
|
|
||||||
|
1. Usiamo `brew` per installare Git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install git
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Linux**
|
||||||
|
|
||||||
|
1. Installiamo Git usando il nostro gestore di pacchetti:
|
||||||
|
|
||||||
|
- **Debian/Ubuntu**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install git
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Arch Linux**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S git
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Configurazione iniziale**
|
||||||
|
|
||||||
|
Una volta installato, configuriamo Git con il nostro nome e indirizzo email:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global user.name "Il Nostro Nome"
|
||||||
|
git config --global user.email "nostro@email.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verifica configurazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **4. Concetti fondamentali**
|
||||||
|
|
||||||
|
### **Repository**
|
||||||
|
|
||||||
|
- **Repository locale**: Una cartella sul nostro computer che contiene il nostro progetto.
|
||||||
|
|
||||||
|
- **Repository remoto**: Una versione del progetto ospitata su un server (es. GitHub, GitLab).
|
||||||
|
|
||||||
|
### **Branch**
|
||||||
|
|
||||||
|
Un ramo permette di lavorare su modifiche isolate rispetto al codice principale (branch `main` o `master`).
|
||||||
|
|
||||||
|
### **Commit**
|
||||||
|
|
||||||
|
Una snapshot del nostro codice in un determinato momento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **5. Creazione e gestione di un repository**
|
||||||
|
|
||||||
|
### **Inizializzare un nuovo repository**
|
||||||
|
|
||||||
|
Se stiamo iniziando un nuovo progetto, possiamo creare un nuovo repository con il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Clonare un repository esistente**
|
||||||
|
|
||||||
|
Se invece vogliamo lavorare su un progetto esistente, possiamo clonare da remoto con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **6. Lavorare con Git**
|
||||||
|
|
||||||
|
### **Aggiungere file**
|
||||||
|
|
||||||
|
Aggiungiamo file allo stage per includerli nel prossimo commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <nome-file>
|
||||||
|
# Oppure, per aggiungere tutti i file:
|
||||||
|
git add .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Commit**
|
||||||
|
|
||||||
|
Il comando **`git commit`** è utilizzato per registrare le modifiche nel repository locale. Ogni commit è una snapshot del progetto, contenente tutte le modifiche che sono state aggiunte tramite `git add`.
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. **Aggiungiamo modifiche all'area di staging**:
|
||||||
|
Prima di fare un commit, dobbiamo aggiungere i file che vogliamo includere al prossimo commit usando `git add`. Questo comando prepara i file per essere salvati nella cronologia del repository.
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <nome-file>
|
||||||
|
# Oppure per aggiungere tutti i file modificati
|
||||||
|
git add .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Effettuiamo il commit**:
|
||||||
|
Una volta che i file sono nell'area di staging, possiamo fare un commit. Ogni commit dovrebbe avere un messaggio descrittivo che spieghi cosa è stato cambiato nel progetto.
|
||||||
|
|
||||||
|
Comando per fare un commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "Descrizione chiara del cambiamento"
|
||||||
|
```
|
||||||
|
|
||||||
|
L'opzione `-m` permette di aggiungere il messaggio direttamente dalla linea di comando. Se omettiamo `-m`, Git aprirà un editor di testo per scrivere il messaggio di commit.
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- Git salva lo stato dei file nell'area di staging in un commit, che viene aggiunto alla cronologia del repository locale.
|
||||||
|
|
||||||
|
- Ogni commit ha un identificatore unico (hash) che consente di risalire facilmente alle modifiche in qualsiasi momento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Push**
|
||||||
|
|
||||||
|
**`git push`** è il comando che ci permette di inviare le modifiche dal nostro repository locale a un repository remoto (ad esempio su GitHub, GitLab, Bitbucket, ecc.).
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. Dopo aver fatto uno o più commit locali, dobbiamo inviare queste modifiche al repository remoto.
|
||||||
|
|
||||||
|
2. Per fare questo, usiamo il comando **`git push`** seguito dal nome del remoto (di solito `origin` per il repository remoto di default) e dal nome del branch (di solito `main` o `master`, ma potrebbe essere qualsiasi altro nome di branch che stiamo utilizzando).
|
||||||
|
|
||||||
|
Comando per inviare le modifiche:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- Git confronta il nostro branch locale con il branch remoto. Se ci sono nuovi commit nel branch remoto che non sono ancora nel nostro branch locale, ci verrà richiesto di fare un **pull** per aggiornare prima di fare il push.
|
||||||
|
|
||||||
|
- Il nostro repository locale viene sincronizzato con il remoto, rendendo le modifiche visibili a tutti gli altri che hanno accesso al repository remoto.
|
||||||
|
|
||||||
|
#### Errori comuni:
|
||||||
|
|
||||||
|
- Se il repository remoto è stato aggiornato nel frattempo da qualcun altro (ad esempio, con un altro push), riceveremo un errore che ci avvisa che dobbiamo fare prima un `git pull` per sincronizzare il nostro lavoro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Pull**
|
||||||
|
|
||||||
|
**`git pull`** è il comando che ci permette di scaricare e integrare le modifiche dal repository remoto al nostro repository locale. È una combinazione di due comandi: **`git fetch`** (scarica i cambiamenti dal remoto) e **`git merge`** (integra questi cambiamenti nel nostro branch attuale).
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. Se altri collaboratori hanno fatto modifiche al repository remoto, possiamo ottenere queste modifiche con **`git pull`**. Questo comando aggiorna il nostro branch locale con le modifiche più recenti dal repository remoto.
|
||||||
|
|
||||||
|
2. Eseguiamo il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo caso, `origin` è il nome del repository remoto (il nome predefinito quando cloni un repository), e `main` è il branch che vogliamo aggiornare.
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- **`git fetch`** scarica tutte le modifiche dal repository remoto, ma non le integra ancora nel nostro codice.
|
||||||
|
|
||||||
|
- **`git merge`** unisce le modifiche scaricate al nostro branch attuale, risolvendo eventuali conflitti, se necessario.
|
||||||
|
|
||||||
|
#### Errori comuni:
|
||||||
|
|
||||||
|
- Se ci sono conflitti tra il nostro lavoro e quello degli altri, Git ci avviserà che dovremo risolverli manualmente. Dopo aver risolto i conflitti, dovremo aggiungere i file risolti (`git add`) e completare il merge con un commit.
|
||||||
|
|
||||||
|
## **7. Lavorare con branch**
|
||||||
|
|
||||||
|
### Creare un nuovo branch
|
||||||
|
|
||||||
|
Per creare un nuovo branch in Git, utilizziamo il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituiamo `<nome-branch>` con il nome desiderato per il nuovo branch.
|
||||||
|
|
||||||
|
### Spostarsi su un branch
|
||||||
|
|
||||||
|
Per spostarci su un branch esistente, usiamo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Oppure, per creare e spostarci su un nuovo branch in un solo comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch -c <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unire un branch nel branch principale
|
||||||
|
|
||||||
|
Per unire un branch nel branch principale (di solito chiamato `main`):
|
||||||
|
|
||||||
|
1. Spostiamoci sul branch principale:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Eseguiamo il merge del branch desiderato:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituiamo `<nome-branch>` con il nome del branch che vogliamo unire.
|
||||||
|
|
||||||
|
### Risoluzione dei conflitti
|
||||||
|
|
||||||
|
Quando due persone modificano lo stesso file, Git può generare un conflitto. Ecco come risolverlo:
|
||||||
|
|
||||||
|
1. Identifichiamo il file in conflitto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Modifichiamo manualmente il file per risolvere il conflitto. Cerchiamo i segni di conflitto (`<<<<<<<`, `=======`, `>>>>>>>`) e scegliamo quali modifiche mantenere.
|
||||||
|
|
||||||
|
3. Aggiungiamo il file risolto allo stage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Concludiamo con un commit per salvare le modifiche risolte:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **9. Comandi utili**
|
||||||
|
|
||||||
|
### **Visualizzare le differenze**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Annullare modifiche**
|
||||||
|
|
||||||
|
1. **Prima del commit**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -- <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Dopo il commit**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git reset --soft HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Eliminare un branch**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch -d <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **10. Best practices**
|
||||||
|
|
||||||
|
- Scriviamo messaggi di commit chiari e descrittivi.
|
||||||
|
|
||||||
|
- Creiamo branch per nuove funzionalità o bugfix.
|
||||||
|
|
||||||
|
- Sincronizziamo frequentemente il nostro repository locale con quello remoto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **11. Risorse aggiuntive**
|
||||||
|
|
||||||
|
- [Documentazione ufficiale di Git](https://git-scm.com/doc)
|
||||||
|
|
||||||
|
- [Guida interattiva Learn Git Branching](https://learngitbranching.js.org/)
|
||||||
|
|
||||||
|
- [GitHub Docs](https://docs.github.com/)
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
id: pagina-poisson-con-astro
|
||||||
|
title: Pagina Poisson con Astro
|
||||||
|
description: Vediamo come creare una pagina Poisson moderna ⚡ con Astro
|
||||||
|
author: Antonio De Lucreziis
|
||||||
|
tags: [astro, website]
|
||||||
|
---
|
||||||
|
|
||||||
|
In questa guida vedremo come creare una pagina Poisson moderna utilizzando Astro, un nuovo framework di sviluppo web statico. Per prima cosa installeremo NodeJS sul nostro computer, poi creeremo un nuovo progetto Astro e infine dopo averlo generato, lo caricheremo su Poisson.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Se siete sul vostro pc installate VSCode ed il plugin di Astro.
|
||||||
|
|
||||||
|
Poi installiamo NodeJS (se siete su Windows è consigliato [installare WSL con `wsl --install`](https://learn.microsoft.com/en-us/windows/wsl/install) e poi installare i seguenti pacchetti nell'ambiente Linux)
|
||||||
|
|
||||||
|
> NodeJS: https://nodejs.org/en/download/package-manager
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://fnm.vercel.app/install | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
fnm use --install-if-missing 20
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creazione di un nuovo progetto Astro
|
||||||
|
|
||||||
|
Per prima cosa dobbiamo creare un nuovo progetto di Astro sul nostro computer, possiamo scegliere [uno dei tanti temi disponibili per Astro](https://astro.build/themes/) o partire da un blog di esempio con il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create astro@latest -- --template blog
|
||||||
|
cd nome-del-progetto
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Se ad esempio volessimo usare un tema come "[Astro Nano](https://github.com/markhorn-dev/astro-nano)" possiamo fare così:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/markhorn-dev/astro-nano sito-poisson
|
||||||
|
cd sito-poisson
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ultima cosa importante che c'è da cambiare è che le pagine Poisson sono hostate su `https://poisson.phc.dm.unipi.it/~nomeutente/` che non è la radice del dominio del sito. Quindi dobbiamo cambiare il file `astro.config.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default defineConfig({
|
||||||
|
...
|
||||||
|
base: '/~nomeutente/',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lavorare con Astro
|
||||||
|
|
||||||
|
Per vedere il nostro progetto in locale possiamo eseguire il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
A questo punto in base al tema scelto possiamo modificare i file dentro `src/pages` per cambiare il contenuto delle pagine. Molti temi sono preimpostati per scrivere contenuti in Markdown, ad esempio per il _template blog_ possiamo scrivere gli articoli per il nostro blog in `src/content/blog/{nome_post}.md`.
|
||||||
|
|
||||||
|
## Appunti
|
||||||
|
|
||||||
|
Una volta creato il progetto possiamo caricare appunti e dispense nella cartella `/public`
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Per caricare il nostro sito su Poisson possiamo usare il comando `rsync`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
rsync -avz dist/ username@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove `username` è il nostro username Poisson. Da notare che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore.
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
id: deploy-with-github-actions
|
||||||
|
title: Deploy automatico per Poisson da GitHub
|
||||||
|
description: Come impostare il deploy automatico per la propria pagina Poisson tramite le GitHub Actions
|
||||||
|
author: Antonio De Lucreziis
|
||||||
|
tags: [github, poisson, sito]
|
||||||
|
---
|
||||||
|
|
||||||
|
Supponiamo di avere un sito web statico che vogliamo caricare su Poisson, ad esempio un progetto NodeJS che genera in `dist/` o `out/` i file da caricare. Come possiamo automatizzare il processo di deploy su Poisson?
|
||||||
|
|
||||||
|
Vedremo come deployare il nostro sito, e successivamente come automatizzare il deployment con le GitHub Actions.
|
||||||
|
|
||||||
|
## Setup manuale
|
||||||
|
|
||||||
|
Come primo approccio, potremmo compilare il nostro progetto in locale e poi caricare i file su Poisson utilizzando `rsync`, ad esempio come segue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm run build
|
||||||
|
$ rsync -avz dist/ <username>@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
(osserviamo che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore)
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
Per automatizzare questo processo possiamo caricare il nostro progetto su GitHub ed aggiungere un _workflow_ che esegue il build e il deploy ogni volta che facciamo un push sul branch `main`. Ad esempio, possiamo creare un file `.github/workflows/deploy-poison.yaml` con quanto segue:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy to Poisson
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Write SSH keys
|
||||||
|
run: |
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/known_hosts
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/id_ed25519
|
||||||
|
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '23'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: rsync -cavz dist/ ${{ secrets.SSH_USER }}@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comando rsync
|
||||||
|
|
||||||
|
Il comando `rsync` ha le seguenti opzioni:
|
||||||
|
|
||||||
|
- `-c` per controllare i file tramite checksum invece che per data e dimensione (che sono sempre diverse visto che stiamo ricosruendo il sito ogni volta con le GitHub Actions)
|
||||||
|
|
||||||
|
- `-a` per copiare ricorsivamente i file e mantenere i permessi
|
||||||
|
|
||||||
|
- `-v` per mostrare i file copiati
|
||||||
|
|
||||||
|
- `-z` per comprimere i file durante il trasferimento
|
||||||
|
|
||||||
|
## SSH Segreti
|
||||||
|
|
||||||
|
Per stabilire una connessione SSH a Poisson dalle GitHub Actions in modo sicuro, dobbiamo aggiungere alcuni segreti alla nostra repository. Vediamo meglio il workflow:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Write SSH keys
|
||||||
|
run: |
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/known_hosts
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/id_ed25519
|
||||||
|
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
Questa è la parte più importante del workflow, che permette di autenticarsi su Poisson senza dover inserire la password ogni volta (cosa che non possiamo materialmente fare dall GitHub Actions).
|
||||||
|
|
||||||
|
Per farlo, creiamo in locale una coppia di chiavi SSH apposta per le GitHub Actions e aggiungiamo la chiave pubblica su Poisson. Per farlo, possiamo seguire questi passaggi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ssh-keygen -t ed25519 -C "deploy@github-actions" -f actions-deploy-key
|
||||||
|
$ ssh-copy-id -i actions-deploy-key <username>@poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
Qui generiamo una chiave ssh utilizzando l'algoritmo `ed25519` (leggermente più consigliato rispetto a `rsa`, in particolare ha anche chiavi più corte), `-C` aggiunge semplicemente un commento alla chiave e `-f` specifica il file in cui salvare la chiave.
|
||||||
|
|
||||||
|
Poi eseguendo `cat actions-deploy-key` possiamo copiare il contenuto della chiave privata ed aggiungiamo il contenuto in un segreto chiamato `SSH_PRIVATE_KEY` nella nostra repository GitHub.
|
||||||
|
|
||||||
|
Poi, per evitare che la connessione venga rifiutata, eseguiamo in locale anche uno scan delle chiavi SSH di Poisson:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ssh-keyscan poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
(se l'output è vuoto riprovare con `ssh-keyscan -4 ...`) e copiamo l'output in un segreto della nostra repository chiamato `SSH_KNOWN_HOSTS`.
|
||||||
|
|
||||||
|
Infine possiamo aggiungere anche un segreto `SSH_USER` con il nostro username o modificare anche direttamente il workflow ed inserire l'username direttamente nel file.
|
||||||
|
|
||||||
|
Ora ogni volta che facciamo un push sul branch `main` il nostro sito verrà automaticamente costruito e caricato su Poisson!
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
id: attivazione-poisson
|
||||||
|
title: Come attivare il proprio account Poisson
|
||||||
|
description: Guida all'attivazione dell'account Poisson, con istruzioni per il primo accesso e configurazione del proprio sito
|
||||||
|
author: Luca Lombardo
|
||||||
|
tags: [poisson, sito, ssh]
|
||||||
|
---
|
||||||
|
|
||||||
|
Poisson è un server autogestito dalla comunità studentesca di matematica, da sempre gestito con cura dai membri del PHC. Ogni studentə ha la possibilità di attivare un account personale, che consente l'accesso alla macchina via SSH e la creazione di uno spazio web personale.
|
||||||
|
|
||||||
|
## Come richiedere un account
|
||||||
|
|
||||||
|
Se non si è mai creato un account Poisson, è necessario inviare una richiesta via email a **macchinisti@lists.dm.unipi.it** includendo:
|
||||||
|
|
||||||
|
- Nome
|
||||||
|
|
||||||
|
- Cognome
|
||||||
|
|
||||||
|
- Username di ateneo (quello associato alla propria email istituzionale)
|
||||||
|
|
||||||
|
Nella mail è sufficiente specificare che si desidera attivare un account Poisson. I "macchinisti" si occuperanno di attivare l'account il prima possibile.
|
||||||
|
|
||||||
|
### Come ottenere le credenziali
|
||||||
|
|
||||||
|
Dopo l'attivazione, le credenziali del proprio account saranno disponibili accedendo al seguente sito:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://credenziali.phc.dm.unipi.it/">https://credenziali.phc.dm.unipi.it/</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Assicuriamoci di accedere con le credenziali di ateneo per recuperare username e password assegnati.
|
||||||
|
|
||||||
|
## Primo accesso al server
|
||||||
|
|
||||||
|
Per accedere a Poisson via SSH, è necessario:
|
||||||
|
|
||||||
|
1. Aprire un terminale (su Linux/Mac) o utilizzare un client SSH come PuTTY (su Windows).
|
||||||
|
|
||||||
|
2. Eseguire il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh <username>@poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove `<username>` è il proprio username che è stato fornito con le credenziali.
|
||||||
|
|
||||||
|
## Configurazione della pagina web
|
||||||
|
|
||||||
|
Dopo aver effettuato il primo accesso, è possibile configurare la propria pagina web personale. Nella home directory del proprio account, è presente una cartella `public_html` in cui è possibile inserire i file necessari per la propria pagina web.
|
||||||
|
|
||||||
|
Vediamo un piccolo esempio di file `index.html` che possiamo creare:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Sergio Steffè</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Sergio Steffè</h1>
|
||||||
|
<img
|
||||||
|
src="https://people.cs.dm.unipi.it/steffe/sergio.jpg"
|
||||||
|
alt="Foto di Sergio Steffè"
|
||||||
|
style="max-width: 300px; border-radius: 10px;"
|
||||||
|
/>
|
||||||
|
<p>Ciao! Sono Sergio Steffè.</p>
|
||||||
|
<p>
|
||||||
|
Email:
|
||||||
|
<a href="mailto:sergio.steffe@example.com">sergio.steffe@example.com</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Una volta salvato il file `index.html` nella cartella `public_html`, sarà possibile visualizzarlo accedendo al seguente indirizzo:
|
||||||
|
|
||||||
|
https://poisson.phc.dm.unipi.it/~<username>
|
||||||
|
|
||||||
|
Dove `<username>` è il proprio username.
|
||||||
|
|
||||||
|
### Creazione di pagine web più complesse
|
||||||
|
|
||||||
|
Per creare pagine web più complesse, suggeriamo di utilizzare il framework [Astro](https://astro.build/), che permette di creare siti web statici in modo semplice e veloce. [Abbiamo scritto una guida](/guide/pagina-poisson-con-astro) su come iniziare a utilizzare Astro per creare la nostra pagina web e caricarla su Poisson.
|
||||||