Compare commits
No commits in common. 'main' and 'next' have entirely different histories.
@ -1,54 +0,0 @@
|
|||||||
# 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
|
|
||||||
type: docker
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: deploy
|
|
||||||
image: node:22-alpine
|
|
||||||
volumes:
|
|
||||||
- name: host-website-dist
|
|
||||||
path: /mnt/website
|
|
||||||
commands:
|
|
||||||
- uname -a
|
|
||||||
- node -v
|
|
||||||
- npm ci
|
|
||||||
- node -e 'import Sharp from "sharp"; console.log(Sharp)'
|
|
||||||
- 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
|
|
||||||
name: caddy-permissions
|
|
||||||
type: exec # this job is executed on the host machine
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- default
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: chown
|
|
||||||
commands:
|
|
||||||
- chown -R caddy:caddy /var/www/website
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- main
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
@ -1,23 +1,10 @@
|
|||||||
# build output
|
.env
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# local data
|
|
||||||
*.local*
|
*.local*
|
||||||
|
bin/
|
||||||
|
|
||||||
# environment variables
|
.out/
|
||||||
.env
|
out/
|
||||||
.env.production
|
dist/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# macOS-specific files
|
.vscode/
|
||||||
.DS_Store
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
# Needed by pnpm to work with "@preact/preset-vite"
|
||||||
|
shamefully-hoist=true
|
||||||
@ -1,27 +0,0 @@
|
|||||||
/** @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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"npm.packageManager": "bun",
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"[astro]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[yaml]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|

|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
.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,35 +1,25 @@
|
|||||||
# PHC Website
|
# Website 2
|
||||||
|
|
||||||
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
Repo per il nuovo sito del PHC
|
||||||
|
|
||||||
## Installazione
|
## Docs
|
||||||
|
|
||||||
```bash
|
- [./ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sviluppo
|
Alcune note sull'architettura di questo progetto.
|
||||||
|
|
||||||
```bash
|
## Usage
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash shell
|
||||||
bun run build
|
# Starts the backend on port :4000 and the frontend development server on port :3000
|
||||||
|
$ go run -v ./cmd/devserver
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy
|
## Production
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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ì.
|
```bash shell
|
||||||
|
# Generates "routes.json", builds all frontend artifacts and finally the main binary
|
||||||
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.
|
$ make build
|
||||||
|
```
|
||||||
### Cose da fare
|
|
||||||
|
|
||||||
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config'
|
|
||||||
import preact from '@astrojs/preact'
|
|
||||||
|
|
||||||
import mdx from '@astrojs/mdx'
|
|
||||||
import remarkMath from 'remark-math'
|
|
||||||
|
|
||||||
import yaml from '@rollup/plugin-yaml'
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
vite: {
|
|
||||||
plugins: [yaml()],
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
markdown: {
|
|
||||||
remarkPlugins: [remarkMath],
|
|
||||||
shikiConfig: {
|
|
||||||
theme: 'github-light',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
integrations: [
|
|
||||||
preact({
|
|
||||||
compat: true,
|
|
||||||
}),
|
|
||||||
mdx({
|
|
||||||
remarkPlugins: [remarkMath],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
output: 'static',
|
|
||||||
})
|
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
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"`)
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 251 KiB |
@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<!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>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.list {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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')
|
||||||
|
)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
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=
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id string
|
||||||
|
|
||||||
|
FullName string
|
||||||
|
Nickname string
|
||||||
|
|
||||||
|
AuthSources map[string]AuthSource
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthSource struct {
|
||||||
|
Provider string
|
||||||
|
AuthToken string
|
||||||
|
}
|
||||||
@ -1,54 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"type": "module",
|
"version": "1.0.0",
|
||||||
"version": "0.0.1",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "run-s astro:sync astro:dev",
|
"dev": "node server.js",
|
||||||
"build": "run-s astro:build",
|
"build": "vite build --emptyOutDir"
|
||||||
"astro:sync": "astro sync",
|
},
|
||||||
"astro:dev": "astro dev",
|
"devDependencies": {
|
||||||
"astro:build": "astro check && astro build"
|
"@preact/preset-vite": "^2.5.0",
|
||||||
},
|
"axios": "^1.2.6",
|
||||||
"dependencies": {
|
"express": "^4.18.2",
|
||||||
"@astrojs/check": "^0.9.4",
|
"morgan": "^1.10.0",
|
||||||
"@astrojs/node": "^9.4.3",
|
"node-fetch": "^3.3.0",
|
||||||
"@astrojs/preact": "^4.1.1",
|
"sass": "^1.57.1",
|
||||||
"@fontsource-variable/material-symbols-outlined": "^5.2.21",
|
"vite": "^4.0.4"
|
||||||
"@fontsource/iosevka": "^5.2.5",
|
},
|
||||||
"@fontsource/mononoki": "^5.2.5",
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^5.2.6",
|
"preact": "^10.11.3"
|
||||||
"@fontsource/source-code-pro": "^5.2.6",
|
}
|
||||||
"@fontsource/source-sans-pro": "^5.2.5",
|
|
||||||
"@fontsource/space-mono": "^5.2.8",
|
|
||||||
"@phosphor-icons/core": "^2.1.1",
|
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
|
||||||
"@preact/signals": "^1.3.2",
|
|
||||||
"@types/jsdom": "^21.1.7",
|
|
||||||
"astro": "^5.13.7",
|
|
||||||
"fuse.js": "^7.1.0",
|
|
||||||
"katex": "^0.16.22",
|
|
||||||
"lucide-static": "^0.468.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"node-addon-api": "^8.5.0",
|
|
||||||
"node-gyp": "^11.4.2",
|
|
||||||
"preact": "^10.27.2",
|
|
||||||
"sharp": "^0.34.3",
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@astrojs/mdx": "^4.3.5",
|
|
||||||
"@rollup/plugin-yaml": "^4.1.2",
|
|
||||||
"@types/katex": "^0.16.7",
|
|
||||||
"jsdom": "^24.1.3",
|
|
||||||
"linkedom": "^0.18.12",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"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.92.1",
|
|
||||||
"tsx": "^4.20.5"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 6.6 KiB |
@ -1,87 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 877 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
@ -1,8 +0,0 @@
|
|||||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
|
||||||
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 344 B |
@ -0,0 +1,93 @@
|
|||||||
|
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()
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/sl"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Root = sl.NewSlot[fiber.Router]()
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
// 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:]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 295 KiB |
@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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>
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
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 => {
|
|
||||||
if (!el) return
|
|
||||||
setItemWidth(el.offsetWidth)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.keys(children).map(key => (
|
|
||||||
<div
|
|
||||||
class="option"
|
|
||||||
onClick={() => {
|
|
||||||
setValue(key)
|
|
||||||
setOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children[key]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
|
||||||
import { FunnelIcon } 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">
|
|
||||||
<FunnelIcon />
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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} />
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
type Props = {
|
|
||||||
large?: boolean
|
|
||||||
style?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const { large, ...props } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class:list={['card', large && 'large']} {...props}>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<footer>
|
|
||||||
<div class="text">
|
|
||||||
<p>
|
|
||||||
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
|
||||||
>macchinisti@lists.dm.unipi.it</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
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: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto
|
|
||||||
{ 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>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
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} />
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
// 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 text">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
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} />}
|
|
||||||