Compare commits
No commits in common. 'next' and 'main' have entirely different histories.
@ -1,10 +1,26 @@
|
|||||||
.env
|
# build output
|
||||||
*.local*
|
|
||||||
bin/
|
|
||||||
|
|
||||||
.out/
|
|
||||||
out/
|
|
||||||
dist/
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
.vscode/
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# local data
|
||||||
|
*.local*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Drizzle build output
|
||||||
|
out/
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
# Needed by pnpm to work with "@preact/preset-vite"
|
|
||||||
shamefully-hoist=true
|
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 110,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"npm.packageManager": "bun"
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
# Architettura
|
|
||||||
|
|
||||||
Questo è un progetto _fullstack_ con una backend scritta in [Go](https://go.dev/) ed una frontend in JS, più precisamente utilizziamo NodeJS con [Vite](https://vitejs.dev/) per build-are la frontend.
|
|
||||||
|
|
||||||
La cosa più interessante è l'integrazione tra **Vite** e la backend in **Go** per siti con più pagine (il nostro caso). Di base Vite supporta siti con più file html ma ha bisogno che gli venga detto quali sono tutti gli _entrypoint_ HTML. Vedremo che questo progetto usa una tecnica che ci permette di indicare una volta sola le cose nel codice in Go senza stare a tenere sincronizzato il codice in Go e la configurazione di Vite.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Ci sono vari modi per lanciare la nostra applicazione in base all'_environment_, in particolare sono tutti isolati sotto forma di eseguibili in Go:
|
|
||||||
|
|
||||||
- Quando saremo in produzione l'unico server sarà quello di Go lanciato attraverso l'_entry-point_ [./cmd/server/main.go](./cmd/server/main.go)
|
|
||||||
|
|
||||||
In particolare prima di poter lanciare questo server bisogna aver eseguito [./cmd/build/main.go](./cmd/build/main.go) che esegue solo il codice relativo al router della nostra applicazione e genera un file `out/routes.json`. Poi bisogna eseguire `npm run build` che chiama Vite e genera il codice per tutte le route dentro la cartella `out/frontend/`.
|
|
||||||
|
|
||||||
- Quando siamo in development usiamo solo [./cmd/devserver/main.go](./cmd/devserver/main.go) che lancia in background il server di Vite (chiama `npm run dev` che a sua volta è un alias per `node server.js`) quindi possiamo vedere tutto in tempo reale da `localhost:3000`.
|
|
||||||
|
|
||||||
Più precisamente il server di Vite all'avvio richiede al server in Go tutte le route da montare utilizzando la route speciale `/api/development/routes` (in particolare Fiber ed ExpressJS hanno la stessa sintassi per definire le route quindi questa cosa è facile da fare).
|
|
||||||
|
|
||||||
Poi quando si prova ad andare su una pagina ci sono due casi
|
|
||||||
|
|
||||||
- Se la route era **statica** allora leggiamo il file html, lo facciamo processare a Vite e poi lo rimandiamo all'utente.
|
|
||||||
|
|
||||||
- Se invece la route era di tipo **dinamico** allora leggiamo sempre il file e lo processiamo con Vite però ora utilizziamo l'altra route speciale che esiste solo in fase di sviluppo `/api/development/render` che renderizza la pagina applicando il _templating del server_ e poi una volta finito inviamo la pagina al client.
|
|
||||||
|
|
||||||
Invece quando saremo in produzione tutte le pagina saranno già state renderizzate da Vite quindi saremo nel caso standard di _http server_ con views da renderizzare con il _template engine_ del caso prima di mandare la pagina al client.
|
|
||||||
|
|
||||||
- L'ultimo _entrypoint_ è [./cmd/build/main.go](./cmd/build/main.go) e lancia la nostra applicazione in una modalità "finta" senza server http ma vengono comunque registrate tutte le route utilizzando sempre il modulo `dev`. Questo ci permette di costruire l'albero delle route (statiche e dinamiche) che poi servirà a Vite quando faremo `npm run build`.
|
|
||||||
|
|
||||||
Ciò serve perché così ci basta definire tutte le route una volta sola nel Go e poi in automatico funzioneranno anche nel server di Vite senza dover ripetere due volte il codice. (questa è la parte più magica di _meta-programming_ di tutto il progetto)
|
|
||||||
|
|
||||||
![architecture-1](./docs/architecture-1.svg)
|
|
@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
|
||||||
PROJECT_DIR="$(shell pwd)" go test -v ./...
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build:
|
|
||||||
go run -v ./cmd/build
|
|
||||||
pnpm run build
|
|
||||||
go build -o ./out/bin/server ./cmd/server
|
|
@ -1,25 +1,38 @@
|
|||||||
# Website 2
|
# PHC Website
|
||||||
|
|
||||||
Repo per il nuovo sito del PHC
|
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||||
|
|
||||||
## Docs
|
## Installazione
|
||||||
|
|
||||||
- [./ARCHITECTURE.md](./ARCHITECTURE.md)
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
Alcune note sull'architettura di questo progetto.
|
## Sviluppo
|
||||||
|
|
||||||
## Usage
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Build
|
||||||
|
|
||||||
```bash shell
|
```bash
|
||||||
# Starts the backend on port :4000 and the frontend development server on port :3000
|
bun build
|
||||||
$ go run -v ./cmd/devserver
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
## Deploy [TODO]
|
||||||
|
|
||||||
```bash shell
|
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro).
|
||||||
# Generates "routes.json", builds all frontend artifacts and finally the main binary
|
|
||||||
$ make build
|
```bash
|
||||||
|
docker build -t phc-website .
|
||||||
|
docker run -p 3000:3000 phc-website
|
||||||
```
|
```
|
||||||
|
|
||||||
|
C'è anche un `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD.
|
||||||
|
|
||||||
|
## Come Contribuire
|
||||||
|
|
||||||
|
Cose da fare
|
||||||
|
|
||||||
|
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
@ -0,0 +1,31 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import preact from '@astrojs/preact'
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
|
||||||
|
import mdx from '@astrojs/mdx'
|
||||||
|
|
||||||
|
import remarkToc from 'remark-toc'
|
||||||
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'github-light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: [preact(), mdx()],
|
||||||
|
// adapter: node({
|
||||||
|
// mode: 'standalone',
|
||||||
|
// }),
|
||||||
|
output: 'hybrid',
|
||||||
|
outDir: './out/astro',
|
||||||
|
build: {
|
||||||
|
client: './out/astro/client',
|
||||||
|
server: './out/astro/server',
|
||||||
|
},
|
||||||
|
})
|
@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
// sl.Inject[config.Interface](l, &config.EnvConfig{})
|
|
||||||
|
|
||||||
sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "production",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue(l, database.Slot, database.Database(
|
|
||||||
&database.Memory{}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := server.Configure(l); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create("out/routes.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(f)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
if err := enc.Encode(dev.UseRoutesMetadata(l)); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf(`generated "out/routes.json"`)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
cfg := sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "development",
|
|
||||||
Host: ":4000",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
srv, err := server.Configure(l)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Fatal(srv.Router.Listen(cfg.Host))
|
|
||||||
}()
|
|
||||||
|
|
||||||
r, w := io.Pipe()
|
|
||||||
|
|
||||||
cmd := exec.Command("npm", "run", "dev")
|
|
||||||
cmd.Stdout = w
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
cfg := sl.InjectValue(l, config.Slot, &config.Config{
|
|
||||||
Mode: "production",
|
|
||||||
Host: ":4000",
|
|
||||||
})
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
srv, err := server.Configure(l)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatal(srv.Router.Listen(cfg.Host))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 251 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "src/db/schema.ts",
|
||||||
|
driver: "better-sqlite",
|
||||||
|
out: "out/drizzle",
|
||||||
|
});
|
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Homepage</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Homepage
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<title>{{ .Title }} • Articoli • PHC</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/styles/main.scss" />
|
|
||||||
<link rel="stylesheet" href="./typography.scss" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Articolo "{{ .Example }}"</h1>
|
|
||||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Neque, quasi...</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<title>Articoli • PHC</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/styles/main.scss" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Articoli</h1>
|
|
||||||
{{ .Example }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||||||
h1 {
|
|
||||||
color: red;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>ListaUtenti</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="page-lista-utenti"></main>
|
|
||||||
<script type="module" src="./src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||||||
.list {
|
|
||||||
background: red;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { render } from 'preact'
|
|
||||||
import './lista-utenti.scss'
|
|
||||||
|
|
||||||
render(
|
|
||||||
<>
|
|
||||||
<h1>Lista Utenti</h1>
|
|
||||||
<div class="list">
|
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Odit, illo molestiae. Sapiente cumque saepe maxime, temporibus ad
|
|
||||||
nulla id officiis impedit ut dolorem asperiores voluptate illo, molestiae facilis inventore. Ea.
|
|
||||||
</div>
|
|
||||||
</>,
|
|
||||||
document.querySelector('main')
|
|
||||||
)
|
|
@ -1,5 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
module git.phc.dm.unipi.it/phc/website
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/alecthomas/repr v0.2.0
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0
|
|
||||||
github.com/joho/godotenv v1.4.0
|
|
||||||
github.com/valyala/fasthttp v1.43.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
|
||||||
github.com/klauspost/compress v1.15.9 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
|
||||||
)
|
|
@ -1,40 +0,0 @@
|
|||||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
|
||||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0 h1:YhNoUS/OTjEz+/WLYuQ01xI7RXgKEFnGBKMagAu5f0M=
|
|
||||||
github.com/gofiber/fiber/v2 v2.41.0/go.mod h1:RdebcCuCRFp4W6hr3968/XxwJVg0K+jr9/Ae0PFzZ0Q=
|
|
||||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
|
||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
|
||||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
|
|
||||||
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
@ -1,18 +0,0 @@
|
|||||||
import fetch from 'node-fetch'
|
|
||||||
import { readFile } from 'fs/promises'
|
|
||||||
|
|
||||||
export async function getBuildRoutesMetadata(file) {
|
|
||||||
console.log('Loading routes from disk...')
|
|
||||||
|
|
||||||
const routesRaw = await readFile(file, 'utf8')
|
|
||||||
return JSON.parse(routesRaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDevRoutesMetadata(url) {
|
|
||||||
console.log('Loading routes from go server...')
|
|
||||||
|
|
||||||
const routesReq = await fetch(url)
|
|
||||||
const routes = await routesReq.json()
|
|
||||||
|
|
||||||
return routes
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Id string
|
|
||||||
|
|
||||||
FullName string
|
|
||||||
Nickname string
|
|
||||||
|
|
||||||
AuthSources map[string]AuthSource
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthSource struct {
|
|
||||||
Provider string
|
|
||||||
AuthToken string
|
|
||||||
}
|
|
@ -1,21 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node server.js",
|
"dev": "run-s astro:sync drizzle:* astro:dev",
|
||||||
"build": "vite build --emptyOutDir"
|
"build": "run-s drizzle:generate astro:build",
|
||||||
},
|
"astro:sync": "astro sync",
|
||||||
"devDependencies": {
|
"astro:dev": "astro dev",
|
||||||
"@preact/preset-vite": "^2.5.0",
|
"astro:build": "astro check && astro build",
|
||||||
"axios": "^1.2.6",
|
"drizzle:generate": "drizzle-kit generate:sqlite",
|
||||||
"express": "^4.18.2",
|
"drizzle:migrate": "tsx src/db/migrate.ts"
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"node-fetch": "^3.3.0",
|
|
||||||
"sass": "^1.57.1",
|
|
||||||
"vite": "^4.0.4"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"preact": "^10.11.3"
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/node": "^8.3.4",
|
||||||
|
"@astrojs/preact": "^3.5.3",
|
||||||
|
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||||
|
"@fontsource/iosevka": "^5.0.11",
|
||||||
|
"@fontsource/mononoki": "^5.0.11",
|
||||||
|
"@fontsource/open-sans": "^5.0.24",
|
||||||
|
"@fontsource/source-code-pro": "^5.0.16",
|
||||||
|
"@fontsource/source-sans-pro": "^5.0.8",
|
||||||
|
"@fontsource/space-mono": "^5.0.20",
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"astro": "^4.15.11",
|
||||||
|
"better-sqlite3": "^9.4.3",
|
||||||
|
"drizzle-orm": "^0.29.4",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
|
"preact": "^10.19.6",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/mdx": "^3.1.7",
|
||||||
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"drizzle-kit": "^0.20.14",
|
||||||
|
"jsdom": "^24.1.1",
|
||||||
|
"linkedom": "^0.18.4",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"tsx": "^4.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
@ -0,0 +1,87 @@
|
|||||||
|
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="80" y="190" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="160" y="50" width="150" height="60" fill="#1E6733"/>
|
||||||
|
<rect x="140" y="90" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="140" y="200" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="140" y="410" width="10" height="20" fill="#ECC333"/>
|
||||||
|
<rect x="140" y="350" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="240" y="110" width="70" height="10" fill="#ECC333"/>
|
||||||
|
<rect x="250" y="130" width="60" height="130" fill="#1E6733"/>
|
||||||
|
<rect x="340" y="50" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="340" y="190" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="590" y="190" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="690" y="180" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="690" y="310" width="60" height="140" fill="#1E6733"/>
|
||||||
|
<rect x="690" y="50" width="60" height="120" fill="#1E6733"/>
|
||||||
|
<rect x="590" y="320" width="60" height="130" fill="#1E6733"/>
|
||||||
|
<rect x="590" y="50" width="60" height="130" fill="#1E6733"/>
|
||||||
|
<rect x="420" y="240" width="150" height="60" fill="#1E6733"/>
|
||||||
|
<rect x="340" y="320" width="60" height="130" fill="#1E6733"/>
|
||||||
|
<rect x="240" y="140" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="350" y="170" width="40" height="10" fill="#ECC333"/>
|
||||||
|
<rect x="330" y="330" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="160" y="200" width="80" height="60" fill="#1E6733"/>
|
||||||
|
<rect x="650" y="200" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="750" y="330" width="10" height="60" fill="#ECC333"/>
|
||||||
|
<rect x="800" y="450" width="40" height="10" fill="#ECC333"/>
|
||||||
|
<rect x="850" y="450" width="30" height="10" fill="#ECC333"/>
|
||||||
|
<rect x="750" y="90" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="810" y="110" width="60" height="10" fill="#ECC333"/>
|
||||||
|
<rect x="580" y="330" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="580" y="60" width="10" height="50" fill="#ECC333"/>
|
||||||
|
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="350" y="430" width="40" height="10" fill="#303030"/>
|
||||||
|
<rect x="350" y="410" width="40" height="10" fill="#303030"/>
|
||||||
|
<rect x="350" y="390" width="40" height="10" fill="#303030"/>
|
||||||
|
<rect x="700" y="70" width="20" height="40" fill="#303030"/>
|
||||||
|
<rect x="700" y="120" width="20" height="40" fill="#303030"/>
|
||||||
|
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4"/>
|
||||||
|
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4"/>
|
||||||
|
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4"/>
|
||||||
|
<rect x="370" y="70" width="20" height="30" fill="#303030"/>
|
||||||
|
<rect x="370" y="110" width="20" height="30" fill="#303030"/>
|
||||||
|
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030"/>
|
||||||
|
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030"/>
|
||||||
|
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030"/>
|
||||||
|
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030"/>
|
||||||
|
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030"/>
|
||||||
|
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030"/>
|
||||||
|
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030"/>
|
||||||
|
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z" fill="#1E6733"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z" fill="#1E6733"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z" fill="#1E6733"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z" fill="#1E6733"/>
|
||||||
|
<rect x="750" y="190" width="10" height="40" fill="#ECC333"/>
|
||||||
|
<rect x="750" y="240" width="10" height="20" fill="#ECC333"/>
|
||||||
|
<rect x="400" y="200" width="10" height="40" fill="#ECC333"/>
|
||||||
|
<rect x="400" y="250" width="10" height="20" fill="#ECC333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 765 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 4.6 MiB |
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
||||||
|
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 344 B |
@ -1,93 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { createServer as createViteServer } from 'vite'
|
|
||||||
import { getDevRoutesMetadata } from './meta/routes.js'
|
|
||||||
|
|
||||||
import fetch from 'node-fetch'
|
|
||||||
|
|
||||||
import { readFile } from 'fs/promises'
|
|
||||||
import { dirname, resolve } from 'path'
|
|
||||||
|
|
||||||
import morgan from 'morgan'
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const routes = await getDevRoutesMetadata('http://127.0.0.1:4000/api/development/routes')
|
|
||||||
|
|
||||||
console.log('Found static routes:')
|
|
||||||
for (const [route, file] of Object.entries(routes.static)) {
|
|
||||||
console.log(`- ${route} -> "${file}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found dynamic routes:')
|
|
||||||
for (const [route, file] of Object.entries(routes.dynamic)) {
|
|
||||||
console.log(`- ${route} -> "${file}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'))
|
|
||||||
|
|
||||||
const vite = await createViteServer({
|
|
||||||
server: { middlewareMode: true },
|
|
||||||
appType: 'custom',
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(vite.middlewares)
|
|
||||||
|
|
||||||
for (const [route, file] of Object.entries(routes.static)) {
|
|
||||||
app.get(route, async (req, res) => {
|
|
||||||
console.log(`Requested static route "${route}":`)
|
|
||||||
|
|
||||||
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
|
|
||||||
|
|
||||||
// Replace "./" with the absolute path of the html page
|
|
||||||
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
|
|
||||||
|
|
||||||
console.log(`- applying vite transformations for "${file}"`)
|
|
||||||
const html = await vite.transformIndexHtml(file, htmlPage)
|
|
||||||
|
|
||||||
console.log(`- sending resulting page for "${route}"`)
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [route, file] of Object.entries(routes.dynamic)) {
|
|
||||||
app.get(route, async (req, res) => {
|
|
||||||
console.log(`Requested dynamic route "${route}":`)
|
|
||||||
|
|
||||||
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
|
|
||||||
|
|
||||||
// Replace "./" with the absolute path of the html page
|
|
||||||
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
|
|
||||||
|
|
||||||
console.log(`- applying vite transformations for "${file}"`)
|
|
||||||
const html = await vite.transformIndexHtml(file, htmlPage)
|
|
||||||
|
|
||||||
console.log(`- applying server transformations for "${file}"`)
|
|
||||||
const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
route,
|
|
||||||
page: html,
|
|
||||||
request: {
|
|
||||||
params: req.params,
|
|
||||||
query: req.query,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderedHtml = await templateHtmlReq.json()
|
|
||||||
|
|
||||||
console.log(`- sending resulting page for "${route}"`)
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log(`Listening on port 3000...`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
@ -1,35 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[*Config]()
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Mode string
|
|
||||||
Host string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load(l *sl.ServiceLocator) (*Config, error) {
|
|
||||||
m, err := godotenv.Read(".env")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := "production"
|
|
||||||
if v, ok := m["MODE"]; ok {
|
|
||||||
mode = v
|
|
||||||
}
|
|
||||||
host := ":4000"
|
|
||||||
if v, ok := m["HOST"]; ok {
|
|
||||||
host = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Config{
|
|
||||||
mode,
|
|
||||||
host,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[Database]()
|
|
||||||
|
|
||||||
type Database interface {
|
|
||||||
CreateUser(user model.User) error
|
|
||||||
ReadUser(id string) (model.User, error)
|
|
||||||
ReadUsers() ([]model.User, error)
|
|
||||||
UpdateUser(id string, user model.User) error
|
|
||||||
DeleteUser(id string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Memory struct {
|
|
||||||
Users []model.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) CreateUser(user model.User) error {
|
|
||||||
m.Users = append(m.Users, user)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) ReadUser(id string) (model.User, error) {
|
|
||||||
for _, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) ReadUsers() ([]model.User, error) {
|
|
||||||
return m.Users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) UpdateUser(id string, user model.User) error {
|
|
||||||
for i, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
m.Users[i] = user
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) DeleteUser(id string) error {
|
|
||||||
for i, u := range m.Users {
|
|
||||||
if u.Id == id {
|
|
||||||
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(`no user with id "%s"`, id)
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package articles
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/router"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) error {
|
|
||||||
router.UseRouteTemplatedPage(l, "/articles",
|
|
||||||
"pages/articles/index.html",
|
|
||||||
func(w dev.ResponseWriter, r dev.Request) error {
|
|
||||||
tmpl := template.New("")
|
|
||||||
|
|
||||||
tmpl, err := tmpl.Parse(string(r.Page()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := map[string]any{
|
|
||||||
"Example": "Bla bla",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpl.Execute(w, ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
router.UseRouteTemplatedPage(l, "/articles/:slug",
|
|
||||||
"pages/articles/article.html",
|
|
||||||
func(w dev.ResponseWriter, r dev.Request) error {
|
|
||||||
tmpl := template.New("")
|
|
||||||
|
|
||||||
tmpl, err := tmpl.Parse(string(r.Page()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := map[string]any{
|
|
||||||
"Title": r.Param("slug"),
|
|
||||||
"Example": "Bla bla " + r.Param("slug"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpl.Execute(w, ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
package dev
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger is the debug logger, in the future this will be disabled and discard by default.
|
|
||||||
var Logger *log.Logger = log.New(os.Stderr, "[services/server/dev] ", log.Lmsgprefix)
|
|
||||||
|
|
||||||
// slot represents a private "write only" service
|
|
||||||
var slot = sl.NewSlot[*devService]()
|
|
||||||
|
|
||||||
// InjectInto a [*sl.ServiceLocator] an instance of the dev service
|
|
||||||
func InjectInto(l *sl.ServiceLocator) {
|
|
||||||
sl.InjectLazy(l, slot, Configure)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"static": dev.staticRoutes,
|
|
||||||
"dynamic": dev.dynamicRoutes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request interface {
|
|
||||||
Page() []byte
|
|
||||||
Param(key string) string
|
|
||||||
Query(key string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseWriter interface {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// devServerRequest is used when handling request from the dev server where params and queries are parsed by express
|
|
||||||
type devServerRequest struct {
|
|
||||||
page []byte
|
|
||||||
params map[string]string
|
|
||||||
query map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Page() []byte {
|
|
||||||
return r.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Param(key string) string {
|
|
||||||
return r.params[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r devServerRequest) Query(key string) string {
|
|
||||||
return r.query[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler is a custom routes handler
|
|
||||||
type Handler func(ResponseWriter, Request) error
|
|
||||||
|
|
||||||
type devService struct {
|
|
||||||
staticRoutes map[string]string
|
|
||||||
dynamicRoutes map[string]string
|
|
||||||
|
|
||||||
dynamicRoutesHandlers map[string]Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (*devService, error) {
|
|
||||||
d := &devService{
|
|
||||||
map[string]string{},
|
|
||||||
map[string]string{},
|
|
||||||
map[string]Handler{},
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config, _ := sl.Use(l, config.Slot)
|
|
||||||
if config.Mode != "development" {
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Get("/api/development/routes", func(c *fiber.Ctx) error {
|
|
||||||
return c.JSON(map[string]any{
|
|
||||||
"static": d.staticRoutes,
|
|
||||||
"dynamic": d.dynamicRoutes,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Post("/api/development/render", func(c *fiber.Ctx) error {
|
|
||||||
var data struct {
|
|
||||||
Route string `json:"route"`
|
|
||||||
HtmlPage string `json:"page"`
|
|
||||||
Request struct {
|
|
||||||
ParamsMap map[string]string `json:"params"`
|
|
||||||
QueryMap map[string]string `json:"query"`
|
|
||||||
} `json:"request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(&data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`server rendering route "%s"`, data.Route)
|
|
||||||
Logger.Printf(`- params: %s`, repr.String(data.Request.ParamsMap))
|
|
||||||
Logger.Printf(`- query: %s`, repr.String(data.Request.QueryMap))
|
|
||||||
|
|
||||||
handler, ok := d.dynamicRoutesHandlers[data.Route]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(`no handler for "%s"`, data.Route)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := handler(&buf, devServerRequest{
|
|
||||||
[]byte(data.HtmlPage),
|
|
||||||
data.Request.ParamsMap,
|
|
||||||
data.Request.QueryMap,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(buf.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page
|
|
||||||
func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.staticRoutes[mountPoint] = frontendFile
|
|
||||||
|
|
||||||
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterDynamicRoute(l *sl.ServiceLocator, mountPoint, frontendFile string, handler Handler) {
|
|
||||||
dev, err := sl.Use(l, slot)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.dynamicRoutes[mountPoint] = frontendFile
|
|
||||||
dev.dynamicRoutesHandlers[mountPoint] = handler
|
|
||||||
|
|
||||||
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetArtifactPath(frontendFile string) string {
|
|
||||||
return path.Join("./out/frontend/", frontendFile)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package listautenti
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/router"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) error {
|
|
||||||
router.UseRoutePage(l, "/utenti", "pages/lista-utenti/index.html")
|
|
||||||
|
|
||||||
db, err := sl.Use(l, database.Slot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Get("/api/lista-utenti", func(c *fiber.Ctx) error {
|
|
||||||
users, err := db.ReadUsers()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package listautenti_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/model"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
"github.com/valyala/fasthttp/fasthttputil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test1(t *testing.T) {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "claire",
|
|
||||||
FullName: "Claire Doe",
|
|
||||||
Nickname: "claire-doe",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "john",
|
|
||||||
FullName: "John Smith",
|
|
||||||
Nickname: "john-smith",
|
|
||||||
AuthSources: map[string]model.AuthSource{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
r := fiber.New()
|
|
||||||
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := fasthttp.Serve(ln, r.Handler())
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to serve: %v", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return ln.Dial()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log(string(body))
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// assert type of [ServerRequest] is [dev.Request]
|
|
||||||
var _ dev.Request = ServerRequest{}
|
|
||||||
|
|
||||||
// ServerRequest is used when the request is directly for the Go server
|
|
||||||
type ServerRequest struct {
|
|
||||||
page []byte
|
|
||||||
fiberContext *fiber.Ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ServerRequest) Page() []byte {
|
|
||||||
return r.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx ServerRequest) Param(key string) string {
|
|
||||||
return ctx.fiberContext.Params(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx ServerRequest) Query(key string) string {
|
|
||||||
return ctx.fiberContext.Query(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRoutePage(l *sl.ServiceLocator, route, frontendFile string) {
|
|
||||||
root, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.RegisterRoute(l, route, frontendFile)
|
|
||||||
|
|
||||||
root.Get(route, func(c *fiber.Ctx) error {
|
|
||||||
return c.SendFile(dev.GetArtifactPath(frontendFile))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseRouteTemplatedPage(l *sl.ServiceLocator, route, frontendFile string, handler dev.Handler) {
|
|
||||||
r, err := sl.Use(l, routes.Root)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dev.RegisterDynamicRoute(l, route, frontendFile, handler)
|
|
||||||
|
|
||||||
r.Get(route, func(c *fiber.Ctx) error {
|
|
||||||
rawPage, err := os.ReadFile(dev.GetArtifactPath(frontendFile))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := handler(&buf, ServerRequest{
|
|
||||||
rawPage,
|
|
||||||
c,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Type(".html").Send(buf.Bytes())
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Root = sl.NewSlot[fiber.Router]()
|
|
@ -1,30 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/articles"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/dev"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/listautenti"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/services/server/routes"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/sl"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct{ Router *fiber.App }
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (*Server, error) {
|
|
||||||
r := fiber.New(fiber.Config{})
|
|
||||||
r.Static("/assets", "./out/frontend/assets")
|
|
||||||
|
|
||||||
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
|
||||||
dev.InjectInto(l)
|
|
||||||
|
|
||||||
if err := listautenti.Configure(l); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := articles.Configure(l); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Server{r}, nil
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
// The [sl] package has two main concepts, the [ServiceLocator] itself is the main object that one should pass around through the application. A [ServiceLocator] has a list of slots that can be filled with [InjectLazy] and [InjectValue] and retrieved with [Use]. As slots should be unique they can only be created with the [NewSlot] function.
|
|
||||||
//
|
|
||||||
// The usual way to use this module is to make slots for go interfaces and then pass implementations using the [InjectValue] and [InjectLazy] functions.
|
|
||||||
//
|
|
||||||
// Services can be of various types:
|
|
||||||
// - a service with no dependencies can be directly injected inside a ServiceLocator using [InjectValue].
|
|
||||||
// - a service with dependencies on other service should use [InjectLazy]. This lets the service to initialize itself when required and makes the developer not think the topological sort to put onto the DAG of service dependencies.
|
|
||||||
// - a service can also be private, in this case the slot for a service should be a private field in the service package. This kind of services should also provide a way to inject them into a ServiceLocator.
|
|
||||||
// - a package also just provide a slot. This is useful for using the ServiceLocator to easily pass around values, effectively threating slots just as dynamically scoped variables.
|
|
||||||
package sl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger is the debug logger, in the future this will be disabled and discard by default.
|
|
||||||
//
|
|
||||||
// As this is the service locator module it was meaning less to pass this through the ServiceLocator itself (without making the whole module more complex)
|
|
||||||
var Logger *log.Logger = log.New(os.Stderr, "[sl] ", log.Lmsgprefix)
|
|
||||||
|
|
||||||
// slot is just a "typed" unique "symbol".
|
|
||||||
type slot[T any] *struct{}
|
|
||||||
|
|
||||||
// NewSlot is the only way to create instances of the slot type. Each instance is unique.
|
|
||||||
//
|
|
||||||
// This then lets you attach a service instance of type "T" to a [ServiceLocator] object.
|
|
||||||
func NewSlot[T any]() slot[T] {
|
|
||||||
return slot[T](new(struct{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// slotEntry represents a service that can lazily initialized (using "createFunc"). Once initialized the instance is kept in the "value" field. The field "typeName" just for debugging purposes.
|
|
||||||
type slotEntry struct {
|
|
||||||
createFunc func(*ServiceLocator) (any, error)
|
|
||||||
created bool
|
|
||||||
value any
|
|
||||||
|
|
||||||
typeName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *slotEntry) checkInitialized(l *ServiceLocator) error {
|
|
||||||
if !s.created {
|
|
||||||
v, err := s.createFunc(l)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`initialized lazy value of type %T for slot of type %s`, v, s.typeName)
|
|
||||||
|
|
||||||
s.created = true
|
|
||||||
s.value = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceLocator is the main context passed around to retrive service instances, the interface uses generics so to inject and retrive service instances you should use the functions [InjectValue], [InjectLazy] and [Use].
|
|
||||||
type ServiceLocator struct {
|
|
||||||
providers map[any]*slotEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new [ServiceLocator] context to pass around in the application.
|
|
||||||
func New() *ServiceLocator {
|
|
||||||
return &ServiceLocator{
|
|
||||||
providers: map[any]*slotEntry{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InjectValue will inject a concrete instance inside the ServiceLocator "l" for the given "slotKey". This should be used for injecting "static" services, for instances whose construction depend on other services you should use the [InjectLazy] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
|
|
||||||
func InjectValue[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
|
||||||
Logger.Printf(`injected value of type %T for slot of type %s`, value, getTypeName[T]())
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
nil,
|
|
||||||
true,
|
|
||||||
value,
|
|
||||||
getTypeName[T](),
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// InjectLazy will inject an instance inside the given ServiceLocator and "slotKey" that is created only when requested with a call to the [Use] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type check as "T" can also be an interface.
|
|
||||||
func InjectLazy[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
|
||||||
Logger.Printf(`injected lazy for slot of type %s`, getTypeName[T]())
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
createFunc: func(l *ServiceLocator) (any, error) {
|
|
||||||
return createFunc(l)
|
|
||||||
},
|
|
||||||
created: false,
|
|
||||||
value: nil,
|
|
||||||
typeName: getTypeName[T](),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use retrieves the value of type T associated with the given slot key from the provided ServiceLocator instance.
|
|
||||||
//
|
|
||||||
// If the ServiceLocator does not have a value for the slot key, or if the value wasn't correctly initialized (in the case of a lazy slot), an error is returned.
|
|
||||||
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
|
||||||
var zero T
|
|
||||||
|
|
||||||
slot, ok := l.providers[slotKey]
|
|
||||||
if !ok {
|
|
||||||
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
|
||||||
}
|
|
||||||
|
|
||||||
err := slot.checkInitialized(l)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := slot.value.(T)
|
|
||||||
|
|
||||||
Logger.Printf(`using slot of type %s with value of type %T`, getTypeName[T](), v)
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTypeName is a trick to get the name of a type (even if it is an interface type)
|
|
||||||
func getTypeName[T any]() string {
|
|
||||||
var zero T
|
|
||||||
return fmt.Sprintf(`%T`, &zero)[1:]
|
|
||||||
}
|
|
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 2.0 MiB |
After Width: | Height: | Size: 2.0 MiB |
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* image?: string,
|
||||||
|
* course?: string,
|
||||||
|
* title?: string,
|
||||||
|
* author: string,
|
||||||
|
* courseYear: string
|
||||||
|
* }} AppuntiCardProps
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AppuntiCardProps} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const AppuntiCard = ({ image, course, title, author, courseYear }) => {
|
||||||
|
return (
|
||||||
|
<div class="appunti-item">
|
||||||
|
<div class="thumbnail"></div>
|
||||||
|
{title && <div class="title">{title}</div>}
|
||||||
|
{course && <div class="course">{course}</div>}
|
||||||
|
<div class="author">@{author}</div>
|
||||||
|
<div class="course-year">{courseYear}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppuntiList = ({ children }) => {
|
||||||
|
return <div class="appunti-list">{children}</div>
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { type ComponentChildren } from 'preact'
|
||||||
|
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||||
|
import { clsx, isMobile } from './lib/util'
|
||||||
|
|
||||||
|
export const ComboBox = ({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
setValue: (s: string) => void
|
||||||
|
children: Record<string, ComponentChildren>
|
||||||
|
}) => {
|
||||||
|
const [cloak, setCloak] = useState(true)
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const comboRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [itemWidth, setItemWidth] = useState<number>(200)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false)
|
||||||
|
setCloak(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="combobox"
|
||||||
|
ref={comboRef}
|
||||||
|
style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}
|
||||||
|
>
|
||||||
|
<div class="selected" onClick={() => setOpen(!open)}>
|
||||||
|
<div class="content">{children[value]}</div>
|
||||||
|
<span class="material-symbols-outlined">expand_more</span>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
class={clsx('dropdown', cloak && 'invisible')}
|
||||||
|
ref={el => el && setItemWidth(el.offsetWidth)}
|
||||||
|
>
|
||||||
|
{Object.keys(children).map(key => (
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
onClick={() => {
|
||||||
|
setValue(key)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children[key]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const Counter = ({}) => {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="counter">
|
||||||
|
<button onClick={() => setCount(value => value - 1)}>-</button>
|
||||||
|
<div class="value">{count}</div>
|
||||||
|
<button onClick={() => setCount(value => value + 1)}>+</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||||
|
import type { JSX } from 'preact/jsx-runtime'
|
||||||
|
|
||||||
|
export const ShowMore = <T extends any>({
|
||||||
|
items,
|
||||||
|
pageSize,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
items: ReadonlySignal<T[]>
|
||||||
|
pageSize: number
|
||||||
|
children: (item: T) => JSX.Element
|
||||||
|
}) => {
|
||||||
|
const $shownItems = useSignal(pageSize)
|
||||||
|
|
||||||
|
const $paginatedItems = useComputed(() => {
|
||||||
|
return items.value.slice(0, $shownItems.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{$paginatedItems.value.map(children)}
|
||||||
|
<div class="show-more">
|
||||||
|
{$shownItems.value < items.value.length && (
|
||||||
|
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
import { effect, useComputed, useSignal } from '@preact/signals'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
import { ShowMore } from './Paginate'
|
||||||
|
import { ComboBox } from './ComboBox'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
uid: string
|
||||||
|
gecos: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
utenti: {
|
||||||
|
icon: 'person',
|
||||||
|
label: 'Utenti',
|
||||||
|
},
|
||||||
|
macchinisti: {
|
||||||
|
icon: 'construction',
|
||||||
|
label: 'Macchinisti',
|
||||||
|
},
|
||||||
|
rappstud: {
|
||||||
|
icon: 'account_balance',
|
||||||
|
label: 'Rappresentanti',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches(users: User[]) {
|
||||||
|
users.forEach(user => {
|
||||||
|
// strip ",+" from the end of the gecos field
|
||||||
|
user.gecos = user.gecos.replace(/,+$/, '')
|
||||||
|
|
||||||
|
// capitalize the first letter of each word
|
||||||
|
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
|
||||||
|
})
|
||||||
|
|
||||||
|
// reverse the order of the users
|
||||||
|
users.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
|
||||||
|
|
||||||
|
const RAPPSTUD = [
|
||||||
|
'smannella',
|
||||||
|
'lotti',
|
||||||
|
'rotolo',
|
||||||
|
'saccani',
|
||||||
|
'carbone',
|
||||||
|
'mburatti',
|
||||||
|
'ppuddu',
|
||||||
|
'marinari',
|
||||||
|
'evsilvestri',
|
||||||
|
'tateo',
|
||||||
|
'graccione',
|
||||||
|
'dilella',
|
||||||
|
'rocca',
|
||||||
|
'odetti',
|
||||||
|
'borso',
|
||||||
|
'numero',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UtentiPage = () => {
|
||||||
|
const $utentiData = useSignal<User[]>([])
|
||||||
|
|
||||||
|
const $filter = useSignal('utenti')
|
||||||
|
|
||||||
|
const $filteredData = useComputed(() =>
|
||||||
|
$filter.value === 'macchinisti'
|
||||||
|
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
|
||||||
|
: $filter.value === 'rappstud'
|
||||||
|
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
|
||||||
|
: $utentiData.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const $fuse = useComputed(
|
||||||
|
() =>
|
||||||
|
new Fuse($filteredData.value, {
|
||||||
|
keys: ['gecos', 'uid'],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const $searchText = useSignal('')
|
||||||
|
const $searchResults = useComputed(() =>
|
||||||
|
$searchText.value.trim().length > 0
|
||||||
|
? ($fuse.value?.search($searchText.value).map(result => result.item) ?? [])
|
||||||
|
: $filteredData.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
applyPatches(data)
|
||||||
|
|
||||||
|
$utentiData.value = data
|
||||||
|
|
||||||
|
$fuse.value.setCollection(data)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="search-bar">
|
||||||
|
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
|
||||||
|
{Object.fromEntries(
|
||||||
|
Object.entries(FILTERS).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
<>
|
||||||
|
<span class="material-symbols-outlined">{v.icon}</span> {v.label}
|
||||||
|
</>,
|
||||||
|
]),
|
||||||
|
)}
|
||||||
|
</ComboBox>
|
||||||
|
<div class="search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cerca un utente Poisson..."
|
||||||
|
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||||
|
value={$searchText.value}
|
||||||
|
/>
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search-results">
|
||||||
|
{$searchResults.value ? (
|
||||||
|
<ShowMore items={$searchResults} pageSize={100}>
|
||||||
|
{poissonUser => (
|
||||||
|
<div class="search-result">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">
|
||||||
|
{RAPPSTUD.includes(poissonUser.uid)
|
||||||
|
? 'account_balance'
|
||||||
|
: MACCHINISTI.includes(poissonUser.uid)
|
||||||
|
? 'construction'
|
||||||
|
: 'person'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text">{poissonUser.gecos}</div>
|
||||||
|
<div class="right">
|
||||||
|
<a
|
||||||
|
href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">open_in_new</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ShowMore>
|
||||||
|
) : (
|
||||||
|
<>Nessun risultato</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const trottleDebounce = <T extends any[], R>(
|
||||||
|
fn: (...args: T) => R,
|
||||||
|
delay: number,
|
||||||
|
options: { leading?: boolean; trailing?: boolean } = {},
|
||||||
|
): ((...args: T) => R | undefined) => {
|
||||||
|
let lastCall = 0
|
||||||
|
let lastResult: R | undefined
|
||||||
|
let lastArgs: T | undefined
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
const leading = options.leading ?? true
|
||||||
|
const trailing = options.trailing ?? true
|
||||||
|
|
||||||
|
return (...args: T): R | undefined => {
|
||||||
|
lastArgs = args
|
||||||
|
if (leading && Date.now() - lastCall >= delay) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...args)
|
||||||
|
} else {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (trailing && lastArgs) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...lastArgs)
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
return lastResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
|
||||||
|
|
||||||
|
export function clsx(...args: ClassValue[]): string {
|
||||||
|
return args
|
||||||
|
.flatMap(arg => {
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
return arg
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
return clsx(...arg)
|
||||||
|
} else if (typeof arg === 'boolean') {
|
||||||
|
return []
|
||||||
|
} else if (typeof arg === 'object') {
|
||||||
|
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMobile = () => {
|
||||||
|
const [windowWidth, setWindowWidth] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
|
||||||
|
const handleResize = () => setWindowWidth(window.innerWidth)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return windowWidth < 1024
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<footer>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
||||||
|
>macchinisti@lists.dm.unipi.it</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
@ -0,0 +1,32 @@
|
|||||||
|
<header>
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||||
|
</a>
|
||||||
|
<input type="checkbox" id="header-menu-toggle" />
|
||||||
|
<div class="links desktop-only">
|
||||||
|
<a role="button" href="/utenti">Utenti</a>
|
||||||
|
<!-- <a role="button" href="/appunti">Appunti</a> -->
|
||||||
|
<a role="button" href="/notizie">Notizie</a>
|
||||||
|
<a role="button" href="/guide">Guide</a>
|
||||||
|
<a role="button" href="/storia">Storia</a>
|
||||||
|
<!-- <a class="primary" role="button" href="/login">Login</a> -->
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<div class="side-menu">
|
||||||
|
<div class="links">
|
||||||
|
<a role="button" href="/utenti">Utenti</a>
|
||||||
|
<!-- <a role="button" href="/appunti">Appunti</a> -->
|
||||||
|
<a role="button" href="/notizie">Notizie</a>
|
||||||
|
<a role="button" href="/guide">Guide</a>
|
||||||
|
<a role="button" href="/storia">Storia</a>
|
||||||
|
<!-- <a class="primary" role="button" href="/login">Login</a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
const { year, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">{year} • {title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { JSDOM } from 'jsdom'
|
||||||
|
import Container from './Container.astro'
|
||||||
|
|
||||||
|
const language = Astro.props['data-language'] ?? 'text'
|
||||||
|
|
||||||
|
const html = await Astro.slots.render('default')
|
||||||
|
|
||||||
|
const rawCode = new JSDOM(html).window.document.body.textContent
|
||||||
|
---
|
||||||
|
|
||||||
|
<pre {...Astro.props}><slot /></pre>
|
||||||
|
|
||||||
|
{language === 'astro' && <Container set:html={rawCode} />}
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
const { size, ...rest } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['container', size ?? 'normal']} {...rest}>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
colors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { colors } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="palette">
|
||||||
|
{
|
||||||
|
colors.map(value => (
|
||||||
|
<>
|
||||||
|
<div class="color">
|
||||||
|
<div class="region" style={{ backgroundColor: value }} />
|
||||||
|
</div>
|
||||||
|
<div class="label">{value}</div>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
@ -0,0 +1,51 @@
|
|||||||
|
import { z, defineCollection } from 'astro:content'
|
||||||
|
|
||||||
|
// Una notizia ha una data di pubblicazione ma non ha un autore in quanto sono
|
||||||
|
// notizie generiche sul PHC e non sono scritte da un autore specifico
|
||||||
|
const newsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
image: z
|
||||||
|
.object({
|
||||||
|
url: z.string(),
|
||||||
|
alt: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Una guida ha un autore ma non ha una data di pubblicazione in quanto è un
|
||||||
|
// contenuto statico e non è importante sapere quando è stata pubblicata
|
||||||
|
const guidesCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
series: z.string().optional(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Per ora sono su un sito a parte ma prima o poi verranno migrati qui
|
||||||
|
const seminariettiCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
eventDate: z.date(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Export a single `collections` object to register your collection(s)
|
||||||
|
export const collections = {
|
||||||
|
news: newsCollection,
|
||||||
|
guides: guidesCollection,
|
||||||
|
seminarietti: seminariettiCollection,
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
id: git-101-example
|
||||||
|
title: Git 101 (Esempio)
|
||||||
|
description: Una guida introduttiva alle basi di Git (Esempio)
|
||||||
|
author: Copilot
|
||||||
|
tags: [git]
|
||||||
|
---
|
||||||
|
|
||||||
|
Benvenuto alla guida introduttiva alle basi di Git. In questa guida imparerai come iniziare a usare Git per il controllo di versione dei tuoi progetti.
|
||||||
|
|
||||||
|
## Cos'è Git?
|
||||||
|
|
||||||
|
Git è un sistema di controllo di versione distribuito. Questo significa che puoi tenere traccia delle modifiche ai tuoi file e sincronizzarle con altri membri del tuo team.
|
||||||
|
|
||||||
|
## Installazione
|
||||||
|
|
||||||
|
Per iniziare ad usare Git, devi prima installarlo sul tuo computer. Puoi scaricare l'ultima versione di Git dal [sito ufficiale](https://git-scm.com/).
|
||||||
|
|
||||||
|
## Inizializzazione di un repository
|
||||||
|
|
||||||
|
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
|
||||||
|
|
||||||
|
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
|
||||||
|
|
||||||
|
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
|
||||||
|
|
||||||
|
Una volta installato Git, puoi inizializzare un nuovo repository in una cartella esistente. Apri il terminale e spostati nella cartella del tuo progetto. Quindi esegui il seguente comando:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\int_0^1 x^2 \ dx
|
||||||
|
$$
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo creerà un nuovo repository $1+a+\zeta_7-\xi^2$ Git nella cartella del tuo progetto.
|
||||||
|
|
||||||
|
## Comandi vari
|
||||||
|
|
||||||
|
### Aggiunta di file
|
||||||
|
|
||||||
|
Ora che hai inizializzato il repository, puoi iniziare ad aggiungere file ad esso. Per aggiungere un file, esegui il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <nome del file>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituisci `<nome del file>` con il nome del file che vuoi aggiungere.
|
||||||
|
|
||||||
|
### Commit delle modifiche
|
||||||
|
|
||||||
|
Una volta aggiunti i file, puoi fare un commit delle modifiche. Questo salverà le modifiche nel repository. Per fare un commit, esegui il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "Messaggio del commit"
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituisci `"Messaggio del commit"` con un messaggio che descrive le modifiche che hai fatto.
|
||||||
|
|
||||||
|
### Sincronizzazione con un repository remoto
|
||||||
|
|
||||||
|
Ora che hai fatto un commit delle modifiche, puoi sincronizzare il repository con un repository remoto. Questo ti permetterà di condividere le tue modifiche con altri membri del tuo team. Per sincronizzare il repository con un repository remoto, esegui il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add origin <url del repository remoto>
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
L'opzione `-u` imposta il repository remoto come repository predefinito per il ramo main, in modo che tu possa semplicemente eseguire `git push` in futuro per sincronizzare le tue modifiche con il repository remoto.
|
||||||
|
|
||||||
|
Sostituisci `<url del repository remoto>` con l'URL del repository remoto. Questo ti chiederà di inserire le tue credenziali per il repository remoto. Una volta fatto, le tue modifiche saranno sincronizzate con il repository remoto.
|
||||||
|
|
||||||
|
### Aggiornamento del repository
|
||||||
|
|
||||||
|
Una volta che il repository è sincronizzato con un repository remoto, puoi aggiornare il repository con le modifiche degli altri membri del tuo team. Per aggiornare il repository, esegui il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo aggiornerà il repository con le modifiche degli altri membri del tuo team.
|
||||||
|
|
||||||
|
## Conclusioni
|
||||||
|
|
||||||
|
Questo è solo un'introduzione alle basi di Git. Ci sono molte altre funzionalità che puoi esplorare, ma queste sono le basi per iniziare. Buon divertimento!
|
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
id: pagina-poisson-con-astro
|
||||||
|
title: Pagina Poisson con Astro
|
||||||
|
description: Vediamo come creare una pagina Poisson moderna con Astro
|
||||||
|
author: Antonio De Lucreziis
|
||||||
|
tags: [astro, website]
|
||||||
|
---
|
||||||
|
|
||||||
|
In questa guida vedremo come creare una pagina Poisson moderna utilizzando Astro, un nuovo framework di sviluppo web statico.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Se siete sul vostro pc installate VSCode ed il plugin di Astro.
|
||||||
|
|
||||||
|
Poi installiamo NodeJS (se siete su Windows è consigliato [installare WSL con `wsl --install`](https://learn.microsoft.com/en-us/windows/wsl/install) e poi installare i seguenti pacchetti nell'ambiente Linux)
|
||||||
|
|
||||||
|
- NodeJS: https://nodejs.org/en/download/package-manager
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://fnm.vercel.app/install | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
fnm use --install-if-missing 20
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creazione di un nuovo progetto Astro
|
||||||
|
|
||||||
|
Per prima cosa dobbiamo creare un nuovo progetto di Astro sul nostro computer, possiamo scegliere [uno dei tanti temi disponibili per Astro](https://astro.build/themes/) o partire da un blog di esempio con il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create astro@latest -- --template blog
|
||||||
|
cd nome-del-progetto
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Se ad esempio volessimo usare un tema come [Astro Nano](https://github.com/markhorn-dev/astro-nano) possiamo fare così:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/markhorn-dev/astro-nano sito-poisson
|
||||||
|
cd sito-poisson
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ultima cosa importante che c'è da cambiare è che le pagine Poisson sono hostate su `https://poisson.phc.dm.unipi.it/~nomeutente/` che non è la radice del dominio del sito. Quindi dobbiamo cambiare il file `astro.config.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default defineConfig({
|
||||||
|
...
|
||||||
|
base: '/~nomeutente/',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Lavorare con Astro
|
||||||
|
|
||||||
|
Per vedere il nostro progetto in locale possiamo eseguire il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
A questo punto in base al tema scelto possiamo modificare i file dentro `src/pages` per cambiare il contenuto delle pagine. Molti temi sono preimpostati per scrivere contenuti in Markdown, ad esempio per il _template blog_ possiamo scrivere gli articoli per il nostro blog in `src/content/blog/$NOME_POST.md`.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Una volta creato il progetto possiamo caricare appunti e dispense nella cartella `/public`
|
||||||
|
|
@ -0,0 +1,209 @@
|
|||||||
|
import Container from '../../components/meta/Container.astro'
|
||||||
|
import Palette from '../../components/meta/Palette.astro'
|
||||||
|
|
||||||
|
# Meta > Design
|
||||||
|
|
||||||
|
In questa pagina tento di spiegare come funziona il design di questo sito. I blocchi di codice con sfondo arancione chiaro sono esempi di codice Astro e sotto hanno un'anteprima del risultato _generata automaticamente_. Ad esempio
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<p>Questo è un paragrafo</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Molti di questi esempi hanno alcuni stili impostati in `style` per mostrare come funzionano. Nella pratica invece è consigliato create una classe per ogni tipo di componente ed impostare le proprietà via CSS, ad esempio
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-custom-form {
|
||||||
|
--card-base: var(--palette-red);
|
||||||
|
max-width: 25rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="my-custom-form card">
|
||||||
|
<p>Questo è un paragrafo</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Card
|
||||||
|
|
||||||
|
Le card sono uno dei componenti più importanti di questo sito. Sono utilizzate per mostrare i post, i progetti e le pagine. Ecco alcuni esempi per dare un'idea
|
||||||
|
|
||||||
|
### Esempio di base
|
||||||
|
|
||||||
|
Una card semplice ha un titolo ed una descrizione.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="--card-base: var(--guide-base); max-width: 25rem;">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Varianti
|
||||||
|
|
||||||
|
#### Grande
|
||||||
|
|
||||||
|
Le card possono essere di dimensioni diverse. Questa è una card grande.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card large" style="--card-base: lightgreen; max-width: 25rem;">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Low Level: Mixin SCSS
|
||||||
|
|
||||||
|
Non dovrebbe essere mai necessario usarlo direttamente ma l'effetto di ombra delle card è ottenuto con questo mixin SCSS (che si trova in `src/styles/mixins.scss`).
|
||||||
|
|
||||||
|
```scss
|
||||||
|
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
|
||||||
|
border: $size solid #222;
|
||||||
|
border-radius: $size * 2;
|
||||||
|
|
||||||
|
@if $shadow {
|
||||||
|
box-shadow: $offset $offset 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $hoverable {
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: $offset + 1 $offset + 1 0 0 #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ad esempio tutti i bottoni utilizzano direttamente questo mixin senza cambiare i parametri di default.
|
||||||
|
|
||||||
|
### Sotto-componenti
|
||||||
|
|
||||||
|
#### Titolo
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testo & Modificatori
|
||||||
|
|
||||||
|
Se c'è poco testo, può essere inserito direttamente nella card.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Altrimenti può essere inserito in un tag `<p>`.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
|
||||||
|
Distinctio, vel! Veritatis est sit beatae eveniet.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Error, minus, asperiores quaerat nulla cumque, nisi ipsam
|
||||||
|
assumenda consectetur accusamus tempore consequatur quae. Fugit?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Quos sapiente amet numquam quis, libero odit eum, eius
|
||||||
|
perspiciatis repellat nesciunt cupiditate asperiores maiores?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
C'è anche il modificatore `small` e `dimmed` per ridurre la grandezza del testo e renderlo grigio rispettivamente.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="max-width: 25rem;">
|
||||||
|
<div class="text">
|
||||||
|
Some normal text, this is a very long
|
||||||
|
text that should wrap on the next line
|
||||||
|
</div>
|
||||||
|
<div class="text small">
|
||||||
|
This is some small text
|
||||||
|
</div>
|
||||||
|
<div class="text dimmed">
|
||||||
|
This is some dimmed text
|
||||||
|
</div>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
This is some small dimmed text
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
I tag sono una lista di link con `display: flex` e `flex-wrap: wrap`.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="max-width: 25rem;">
|
||||||
|
<div class="tags">
|
||||||
|
<a href="#">#tag1</a>
|
||||||
|
<a href="#">#tagg2</a>
|
||||||
|
<a href="#">#tag3</a>
|
||||||
|
<a href="#">#taggg4</a>
|
||||||
|
<a href="#">#tagg5</a>
|
||||||
|
<a href="#">#taggg6</a>
|
||||||
|
<a href="#">#tag7</a>
|
||||||
|
<a href="#">#taggg8</a>
|
||||||
|
<a href="#">#tag9</a>
|
||||||
|
<a href="#">#taggggg10</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Palette
|
||||||
|
|
||||||
|
Varie sezioni del sito utilizzano diverse palette di colori. Questa è la palette di base.
|
||||||
|
|
||||||
|
### Guide
|
||||||
|
|
||||||
|
<Palette
|
||||||
|
colors={[
|
||||||
|
'var(--guide-base)',
|
||||||
|
'var(--guide-darkest)',
|
||||||
|
'var(--guide-darker)',
|
||||||
|
'var(--guide-dark)',
|
||||||
|
'var(--guide-light)',
|
||||||
|
'var(--guide-lighter)',
|
||||||
|
'var(--guide-lightest)',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Combo Box
|
||||||
|
|
||||||
|
I combo box sono un componente per fare dropdown scritto in Preact. Questo è un esempio di come funzionano.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { ComboBox } from '@/lib/components/ComboBox'
|
||||||
|
|
||||||
|
const [value, setValue] = useState('option-1')
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ComboBox value={value} setValue={setValue}>
|
||||||
|
{{
|
||||||
|
'option-1': <>Option 1</>
|
||||||
|
'option-2': <>Option 2</>
|
||||||
|
'option-3': <>Option 3</>
|
||||||
|
}}
|
||||||
|
</ComboBox>
|
||||||
|
```
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
# Questo documento è utilizzato nella homepage del sito nella card principale
|
||||||
|
title: Cos'è il PHC?
|
||||||
|
---
|
||||||
|
|
||||||
|
Il <span title="Pisa Happy Computing">**PHC**</span> è un laboratorio informatico, gestito dagli studenti del **Dipartimento di Matematica** di Pisa e nato nel 1999, che offre vari servizi agli studenti come [Poisson](https://poisson.phc.dm.unipi.it), che ospita le pagine degli studenti.
|
||||||
|
|
||||||
|
La sede del PHC è la [stanza 106](https://www.dm.unipi.it/mappa/?sel=638cd24b50cf34e03924a00c) del Dipartimento, dove si trovano i **macchinisti** per discutere e realizzare progetti [hardware](http://steffe.cs.dm.unipi.it/) e [software](https://lab.phc.dm.unipi.it/orario), e occuparsi di server autogestiti.
|
||||||
|
|
||||||
|
Le macchine del PHC girano principalmente Linux/Unix come sistemi operativi e i macchinisti sono grandi sostenitori di software [FOSS](https://it.wikipedia.org/wiki/Free_and_Open_Source_Software) (che loro stessi sviluppano sull'[istanza Gitea del PHC](https://git.phc.dm.unipi.it/phc)).
|
||||||
|
|
||||||
|
La lista dei vari macchinisti e di altri eventi notevoli si trova nella [pagina della storia](/storia) del PHC.
|
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Il Nuovo Sito del PHC
|
||||||
|
description: Benvenuti nel nuovo sito web del PHC, realizzato con la tecnologia web Astro! In questo articolo, ne vedremo le feature principali e cosa serba il futuro.
|
||||||
|
publishDate: 2024-10-05
|
||||||
|
---
|
||||||
|
|
||||||
|
# Il Nuovo Sito del PHC
|
||||||
|
|
||||||
|
Benvenuti nel nuovo sito web, realizzato con la tecnologia web [Astro](https://astro.build/)! In questo articolo, ne vedremo le feature principali e cosa serba il futuro.
|
||||||
|
|
||||||
|
## Astro: Un Potente Framework Statico
|
||||||
|
|
||||||
|
Astro è un framework statico che ci consente di creare siti web veloci e performanti. Grazie alla sua natura statica, il sito viene generato in anticipo, consentendo un caricamento rapido delle pagine e un'esperienza utente ottimale.
|
||||||
|
|
||||||
|
## Caratteristiche del Nuovo Sito
|
||||||
|
|
||||||
|
Il nuovo sito web realizzato in Astro offre diverse caratteristiche interessanti:
|
||||||
|
|
||||||
|
1. **Interattività**: Grazie all'utilizzo di Astro, siamo in grado di creare pagine web interattive che offrono un'esperienza coinvolgente agli utenti. Questo ci permette di creare una navigazione fluida e dinamica all'interno del sito.
|
||||||
|
|
||||||
|
2. **Pagina di Appunti dei Corsi**: Una delle prossime funzionalità che stiamo sviluppando è una pagina di appunti dei corsi interattiva e ricercabile. Questa pagina consentirà agli utenti di cercare e accedere facilmente agli appunti dei corsi, rendendo lo studio più efficiente e organizzato.
|
||||||
|
|
||||||
|
3. **Utenti Registrati**: In futuro, prevediamo di implementare un sistema di registrazione degli utenti. Ciò consentirà agli utenti di creare un account personalizzato e accedere a funzionalità esclusive, come mettere mi piace ai propri appunti preferiti o uplodare le proprie risorse.
|
||||||
|
|
||||||
|
## Cosa Aspettarsi in Futuro
|
||||||
|
|
||||||
|
Il nostro obiettivo è continuare a migliorare il sito web e offrire un'esperienza sempre più completa agli utenti. Alcune delle prossime caratteristiche che stiamo sviluppando includono:
|
||||||
|
|
||||||
|
- **Integrazione con Social Media**: Vogliamo consentire agli utenti di condividere facilmente il contenuto del sito web sui social media, ampliando così la visibilità e l'accessibilità del nostro materiale.
|
||||||
|
|
||||||
|
- **Miglioramenti dell'Interfaccia Utente**: Stiamo lavorando per migliorare l'aspetto e la sensazione del sito web, rendendolo più intuitivo e piacevole da navigare.
|
||||||
|
|
||||||
|
- **Espansione dei Contenuti**: Continueremo ad aggiungere nuovi articoli, tutorial e risorse per fornire agli utenti un'ampia gamma di contenuti di qualità.
|
||||||
|
|
||||||
|
## Conclusioni
|
||||||
|
|
||||||
|
Il nuovo sito web realizzato in Astro offre un'esperienza interattiva e performante. Con l'aggiunta di una pagina di appunti dei corsi ricercabile e la possibilità di registrarsi come utenti, stiamo lavorando per rendere il sito ancora più utile e coinvolgente. Continuate a seguirci per rimanere aggiornati sulle ultime novità e miglioramenti!
|
@ -0,0 +1,5 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||||
|
import Database from 'better-sqlite3'
|
||||||
|
|
||||||
|
const sql = new Database('out/website.sqlite')
|
||||||
|
export const db = drizzle(sql)
|
@ -0,0 +1,4 @@
|
|||||||
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
||||||
|
import { db } from './index'
|
||||||
|
|
||||||
|
migrate(db, { migrationsFolder: 'out/drizzle' })
|
@ -0,0 +1,43 @@
|
|||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
import { text, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||||
|
|
||||||
|
export const users = sqliteTable('users', {
|
||||||
|
// id è l'id unico di questo utente, non è modificabile una volta creato l'utente.
|
||||||
|
id: text('id')
|
||||||
|
.primaryKey()
|
||||||
|
.default(sql`(lower(hex(randomblob(16))))`),
|
||||||
|
|
||||||
|
// Username è il nome leggibile di questo utente utilizzato anche per le
|
||||||
|
// route per singolo utente, deve essere unico nel sito.
|
||||||
|
//
|
||||||
|
// NOTE: Quando un utente accede per la prima volta di default gli viene
|
||||||
|
// chiesto se usare quello dell'account che sta usando o se cambiarlo
|
||||||
|
// (in teoria non dovrebbe essere un problema poterlo modificare
|
||||||
|
// successivamente).
|
||||||
|
username: text('username').unique().notNull(),
|
||||||
|
|
||||||
|
// FullName da mostrare in giro per il sito
|
||||||
|
fullName: text('fullname'),
|
||||||
|
|
||||||
|
// Email per eventuale contatto
|
||||||
|
email: text('email'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type User = typeof users.$inferSelect // return type when queried
|
||||||
|
export type InsertUser = typeof users.$inferInsert // insert type
|
||||||
|
|
||||||
|
export const accounts = sqliteTable('accounts', {
|
||||||
|
// id è l'id unico di questo account, non è modificabile una volta creato l'account.
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
|
||||||
|
userId: text('userId')
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
|
||||||
|
provider: text('provider').$type<'poisson' | 'ateneo'>().notNull(),
|
||||||
|
|
||||||
|
token: text('token').notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Account = typeof accounts.$inferSelect // return type when queried
|
||||||
|
export type InsertAccount = typeof accounts.$inferInsert // insert type
|
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<Header />
|
||||||
|
<main class="article card large">
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
import '@fontsource/open-sans/latin.css'
|
||||||
|
import '@fontsource/source-sans-pro/latin.css'
|
||||||
|
import '@fontsource/source-code-pro/latin.css'
|
||||||
|
import '@fontsource/space-mono/latin.css'
|
||||||
|
import '@fontsource/iosevka/latin.css'
|
||||||
|
|
||||||
|
import '@fontsource-variable/material-symbols-outlined/full.css'
|
||||||
|
|
||||||
|
import '../styles/main.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
thumbnail?: string
|
||||||
|
|
||||||
|
/** Tags for the page, used for styling */
|
||||||
|
pageTags?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, thumbnail, pageTags } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta property="og:title" content={title ?? 'PHC'} />
|
||||||
|
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
||||||
|
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import renderMathInElement from 'katex/contrib/auto-render'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
renderMathInElement(document.body, {
|
||||||
|
delimiters: [
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '$', right: '$', display: false },
|
||||||
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
|
{ left: '\\[', right: '\\]', display: true },
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body class:list={typeof pageTags === 'string' ? [pageTags] : pageTags}>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
import { AppuntiList, AppuntiCard } from '@client/Appunti'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="appunti">
|
||||||
|
<h1>Appunti & Dispense</h1>
|
||||||
|
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" />
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>In primo piano</h2>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable center">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="someuser" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load course="Geometria 1" author="exampleuser" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load course="Algebra 1" author="anotheruser" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Categorie</h2>
|
||||||
|
|
||||||
|
<h3>Analisi</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Algebra Lineare</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Geometria</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import ArticleLayout from '../../layouts/ArticleLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
|
||||||
|
return guides.map(entry => ({
|
||||||
|
params: { id: entry.slug },
|
||||||
|
props: { entry },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
const { Content } = await entry.render()
|
||||||
|
---
|
||||||
|
|
||||||
|
<ArticleLayout {...entry.data} pageTags={['guida', entry.data.id, entry.data.series && 'series']}>
|
||||||
|
<h1>{entry.data.title}</h1>
|
||||||
|
{entry.data.series && <div class="series">Serie: {entry.data.series}</div>}
|
||||||
|
<Content />
|
||||||
|
</ArticleLayout>
|
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="guide">
|
||||||
|
<h1>Guide</h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
guides.map(guide => (
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">
|
||||||
|
<a href={`/guide/${guide.slug}`}>{guide.data.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text">{guide.data.description}</div>
|
||||||
|
<div class="tags">
|
||||||
|
{guide.data.tags.map(tag => (
|
||||||
|
<a href={`/guide/tags/${tag}`}>#{tag}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import PageLayout from '@/layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
|
||||||
|
const tags: string[] = []
|
||||||
|
|
||||||
|
guides.forEach(post => {
|
||||||
|
post.data.tags.forEach(tag => {
|
||||||
|
tags.push(tag)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(new Set(tags)).map(tag => {
|
||||||
|
return {
|
||||||
|
params: { tag },
|
||||||
|
props: {
|
||||||
|
tag,
|
||||||
|
guides: guides.filter(post => post.data.tags.includes(tag)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tag: string
|
||||||
|
guides: CollectionEntry<'guides'>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag, guides } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="guide tag">
|
||||||
|
<h1>Guide > #{tag}</h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
guides.map(guide => (
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">
|
||||||
|
<a href={`/guide/${guide.slug}`}>{guide.data.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text">{guide.data.description}</div>
|
||||||
|
<div class="tags">
|
||||||
|
{guide.data.tags.map(tag => (
|
||||||
|
<a href={`/guide/tags/${tag}`}>#{tag}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
import { Content as WhatPhcContent, frontmatter as whatsPhcFrontmatter } from '../content/meta/whats-phc.md'
|
||||||
|
|
||||||
|
const news = await getCollection('news')
|
||||||
|
|
||||||
|
const galleryCollage = (await Astro.glob('@/assets/gallery/*')).map(({ default: { src } }) => src)
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="homepage">
|
||||||
|
<section class="principal">
|
||||||
|
<div class="circuit-layer">
|
||||||
|
<canvas id="circuits-art"></canvas>
|
||||||
|
<script src="../scripts/circuits-art.ts"></script>
|
||||||
|
</div>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||||
|
</div>
|
||||||
|
<div class="whats-phc card large">
|
||||||
|
<input type="checkbox" class="hide" id="mobile-whats-phc-read-more" checked />
|
||||||
|
<div class="title">{whatsPhcFrontmatter.title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<WhatPhcContent />
|
||||||
|
</div>
|
||||||
|
<div class="mobile-only">
|
||||||
|
<label for="mobile-whats-phc-read-more">
|
||||||
|
<div class="button">
|
||||||
|
<span>Mostra di più</span>
|
||||||
|
<span>Mostra meno</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="news">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-1" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#C2A8EB" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-1)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Ultime Notizie</div>
|
||||||
|
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
news.map(newsItem => (
|
||||||
|
<div class="card">
|
||||||
|
<a href={`/notizie/${newsItem.slug}`} class="title">
|
||||||
|
{newsItem.data.title}
|
||||||
|
</a>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="text">{newsItem.data.description}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="primary" href="/notizie" role="button">Vai all'Archivio</a>
|
||||||
|
</section>
|
||||||
|
<section class="projects">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Progetti</div>
|
||||||
|
|
||||||
|
<div class="project-list">
|
||||||
|
<a target="_blank" href="https://git.phc.dm.unipi.it/" style="--masonry-height: 2;">
|
||||||
|
<div class="project" style="--card-bg: rgb(150, 197, 150);">
|
||||||
|
<div class="image">
|
||||||
|
<img
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/b/bb/Gitea_Logo.svg"
|
||||||
|
alt="logo Gitea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="title">Gitea</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
Gitea è un servizio di hosting per progetti software, come GitHub ma autogestito.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Qui puoi trovare i progetti del PHC, e accedendo con un account di Ateneo potrai
|
||||||
|
crearne di nuovi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!-- <a href="#" style="--masonry-height: 2;">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">PHC-Bot</div>
|
||||||
|
<div class="description">
|
||||||
|
Un bot con cui chattare per chiedere informazioni o supporto tecnico.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a> -->
|
||||||
|
<a target="_blank" href="https://lab.phc.dm.unipi.it/orario/" style="--masonry-height: 1;">
|
||||||
|
<div class="project" style="--card-bg: #75ca75;">
|
||||||
|
<div class="image auto">
|
||||||
|
<img src="https://lab.phc.dm.unipi.it/orario/icon.png" alt="Logo Orario" />
|
||||||
|
</div>
|
||||||
|
<div class="title">Orario</div>
|
||||||
|
<div class="description">
|
||||||
|
Questo sito permette di visualizzare il proprio orario delle lezioni, con informazioni
|
||||||
|
sui docenti e le aule.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://lab.phc.dm.unipi.it/problemi/" style="--masonry-height: 2;">
|
||||||
|
<div class="project" style="--card-bg: #aa88c0;">
|
||||||
|
<div class="image">
|
||||||
|
<img
|
||||||
|
src="https://lab.phc.dm.unipi.it/problemi/favicon/android-chrome-512x512.png"
|
||||||
|
alt="logo Problemi"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="title">Problemi</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
Bacheca di problemi di Matematica da risolvere, inviandone le soluzioni in LaTeX.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>La sera i problemi compaiono sullo schermo nell'atrio del Dipartimento.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="http://steffe.cs.dm.unipi.it/" style="--masonry-height: 2;">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image cover">
|
||||||
|
<img src="https://steffe.lb.cs.dm.unipi.it/assets/img/logo.png" alt="Logo Cluster" />
|
||||||
|
</div>
|
||||||
|
<div class="title">Cluster "Steffè"</div>
|
||||||
|
<div class="description">
|
||||||
|
Cluster progettato ed assemblato durante il progetto speciale per la didattica
|
||||||
|
"Calcolo Parallelo dall'Infrastruttura alla Matematica".
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://seminarietti.phc.dm.unipi.it/" style="--masonry-height: 2;">
|
||||||
|
<div class="project" style="--card-bg: #bd9fec;">
|
||||||
|
<div class="image">
|
||||||
|
<img src="https://seminarietti.phc.dm.unipi.it/favicon.png" alt="Logo Seminarietti" />
|
||||||
|
</div>
|
||||||
|
<div class="title">Seminarietti</div>
|
||||||
|
<div class="description">
|
||||||
|
Varie conferenze del PHC su argomenti di informatica, matematica e tecnologia.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://tutorato.phc.dm.unipi.it/" style="--masonry-height: 2;">
|
||||||
|
<div class="project" style="--card-bg: #c55;">
|
||||||
|
<div class="image">
|
||||||
|
<img src="https://tutorato.phc.dm.unipi.it/favicon.svg" alt="Logo Tutorato" />
|
||||||
|
</div>
|
||||||
|
<div class="title">Tutorato</div>
|
||||||
|
<div class="description">
|
||||||
|
Un sito con tutte le informazioni sui tutorati di Matematica, con tanto di archivio
|
||||||
|
degli anni passati.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="wanna-be-macchinista">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-3" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#888" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-3)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<div class="card large" style="--card-base: #ddd;">
|
||||||
|
<div class="title">E cosa si fa in PHC?</div>
|
||||||
|
<div class="text">
|
||||||
|
<p>Nessuno lo sa di preciso, ma facciamo molte cose:</p>
|
||||||
|
<ul>
|
||||||
|
<li>amministrazione di sistemi Linux/Unix e macchine virtuali</li>
|
||||||
|
<li>supporto tecnico per installare e usare Linux sul proprio portatile</li>
|
||||||
|
<li>costruire, smontare ed aggiustare computer antichi e moderni</li>
|
||||||
|
<li>sviluppo di software backend e web development</li>
|
||||||
|
<li>organizzazione di seminari a tema tecnologico</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gallery-collage">
|
||||||
|
{
|
||||||
|
galleryCollage.map((src, i) => (
|
||||||
|
<div class="card">
|
||||||
|
<img src={src} alt={`gallery image ${i}`} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="card large" style="--card-base: #ddd;">
|
||||||
|
<div class="title">Vuoi diventare macchinista?</div>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
<strong>Macchinista non si nasce, si diventa:</strong> se sei uno studente di Matematica e
|
||||||
|
vuoi diventare un macchinista, vienici a trovare!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
L'unico prerequisito è la voglia di imparare! Di solito, c'è un periodo di "apprendistato"
|
||||||
|
nel quale si apprendono le basi, ed una volta superato potrai diventare un macchinista a
|
||||||
|
tutti gli effetti. <a href="mailto:macchinisti@lists.dm.unipi.it"> </a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="login">
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<!-- form with username and password, and a button for oauth login -->
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<h3 class="center">Accedi con Poisson</h3>
|
||||||
|
<input type="text" id="username" placeholder="Username" name="username" required />
|
||||||
|
<input type="password" id="password" placeholder="Password" name="password" required />
|
||||||
|
|
||||||
|
<button class="primary center" type="submit">Login</button>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3 class="center">Accedi con Ateneo</h3>
|
||||||
|
<a href="/auth/ateneo" class="primary center" role="button">Login</a>
|
||||||
|
</form>
|
||||||
|
<!-- <span class="material-symbols-outlined">person</span> -->
|
||||||
|
</PageLayout>
|
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../../components/Header.astro'
|
||||||
|
import Footer from '../../components/Footer.astro'
|
||||||
|
|
||||||
|
import CodeBlockPreview from '../../components/meta/CodeBlockPreview.astro'
|
||||||
|
|
||||||
|
import { Content, getHeadings } from '../../content/meta/design.mdx'
|
||||||
|
|
||||||
|
const headings = getHeadings()
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props} pageTags="design">
|
||||||
|
<Header />
|
||||||
|
<aside>
|
||||||
|
<nav>
|
||||||
|
<h3>Indice</h3>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
headings.map(heading => (
|
||||||
|
<li style={{ '--depth': heading.depth }}>
|
||||||
|
<a href={`#${heading.slug}`}>{heading.text}</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="text">
|
||||||
|
<Content components={{ pre: CodeBlockPreview }} />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import ArticleLayout from '../../layouts/ArticleLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('news')
|
||||||
|
return guides.map(entry => ({
|
||||||
|
params: { id: entry.slug },
|
||||||
|
props: { entry },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
const { Content } = await entry.render()
|
||||||
|
---
|
||||||
|
|
||||||
|
<ArticleLayout {...entry.data} pageTags={['notizia']}>
|
||||||
|
<Content />
|
||||||
|
</ArticleLayout>
|
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
const news = await getCollection('news')
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="notizie">
|
||||||
|
<h1>Notizie</h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
news.map(newsItem => (
|
||||||
|
<div class="card">
|
||||||
|
<a href={`/notizie/${newsItem.slug}`} class="title">
|
||||||
|
{newsItem.data.title}
|
||||||
|
</a>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="text">{newsItem.data.description}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
import Timeline from '../components/Timeline.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props} pageTags={'storia'}>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<h1>Storia</h1>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
Qui annoverariamo la storia del PHC in una timeline con gli eventi più salienti del progetto.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Per delle note storiche un po' più dettagliate, si legga l'ottima pagine sul <a
|
||||||
|
href="http://betti.dm.unipi.it/servizi/PHC.html">sito del dipartimento</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="timeline">
|
||||||
|
<Timeline title="Tanti Francesco" year="2022">
|
||||||
|
<p>
|
||||||
|
Nel 2022, entrano a far parte del PHC <strong>Francesco Minnocci</strong>, <strong
|
||||||
|
>Francesco Manicastri</strong
|
||||||
|
> e <strong>Francesco Baldino</strong>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Nuovi macchinisti" year="2019">
|
||||||
|
<p>
|
||||||
|
Nel 2019, entrano a far parte del PHC <strong>Antonio De Lucreziis</strong> e <strong
|
||||||
|
>Illya Serdyuk</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Nuovi macchinisti" year="2018">
|
||||||
|
<p>
|
||||||
|
Nel 2018, entrano a far parte del PHC <strong>Francesco Caporali</strong> e <strong
|
||||||
|
>Letizia D'Achille</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Rinnovo del sito" year="2004">
|
||||||
|
<p>
|
||||||
|
Dopo un periodo di inattività del progetto, il sito del PHC viene riscritto in PHP e
|
||||||
|
trasferito sul dominio <a
|
||||||
|
href="https://web.archive.org/web/20040823112401/http://poisson.phc.unipi.it/"
|
||||||
|
>poisson.phc.unipi.it</a
|
||||||
|
>, il cui design è caratterizzato da un <a
|
||||||
|
href="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
|
||||||
|
>logo</a
|
||||||
|
> creato da <strong>Michele Cerulli</strong>.
|
||||||
|
</p><img
|
||||||
|
class="dropdown"
|
||||||
|
src="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Seminari del PHC" year="2000">
|
||||||
|
<p>
|
||||||
|
Nel primo semestre del 2000, vengono tenuti diversi <a
|
||||||
|
href="https://web.archive.org/web/20010430151939/http://www.phc.unipi.it/seminari2000/"
|
||||||
|
>seminari</a
|
||||||
|
> su temi quali: installazione di programmi e sistemi operativi (come Linux), sicurezza informatica,
|
||||||
|
esperimenti con matematica e musica e creazione di pagine web.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Rete del PHC e DNS" year="1999">
|
||||||
|
<p>
|
||||||
|
Nel maggio del 1999 viene attivata la rete 131.114.10.0, con tanto di nameserver sul
|
||||||
|
dominio <a href="https://web.archive.org/web/20010410215451/http://www.phc.unipi.it/"
|
||||||
|
>phc.unipi.it</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Fondazione del PHC" year="1999">
|
||||||
|
<p>
|
||||||
|
In seguito alla proposta del prof. <strong>Sergio Steffè</strong>, in data 26 febbraio
|
||||||
|
1999 il Dipartimento di Matematica approva una delibera per stanziare la stanza 106 ed
|
||||||
|
alcuni computer ad uso di un gruppo di studenti, così da avere un luogo in cui
|
||||||
|
"smanettare", dare supporto informatica agli studenti e gestire il sito Poisson.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Apertura di Poisson" year="~1994">
|
||||||
|
<p>
|
||||||
|
Nell'attuale Aula 4, allora semplice Aula studenti, nasce il sito web <strong
|
||||||
|
>poisson.dm.unipi.it</strong
|
||||||
|
>
|
||||||
|
su dei computer messi a disposizione agli studenti da Vinicio Villani. Una versione del 1996
|
||||||
|
di tale sito si trova nel <a
|
||||||
|
href="https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it/"
|
||||||
|
>Web Archive</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
import { UtentiPage } from '../client/UtentiPage.tsx'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageTags="utenti">
|
||||||
|
<h1>Utenti</h1>
|
||||||
|
<UtentiPage client:load />
|
||||||
|
</PageLayout>
|
@ -0,0 +1,333 @@
|
|||||||
|
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
|
||||||
|
|
||||||
|
interface Grid<T> extends Iterable<[number, number, T]> {
|
||||||
|
has(point: [number, number]): boolean
|
||||||
|
get(point: [number, number]): T
|
||||||
|
set(point: [number, number], value: T): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrid<T>(): Grid<T> {
|
||||||
|
const cells: Record<string, T> = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
has([x, y]) {
|
||||||
|
return cells[`${x},${y}`] !== undefined
|
||||||
|
},
|
||||||
|
get([x, y]) {
|
||||||
|
return cells[`${x},${y}`]
|
||||||
|
},
|
||||||
|
set([x, y], value) {
|
||||||
|
cells[`${x},${y}`] = value
|
||||||
|
},
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (const [coord, value] of Object.entries(cells)) {
|
||||||
|
const [x, y] = coord.split(',').map(s => parseInt(s))
|
||||||
|
yield [x, y, value]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
|
||||||
|
|
||||||
|
type WireDirection = 'down' | 'down-left' | 'down-right'
|
||||||
|
|
||||||
|
type WireSteps = { position: Point2; direction: WireCell }[]
|
||||||
|
|
||||||
|
type Point2 = [number, number]
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
grid: Grid<WireCell>
|
||||||
|
queuedWire: {
|
||||||
|
index: number
|
||||||
|
steps: WireSteps
|
||||||
|
} | null
|
||||||
|
badTries: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderer = {
|
||||||
|
timer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer: Renderer | null = null
|
||||||
|
|
||||||
|
const RENDERER_FPS = 30
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
console.log('Setting up circuits art...')
|
||||||
|
|
||||||
|
$canvas.width = $canvas.clientWidth
|
||||||
|
$canvas.height = $canvas.clientHeight
|
||||||
|
|
||||||
|
const g = $canvas.getContext('2d')!
|
||||||
|
const state: State = {
|
||||||
|
grid: createGrid(),
|
||||||
|
queuedWire: null,
|
||||||
|
badTries: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = new Date()
|
||||||
|
|
||||||
|
const handle = window.setInterval(() => {
|
||||||
|
const time = new Date().getTime() - startTime.getTime()
|
||||||
|
|
||||||
|
update(state, g.canvas.width, g.canvas.height, time)
|
||||||
|
render(g, state, time)
|
||||||
|
}, 1000 / RENDERER_FPS)
|
||||||
|
|
||||||
|
renderer = { timer: handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(state: State, width: number, height: number, time: number) {
|
||||||
|
const w = (width / CELL_SIZE) | 0
|
||||||
|
// const h = (height / CELL_SIZE) | 0
|
||||||
|
|
||||||
|
if (state.badTries > 1000) {
|
||||||
|
console.log('finished')
|
||||||
|
clearInterval(renderer!.timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.queuedWire) {
|
||||||
|
const sx = randomInt(0, w)
|
||||||
|
if (!state.grid.has([sx, 0])) {
|
||||||
|
const steps = generateWire(state.grid, [sx, 0])
|
||||||
|
if (steps.length < 7) {
|
||||||
|
state.badTries++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.queuedWire = {
|
||||||
|
index: 0,
|
||||||
|
steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
state.grid.set(state.queuedWire.steps[0].position, 'dot')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.badTries++
|
||||||
|
} else {
|
||||||
|
const wire = state.queuedWire
|
||||||
|
|
||||||
|
const step = wire.steps[wire.index]
|
||||||
|
state.grid.set(step.position, step.direction)
|
||||||
|
|
||||||
|
if (wire.index + 1 < wire.steps.length) {
|
||||||
|
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
|
||||||
|
}
|
||||||
|
|
||||||
|
wire.index++
|
||||||
|
|
||||||
|
if (wire.index >= wire.steps.length) {
|
||||||
|
state.queuedWire = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CELL_SIZE = 27
|
||||||
|
|
||||||
|
const BACKGROUND_COLOR = '#ecffe3'
|
||||||
|
const WIRE_COLOR = '#a6ce94'
|
||||||
|
|
||||||
|
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
|
||||||
|
'down': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(0, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-left': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(-1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-right': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(+1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'dot': g => {
|
||||||
|
g.fillStyle = BACKGROUND_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.fill()
|
||||||
|
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(g: CanvasRenderingContext2D, state: State, time: number) {
|
||||||
|
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
|
||||||
|
g.resetTransform()
|
||||||
|
g.scale(CELL_SIZE, CELL_SIZE)
|
||||||
|
g.lineWidth = 3 / CELL_SIZE
|
||||||
|
|
||||||
|
const w = (g.canvas.width / CELL_SIZE) | 0
|
||||||
|
const h = (g.canvas.height / CELL_SIZE) | 0
|
||||||
|
for (let y = 0; y <= h + 1; y++) {
|
||||||
|
for (let x = 0; x <= w + 1; x++) {
|
||||||
|
if (!state.grid.has([x, y])) continue
|
||||||
|
|
||||||
|
const cell = state.grid.get([x, y])
|
||||||
|
|
||||||
|
g.save()
|
||||||
|
g.translate(x, y)
|
||||||
|
|
||||||
|
// g.fillStyle = '#f008'
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
|
||||||
|
RENDER_CELL[cell](g)
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (state.queuedWire) {
|
||||||
|
// for (const step of state.queuedWire.steps) {
|
||||||
|
// const [x, y] = step.position
|
||||||
|
|
||||||
|
// g.fillStyle = '#00f8'
|
||||||
|
// g.save()
|
||||||
|
// g.translate(x, y)
|
||||||
|
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const [mx, my] = state.mouse
|
||||||
|
// g.save()
|
||||||
|
// g.fillStyle = '#0008'
|
||||||
|
// g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(0, 0, 1, 1)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
setup()
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (renderer) {
|
||||||
|
clearInterval(renderer.timer)
|
||||||
|
}
|
||||||
|
setup()
|
||||||
|
})
|
||||||
|
|
||||||
|
function randomInt(from: number, to: number) {
|
||||||
|
return Math.floor(Math.random() * (to - from + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomChoice<T>(choices: T[]): T {
|
||||||
|
return choices[randomInt(0, choices.length - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomWeightedChoice<T>(choices: [T, number][]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 + 4 + 5 + 6
|
||||||
|
randomWeightedChoice([
|
||||||
|
['a', 3],
|
||||||
|
['b', 4],
|
||||||
|
['c', 5],
|
||||||
|
['d', 6],
|
||||||
|
])
|
||||||
|
|
||||||
|
const DIR_TO_VEC: Record<WireDirection, Point2> = {
|
||||||
|
['down']: [0, 1],
|
||||||
|
['down-left']: [-1, 1],
|
||||||
|
['down-right']: [+1, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortCircuitBoolean = boolean | (() => boolean)
|
||||||
|
|
||||||
|
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
|
||||||
|
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
|
||||||
|
*/
|
||||||
|
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
|
||||||
|
['down']: ([x, y], grid) =>
|
||||||
|
!grid.has([x, y + 1]) &&
|
||||||
|
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
|
||||||
|
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
|
||||||
|
['down-left']: ([x, y], grid) =>
|
||||||
|
!grid.has([x - 1, y + 1]) &&
|
||||||
|
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
|
||||||
|
['down-right']: ([x, y], grid) =>
|
||||||
|
!grid.has([x + 1, y + 1]) &&
|
||||||
|
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneDirections(
|
||||||
|
grid: Grid<WireCell>,
|
||||||
|
position: Point2,
|
||||||
|
directions: WireDirection[],
|
||||||
|
): WireDirection[] {
|
||||||
|
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWire(
|
||||||
|
grid: Grid<WireCell>,
|
||||||
|
startingPoint: Point2,
|
||||||
|
): { position: Point2; direction: WireCell }[] {
|
||||||
|
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
|
||||||
|
let currentPosition = startingPoint
|
||||||
|
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
|
||||||
|
|
||||||
|
const steps: { position: Point2; direction: WireCell }[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < segmentLength; i++) {
|
||||||
|
const availableDirections = pruneDirections(grid, currentPosition, [
|
||||||
|
'down',
|
||||||
|
'down-left',
|
||||||
|
'down-right',
|
||||||
|
])
|
||||||
|
if (availableDirections.length === 0) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
const dir =
|
||||||
|
availableDirections.includes(currentDirection) && Math.random() < 0.25
|
||||||
|
? currentDirection
|
||||||
|
: randomChoice(availableDirections)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(currentDirection === 'down-left' && dir === 'down-right') ||
|
||||||
|
(currentDirection === 'down-right' && dir === 'down-left')
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = currentPosition
|
||||||
|
const [dx, dy] = DIR_TO_VEC[dir]
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
position: [x, y],
|
||||||
|
direction: dir,
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPosition = [x + dx, y + dy]
|
||||||
|
currentDirection = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = steps.at(-1)
|
||||||
|
if (last) {
|
||||||
|
last.direction = 'dot'
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps
|
||||||
|
}
|
@ -0,0 +1,781 @@
|
|||||||
|
// $news-bg: #fffbeb;
|
||||||
|
// $news-accent-bg: #f8e8b1;
|
||||||
|
|
||||||
|
@import './mixins.scss';
|
||||||
|
|
||||||
|
@layer component {
|
||||||
|
//
|
||||||
|
// Components - for complex parts of the UI like search bars or compound buttons
|
||||||
|
//
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-family: 'Material Symbols Outlined Variable';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
|
||||||
|
display: inline-grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 0,
|
||||||
|
'wght' 300,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24;
|
||||||
|
|
||||||
|
max-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Iosevka', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover input[type='text'] {
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just to know for reference
|
||||||
|
.fake-masonry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--columns), 1fr);
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-row: span var(--masonry-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr auto;
|
||||||
|
grid-template-areas: 'icon text . right';
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .icon {
|
||||||
|
grid-area: icon;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
grid-area: text;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .right {
|
||||||
|
grid-area: right;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-scrollable {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.appunti-list {
|
||||||
|
padding: 2px;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
color: #444;
|
||||||
|
|
||||||
|
& > .thumbnail {
|
||||||
|
width: 10rem;
|
||||||
|
aspect-ratio: 10 / 14;
|
||||||
|
background: #e0e0e0;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($hoverable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .thumbnail + * {
|
||||||
|
font-weight: 700;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .title,
|
||||||
|
& > .course,
|
||||||
|
& > .author,
|
||||||
|
& > .course-year {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
|
||||||
|
.article {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
|
||||||
|
background: var(--card-bg, $project-card-bg);
|
||||||
|
color: #000e;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
// .news-item {
|
||||||
|
// background: $news-bg;
|
||||||
|
// color: #111;
|
||||||
|
|
||||||
|
// @include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
|
||||||
|
// width: 22rem;
|
||||||
|
// max-height: 27rem;
|
||||||
|
|
||||||
|
// overflow: hidden;
|
||||||
|
|
||||||
|
// ::-webkit-scrollbar {
|
||||||
|
// width: 10px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ::-webkit-scrollbar-thumb {
|
||||||
|
// background-color: #c67e14;
|
||||||
|
// border: 2px solid #222;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// background-color: #e69419;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// a {
|
||||||
|
// font-weight: 600;
|
||||||
|
// text-decoration: none;
|
||||||
|
// color: #c67e14;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// text-decoration: underline solid 2px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .title {
|
||||||
|
// padding: 1rem;
|
||||||
|
// background: $news-accent-bg;
|
||||||
|
// line-height: 1;
|
||||||
|
// font-size: 26px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// a.title {
|
||||||
|
// color: #83530c;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .abstract {
|
||||||
|
// flex-grow: 1;
|
||||||
|
|
||||||
|
// padding: 1rem;
|
||||||
|
|
||||||
|
// overflow-y: auto;
|
||||||
|
|
||||||
|
// @extend .text;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .content {
|
||||||
|
// display: flex;
|
||||||
|
// padding: 1rem;
|
||||||
|
// flex-direction: column;
|
||||||
|
// gap: 0.5rem;
|
||||||
|
|
||||||
|
// background: #fff8da;
|
||||||
|
|
||||||
|
// & > .continue {
|
||||||
|
// padding: 1rem;
|
||||||
|
|
||||||
|
// display: grid;
|
||||||
|
// align-items: end;
|
||||||
|
// justify-content: end;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .description {
|
||||||
|
// font-size: 16px;
|
||||||
|
// line-height: 1.5;
|
||||||
|
|
||||||
|
// flex-grow: 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .tags {
|
||||||
|
// display: flex;
|
||||||
|
// gap: 0.5rem;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
// font-size: 14px;
|
||||||
|
// color: #555;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .date {
|
||||||
|
// font-size: 14px;
|
||||||
|
// font-style: italic;
|
||||||
|
// font-weight: 600;
|
||||||
|
// color: #0008;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .author {
|
||||||
|
// font-weight: 600;
|
||||||
|
// font-size: 15px;
|
||||||
|
// color: #555;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
--timeline-track-size: 6rem;
|
||||||
|
--timeline-thickness: 6px;
|
||||||
|
--timeline-color: #333;
|
||||||
|
|
||||||
|
grid-template-columns: 1fr var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
--timeline-track-size: 3rem;
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
max-width: 120ch;
|
||||||
|
|
||||||
|
& > .timeline-item {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding: 1rem 1rem 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .date {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeline vertical line
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: var(--timeline-thickness);
|
||||||
|
|
||||||
|
background: var(--timeline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child::before {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child::before {
|
||||||
|
bottom: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeline circle
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background: var(--timeline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
&:nth-child(odd)::before {
|
||||||
|
grid-column: 2 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even)::before {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd)::after {
|
||||||
|
grid-column: 2 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even)::after {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
max-width: 15rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(-10rem - 1px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(calc(-50% - var(--timeline-track-size) / 2));
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
&:nth-child(odd) {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
grid-template-columns: 1fr var(--timeline-track-size);
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
left: auto;
|
||||||
|
right: 50%;
|
||||||
|
transform: translateX(calc(50% - var(--timeline-track-size) / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
grid-column: 2 / span 2;
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(calc(-50% + var(--timeline-track-size) / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Cards
|
||||||
|
//
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
--card-base-internal: var(--card-base, #f8f8f8);
|
||||||
|
|
||||||
|
background: var(--card-base-internal);
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
// Variants
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 4px, $offset: 8px);
|
||||||
|
|
||||||
|
row-gap: 1rem;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child Items
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dimmed {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 60%);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline solid 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline solid 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.title {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline solid 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Card List
|
||||||
|
//
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
|
||||||
|
& > .card {
|
||||||
|
max-width: 25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
place-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
& > .search {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .filter-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover select {
|
||||||
|
background: var(--filter-bg-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
.combobox {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0.25rem 0 0.25rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
height: 100%;
|
||||||
|
gap: 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: -3px;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.5rem 0 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
&:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
.dropdown {
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
|
||||||
|
top: calc(100% + 9px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-collage {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
place-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
max-width: 64rem;
|
||||||
|
|
||||||
|
& > .card {
|
||||||
|
padding: 3px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
max-width: 25rem;
|
||||||
|
max-height: 25rem;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
max-height: none;
|
||||||
|
|
||||||
|
width: 25rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
@import './mixins.scss';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Controls - for things like buttons, input, select
|
||||||
|
//
|
||||||
|
|
||||||
|
@layer common {
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
[role='button'] {
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: 5px 5px 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.25rem 1.5rem;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #1e6733;
|
||||||
|
color: #f4fef7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2b8b47;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flat {
|
||||||
|
background: transparent;
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0002;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='password'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fdfdfd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
background: #38adc1;
|
||||||
|
|
||||||
|
min-width: 40ch;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
[role='button'] {
|
||||||
|
padding-left: 3rem;
|
||||||
|
padding-right: 3rem;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #1c7f90;
|
||||||
|
color: #f4fef7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4ea2b1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #0003;
|
||||||
|
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
$black: #222;
|
||||||
|
$border-large: 4px solid $black;
|
||||||
|
|
||||||
|
$header-bg: #fff;
|
||||||
|
|
||||||
|
$footer-bg: #444;
|
||||||
|
$footer-fg: #fdfdfd;
|
||||||
|
|
||||||
|
$homepage-principal-bg: #ecffe3;
|
||||||
|
$homepage-whatsphc-bg: #e4c5ff;
|
||||||
|
$homepage-news-bg: #c2a8eb;
|
||||||
|
$homepage-projects-bg: #f5f2cc;
|
||||||
|
$homepage-macchinisti-bg: #888;
|
||||||
|
|
||||||
|
$project-card-bg: #a2d4f3;
|
||||||
|
|
||||||
|
$screen-desktop-min: 1024px;
|
||||||
|
|
||||||
|
@layer common, typography, component, page;
|
||||||
|
|
||||||
|
@import './mixins.scss';
|
||||||
|
@import './typography.scss';
|
||||||
|
|
||||||
|
@layer common {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Typography
|
||||||
|
//
|
||||||
|
|
||||||
|
@import './controls.scss';
|
||||||
|
@import './components.scss';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Custom Page Styles
|
||||||
|
//
|
||||||
|
|
||||||
|
body {
|
||||||
|
// for the header spacing
|
||||||
|
// padding-top: 6rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
|
||||||
|
header {
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
height: 6rem;
|
||||||
|
border-bottom: $border-large;
|
||||||
|
background: $header-bg;
|
||||||
|
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
position: sticky;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-family: 'Iosevka', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
// top: 0;
|
||||||
|
// right: 0;
|
||||||
|
// bottom: 0;
|
||||||
|
// left: 3rem;
|
||||||
|
|
||||||
|
top: 5rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
background: #f0f0f0;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
// grid-template-rows: auto 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
// & > :first-child {
|
||||||
|
// justify-self: end;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
align-content: start;
|
||||||
|
|
||||||
|
width: 20rem;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-menu-toggle {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:not(:checked) ~ .side-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(#header-menu-toggle:checked) #header-menu-toggle-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(#header-menu-toggle:not(:checked)) #header-menu-toggle-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
height: 5rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
z-index: 10;
|
||||||
|
padding: 1rem 0;
|
||||||
|
--paragraph-margin: 0.5rem;
|
||||||
|
--zone-color: #aaa;
|
||||||
|
|
||||||
|
min-height: 6rem;
|
||||||
|
border-top: $border-large;
|
||||||
|
background: $footer-bg;
|
||||||
|
color: $footer-fg;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
min-height: 5rem;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@import './pages.scss';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Misc
|
||||||
|
//
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:vertical {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
border-top: 2px solid #222;
|
||||||
|
border-bottom: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:horizontal {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-top: 2px solid #222;
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
border-right: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #1e6733;
|
||||||
|
border: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #2b8b47;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
// border-left: 2px solid #222;
|
||||||
|
// border-top: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #0002;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:has(#header-menu-toggle:checked) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Utility Classes
|
||||||
|
//
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
|
||||||
|
border: $size solid #222;
|
||||||
|
border-radius: $size * 2;
|
||||||
|
|
||||||
|
@if $shadow {
|
||||||
|
box-shadow: $offset $offset 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $hoverable {
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: $offset + 1 $offset + 1 0 0 #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,749 @@
|
|||||||
|
:root {
|
||||||
|
--guide-base: #a2d4f3;
|
||||||
|
|
||||||
|
--guide-darkest: color-mix(in srgb, var(--guide-base), #000 75%);
|
||||||
|
--guide-darker: color-mix(in srgb, var(--guide-base), #000 50%);
|
||||||
|
--guide-dark: color-mix(in srgb, var(--guide-base), #000 25%);
|
||||||
|
|
||||||
|
--guide-light: color-mix(in srgb, var(--guide-base), #fff 25%);
|
||||||
|
--guide-lighter: color-mix(in srgb, var(--guide-base), #fff 50%);
|
||||||
|
--guide-lightest: color-mix(in srgb, var(--guide-base), #fff 75%);
|
||||||
|
|
||||||
|
--news-base: #f8e8b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer page {
|
||||||
|
.homepage {
|
||||||
|
header:has(#header-menu-toggle:not(:checked)) .logo {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 6rem);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
min-height: calc(100vh - 10rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
justify-items: center;
|
||||||
|
align-content: start;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
scroll-snap-align: start;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 48px;
|
||||||
|
padding-top: 4rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zig-zag {
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.principal {
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
min-height: calc(100vh - 7rem);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
gap: 4rem;
|
||||||
|
|
||||||
|
padding: 3rem 0 6rem;
|
||||||
|
|
||||||
|
background: $homepage-principal-bg;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.circuit-layer {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .logo {
|
||||||
|
z-index: 2;
|
||||||
|
max-width: 640px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
filter: drop-shadow(6px 6px 0 $black) drop-shadow(4px 0 0 $black)
|
||||||
|
drop-shadow(0 4px 0 $black) drop-shadow(-4px 0 0 $black) drop-shadow(0 -4px 0 $black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .whats-phc {
|
||||||
|
z-index: 2;
|
||||||
|
background: #e4c5ff;
|
||||||
|
|
||||||
|
--zone-color: color-mix(in lab, #e4c5ff, #000 75%);
|
||||||
|
|
||||||
|
max-width: 37rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline 2px solid;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--zone-color: color-mix(in lab, #e4c5ff, #000 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.news {
|
||||||
|
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
|
||||||
|
--zone-color: color-mix(in lab, var(--news-base), #000 50%);
|
||||||
|
|
||||||
|
background: $homepage-news-bg;
|
||||||
|
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
|
& > .news-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
padding: 0 3rem;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role='button'] {
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #ffdd6e;
|
||||||
|
color: #000d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.projects {
|
||||||
|
background: $homepage-projects-bg;
|
||||||
|
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
|
.project-list {
|
||||||
|
width: calc(20rem * 3 + 1.5rem * 2 + 6rem * 2);
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-row: span var(--masonry-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
padding: 0 6rem;
|
||||||
|
|
||||||
|
.project {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// background: #fcddff;
|
||||||
|
// background: #ffa89c;
|
||||||
|
background: var(--card-bg, $project-card-bg);
|
||||||
|
color: #000e;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
|
||||||
|
gap: 0.25rem 1rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
grid-row: span 2;
|
||||||
|
// place-self: center;
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background: #0003;
|
||||||
|
border: 3px solid #0006;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cover {
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.auto {
|
||||||
|
img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
@extend .text;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: all 128ms ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(0, -4px);
|
||||||
|
box-shadow: 9px 13px 0 0 #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.wanna-be-macchinista {
|
||||||
|
background: $homepage-macchinisti-bg;
|
||||||
|
color: #fdfdfd;
|
||||||
|
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@extend .text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
& > main {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.principal {
|
||||||
|
padding: 3rem 0 6rem;
|
||||||
|
|
||||||
|
.whats-phc {
|
||||||
|
padding: 1.5rem;
|
||||||
|
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-whats-phc-read-more {
|
||||||
|
&:checked ~ .text {
|
||||||
|
max-height: 7lh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3lh;
|
||||||
|
|
||||||
|
background: linear-gradient(to bottom, transparent, #e4c5ff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:checked) ~ * .button span:nth-child(1) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&:checked ~ * .button span:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.news {
|
||||||
|
& > .news-list {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.projects {
|
||||||
|
.project-list {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.wanna-be-macchinista {
|
||||||
|
.content {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 1rem 1rem 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.utenti {
|
||||||
|
background: #ffffe4;
|
||||||
|
|
||||||
|
--filter-bg-color: #ffd270;
|
||||||
|
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5rem 0;
|
||||||
|
|
||||||
|
gap: 5rem;
|
||||||
|
|
||||||
|
max-width: 80ch;
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
background: #ffeabc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #ffd270;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .appunti {
|
||||||
|
// main {
|
||||||
|
// background: #fdfdf0;
|
||||||
|
|
||||||
|
// justify-self: center;
|
||||||
|
|
||||||
|
// display: grid;
|
||||||
|
// grid-auto-flow: row;
|
||||||
|
|
||||||
|
// justify-items: center;
|
||||||
|
|
||||||
|
// padding: 3rem;
|
||||||
|
|
||||||
|
// gap: 3rem;
|
||||||
|
|
||||||
|
// width: 100%;
|
||||||
|
|
||||||
|
// .search {
|
||||||
|
// max-width: 80ch;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .appunti-scrollable {
|
||||||
|
// justify-self: stretch;
|
||||||
|
|
||||||
|
// &.center {
|
||||||
|
// justify-self: center;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .login {
|
||||||
|
// background: #ddfaff;
|
||||||
|
|
||||||
|
// main {
|
||||||
|
// justify-self: center;
|
||||||
|
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
// max-width: 80ch;
|
||||||
|
// padding: 3rem 0;
|
||||||
|
|
||||||
|
// gap: 3rem;
|
||||||
|
|
||||||
|
// h3 {
|
||||||
|
// font-size: 28px;
|
||||||
|
// font-weight: 600;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Notizie
|
||||||
|
//
|
||||||
|
|
||||||
|
.notizie,
|
||||||
|
.notizia {
|
||||||
|
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
|
||||||
|
--zone-color: color-mix(in lab, var(--news-base), #000 75%);
|
||||||
|
background: color-mix(in lab, var(--news-base), #fff 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notizie {
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5rem;
|
||||||
|
gap: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notizia {
|
||||||
|
main {
|
||||||
|
--card-base: color-mix(in lab, var(--news-base), #fff 25%);
|
||||||
|
|
||||||
|
max-width: calc(46rem + 2rem * 2);
|
||||||
|
justify-self: center;
|
||||||
|
padding: 3rem 2rem 2rem;
|
||||||
|
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 6rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Guide
|
||||||
|
//
|
||||||
|
|
||||||
|
.guide,
|
||||||
|
.guida {
|
||||||
|
--card-base: color-mix(in lab, var(--guide-base), #fff 25%);
|
||||||
|
--zone-color: color-mix(in lab, var(--guide-base), #000 75%);
|
||||||
|
background: color-mix(in lab, var(--guide-base), #fff 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide {
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5rem;
|
||||||
|
gap: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guida {
|
||||||
|
main {
|
||||||
|
--card-base: color-mix(in lab, var(--guide-base), #fff 50%);
|
||||||
|
|
||||||
|
max-width: calc(46rem + 2rem * 2);
|
||||||
|
justify-self: center;
|
||||||
|
padding: 3rem 2rem 2rem;
|
||||||
|
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 6rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5rem 0;
|
||||||
|
gap: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storia {
|
||||||
|
--card-base: #e4c5ff;
|
||||||
|
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 5rem 0;
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
// background horizontal linear gradient that is black in the center
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#ffe4c544 0%,
|
||||||
|
// #ffe4c599 25%,
|
||||||
|
#ffe4c5ff 50%,
|
||||||
|
// #ffe4c599 75%,
|
||||||
|
#ffe4c544 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
|
||||||
|
.design {
|
||||||
|
grid-template-columns: minmax(15rem, 2fr) 10fr;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card();
|
||||||
|
|
||||||
|
background: #f0f0f0;
|
||||||
|
|
||||||
|
align-self: start;
|
||||||
|
position: sticky;
|
||||||
|
top: 7rem;
|
||||||
|
height: calc(100dvh - 8rem - 6rem);
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-left: calc((var(--depth) - 1) * 1rem);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
transform: translate(-0.25rem, 0);
|
||||||
|
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
|
||||||
|
color: #444;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #00000018;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[data-language='astro'] {
|
||||||
|
--code-bg: #fff7ef;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 46rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 2rem auto;
|
||||||
|
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
min-width: calc(100% - 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.large) {
|
||||||
|
& > .content {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// label in the top left corner
|
||||||
|
&::before {
|
||||||
|
content: 'Preview';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
|
||||||
|
background: #eee;
|
||||||
|
color: #000;
|
||||||
|
|
||||||
|
font-family: 'Iosevka', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
transform: translate(-2px, -4px);
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 64ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #bbb;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette {
|
||||||
|
margin: 2rem auto;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
& > .color {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
border: 2px solid #000;
|
||||||
|
box-shadow: 4px 4px 0 0 #000;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& > .region {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
border: 2px solid #fff;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .label {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
@function pow($number, $exponent) {
|
||||||
|
$value: 1;
|
||||||
|
|
||||||
|
@if $exponent > 0 {
|
||||||
|
@for $i from 1 through $exponent {
|
||||||
|
$value: $value * $number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin geometric-headings {
|
||||||
|
$base-font-size: 16px;
|
||||||
|
$heading-scale: 1.25;
|
||||||
|
|
||||||
|
@for $i from 1 through 5 {
|
||||||
|
$factor: pow($heading-scale, 5 - $i);
|
||||||
|
|
||||||
|
h#{$i} {
|
||||||
|
font-size: $base-font-size * $factor;
|
||||||
|
font-family: 'Iosevka', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// p + h#{$i} {
|
||||||
|
// margin-top: 0.75rem * $factor;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer typography {
|
||||||
|
@include geometric-headings;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
// text-align: justify;
|
||||||
|
// hyphens: auto;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
max-width: 46rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
background: color-mix(in lab, var(--card-base-internal, #ededed), #fff 35%) !important;
|
||||||
|
// background: color-mix(in lab, var(--zone-color), #fff 75%) !important;
|
||||||
|
// background: var(--code-bg, #00000022) !important;
|
||||||
|
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.125rem 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 2rem auto;
|
||||||
|
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
|
// width: 100%;
|
||||||
|
max-width: 80ch;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
box-shadow: 0.25rem 0.25rem 0 0 #333;
|
||||||
|
border: 2px solid #333;
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
width: calc(100vw - 2rem);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.75;
|
||||||
|
margin: var(--paragraph-margin, 1rem) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// p + p {
|
||||||
|
// margin-top: 1rem;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h1 + p,
|
||||||
|
// h2 + p,
|
||||||
|
// h3 + p,
|
||||||
|
// h4 + p {
|
||||||
|
// margin-top: 1rem;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// p:has(+ h1, + h2, + h3, + h4) {
|
||||||
|
// margin-bottom: 1rem;
|
||||||
|
// }
|
||||||
|
|
||||||
|
p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include geometric-headings;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
// trick to fix anchor links with sticky header
|
||||||
|
padding-top: 7rem;
|
||||||
|
margin-top: -6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: var(--zone-color, #1e6733);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline 2px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
& > * {
|
||||||
|
margin: 0 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|