Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9df12a8a3c | ||
|
|
c29f90ba73 | ||
|
|
59f21ebf34 | ||
|
|
9d5100e066 | ||
|
|
efd47f2a89 | ||
|
|
8a86733d5c | ||
|
|
069af4cd09 | ||
|
|
a8172f7e66 | ||
|
|
d2a866cb0f | ||
|
|
b3251906d5 | ||
|
|
55d0dc8345 | ||
|
|
74ed415980 | ||
|
|
cc12c4fc18 | ||
|
|
eb667f7f04 | ||
|
|
575a37accc | ||
|
|
bbad670baa | ||
|
|
4c19ffdd9f | ||
|
|
15c2b44fcf | ||
|
|
34597fe9f8 | ||
|
|
4780133019 | ||
|
|
55352af0f2 | ||
|
|
a02801c0c8 | ||
|
|
b595169e3b | ||
|
|
d75ccecdf2 | ||
|
|
a694817c28 | ||
|
|
a2125bdab7 | ||
|
|
ddca56e868 | ||
|
|
da7f526cb0 | ||
|
|
370f8204a0 | ||
|
|
2b5aea6eb8 | ||
|
|
0179e7a858 | ||
|
|
5c7342c91e | ||
|
|
3ed6cbf57a | ||
|
|
1ab42ed2e2 | ||
|
|
419b0c6ab5 | ||
|
|
f85991c409 | ||
|
|
d115d69b1b | ||
|
|
18563979a2 | ||
|
|
4b7a867a19 | ||
|
|
517a8ee8f1 | ||
|
|
0b4bf289d4 | ||
|
|
db083e20c2 | ||
|
|
fa564754f2 | ||
|
|
ae70cfc5e3 | ||
|
|
d46f62dd3f | ||
|
|
4757c3e0a9 | ||
|
|
7c36f87149 | ||
|
|
58d0378d49 | ||
|
|
84caa87951 | ||
|
|
23baa758cf | ||
|
|
1a3948a8eb | ||
|
|
4fca2f8b14 | ||
|
|
3173968f0e | ||
|
|
6330a13d92 | ||
|
|
2d0d34c4ff | ||
|
|
af4f3d35f0 | ||
|
|
9684ff98ea | ||
|
|
5feab1ba69 | ||
|
|
0a2df372d5 | ||
|
|
e037e46bd3 | ||
|
|
c2ba681cb8 | ||
|
|
e34663edca | ||
|
|
7dde9b1c22 | ||
|
|
b93e7223f5 | ||
|
|
21f5d319c3 | ||
|
|
86935b8eb5 | ||
|
|
8a71252eba | ||
|
|
119f356dc1 | ||
|
|
7c34ed2600 | ||
|
|
b10c50d9f2 | ||
|
|
e59bc41035 | ||
|
|
d511ce3663 | ||
|
|
e49e2ff0c5 | ||
|
|
2dbdf99827 | ||
|
|
810e07bff1 | ||
|
|
f608ea4f93 | ||
|
|
39133d8aed | ||
|
|
1dcd0705b5 | ||
|
|
0abd1c4886 | ||
|
|
177b469b19 | ||
|
|
75d2bb00ca | ||
|
|
a8a327e5a4 | ||
|
|
61b43f8868 | ||
|
|
c5e0241354 | ||
|
|
a5d3edf6db | ||
|
|
1d797e2dbe | ||
|
|
58e8e5d43c | ||
|
|
48bd9e459c | ||
|
|
ae490db61f | ||
|
|
a6d5fdec57 | ||
|
|
5844dbb422 | ||
|
|
a4a64e1d0b | ||
|
|
10634b3293 | ||
|
|
8a5727fcd6 | ||
|
|
dda3090069 | ||
|
|
33d7e654c5 | ||
|
|
b6ad4ebf4d | ||
|
|
0c6218f27f | ||
|
|
9653a14cdc | ||
|
|
3bf7b77b0f | ||
|
|
f90493ea34 | ||
|
|
7c9533cf62 | ||
|
|
7e257ea7c3 | ||
|
|
c868cd4706 | ||
|
|
f7d3d394d0 | ||
|
|
07f947edb6 | ||
|
|
0dee1e8bff | ||
|
|
cd15b3733b | ||
|
|
c44b370652 | ||
|
|
529400fde4 | ||
|
|
eee08e274d | ||
|
|
31b14252cb | ||
|
|
1df06a8c88 | ||
|
|
46a3a76660 | ||
|
|
af460ae9dc | ||
|
|
5d57dd7a9a | ||
|
|
d9b4834268 | ||
|
|
556d363bc9 | ||
|
|
c77123d062 | ||
|
|
3600ae60d4 | ||
|
|
d8c7b0f562 | ||
|
|
da72cd3411 | ||
|
|
66c8869671 | ||
|
|
dcbb069ef1 | ||
|
|
d8208742f2 | ||
|
|
68afc97c07 | ||
|
|
2cbef56377 | ||
|
|
f287071674 | ||
|
|
c8771142d5 | ||
|
|
890fbc14c3 | ||
|
|
0d2dd05f50 | ||
|
|
9a10fe1ff0 | ||
|
|
de28d3ad6e | ||
|
|
7398f27035 | ||
|
|
87d65b52b5 | ||
|
|
2b09c6c29c | ||
|
|
3e041421e5 | ||
|
|
69a0abc1f2 | ||
|
|
7e99cfa9d7 | ||
|
|
d06e731edd | ||
|
|
a87c87eb10 | ||
|
|
3316f6a6ec | ||
|
|
27317b38b1 | ||
|
|
1509984afb | ||
|
|
fb43eddefc | ||
|
|
3885b5135b | ||
|
|
3428bda4bc | ||
|
|
502ef6ea73 | ||
|
|
eb857b6dba | ||
|
|
ce42b2bf15 | ||
|
|
8f1d6cfff1 | ||
|
|
13b0c03e5f | ||
|
|
f16e340fd2 | ||
|
|
182238f89d | ||
|
|
16bffed2be | ||
|
|
b4722ac030 | ||
|
|
cf9ac2994a | ||
|
|
16c8a6dd7a | ||
|
|
d34d169d17 | ||
|
|
6c15c6ac7b | ||
|
|
eca5d60fd6 | ||
|
|
70ccf24c6b | ||
|
|
a91ad8398d | ||
|
|
ff3a44e23c |
@@ -0,0 +1,4 @@
|
|||||||
|
Dockerfile
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
*.local*
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# This file defines a Drone pipeline that builds a static website with "npm run build". This
|
||||||
|
# pipeline must be marked as "Trusted" in the Drone project settings.
|
||||||
|
#
|
||||||
|
# We mount the target directory of the project at "/var/www/{project}" to the container
|
||||||
|
# "dist/" directory and the run the build. A caveat is that the container builds files
|
||||||
|
# with "root" permissions, so we need to fix those after each build with a second pipeline.
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: deploy
|
||||||
|
image: node:latest
|
||||||
|
volumes:
|
||||||
|
- name: host-website-dist
|
||||||
|
path: /mnt/website
|
||||||
|
commands:
|
||||||
|
- npm install
|
||||||
|
- npm run build
|
||||||
|
- cp -rT ./dist /mnt/website
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: host-website-dist
|
||||||
|
host: # this volume is mounted on the host machine
|
||||||
|
path: /var/www/website
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: exec # this job is executed on the host machine
|
||||||
|
name: caddy-permissions
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: chown
|
||||||
|
commands:
|
||||||
|
- chown -R caddy:caddy /var/www/website
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
DB_FILE_NAME=file:data.local/database.db
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
OAUTH_CLIENT_ID=
|
||||||
|
OAUTH_CLIENT_SECRET=
|
||||||
|
OAUTH_AUTH_URL=https://iam.unipi.it/oauth2/authorize
|
||||||
|
OAUTH_TOKEN_HOST=https://iam.unipi.it
|
||||||
|
OAUTH_TOKEN_PATH=/oauth2/token
|
||||||
|
OAUTH_REDIRECT_URL=https://phc.dm.unipi.it/api/auth/oauth2/callback/unipi
|
||||||
|
OAUTH_USER_INFO_URL=https://iam.unipi.it/oauth2/userinfo
|
||||||
|
OAUTH_SCOPES="openid profile email"
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
.env
|
# build output
|
||||||
*.local*
|
|
||||||
bin/
|
|
||||||
|
|
||||||
.out/
|
|
||||||
out/
|
|
||||||
dist/
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# local data
|
||||||
|
*.local*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
export default {
|
||||||
|
printWidth: 120,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: 'consistent',
|
||||||
|
tabWidth: 4,
|
||||||
|
useTabs: false,
|
||||||
|
semi: false,
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
|
||||||
|
plugins: ['prettier-plugin-astro'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '*.astro',
|
||||||
|
options: {
|
||||||
|
parser: 'astro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '*.{yml,yaml,json}',
|
||||||
|
excludeFiles: 'package-lock.json',
|
||||||
|
options: {
|
||||||
|
tabWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"npm.packageManager": "bun",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# https://dev.to/code42cate/how-to-dockerize-and-deploy-astro-6ll
|
||||||
|
|
||||||
|
# use the official Bun image
|
||||||
|
# see all versions at https://hub.docker.com/r/oven/bun/tags
|
||||||
|
FROM oven/bun:1-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
build-base \
|
||||||
|
python3
|
||||||
|
|
||||||
|
# install dependencies into temp directory to cache them and speed up future builds
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lockb /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lockb /temp/prod/
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
# copy node_modules from temp directory and copy (non-ignored) project files into the image
|
||||||
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# copy production dependencies and source code into final image
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
COPY --from=prerelease /app .
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=3000
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
CMD bun run start
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: frontend backend
|
|
||||||
|
|
||||||
.PHONY: frontend
|
|
||||||
frontend:
|
|
||||||
$(NPM) run build
|
|
||||||
|
|
||||||
.PHONY: backend
|
|
||||||
backend:
|
|
||||||
go build -v -o ./out/server ./cmd/server
|
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
# PHC Website
|
# PHC Website
|
||||||
|
|
||||||
Sito web del PHC
|
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||||
|
|
||||||
## Usage
|
## Installazione
|
||||||
|
|
||||||
Come usare questo progetto
|
```bash
|
||||||
|
bun install
|
||||||
### Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Start the backend server and the frontend development server
|
|
||||||
$ go run -v ./cmd/dev-server
|
|
||||||
|
|
||||||
# Or using entr lo get live reload for go files
|
|
||||||
$ fd -e go | entr -r go run -v ./cmd/dev-server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
## Sviluppo
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
$ go run ./cmd/server
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploy
|
## Build
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
$ make all
|
bun run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Per ora c'è un file `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD. Al momento il sito è solo statico e non ha ancora una backend.
|
||||||
|
|
||||||
|
## Come Contribuire
|
||||||
|
|
||||||
|
**Nota per Sviluppatori**: SE il branch `dev` non esiste o è indietro rispetto a `main`, bisogna portarlo avanti a `main` e poi continuare con le modifiche dal lì.
|
||||||
|
|
||||||
|
Sentitevi liberi di aprire una PR per qualsiasi modifica o aggiunta al sito web. Il branch `main` è protetto e corrisponde alla versione di produzione del sito web, le modifiche devono prima essere accettate su `dev` e poi mergeate su `main` da un amministratore.
|
||||||
|
|
||||||
|
### Cose da fare
|
||||||
|
|
||||||
|
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config'
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
publicDir: './client/public',
|
|
||||||
srcDir: './client',
|
|
||||||
outDir: './out/client',
|
|
||||||
vite: {
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': 'http://127.0.0.1:4000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import preact from '@astrojs/preact'
|
||||||
|
|
||||||
|
import mdx from '@astrojs/mdx'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
|
||||||
|
import yaml from '@rollup/plugin-yaml'
|
||||||
|
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [yaml()],
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [remarkMath],
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'github-light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
preact({
|
||||||
|
compat: true,
|
||||||
|
}),
|
||||||
|
mdx({
|
||||||
|
remarkPlugins: [remarkMath],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
output: 'server',
|
||||||
|
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
})
|
||||||
@@ -1 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
import PageLayout from './PageLayout.astro'
|
|
||||||
|
|
||||||
const { frontmatter } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<PageLayout {...frontmatter} />
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
import '@fontsource/open-sans/latin.css'
|
|
||||||
import '@fontsource/source-sans-pro/latin.css'
|
|
||||||
import '@fontsource/source-code-pro/latin.css'
|
|
||||||
|
|
||||||
import '../styles/main.scss'
|
|
||||||
|
|
||||||
const { title, description, thumbnail, pageName } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<meta property="og:title" content={title ?? 'PHC'} />
|
|
||||||
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
|
||||||
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
|
|
||||||
|
|
||||||
<title>{title}</title>
|
|
||||||
</head>
|
|
||||||
<body class:list={['page-' + (pageName ?? 'unknown')]}>
|
|
||||||
<slot />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from './BaseLayout.astro'
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout {...Astro.props}>
|
|
||||||
<header>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
|
||||||
</div>
|
|
||||||
<div class="links">
|
|
||||||
<a role="button" href="#">Utenti</a>
|
|
||||||
<a role="button" href="#">Notizie</a>
|
|
||||||
<a role="button" href="#">Progetti</a>
|
|
||||||
<a role="button" href="#">About</a>
|
|
||||||
<a class="primary" role="button" href="#">Accedi</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
<footer>© PHC 2023</footer>
|
|
||||||
</BaseLayout>
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
---
|
|
||||||
import PageLayout from '../layouts/PageLayout.astro'
|
|
||||||
---
|
|
||||||
|
|
||||||
<PageLayout pageName="homepage">
|
|
||||||
<section class="principal">
|
|
||||||
<div class="circuit-layer">
|
|
||||||
<canvas id="circuits-art"></canvas>
|
|
||||||
<script src="../scripts/circuits-art.ts"></script>
|
|
||||||
</div>
|
|
||||||
<div class="logo">
|
|
||||||
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
|
||||||
</div>
|
|
||||||
<div class="whats-phc">
|
|
||||||
<div class="title">Cos'è il PHC?</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Totam vero deserunt tempore reprehenderit atque, voluptate
|
|
||||||
dolorum excepturi libero pariatur sequi?
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Laboriosam soluta ab a illum mollitia quaerat quia, veniam consequatur expedita dolore autem reiciendis quae rem
|
|
||||||
excepturi optio? Maiores, hic?
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Exercitationem error atque amet. Tempora earum nemo eveniet
|
|
||||||
aspernatur quam quas, doloribus expedita quisquam dignissimos cupiditate inventore a modi optio harum veritatis,
|
|
||||||
adipisci ab ullam distinctio odio quod delectus ipsum, rerum animi.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="news">
|
|
||||||
<div class="zig-zag">
|
|
||||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
|
||||||
<defs>
|
|
||||||
<pattern id="zig-zag-1" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
|
||||||
<path fill="#C2A8EB" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#zig-zag-1)" x="0" y="0" width="1000" height="1"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title">Ultime Notizie</div>
|
|
||||||
|
|
||||||
<div class="news-list">
|
|
||||||
<div class="news">
|
|
||||||
<div class="title">Lorem ipsum dolor sit.</div>
|
|
||||||
<div class="abstract">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
|
||||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
|
||||||
voluptatem pariatur eius!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
|
||||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="news">
|
|
||||||
<div class="title">Tempore provident impedit libero?</div>
|
|
||||||
<div class="abstract">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
|
||||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
|
||||||
voluptatem pariatur eius!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
|
||||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
|
||||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
|
||||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="news">
|
|
||||||
<div class="title">Alias molestias consectetur quam</div>
|
|
||||||
<div class="abstract">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
|
||||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
|
||||||
voluptatem pariatur eius!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="news">
|
|
||||||
<div class="title">Inventore dignissimos sapiente nulla</div>
|
|
||||||
<div class="abstract">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
|
||||||
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
|
||||||
voluptatem pariatur eius!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
|
||||||
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a class="primary" href="#" role="button">Vai all'Archivio</a>
|
|
||||||
</section>
|
|
||||||
<section class="projects">
|
|
||||||
<div class="zig-zag">
|
|
||||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
|
||||||
<defs>
|
|
||||||
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
|
||||||
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title">Progetti</div>
|
|
||||||
|
|
||||||
<div class="project-list">
|
|
||||||
<a href="#">
|
|
||||||
<div class="project">
|
|
||||||
<div class="image">
|
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="title">Gitea</div>
|
|
||||||
<div class="description">
|
|
||||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
|
||||||
accusantium labore explicabo quaerat incidunt.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<div class="project">
|
|
||||||
<div class="image">
|
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="title">Zulip</div>
|
|
||||||
<div class="description">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellendus, veritatis.</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<div class="project">
|
|
||||||
<div class="image">
|
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="title">Orario</div>
|
|
||||||
<div class="description">
|
|
||||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
|
||||||
accusantium labore explicabo quaerat incidunt.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<div class="project">
|
|
||||||
<div class="image">
|
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="title">Problemi</div>
|
|
||||||
<div class="description">
|
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non, hic libero beatae voluptatem incidunt.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<div class="project">
|
|
||||||
<div class="image">
|
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="title">Cluster "Steffè"</div>
|
|
||||||
<div class="description">
|
|
||||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
|
||||||
accusantium labore explicabo quaerat incidunt.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="wanna-be-macchinista">
|
|
||||||
<div class="zig-zag">
|
|
||||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
|
||||||
<defs>
|
|
||||||
<pattern id="zig-zag-3" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
|
||||||
<path fill="#888" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#zig-zag-3)" x="0" y="0" width="1000" height="1"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title">Vuoi diventare macchinista?</div>
|
|
||||||
</section>
|
|
||||||
</PageLayout>
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font: inherit;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
min-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-snap-type: y proximity;
|
|
||||||
scroll-padding-top: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Typography
|
|
||||||
//
|
|
||||||
|
|
||||||
.text {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
p + p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Controls
|
|
||||||
//
|
|
||||||
|
|
||||||
button,
|
|
||||||
.button,
|
|
||||||
[role='button'] {
|
|
||||||
appearance: none;
|
|
||||||
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
border: 3px solid #222;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 4px 4px 0 0 #222;
|
|
||||||
|
|
||||||
transition: all 64ms linear;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translate(2px, 2px);
|
|
||||||
box-shadow: 2px 2px 0 0 #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
padding: 0.25rem 1.5rem;
|
|
||||||
|
|
||||||
text-decoration: none;
|
|
||||||
color: #222;
|
|
||||||
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: #1e6733;
|
|
||||||
color: #f4fef7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #2b8b47;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Pages
|
|
||||||
//
|
|
||||||
|
|
||||||
header {
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
height: 6rem;
|
|
||||||
border-bottom: 4px solid #222;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
min-height: 6rem;
|
|
||||||
border-top: 4px solid #222;
|
|
||||||
background: #444;
|
|
||||||
color: #fdfdfd;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-homepage {
|
|
||||||
header .logo {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
min-height: calc(100vh - 6rem);
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
min-height: calc(100vh - 10rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
gap: 2rem;
|
|
||||||
|
|
||||||
scroll-snap-align: start;
|
|
||||||
|
|
||||||
& > .title {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 60px;
|
|
||||||
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
|
|
||||||
padding-top: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.zig-zag {
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
bottom: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.principal {
|
|
||||||
z-index: -2;
|
|
||||||
|
|
||||||
min-height: calc(100vh - 2rem);
|
|
||||||
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6rem;
|
|
||||||
|
|
||||||
padding: 6rem 0;
|
|
||||||
|
|
||||||
background: #ecffe3;
|
|
||||||
// circuit color
|
|
||||||
// background: #a6ce94;
|
|
||||||
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.circuit-layer {
|
|
||||||
z-index: -1;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
padding-top: 6rem;
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
max-width: 640px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
filter: drop-shadow(6px 6px 0 #222) drop-shadow(4px 0 0 #222) drop-shadow(0 4px 0 #222) drop-shadow(-4px 0 0 #222)
|
|
||||||
drop-shadow(0 -4px 0 #222);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.whats-phc {
|
|
||||||
background: #e4c5ff;
|
|
||||||
|
|
||||||
border: 4px solid #222;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 6px 6px 0 0 #222;
|
|
||||||
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
max-width: 37rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
@extend .text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.news {
|
|
||||||
background: #c2a8eb;
|
|
||||||
|
|
||||||
gap: 3rem;
|
|
||||||
|
|
||||||
padding-bottom: 6rem;
|
|
||||||
|
|
||||||
.news-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 3rem;
|
|
||||||
|
|
||||||
padding: 0 3rem;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.news {
|
|
||||||
background: #fffbeb;
|
|
||||||
|
|
||||||
border: 3px solid #222;
|
|
||||||
border-radius: 9px;
|
|
||||||
box-shadow: 9px 9px 0 0 #222;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 22rem;
|
|
||||||
max-height: 27rem;
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #c67e14;
|
|
||||||
border: 2px solid #222;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #e69419;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #c67e14;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline solid 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .title {
|
|
||||||
// border-bottom: 2px solid #222;
|
|
||||||
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f8e8b1;
|
|
||||||
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
& > .abstract {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
@extend .text;
|
|
||||||
}
|
|
||||||
& > .continue {
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
align-items: end;
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[role='button'] {
|
|
||||||
padding: 0.5rem 2rem;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
// background: #824ed4;
|
|
||||||
// color: #f0e6ff;
|
|
||||||
background: #f8e8b1;
|
|
||||||
color: #000d;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
// background: #8e5ddd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.projects {
|
|
||||||
background: #f5f2cc;
|
|
||||||
|
|
||||||
padding-bottom: 6rem;
|
|
||||||
|
|
||||||
.project-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
|
||||||
gap: 3rem;
|
|
||||||
|
|
||||||
padding: 0 6rem;
|
|
||||||
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
.project {
|
|
||||||
// background: #fcddff;
|
|
||||||
// background: #ffa89c;
|
|
||||||
background: #a2d4f3;
|
|
||||||
color: #000e;
|
|
||||||
|
|
||||||
border: 3px solid #222;
|
|
||||||
border-radius: 9px;
|
|
||||||
box-shadow: 9px 9px 0 0 #222;
|
|
||||||
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
|
|
||||||
gap: 0.25rem 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
grid-row: span 2;
|
|
||||||
// place-self: center;
|
|
||||||
|
|
||||||
.box {
|
|
||||||
background: #0003;
|
|
||||||
border: 3px solid #0006;
|
|
||||||
border-radius: 6px;
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: all 128ms ease-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translate(0, -8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.wanna-be-macchinista {
|
|
||||||
background: #888;
|
|
||||||
color: #fdfdfd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Misc
|
|
||||||
//
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track:vertical {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-left: 2px solid #222;
|
|
||||||
border-top: 2px solid #222;
|
|
||||||
border-bottom: 2px solid #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track:horizontal {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-top: 2px solid #222;
|
|
||||||
border-left: 2px solid #222;
|
|
||||||
border-right: 2px solid #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #1e6733;
|
|
||||||
border: 2px solid #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #2b8b47;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
// border-left: 2px solid #222;
|
|
||||||
// border-top: 2px solid #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
//
|
|
||||||
// Setup the application
|
|
||||||
//
|
|
||||||
|
|
||||||
// Config
|
|
||||||
sl.ProvideFunc(l, config.Slot, config.Configure)
|
|
||||||
|
|
||||||
// Database
|
|
||||||
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
|
||||||
Username: "claire-doe",
|
|
||||||
FullName: db.NewOption("Claire Doe"),
|
|
||||||
Email: db.NewOption("claire.doe@example.org"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
|
||||||
Username: "john-smith",
|
|
||||||
FullName: db.NewOption("John Smith"),
|
|
||||||
Email: db.NewOption("john.smith@example.org"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Server
|
|
||||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
|
||||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
|
||||||
listautenti.MountApiRoutesHook,
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Start the application
|
|
||||||
//
|
|
||||||
|
|
||||||
sl.MustInvoke(l, server.Slot)
|
|
||||||
|
|
||||||
r, w := io.Pipe()
|
|
||||||
|
|
||||||
cfg := sl.MustUse(l, config.Slot)
|
|
||||||
|
|
||||||
cmd := exec.Command(cfg.NpmCommand, "run", "dev")
|
|
||||||
cmd.Stdout = w
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
//
|
|
||||||
// Setup the application
|
|
||||||
//
|
|
||||||
|
|
||||||
// Config
|
|
||||||
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
|
||||||
|
|
||||||
// Database
|
|
||||||
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
|
||||||
Username: "claire-doe",
|
|
||||||
FullName: db.NewOption("Claire Doe"),
|
|
||||||
Email: db.NewOption("claire.doe@example.org"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
|
||||||
Username: "john-smith",
|
|
||||||
FullName: db.NewOption("John Smith"),
|
|
||||||
Email: db.NewOption("john.smith@example.org"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Server
|
|
||||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
|
||||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
|
||||||
listautenti.MountApiRoutesHook,
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Start the application
|
|
||||||
//
|
|
||||||
|
|
||||||
err := sl.Invoke(l, server.Slot)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
website:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: website
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# https://gist.github.com/aziis98/88af12b32d9cf3eeae3929b93146fd27
|
||||||
|
# hash2addr "next.phc.dm.unipi.it"
|
||||||
|
- "127.44.207.62:1059:3000"
|
||||||
|
volumes:
|
||||||
|
- /var/lib/website:/app/data.local
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/db/schema.ts',
|
||||||
|
dialect: 'sqlite',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DB_FILE_NAME!,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
CREATE TABLE `accounts` (
|
||||||
|
`user_id` text,
|
||||||
|
`provider` text NOT NULL,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `appunti` (
|
||||||
|
`user_id` text,
|
||||||
|
`hash` text NOT NULL,
|
||||||
|
`link` text,
|
||||||
|
`visibility` text NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`tags` text NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `poisson_requests` (
|
||||||
|
`user_id` text,
|
||||||
|
`request_type` text NOT NULL,
|
||||||
|
`comments` text,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` text,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
`full_name` text,
|
||||||
|
`email` text,
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `account` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`account_id` text NOT NULL,
|
||||||
|
`provider_id` text NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`access_token` text,
|
||||||
|
`refresh_token` text,
|
||||||
|
`id_token` text,
|
||||||
|
`access_token_expires_at` integer,
|
||||||
|
`refresh_token_expires_at` integer,
|
||||||
|
`scope` text,
|
||||||
|
`password` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
`ip_address` text,
|
||||||
|
`user_agent` text,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`email_verified` integer NOT NULL,
|
||||||
|
`image` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `verification` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`identifier` text NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer,
|
||||||
|
`updated_at` integer
|
||||||
|
);
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "84352768-6c24-4412-9d9e-8a3c756d31b1",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"accounts": {
|
||||||
|
"name": "accounts",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"accounts_user_id_user_id_fk": {
|
||||||
|
"name": "accounts_user_id_user_id_fk",
|
||||||
|
"tableFrom": "accounts",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"appunti": {
|
||||||
|
"name": "appunti",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"name": "hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"name": "link",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"appunti_user_id_users_id_fk": {
|
||||||
|
"name": "appunti_user_id_users_id_fk",
|
||||||
|
"tableFrom": "appunti",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"poisson_requests": {
|
||||||
|
"name": "poisson_requests",
|
||||||
|
"columns": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"request_type": {
|
||||||
|
"name": "request_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"poisson_requests_user_id_users_id_fk": {
|
||||||
|
"name": "poisson_requests_user_id_users_id_fk",
|
||||||
|
"tableFrom": "poisson_requests",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"name": "full_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_id_user_id_fk": {
|
||||||
|
"name": "users_id_user_id_fk",
|
||||||
|
"tableFrom": "users",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"name": "account",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1749233567397,
|
||||||
|
"tag": "0000_violet_agent_zero",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module git.phc.dm.unipi.it/phc/website
|
|
||||||
|
|
||||||
go 1.20
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/alecthomas/repr v0.2.0 // indirect
|
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
|
||||||
github.com/gofiber/fiber/v2 v2.44.0 // indirect
|
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
github.com/klauspost/compress v1.16.5 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
|
||||||
github.com/tinylib/msgp v1.1.8 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasthttp v1.46.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
|
||||||
golang.org/x/sys v0.7.0 // indirect
|
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
|
||||||
)
|
|
||||||
@@ -1,93 +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.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
|
||||||
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
|
|
||||||
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
|
||||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
|
||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
|
||||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
|
||||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
|
||||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
|
||||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
|
||||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
|
||||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
|
|
||||||
github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
// This package provides a small framework to work with SQL databases using
|
|
||||||
// generics. The most important part for now is the "Ref[T]" type that lets
|
|
||||||
// us pass around typed IDs to table rows in a type safe manner.
|
|
||||||
//
|
|
||||||
// For now this library uses both generics for keeping the interface type safe
|
|
||||||
// and reflection to extract automatically some metedata from structs that
|
|
||||||
// represent tables.
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Option è un valore che nel database può potenzialmente essere "NULL"
|
|
||||||
//
|
|
||||||
// NOTE/TODO: Magari si può fare una cosa fatta bene e nascondere
|
|
||||||
// l'implementazione al Go, ad esempio Option[string] può essere NULL o testo
|
|
||||||
// nel db mentre Option[time.Time] può essere codificato in json come
|
|
||||||
//
|
|
||||||
// "{ present: false }"
|
|
||||||
//
|
|
||||||
// oppure
|
|
||||||
//
|
|
||||||
// "{ present: true, value: '2023/04/...' }"
|
|
||||||
//
|
|
||||||
// in modo da semplificare leggermente la processazione in Go. Altrimenti si
|
|
||||||
// può separare in Option[T] e Nullable[T].
|
|
||||||
type Option[T any] struct {
|
|
||||||
present bool
|
|
||||||
value T
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Option[T]) MarshalJSON() ([]byte, error) {
|
|
||||||
if !o.present {
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(o.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Option[T]) UnmarshalJSON(v []byte) error {
|
|
||||||
if slices.Equal(v, []byte("null")) {
|
|
||||||
var zero T
|
|
||||||
o.present = false
|
|
||||||
o.value = zero
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(v, &o.value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOption[T any](value T) Option[T] {
|
|
||||||
return Option[T]{true, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEmptyOption[T any]() Option[T] {
|
|
||||||
return Option[T]{present: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Option[T]) SqlType() string {
|
|
||||||
return "BLOB"
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (v *Option[T]) Scan(a any) error {
|
|
||||||
// data, ok := a.([]byte)
|
|
||||||
// if !ok {
|
|
||||||
// return fmt.Errorf(`scan expected []byte`)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// m := map[string]any{}
|
|
||||||
// if err := json.Unmarshal(data, m); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if present, _ := m["present"].(bool); present {
|
|
||||||
|
|
||||||
// m["value"]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ref è un id tipato da un'entità presente nel database. È il tipo fondamentale
|
|
||||||
// esportato da questo package. Non dovrebbe essere troppo importante ma
|
|
||||||
// internamente è una stringa, inoltre è consigliato che una struct
|
|
||||||
// con _primary key_ abbia un campo di tipo Ref a se stessa.
|
|
||||||
//
|
|
||||||
// type User struct {
|
|
||||||
// Id db.Ref[User] // meglio se unico e immutabile
|
|
||||||
// ...
|
|
||||||
// }
|
|
||||||
type Ref[T any] string
|
|
||||||
|
|
||||||
func (Ref[T]) SqlType() string {
|
|
||||||
return "TEXT"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Ref[T]) Scan(a any) error {
|
|
||||||
s, ok := a.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(`scan expected string`)
|
|
||||||
}
|
|
||||||
|
|
||||||
*v = Ref[T](s)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sqlValue interface {
|
|
||||||
SqlType() string
|
|
||||||
sql.Scanner
|
|
||||||
}
|
|
||||||
|
|
||||||
type Table[T any] struct {
|
|
||||||
Name string
|
|
||||||
PrimaryKey func(value *T, pk ...string) Ref[T]
|
|
||||||
Columns [][2]string
|
|
||||||
}
|
|
||||||
|
|
||||||
var commonTypes = map[string]string{
|
|
||||||
"string": "TEXT",
|
|
||||||
"int": "INTEGER",
|
|
||||||
"int32": "INTEGER",
|
|
||||||
"int64": "INTEGER",
|
|
||||||
"float32": "REAL",
|
|
||||||
"float64": "REAL",
|
|
||||||
}
|
|
||||||
|
|
||||||
func toSqlType(typ reflect.Type) string {
|
|
||||||
st, ok := reflect.New(typ).Interface().(sqlValue)
|
|
||||||
if ok {
|
|
||||||
return st.SqlType()
|
|
||||||
}
|
|
||||||
|
|
||||||
return commonTypes[typ.Name()]
|
|
||||||
}
|
|
||||||
|
|
||||||
func AutoTable[T any](name string) Table[T] {
|
|
||||||
columns := [][2]string{}
|
|
||||||
pkIndex := -1
|
|
||||||
|
|
||||||
var zero T
|
|
||||||
typ := reflect.TypeOf(zero)
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
|
||||||
fieldTyp := typ.Field(i)
|
|
||||||
isPK, _, typ, col := fieldInfo(fieldTyp)
|
|
||||||
if isPK {
|
|
||||||
pkIndex = i
|
|
||||||
}
|
|
||||||
columns = append(columns, [2]string{col, toSqlType(typ)})
|
|
||||||
}
|
|
||||||
|
|
||||||
if pkIndex == -1 {
|
|
||||||
panic(fmt.Sprintf("struct %T has no primary key field", zero))
|
|
||||||
}
|
|
||||||
|
|
||||||
// debug logging
|
|
||||||
// log.Printf("[auto table] struct %T has primary key %q", zero, typ.Field(pkIndex).Name)
|
|
||||||
|
|
||||||
return Table[T]{
|
|
||||||
Name: name,
|
|
||||||
PrimaryKey: func(value *T, pk ...string) Ref[T] {
|
|
||||||
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
|
||||||
// they are the same type so this should be safe
|
|
||||||
ptr := reflect.ValueOf(value).Elem().Field(pkIndex).Addr().UnsafePointer()
|
|
||||||
pkPtr := (*string)(ptr)
|
|
||||||
|
|
||||||
if len(pk) > 0 {
|
|
||||||
*pkPtr = pk[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ref[T](*pkPtr)
|
|
||||||
},
|
|
||||||
Columns: columns,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (table Table[T]) CreateIfNotExists(dbConn *sql.DB) error {
|
|
||||||
columns := make([]string, len(table.Columns))
|
|
||||||
|
|
||||||
for i, col := range table.Columns {
|
|
||||||
columns[i] = fmt.Sprintf("%s %s", col[0], col[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s(%s)`,
|
|
||||||
table.Name,
|
|
||||||
strings.Join(columns, ", "),
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err := dbConn.Exec(stmt)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Create[T any](dbConn *sql.DB, table Table[T], value T) (Ref[T], error) {
|
|
||||||
id := randomHex(15)
|
|
||||||
|
|
||||||
table.PrimaryKey(&value, id)
|
|
||||||
|
|
||||||
columns, values := structDatabaseColumnValues(&value)
|
|
||||||
|
|
||||||
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
|
||||||
table.Name,
|
|
||||||
strings.Join(columns, ", "),
|
|
||||||
strings.Join(repeatSlice("?", len(columns)), ", "),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := dbConn.Exec(stmt, values...); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ref[T](id), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Insert[T any](dbConn *sql.DB, table Table[T], value T) error {
|
|
||||||
columns, values := structDatabaseColumnValues(&value)
|
|
||||||
|
|
||||||
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
|
||||||
table.Name,
|
|
||||||
strings.Join(columns, ", "),
|
|
||||||
strings.Join(repeatSlice("?", len(columns)), ", "),
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err := dbConn.Exec(stmt, values...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Read[T any](dbConn *sql.DB, table Table[T], id Ref[T]) (T, error) {
|
|
||||||
panic("todo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadAll[T any](dbConn *sql.DB, table Table[T]) ([]T, error) {
|
|
||||||
panic("todo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Update[T any](dbConn *sql.DB, table Table[T], id Ref[T], value T) error {
|
|
||||||
panic("todo")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete[T any](dbConn *sql.DB, table Table[T], id Ref[T]) error {
|
|
||||||
panic("todo")
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package db_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
"gotest.tools/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockDB struct {
|
|
||||||
*testing.T
|
|
||||||
|
|
||||||
LastQuery string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *mockDB) Prepare(query string) (driver.Stmt, error) { return nil, nil }
|
|
||||||
func (db *mockDB) Close() error { return nil }
|
|
||||||
func (db *mockDB) Begin() (driver.Tx, error) { return nil, nil }
|
|
||||||
|
|
||||||
func (db *mockDB) Query(query string, args []driver.Value) (driver.Rows, error) {
|
|
||||||
db.LastQuery = query
|
|
||||||
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *mockDB) Exec(query string, args []driver.Value) (driver.Result, error) {
|
|
||||||
db.LastQuery = query
|
|
||||||
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *mockDB) Open(name string) (driver.Conn, error) {
|
|
||||||
return &mockDB{T: db.T}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *mockDB) Connect(context.Context) (driver.Conn, error) {
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *mockDB) Driver() driver.Driver {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Tests
|
|
||||||
//
|
|
||||||
|
|
||||||
func TestDb1(t *testing.T) {
|
|
||||||
type Product struct {
|
|
||||||
Id db.Ref[Product] `db:"id*"`
|
|
||||||
Title string `db:"title"`
|
|
||||||
Quantity int `db:"quantity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
products := db.AutoTable[Product]("products")
|
|
||||||
|
|
||||||
mock := &mockDB{T: t}
|
|
||||||
conn := sql.OpenDB(mock)
|
|
||||||
|
|
||||||
err := products.CreateIfNotExists(conn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, mock.LastQuery,
|
|
||||||
`CREATE TABLE IF NOT EXISTS products(id TEXT, title TEXT, quantity INTEGER)`,
|
|
||||||
)
|
|
||||||
|
|
||||||
ref1, err := db.Create(conn, products, Product{Title: "Foo", Quantity: 27})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.Assert(t, string(ref1) != "")
|
|
||||||
assert.Equal(t, mock.LastQuery,
|
|
||||||
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := db.Insert(conn, products, Product{Id: "123", Title: "Bar", Quantity: 42}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, mock.LastQuery,
|
|
||||||
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package db
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func randomHex(len int) string {
|
|
||||||
buff := make([]byte, len/2+1)
|
|
||||||
rand.Read(buff)
|
|
||||||
str := hex.EncodeToString(buff)
|
|
||||||
return str[:len]
|
|
||||||
}
|
|
||||||
|
|
||||||
func fieldInfo(fieldTyp reflect.StructField) (bool, string, reflect.Type, string) {
|
|
||||||
fieldName := fieldTyp.Name
|
|
||||||
columnName, ok := fieldTyp.Tag.Lookup("db")
|
|
||||||
if !ok {
|
|
||||||
columnName = strings.ToLower(fieldName)
|
|
||||||
}
|
|
||||||
if ok && columnName[len(columnName)-1] == '*' {
|
|
||||||
return true, fieldName, fieldTyp.Type, columnName[:len(columnName)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, fieldName, fieldTyp.Type, columnName
|
|
||||||
}
|
|
||||||
|
|
||||||
func fieldPrimaryKey(fieldTyp reflect.StructField) bool {
|
|
||||||
isPK, _, _, _ := fieldInfo(fieldTyp)
|
|
||||||
return isPK
|
|
||||||
}
|
|
||||||
|
|
||||||
// structDatabaseColumnValues takes a pointer to a struct and returns a pair
|
|
||||||
// of slices, one with the column names and the other with pointers to all
|
|
||||||
// its exported fields.
|
|
||||||
//
|
|
||||||
// The column name can be changed using the "db" tag attribute on that field
|
|
||||||
func structDatabaseColumnValues(s any) ([]string, []any) {
|
|
||||||
v := reflect.ValueOf(s).Elem()
|
|
||||||
|
|
||||||
numFields := v.NumField()
|
|
||||||
|
|
||||||
names := []string{}
|
|
||||||
values := []any{}
|
|
||||||
|
|
||||||
for i := 0; i < numFields; i++ {
|
|
||||||
fieldTyp := v.Type().Field(i)
|
|
||||||
fieldVal := v.Field(i)
|
|
||||||
|
|
||||||
if fieldTyp.IsExported() {
|
|
||||||
key := strings.ToLower(fieldTyp.Name)
|
|
||||||
|
|
||||||
if name, ok := fieldTyp.Tag.Lookup("db"); ok {
|
|
||||||
key = name
|
|
||||||
|
|
||||||
// primary key has a "*" at the end, exclude from column name
|
|
||||||
if key[len(key)-1] == '*' {
|
|
||||||
key = key[:len(key)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
names = append(names, key)
|
|
||||||
values = append(values, fieldVal.Addr().Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return names, values
|
|
||||||
}
|
|
||||||
|
|
||||||
func structPrimaryKeyPtr(s any) (*string, bool) {
|
|
||||||
v := reflect.ValueOf(s).Elem()
|
|
||||||
|
|
||||||
for i := 0; i < v.NumField(); i++ {
|
|
||||||
fieldTyp := v.Type().Field(i)
|
|
||||||
fieldVal := v.Field(i)
|
|
||||||
|
|
||||||
if fieldTyp.IsExported() {
|
|
||||||
if fieldPrimaryKey(fieldTyp) {
|
|
||||||
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
|
||||||
// they are the same type so this should be safe
|
|
||||||
ptr := fieldVal.Addr().UnsafePointer()
|
|
||||||
return (*string)(ptr), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// structDatabasePrimaryKeyColumn takes a pointer to a struct and returns the
|
|
||||||
// name of the field with the primary key
|
|
||||||
func structDatabasePrimaryKeyColumn(s any) (field, column string, ok bool) {
|
|
||||||
v := reflect.ValueOf(s).Elem()
|
|
||||||
|
|
||||||
for i := 0; i < v.NumField(); i++ {
|
|
||||||
fieldTyp := v.Type().Field(i)
|
|
||||||
if fieldTyp.IsExported() {
|
|
||||||
key := fieldTyp.Name
|
|
||||||
if value, ok := fieldTyp.Tag.Lookup("db"); ok {
|
|
||||||
if value[len(value)-1] == '*' {
|
|
||||||
return key, value[:len(value)-1], true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func repeatSlice[T any](value T, count int) []T {
|
|
||||||
s := make([]T, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
s[i] = value
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
// The [sl] package has two main concepts, the [ServiceLocator] itself is the
|
|
||||||
// main object that one should pass around through the application. A
|
|
||||||
// [ServiceLocator] has a list of slots that can be filled with
|
|
||||||
// [ProvideFunc] and [Provide] and retrieved with [Use]. Slots should
|
|
||||||
// be unique by type and they can only be created with the [NewSlot] function.
|
|
||||||
//
|
|
||||||
// The usual way to use this module is to make slots for Go interfaces and
|
|
||||||
// then pass implementations using the [Provide] and
|
|
||||||
// [ProvideFunc] functions.
|
|
||||||
//
|
|
||||||
// Services can be of various types:
|
|
||||||
// - a service with no dependencies can be directly injected inside a
|
|
||||||
// ServiceLocator using [Provide].
|
|
||||||
// - a service with dependencies on other service should use
|
|
||||||
// [ProvideFunc]. This lets the service configure itself when needed
|
|
||||||
// and makes the developer not think about correctly ordering the
|
|
||||||
// recursive configuration of its dependencies.
|
|
||||||
// - a service can also be private, in this case the slot for a service
|
|
||||||
// should be a private field in the service package. This kind of
|
|
||||||
// services should also provide a way to inject them into a
|
|
||||||
// ServiceLocator.
|
|
||||||
// - a package can also just provide a slot with some value. This is useful
|
|
||||||
// for using the ServiceLocator to easily pass around values, effectively
|
|
||||||
// threating slots just as dynamically scoped variables.
|
|
||||||
package sl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger is the debug logger
|
|
||||||
//
|
|
||||||
// TODO: in the future this will be disabled and discard by default.
|
|
||||||
//
|
|
||||||
// As this is the service locator module it was meaning less to pass this
|
|
||||||
// through the ServiceLocator itself (without making the whole module more
|
|
||||||
// complex)
|
|
||||||
var Logger *log.Logger = log.New(os.Stderr, "[service locator] ", log.Lmsgprefix)
|
|
||||||
|
|
||||||
// slot is just a "typed" unique "symbol".
|
|
||||||
//
|
|
||||||
// This must be defined like so and not for example "struct{ typeName string }"
|
|
||||||
// because we might want to have more slots for the same type.
|
|
||||||
type slot[T any] *struct{}
|
|
||||||
|
|
||||||
type Hook[T any] func(*ServiceLocator, T) error
|
|
||||||
|
|
||||||
// hook is just a typed unique symbol
|
|
||||||
type hook[T any] *struct{}
|
|
||||||
|
|
||||||
// NewSlot is the only way to create instances of the slot type. Each instance
|
|
||||||
// is unique.
|
|
||||||
//
|
|
||||||
// This then lets you attach a service instance of type "T" to a
|
|
||||||
// [ServiceLocator] object.
|
|
||||||
func NewSlot[T any]() slot[T] {
|
|
||||||
return slot[T](new(struct{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHook is the only way to create instances of the hook type. Each instance
|
|
||||||
// is unique.
|
|
||||||
//
|
|
||||||
// This lets you have a service dispatch an hook
|
|
||||||
func NewHook[T any]() hook[T] {
|
|
||||||
return hook[T](new(struct{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// slotEntry represents a service that can lazily configured
|
|
||||||
// (using "configureFunc"). Once configured the instance is kept in the "value"
|
|
||||||
// field and "created" will always be "true". The field "typeName" just for
|
|
||||||
// debugging purposes.
|
|
||||||
type slotEntry struct {
|
|
||||||
// typeName is just used for debugging purposes
|
|
||||||
typeName string
|
|
||||||
|
|
||||||
// configureFunc is used by lazily provided slot values to tell how to
|
|
||||||
// configure them self when required
|
|
||||||
configureFunc func(*ServiceLocator) (any, error)
|
|
||||||
|
|
||||||
// configured tells if this slot is already configured
|
|
||||||
configured bool
|
|
||||||
|
|
||||||
// value for this slot
|
|
||||||
value any
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkConfigured tries to call configure on this slot entry if not already configured
|
|
||||||
func (s *slotEntry) checkConfigured(l *ServiceLocator) error {
|
|
||||||
if !s.configured {
|
|
||||||
v, err := s.configureFunc(l)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`[slot: %s] configured service of type %T`, s.typeName, v)
|
|
||||||
|
|
||||||
s.configured = true
|
|
||||||
s.value = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type hookEntry struct {
|
|
||||||
// typeName is just used for debugging purposes
|
|
||||||
typeName string
|
|
||||||
|
|
||||||
// listeners is a list of functions to call when this hook is called
|
|
||||||
listeners []func(*ServiceLocator, any) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceLocator is the main context passed around to retrive service
|
|
||||||
// instances, the interface uses generics so to inject and retrive service
|
|
||||||
// instances you should use the functions [Provide], [ProvideFunc] and [Use].
|
|
||||||
// This is essentially a dictionary indexed by slots that are them self just
|
|
||||||
// typed unique symbols
|
|
||||||
type ServiceLocator struct {
|
|
||||||
providers map[any]*slotEntry
|
|
||||||
hooks map[any]*hookEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new [ServiceLocator] context to pass around in the application.
|
|
||||||
func New() *ServiceLocator {
|
|
||||||
return &ServiceLocator{
|
|
||||||
providers: map[any]*slotEntry{},
|
|
||||||
hooks: map[any]*hookEntry{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide will inject a concrete instance inside the ServiceLocator "l" for
|
|
||||||
// the given "slotKey". This should be used for injecting "static" services, for
|
|
||||||
// instances whose construction depend on other services you should use the
|
|
||||||
// [ProvideFunc] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type
|
|
||||||
// check as "T" can also be an interface.
|
|
||||||
func Provide[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
|
||||||
typeName := getTypeName[T]()
|
|
||||||
|
|
||||||
Logger.Printf(`[slot: %s] provided value of type %T`, typeName, value)
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
typeName: typeName,
|
|
||||||
configured: true,
|
|
||||||
value: value,
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProvideFunc will inject an instance inside the given ServiceLocator
|
|
||||||
// and "slotKey" that is created only when requested with a call to the
|
|
||||||
// [Use] function.
|
|
||||||
//
|
|
||||||
// This is generic over "T" to check that instances for the given slot type
|
|
||||||
// check as "T" can also be an interface.
|
|
||||||
func ProvideFunc[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
|
||||||
typeName := getTypeName[T]()
|
|
||||||
Logger.Printf(`[slot: %s] inject lazy provider`, typeName)
|
|
||||||
|
|
||||||
l.providers[slotKey] = &slotEntry{
|
|
||||||
typeName: typeName,
|
|
||||||
configureFunc: func(l *ServiceLocator) (any, error) { return createFunc(l) },
|
|
||||||
configured: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use retrieves the value of type T associated with the given slot key from
|
|
||||||
// the provided ServiceLocator instance.
|
|
||||||
//
|
|
||||||
// If the ServiceLocator does not have a value for the slot key, or if the
|
|
||||||
// value wasn't correctly configured (in the case of a lazy slot), an error
|
|
||||||
// is returned.
|
|
||||||
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
|
||||||
var zero T
|
|
||||||
|
|
||||||
slot, ok := l.providers[slotKey]
|
|
||||||
if !ok {
|
|
||||||
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
|
||||||
}
|
|
||||||
|
|
||||||
err := slot.checkConfigured(l)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := slot.value.(T)
|
|
||||||
|
|
||||||
Logger.Printf(`[slot: %s] using slot with value of type %T`, getTypeName[T](), v)
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustUse[T any](l *ServiceLocator, slotKey slot[T]) T {
|
|
||||||
v, err := Use(l, slotKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func Invoke[T any](l *ServiceLocator, slotKey slot[T]) error {
|
|
||||||
slot, ok := l.providers[slotKey]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
|
||||||
}
|
|
||||||
|
|
||||||
err := slot.checkConfigured(l)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := slot.value.(T)
|
|
||||||
|
|
||||||
Logger.Printf(`[slot: %s] invoked slot with value of type %T`, getTypeName[T](), v)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustInvoke[T any](l *ServiceLocator, slotKey slot[T]) {
|
|
||||||
if err := Invoke(l, slotKey); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProvideHook[T any](l *ServiceLocator, hookKey hook[T], listeners ...Hook[T]) {
|
|
||||||
typeName := getTypeName[T]()
|
|
||||||
Logger.Printf(`[hook: %s] injecting hooks`, typeName)
|
|
||||||
|
|
||||||
// cast type safe listeners to internal untyped version to put inside the hook map
|
|
||||||
anyListeners := make([]func(*ServiceLocator, any) error, len(listeners))
|
|
||||||
for i, l := range listeners {
|
|
||||||
ll := l
|
|
||||||
anyListeners[i] = func(l *ServiceLocator, a any) error {
|
|
||||||
t, ok := a.(T)
|
|
||||||
if !ok {
|
|
||||||
panic(`illegal state`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ll(l, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.hooks[hookKey] = &hookEntry{
|
|
||||||
typeName: typeName,
|
|
||||||
listeners: anyListeners,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseHook[T any](l *ServiceLocator, hookKey hook[T], value T) error {
|
|
||||||
hookEntry, ok := l.hooks[hookKey]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(`no injected hooks for hook of type %s`, hookEntry.typeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Printf(`[hook: %s] calling hook with value of type %T`, hookEntry.typeName, value)
|
|
||||||
for _, hookFunc := range hookEntry.listeners {
|
|
||||||
if err := hookFunc(l, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustUseHook[T any](l *ServiceLocator, hookKey hook[T], value T) {
|
|
||||||
if err := UseHook(l, hookKey, value); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTypeName is a trick to get the name of a type (even if it is an
|
|
||||||
// interface type)
|
|
||||||
func getTypeName[T any]() string {
|
|
||||||
var zero T
|
|
||||||
return fmt.Sprintf(`%T`, &zero)[1:]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CompactIndentedLines removes all indentation from a string and also removes all newlines, usefull in tests
|
|
||||||
func CompactIndentedLines(s string) string {
|
|
||||||
return regexp.MustCompile(`(?m)\n\s+`).ReplaceAllString(s, "")
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,60 @@
|
|||||||
{
|
{
|
||||||
"name": "website-frontend",
|
"name": "website",
|
||||||
"version": "1.0.0",
|
"type": "module",
|
||||||
"description": "Frontend for the PHC website",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"scripts": {
|
||||||
"scripts": {
|
"dev": "run-s astro:sync astro:dev",
|
||||||
"dev": "astro dev",
|
"build": "run-s astro:build",
|
||||||
"start": "astro dev",
|
"astro:server": "bun run ./dist/server/entry.mjs",
|
||||||
"build": "astro build",
|
"drizzle:migrate": "drizzle-kit migrate",
|
||||||
"preview": "astro preview"
|
"drizzle:generate": "drizzle-kit generate",
|
||||||
},
|
"start": "run-s drizzle:migrate astro:server",
|
||||||
"license": "MIT",
|
"astro:sync": "astro sync",
|
||||||
"dependencies": {
|
"astro:dev": "astro dev",
|
||||||
"@fontsource/open-sans": "^4.5.14",
|
"astro:build": "astro check && astro build"
|
||||||
"@fontsource/source-code-pro": "^4.5.14",
|
},
|
||||||
"@fontsource/source-sans-pro": "^4.5.11",
|
"dependencies": {
|
||||||
"astro": "^2.3.1",
|
"@astrojs/check": "^0.9.4",
|
||||||
"sass": "^1.62.1"
|
"@astrojs/node": "9.0.0",
|
||||||
},
|
"@astrojs/preact": "4.0.0",
|
||||||
"devDependencies": {
|
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||||
"typescript": "^5.0.4"
|
"@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",
|
||||||
|
"@libsql/client": "^0.15.8",
|
||||||
|
"@phosphor-icons/core": "^2.1.1",
|
||||||
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"astro": "5.1.0",
|
||||||
|
"better-auth": "^1.2.8",
|
||||||
|
"better-sqlite3": "^11.10.0",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"drizzle-kit": "^0.31.1",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
|
"lucide-static": "^0.468.0",
|
||||||
|
"marked": "^15.0.6",
|
||||||
|
"preact": "^10.19.6",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/mdx": "4.0.2",
|
||||||
|
"@rollup/plugin-yaml": "^4.1.2",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"jsdom": "^24.1.1",
|
||||||
|
"linkedom": "^0.18.4",
|
||||||
|
"prettier": "^3.5.0",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
|
"tsx": "^4.7.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="80" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="160" y="50" width="150" height="60" fill="#1E6733" />
|
||||||
|
<rect x="140" y="90" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="140" y="200" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="140" y="410" width="10" height="20" fill="#ECC333" />
|
||||||
|
<rect x="140" y="350" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="240" y="110" width="70" height="10" fill="#ECC333" />
|
||||||
|
<rect x="250" y="130" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="340" y="50" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="340" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="590" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="690" y="180" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="690" y="310" width="60" height="140" fill="#1E6733" />
|
||||||
|
<rect x="690" y="50" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="590" y="320" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="590" y="50" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="420" y="240" width="150" height="60" fill="#1E6733" />
|
||||||
|
<rect x="340" y="320" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="240" y="140" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="350" y="170" width="40" height="10" fill="#ECC333" />
|
||||||
|
<rect x="330" y="330" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="160" y="200" width="80" height="60" fill="#1E6733" />
|
||||||
|
<rect x="650" y="200" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="750" y="330" width="10" height="60" fill="#ECC333" />
|
||||||
|
<rect x="800" y="450" width="40" height="10" fill="#ECC333" />
|
||||||
|
<rect x="850" y="450" width="30" height="10" fill="#ECC333" />
|
||||||
|
<rect x="750" y="90" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="810" y="110" width="60" height="10" fill="#ECC333" />
|
||||||
|
<rect x="580" y="330" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="580" y="60" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="350" y="430" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="350" y="410" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="350" y="390" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="700" y="70" width="20" height="40" fill="#303030" />
|
||||||
|
<rect x="700" y="120" width="20" height="40" fill="#303030" />
|
||||||
|
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="370" y="70" width="20" height="30" fill="#303030" />
|
||||||
|
<rect x="370" y="110" width="20" height="30" fill="#303030" />
|
||||||
|
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030" />
|
||||||
|
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030" />
|
||||||
|
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030" />
|
||||||
|
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030" />
|
||||||
|
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030" />
|
||||||
|
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030" />
|
||||||
|
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030" />
|
||||||
|
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<rect x="750" y="190" width="10" height="40" fill="#ECC333" />
|
||||||
|
<rect x="750" y="240" width="10" height="20" fill="#ECC333" />
|
||||||
|
<rect x="400" y="200" width="10" height="40" fill="#ECC333" />
|
||||||
|
<rect x="400" y="250" width="10" height="20" fill="#ECC333" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 344 B |
@@ -1,54 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Mode string
|
|
||||||
Host string
|
|
||||||
|
|
||||||
NpmCommand string
|
|
||||||
}
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[Config]()
|
|
||||||
|
|
||||||
func setFromEnvOrDefault(target *string, m map[string]string, key string, defaultValue string) {
|
|
||||||
v, ok := m[key]
|
|
||||||
if ok {
|
|
||||||
*target = v
|
|
||||||
} else {
|
|
||||||
*target = defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (Config, error) {
|
|
||||||
env, err := godotenv.Read(".env")
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg Config
|
|
||||||
|
|
||||||
setFromEnvOrDefault(&cfg.Mode, env, "MODE", "production")
|
|
||||||
setFromEnvOrDefault(&cfg.Host, env, "HOST", ":4000")
|
|
||||||
setFromEnvOrDefault(&cfg.NpmCommand, env, "NPM_COMMAND", "npm")
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExampleProductionConfig = Config{
|
|
||||||
Mode: "production",
|
|
||||||
Host: ":4000",
|
|
||||||
|
|
||||||
NpmCommand: "npm",
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExampleDevelopmentConfig = Config{
|
|
||||||
Mode: "development",
|
|
||||||
Host: ":4000",
|
|
||||||
|
|
||||||
NpmCommand: "npm",
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[Database]()
|
|
||||||
|
|
||||||
// Database is the main database service interface
|
|
||||||
type Database interface {
|
|
||||||
// User
|
|
||||||
|
|
||||||
CreateUser(user model.User) (db.Ref[model.User], error)
|
|
||||||
ReadUser(id db.Ref[model.User]) (model.User, error)
|
|
||||||
ReadUsers() ([]model.User, error)
|
|
||||||
UpdateUser(user model.User) error
|
|
||||||
DeleteUser(id db.Ref[model.User]) error
|
|
||||||
|
|
||||||
// Accounts
|
|
||||||
|
|
||||||
CreateAccount(account model.Account) (db.Ref[model.Account], error)
|
|
||||||
ReadAccount(id db.Ref[model.Account]) (model.Account, error)
|
|
||||||
UpdateAccount(user model.Account) error
|
|
||||||
DeleteAccount(id db.Ref[model.Account]) error
|
|
||||||
|
|
||||||
// Users & Accounts
|
|
||||||
|
|
||||||
ReadUserAccounts(user db.Ref[model.User]) ([]model.Account, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory is an in-memory implementation of [Database] that can be used for testing
|
|
||||||
type Memory struct {
|
|
||||||
Database // TODO: Per ora almeno così facciamo compilare il codice
|
|
||||||
|
|
||||||
Users []model.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) CreateUser(user model.User) (db.Ref[model.User], error) {
|
|
||||||
m.Users = append(m.Users, user)
|
|
||||||
return db.Ref[model.User](user.Id), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) ReadUser(id db.Ref[model.User]) (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(user model.User) error {
|
|
||||||
for i, u := range m.Users {
|
|
||||||
if u.Id == user.Id {
|
|
||||||
m.Users[i] = user
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(`no user with id "%s"`, user.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Memory) DeleteUser(id db.Ref[model.User]) 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,10 +0,0 @@
|
|||||||
package tables
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Users = db.AutoTable[model.User]("users")
|
|
||||||
var Accounts = db.AutoTable[model.Account]("accounts")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package listautenti
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MountApiRoutesHook(l *sl.ServiceLocator, api fiber.Router) error {
|
|
||||||
db, err := sl.Use(l, database.Slot)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Get("/lista-utenti", func(c *fiber.Ctx) error {
|
|
||||||
users, err := db.ReadUsers()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Pre-process data to strip server only fields...
|
|
||||||
return c.JSON(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package listautenti_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gotest.tools/assert"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
"github.com/valyala/fasthttp/fasthttputil"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/util"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/database"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestApiListaUtenti(t *testing.T) {
|
|
||||||
|
|
||||||
memDB := &database.Memory{
|
|
||||||
Users: []model.User{
|
|
||||||
{
|
|
||||||
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
|
||||||
Username: "claire-doe",
|
|
||||||
FullName: db.NewOption("Claire Doe"),
|
|
||||||
Email: db.NewOption("claire.doe@example.org"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
|
||||||
Username: "john-smith",
|
|
||||||
FullName: db.NewOption("John Smith"),
|
|
||||||
Email: db.NewOption("john.smith@example.org"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
l := sl.New()
|
|
||||||
|
|
||||||
// Config
|
|
||||||
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
|
||||||
|
|
||||||
// Database
|
|
||||||
sl.Provide[database.Database](l, database.Slot, memDB)
|
|
||||||
|
|
||||||
// Server
|
|
||||||
sl.ProvideFunc(l, server.Slot, server.Configure)
|
|
||||||
sl.ProvideHook(l, server.ApiRoutesHook,
|
|
||||||
listautenti.MountApiRoutesHook,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize server instance
|
|
||||||
srv, err := sl.Use(l, server.Slot)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Try doing the request
|
|
||||||
//
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := fasthttp.Serve(ln, srv.Router.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)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, string(body), util.CompactIndentedLines(`
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Id":"e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
|
||||||
"Username":"claire-doe",
|
|
||||||
"FullName":"Claire Doe",
|
|
||||||
"Email":"claire.doe@example.org"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id":"9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
|
||||||
"Username":"john-smith",
|
|
||||||
"FullName":"John Smith",
|
|
||||||
"Email":"john.smith@example.org"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
`))
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "git.phc.dm.unipi.it/phc/website/libs/db"
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
// Id è l'id unico di questo utente, non è modificabile una volta creato
|
|
||||||
// l'utente.
|
|
||||||
Id db.Ref[User] `db:"id"`
|
|
||||||
|
|
||||||
// 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 string `db:"username"`
|
|
||||||
|
|
||||||
// FullName da mostrare in giro per il sito
|
|
||||||
FullName db.Option[string] `db:"full_name"`
|
|
||||||
|
|
||||||
// Email per eventuale contatto
|
|
||||||
Email db.Option[string] `db:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderType string
|
|
||||||
|
|
||||||
const AteneoProvider ProviderType = "ateneo"
|
|
||||||
const PoissonProvider ProviderType = "poisson"
|
|
||||||
|
|
||||||
type Account struct {
|
|
||||||
Id db.Ref[Account] `db:"id"` // Id of this entity
|
|
||||||
UserId db.Ref[User] `db:"user_id"` // UserId tells the owner of this account binding
|
|
||||||
Provider ProviderType `db:"provider"` // Provider is the name of the authentication method
|
|
||||||
|
|
||||||
Token string `db:"token"` // Token to use to make requests on behalf of this user
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/server/config"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct{ Router *fiber.App }
|
|
||||||
|
|
||||||
var Slot = sl.NewSlot[*Server]()
|
|
||||||
|
|
||||||
var ApiRoutesHook = sl.NewHook[fiber.Router]()
|
|
||||||
|
|
||||||
func Configure(l *sl.ServiceLocator) (*Server, error) {
|
|
||||||
cfg, err := sl.Use(l, config.Slot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r := fiber.New(fiber.Config{})
|
|
||||||
r.Static("/assets", "./out/frontend/assets")
|
|
||||||
|
|
||||||
api := r.Group("/api")
|
|
||||||
if err := sl.UseHook(l, ApiRoutesHook, api); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Fatal(r.Listen(cfg.Host))
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &Server{r}, nil
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 645 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 295 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
import { betterAuth } from 'better-auth'
|
||||||
|
import { genericOAuth } from 'better-auth/plugins'
|
||||||
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
||||||
|
import * as authSchema from '@/db/auth-schema'
|
||||||
|
import db from '@/db'
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: 'sqlite',
|
||||||
|
schema: authSchema,
|
||||||
|
}),
|
||||||
|
plugins: [
|
||||||
|
genericOAuth({
|
||||||
|
config: [
|
||||||
|
{
|
||||||
|
providerId: 'unipi',
|
||||||
|
clientId: process.env.OAUTH_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
|
||||||
|
scopes: process.env.OAUTH_SCOPES!.split(' '),
|
||||||
|
userInfoUrl: process.env.OAUTH_USER_INFO_URL!,
|
||||||
|
authorizationUrl: process.env.OAUTH_AUTH_URL!,
|
||||||
|
tokenUrl: `${process.env.OAUTH_TOKEN_HOST}${process.env.OAUTH_TOKEN_PATH}`,
|
||||||
|
redirectURI: process.env.OAUTH_REDIRECT_URL!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* image?: string,
|
||||||
|
* course?: string,
|
||||||
|
* title?: string,
|
||||||
|
* author: string,
|
||||||
|
* courseYear: string
|
||||||
|
* }} AppuntiCardProps
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AppuntiCardProps} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const AppuntiCard = ({ course, title, author, courseYear }) => {
|
||||||
|
return (
|
||||||
|
<div class="appunti-item">
|
||||||
|
<div class="thumbnail"></div>
|
||||||
|
{title && <div class="title">{title}</div>}
|
||||||
|
{course && <div class="course">{course}</div>}
|
||||||
|
<div class="author">@{author}</div>
|
||||||
|
<div class="course-year">{courseYear}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppuntiList = ({ children }) => {
|
||||||
|
return <div class="appunti-list">{children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { type ComponentChildren } from 'preact'
|
||||||
|
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||||
|
import { clsx, isMobile } from './lib/util'
|
||||||
|
import { PhosphorIcon } from './Icon'
|
||||||
|
|
||||||
|
export const ComboBox = ({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
setValue: (s: string) => void
|
||||||
|
children: Record<string, ComponentChildren>
|
||||||
|
}) => {
|
||||||
|
const [cloak, setCloak] = useState(true)
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const comboRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [itemWidth, setItemWidth] = useState<number>(200)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false)
|
||||||
|
setCloak(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="combobox" ref={comboRef} style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}>
|
||||||
|
<div class="selected" onClick={() => setOpen(!open)}>
|
||||||
|
<div class="content">{children[value]}</div>
|
||||||
|
{/* <span class="material-symbols-outlined">expand_more</span> */}
|
||||||
|
<PhosphorIcon name="caret-down" />
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div class={clsx('dropdown', cloak && 'invisible')} ref={el => el && setItemWidth(el.offsetWidth)}>
|
||||||
|
{Object.keys(children).map(key => (
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
onClick={() => {
|
||||||
|
setValue(key)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children[key]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const Counter = ({}) => {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="counter">
|
||||||
|
<button onClick={() => setCount(value => value - 1)}>-</button>
|
||||||
|
<div class="value">{count}</div>
|
||||||
|
<button onClick={() => setCount(value => value + 1)}>+</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { Funnel } from '@phosphor-icons/react'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
import extendedLatex from '@/client/lib/marked-latex'
|
||||||
|
|
||||||
|
marked.use(
|
||||||
|
extendedLatex({
|
||||||
|
lazy: false,
|
||||||
|
render: (formula: string, display: boolean) => {
|
||||||
|
return display ? '$$' + formula + '$$' : '$' + formula + '$'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
import type { Database } from '@/data/domande-esami.yaml'
|
||||||
|
|
||||||
|
const useRemoteValue = <T,>(url: string): T | null => {
|
||||||
|
const [value, setValue] = useState<T | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(value => setValue(value))
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
course: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DomandeEsamiCourse = ({ course }: Props) => {
|
||||||
|
const database = useRemoteValue<Database>(`/domande-esami/api/${course}.json`)
|
||||||
|
if (!database) {
|
||||||
|
return <>Loading...</>
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
// @ts-ignore
|
||||||
|
requestIdleCallback(() => window.renderMath())
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
setTimeout(() => window.renderMath(), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseTags = [
|
||||||
|
...new Set(
|
||||||
|
database.questions.filter(question => question.course === course).flatMap(question => question.tags),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const [selectedTag, setSelectedTag] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const filteredQuestions = database.questions
|
||||||
|
.filter(question => question.course === course)
|
||||||
|
.filter(question => (selectedTag ? question.tags.includes(selectedTag) : true))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="grid-center text-center">
|
||||||
|
<h3>
|
||||||
|
<a href="/domande-esami">Domande Orali</a>
|
||||||
|
</h3>
|
||||||
|
<h1>{database.names[course]}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{courseTags.length > 1 && (
|
||||||
|
<div class="card filter">
|
||||||
|
<div class="grid-h">
|
||||||
|
<Funnel />
|
||||||
|
<strong>Filtra Tag</strong>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-wrap">
|
||||||
|
{!selectedTag
|
||||||
|
? courseTags.map(tag => (
|
||||||
|
<div class="chip clickable" onClick={() => setSelectedTag(tag)}>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: courseTags.map(tag => (
|
||||||
|
<div
|
||||||
|
class={tag === selectedTag ? 'chip clickable' : 'chip clickable disabled'}
|
||||||
|
onClick={() => setSelectedTag(tag === selectedTag ? null : tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="wide-card-list" id="questions">
|
||||||
|
{filteredQuestions.length === 0 ? (
|
||||||
|
<div class="grid-center">
|
||||||
|
<em>No questions found</em>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredQuestions.map(question => (
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
class="text"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked(question.content, { async: false }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="metadata">
|
||||||
|
{question.tags.map(tag => (
|
||||||
|
<div class="chip small">{tag}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const icons = Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`, {
|
||||||
|
eager: true,
|
||||||
|
}),
|
||||||
|
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhosphorIcon = ({ name }: Props) => {
|
||||||
|
const icon = icons[name]
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
throw new Error(`Icon "${name}" not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img class="phosphor-icon" src={icon.default.src} alt={name} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
|
import { render } from 'preact'
|
||||||
|
|
||||||
|
// Tipi per la gestione dei dati
|
||||||
|
type TipoStudente = 'triennale' | 'magistrale'
|
||||||
|
|
||||||
|
interface Corso {
|
||||||
|
nome: string
|
||||||
|
anno: '1' | '2' | '3' | 'M' | 'istituzioni'
|
||||||
|
cfu: number
|
||||||
|
passFailOnly?: boolean // Per materie senza voto (pass/fail)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CorsoSelezionato {
|
||||||
|
id: string
|
||||||
|
nome: string
|
||||||
|
cfu: number
|
||||||
|
voto: number | null
|
||||||
|
lode: boolean
|
||||||
|
passFailOnly?: boolean // Per materie senza voto (pass/fail)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CorsoCustom {
|
||||||
|
nome: string
|
||||||
|
cfu: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dati dei corsi aggiornati dalla tabella ufficiale
|
||||||
|
const CORSI_DISPONIBILI: Corso[] = [
|
||||||
|
// Primo Anno
|
||||||
|
{ nome: 'Analisi matematica 1', anno: '1', cfu: 15 },
|
||||||
|
{ nome: 'Aritmetica', anno: '1', cfu: 9 },
|
||||||
|
{ nome: 'Fisica I con laboratorio', anno: '1', cfu: 9 },
|
||||||
|
{ nome: 'Fondamenti di programmazione con laboratorio', anno: '1', cfu: 9 },
|
||||||
|
{ nome: 'Geometria 1', anno: '1', cfu: 15 },
|
||||||
|
{ nome: 'Laboratorio di introduzione alla matematica computazionale', anno: '1', cfu: 6, passFailOnly: true },
|
||||||
|
{ nome: 'Laboratorio di comunicazione mediante calcolatore', anno: '1', cfu: 3, passFailOnly: true },
|
||||||
|
|
||||||
|
// Secondo Anno
|
||||||
|
{ nome: 'Algebra 1', anno: '2', cfu: 6 },
|
||||||
|
{ nome: 'Algoritmi e strutture dati', anno: '2', cfu: 6 },
|
||||||
|
{ nome: 'Analisi matematica 2', anno: '2', cfu: 12 },
|
||||||
|
{ nome: 'Analisi numerica con laboratorio', anno: '2', cfu: 9 },
|
||||||
|
{ nome: 'Elementi di probabilità e statistica', anno: '2', cfu: 6 },
|
||||||
|
{ nome: 'Geometria 2', anno: '2', cfu: 12 },
|
||||||
|
{ nome: 'Inglese scientifico', anno: '2', cfu: 6, passFailOnly: true },
|
||||||
|
{ nome: 'Laboratorio didattico di matematica computazionale', anno: '2', cfu: 3, passFailOnly: true },
|
||||||
|
|
||||||
|
// Terzo Anno
|
||||||
|
{ nome: 'Algebra 2', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Analisi matematica 3', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Analisi reale', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Calcolo scientifico', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di analisi complessa', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di calcolo delle variazioni', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di geometria algebrica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di meccanica celeste', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di teoria degli insiemi', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di teoria delle rappresentazioni', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di topologia algebrica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Equazioni alle derivate parziali', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Fisica II', anno: '3', cfu: 9 },
|
||||||
|
{ nome: 'Fisica III', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Gruppi e rappresentazioni', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Laboratorio computazionale', anno: '3', cfu: 6, passFailOnly: true },
|
||||||
|
{ nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6, passFailOnly: true },
|
||||||
|
{ nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 },
|
||||||
|
{ nome: 'Logica matematica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Matematiche elementari da un punto di vista superiore: aritmetica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Matematiche elementari da un punto di vista superiore: geometria', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Meccanica razionale', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per equazioni differenziali ordinarie', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Metodi topologici in analisi globale', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Ottimizzazione non lineare', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Probabilità', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Ricerca operativa', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Sistemi dinamici', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Spazi di Sobolev', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Statistica matematica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Storia della matematica', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Teoria algebrica dei numeri 1', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Teoria dei campi e teoria di Galois', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Teoria dei numeri elementare', anno: '3', cfu: 6 },
|
||||||
|
{ nome: 'Teoria della misura', anno: '3', cfu: 6 },
|
||||||
|
|
||||||
|
// Istituzioni (Magistrale)
|
||||||
|
{ nome: 'Istituzioni di algebra', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di analisi matematica', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di analisi numerica', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di didattica della matematica', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di fisica matematica', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di geometria', anno: 'istituzioni', cfu: 11 },
|
||||||
|
{ nome: 'Istituzioni di probabilità', anno: 'istituzioni', cfu: 11 },
|
||||||
|
|
||||||
|
// Materie a scelta (Magistrale)
|
||||||
|
{ nome: '4-varietà', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Algebra superiore A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Algebre e gruppi di Lie', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi armonica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi complessa A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi complessa B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi convessa', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi dei dati', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi non standard', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi reale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi su spazi gaussiani', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi superiore', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi superiore A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Analisi superiore B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Aspetti matematici nella computazione quantistica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Calcolo delle variazioni B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Calcolo della variazioni A', anno: 'M', cfu: 6 }, // Variante nome
|
||||||
|
{ nome: 'Combinatoria algebrica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Complementi di analisi funzionale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Complementi di meccanica razionale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Crittografia post-quantistica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Curve ellittiche', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Determinazione orbitale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Didattica della matematica e nuove tecnologie', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Dinamica del sistema solare', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Dinamica iperbolica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Dinamica olomorfa', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Elementi di calcolo in gruppi omogenei', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Equazioni della fluidodinamica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Equazioni differenziali stocastiche e applicazioni', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Equazioni ellittiche', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Finanza matematica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Fisica matematica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Forme modulari', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica C', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica complessa', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica D', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica E', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica F', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria algebrica G', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria e analisi complessa', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria differenziale complessa', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria iperbolica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Geometria riemanniana', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Gruppi algebrici lineari', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Gruppi di Coxeter', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Gruppi di Galois e gruppi fondamentali', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Meccanica celeste', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Meccanica spaziale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Meccanica superiore', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi di analisi armonica in analisi non lineare', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi di approssimazione', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi matematici della crittografia', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi matematici della meccanica quantistica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per catene di Markov', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per equazioni alle derivate parziali', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per il calcolo tensoriale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per il controllo ottimo', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per la grafica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Metodi numerici per problemi inversi', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: "Metodi probabilistici per l'algebra lineare numerica", anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Modelli matematici in biomedicina e fisica matematica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Origini e sviluppo delle matematiche moderne', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Probabilità superiore', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Problemi e metodi della ricerca in didattica della matematica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Problemi e metodi in storia della matematica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Sistemi dinamici aleatori', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Statistica superiore', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Storia della matematica antica e della sua tradizione', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Superfici di Riemann e curve algebriche', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Tecnologie per la didattica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria algebrica dei numeri 2', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria analitica dei numeri A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria dei giochi', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria dei modelli', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria dei nodi A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria degli insiemi', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria degli insiemi A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria degli insiemi B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria delle categorie', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria delle rappresentazioni A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: "Teoria e metodi dell'ottimizzazione", anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria ergodica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Teoria geometrica della misura', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Topologia algebrica', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Topologia algebrica A', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Topologia algebrica B', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Topologia differenziale', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Topologia e geometria in bassa dimensione', anno: 'M', cfu: 6 },
|
||||||
|
{ nome: 'Ultrafiltri e metodi non-standard', anno: 'M', cfu: 6 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function MediaPesataApp() {
|
||||||
|
// Funzioni per localStorage
|
||||||
|
const loadFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const savedData = localStorage.getItem('media-pesata-data')
|
||||||
|
if (savedData) {
|
||||||
|
const parsed = JSON.parse(savedData)
|
||||||
|
return {
|
||||||
|
tipoStudente: parsed.tipoStudente || 'triennale',
|
||||||
|
corsiSelezionati: parsed.corsiSelezionati || [],
|
||||||
|
sezioniAperte: parsed.sezioniAperte || {},
|
||||||
|
mostraRisultati: parsed.mostraRisultati || false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Errore nel caricamento dei dati salvati:', error)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tipoStudente: 'triennale' as TipoStudente,
|
||||||
|
corsiSelezionati: [],
|
||||||
|
sezioniAperte: {},
|
||||||
|
mostraRisultati: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToStorage = (data: any) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('media-pesata-data', JSON.stringify(data))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Errore nel salvataggio dei dati:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inizializzazione con dati salvati
|
||||||
|
// const initialData = loadFromStorage()
|
||||||
|
const [tipoStudente, setTipoStudente] = useState<TipoStudente>('triennale')
|
||||||
|
const [corsiSelezionati, setCorsiSelezionati] = useState<CorsoSelezionato[]>([])
|
||||||
|
const [showCustomForm, setShowCustomForm] = useState(false)
|
||||||
|
const [customCorso, setCustomCorso] = useState<CorsoCustom>({ nome: '', cfu: 0 })
|
||||||
|
const [sezioniAperte, setSezioniAperte] = useState<Record<string, boolean>>({})
|
||||||
|
const [mostraRisultati, setMostraRisultati] = useState(false)
|
||||||
|
|
||||||
|
// Load data from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const initialData = loadFromStorage()
|
||||||
|
setTipoStudente(initialData.tipoStudente)
|
||||||
|
setCorsiSelezionati(initialData.corsiSelezionati)
|
||||||
|
setSezioniAperte(initialData.sezioniAperte)
|
||||||
|
setMostraRisultati(initialData.mostraRisultati)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Salva automaticamente quando cambiano i dati importanti
|
||||||
|
useEffect(() => {
|
||||||
|
const dataToSave = {
|
||||||
|
tipoStudente,
|
||||||
|
corsiSelezionati,
|
||||||
|
sezioniAperte,
|
||||||
|
mostraRisultati,
|
||||||
|
}
|
||||||
|
saveToStorage(dataToSave)
|
||||||
|
}, [tipoStudente, corsiSelezionati, sezioniAperte, mostraRisultati])
|
||||||
|
|
||||||
|
const toggleSezione = (nomeSezione: string) => {
|
||||||
|
setSezioniAperte(prev => ({
|
||||||
|
...prev,
|
||||||
|
[nomeSezione]: !prev[nomeSezione],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcolaMedia = () => {
|
||||||
|
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
|
||||||
|
if (corsiConVoto.length === 0) {
|
||||||
|
alert('Inserisci almeno un voto per calcolare la media!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setMostraRisultati(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzioni per la gestione dei corsi
|
||||||
|
const aggiungiCorso = (corso: Corso) => {
|
||||||
|
// Controlla se la materia esiste già
|
||||||
|
const materiaEsiste = corsiSelezionati.some(c => c.nome.toLowerCase() === corso.nome.toLowerCase())
|
||||||
|
if (materiaEsiste) {
|
||||||
|
alert('Questa materia è già stata aggiunta')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controllo per magistrali: massimo 3 istituzioni
|
||||||
|
if (tipoStudente === 'magistrale' && corso.anno === 'istituzioni') {
|
||||||
|
const istituzioniAttuali = corsiSelezionati.filter(c => c.nome.toLowerCase().includes('istituzioni')).length
|
||||||
|
if (istituzioniAttuali >= 3) {
|
||||||
|
alert('Puoi selezionare al massimo 3 istituzioni per la magistrale')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Date.now().toString()
|
||||||
|
const nuovoCorso: CorsoSelezionato = {
|
||||||
|
id,
|
||||||
|
nome: corso.nome,
|
||||||
|
cfu: corso.cfu,
|
||||||
|
voto: null,
|
||||||
|
lode: false,
|
||||||
|
passFailOnly: corso.passFailOnly,
|
||||||
|
}
|
||||||
|
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggiungiCorsoCustom = () => {
|
||||||
|
// Validazioni
|
||||||
|
if (!customCorso.nome.trim()) {
|
||||||
|
alert('Il nome della materia non può essere vuoto')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (customCorso.cfu <= 0 || customCorso.cfu > 30) {
|
||||||
|
alert('I CFU devono essere tra 1 e 30')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(customCorso.cfu)) {
|
||||||
|
alert('I CFU devono essere un numero intero')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controlla se la materia esiste già
|
||||||
|
const materiaEsiste = corsiSelezionati.some(
|
||||||
|
corso => corso.nome.toLowerCase().trim() === customCorso.nome.toLowerCase().trim(),
|
||||||
|
)
|
||||||
|
if (materiaEsiste) {
|
||||||
|
alert('Questa materia è già stata aggiunta')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Date.now().toString()
|
||||||
|
const nuovoCorso: CorsoSelezionato = {
|
||||||
|
id,
|
||||||
|
nome: customCorso.nome.trim(),
|
||||||
|
cfu: customCorso.cfu,
|
||||||
|
voto: null,
|
||||||
|
lode: false,
|
||||||
|
}
|
||||||
|
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
|
||||||
|
setCustomCorso({ nome: '', cfu: 0 })
|
||||||
|
setShowCustomForm(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rimuoviCorso = (id: string) => {
|
||||||
|
setCorsiSelezionati(corsiSelezionati.filter(corso => corso.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggiornaVoto = (id: string, voto: number | null) => {
|
||||||
|
// Validazione voto: deve essere tra 18 e 30 e intero
|
||||||
|
if (voto !== null && (voto < 18 || voto > 30 || !Number.isInteger(voto))) {
|
||||||
|
return // Ignora valori non validi
|
||||||
|
}
|
||||||
|
|
||||||
|
setCorsiSelezionati(
|
||||||
|
corsiSelezionati.map(corso =>
|
||||||
|
corso.id === id ? { ...corso, voto, lode: voto !== 30 ? false : corso.lode } : corso,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggiornaLode = (id: string, lode: boolean) => {
|
||||||
|
setCorsiSelezionati(corsiSelezionati.map(corso => (corso.id === id ? { ...corso, lode } : corso)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTutto = () => {
|
||||||
|
if (corsiSelezionati.length > 0) {
|
||||||
|
if (confirm('Sei sicuro di voler cancellare tutte le materie selezionate?')) {
|
||||||
|
setCorsiSelezionati([])
|
||||||
|
setCustomCorso({ nome: '', cfu: 0 })
|
||||||
|
setShowCustomForm(false)
|
||||||
|
setSezioniAperte({})
|
||||||
|
setMostraRisultati(false)
|
||||||
|
// Pulisce anche il localStorage
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('media-pesata-data')
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Errore nella pulizia del localStorage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcoli della media pesata
|
||||||
|
const calcolaMediaPesata = () => {
|
||||||
|
// Escludi le materie pass/fail dal calcolo
|
||||||
|
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
|
||||||
|
if (corsiConVoto.length === 0) {
|
||||||
|
return {
|
||||||
|
mediaPesata: 0,
|
||||||
|
votoAmmissione: 0,
|
||||||
|
massimoVotoLaurea: 0,
|
||||||
|
conLode: false,
|
||||||
|
bonusLodi: 0,
|
||||||
|
errore: 'Nessun voto inserito per il calcolo della media',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcola CFU totali con voto
|
||||||
|
const cfuTotaliConVoto = corsiConVoto.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||||
|
const cfuDaEscludere = tipoStudente === 'triennale' ? 15 : 9
|
||||||
|
|
||||||
|
// Se i CFU sono insufficienti per l'esclusione, avvisa l'utente
|
||||||
|
if (cfuTotaliConVoto < cfuDaEscludere) {
|
||||||
|
return {
|
||||||
|
mediaPesata: 0,
|
||||||
|
votoAmmissione: 0,
|
||||||
|
massimoVotoLaurea: 0,
|
||||||
|
conLode: false,
|
||||||
|
bonusLodi: 0,
|
||||||
|
errore: `Hai inserito solo ${cfuTotaliConVoto} CFU con voto. Servono almeno ${cfuDaEscludere} CFU per applicare le regole di esclusione.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordina per voto crescente
|
||||||
|
const corsiOrdinati = [...corsiConVoto].sort((a, b) => a.voto! - b.voto!)
|
||||||
|
|
||||||
|
let cfuEsclusi = 0
|
||||||
|
const corsiValidi: CorsoSelezionato[] = []
|
||||||
|
|
||||||
|
for (const corso of corsiOrdinati) {
|
||||||
|
if (cfuEsclusi < cfuDaEscludere) {
|
||||||
|
const cfuRimanentiDaEscludere = cfuDaEscludere - cfuEsclusi
|
||||||
|
if (corso.cfu <= cfuRimanentiDaEscludere) {
|
||||||
|
// Escludi tutto il corso
|
||||||
|
cfuEsclusi += corso.cfu
|
||||||
|
} else {
|
||||||
|
// Escludi solo una parte del corso
|
||||||
|
const cfuValidi = corso.cfu - cfuRimanentiDaEscludere
|
||||||
|
corsiValidi.push({ ...corso, cfu: cfuValidi })
|
||||||
|
cfuEsclusi = cfuDaEscludere
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
corsiValidi.push(corso)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcola media pesata
|
||||||
|
const sommaPesata = corsiValidi.reduce((sum, corso) => sum + corso.voto! * corso.cfu, 0)
|
||||||
|
const sommaCfu = corsiValidi.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||||
|
|
||||||
|
const mediaPesata = sommaCfu > 0 ? sommaPesata / sommaCfu : 0
|
||||||
|
|
||||||
|
// Calcola bonus lodi
|
||||||
|
const bonusLodi = corsiConVoto.reduce((bonus, corso) => {
|
||||||
|
if (corso.lode) {
|
||||||
|
return bonus + (corso.cfu > 6 ? 0.5 : 0.25)
|
||||||
|
}
|
||||||
|
return bonus
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Cap del bonus lodi basato sul tipo di studente
|
||||||
|
const capBonusLodi = tipoStudente === 'triennale' ? 1.5 : 2
|
||||||
|
const bonusLodiFinal = Math.min(bonusLodi, capBonusLodi)
|
||||||
|
|
||||||
|
// Voto di ammissione alla laurea (media pesata * 11/3)
|
||||||
|
const votoAmmissione = (mediaPesata * 11) / 3
|
||||||
|
|
||||||
|
// Voto di ammissione finale = voto ammissione + bonus lodi
|
||||||
|
const votoAmmissioneFinale = votoAmmissione + bonusLodiFinal
|
||||||
|
|
||||||
|
// Massimo voto di laurea possibile = voto ammissione + 10 (cappato a 110)
|
||||||
|
const massimoVotoLaurea = Math.min(votoAmmissioneFinale + 10, 110)
|
||||||
|
const conLode = massimoVotoLaurea === 110
|
||||||
|
|
||||||
|
return {
|
||||||
|
mediaPesata: Math.round(mediaPesata * 100) / 100,
|
||||||
|
votoAmmissione: Math.round(votoAmmissioneFinale * 100) / 100,
|
||||||
|
massimoVotoLaurea: Math.round(massimoVotoLaurea * 100) / 100,
|
||||||
|
conLode,
|
||||||
|
bonusLodi: Math.round(bonusLodiFinal * 100) / 100,
|
||||||
|
errore: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtra corsi disponibili in base al tipo di studente
|
||||||
|
const getCorsiDisponibili = () => {
|
||||||
|
if (tipoStudente === 'triennale') {
|
||||||
|
return CORSI_DISPONIBILI.filter(corso => corso.anno !== 'istituzioni')
|
||||||
|
} else {
|
||||||
|
return CORSI_DISPONIBILI.filter(
|
||||||
|
corso => corso.anno === 'istituzioni' || corso.anno === '3' || corso.anno === 'M',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raggruppa corsi per categoria
|
||||||
|
const raggruppaCorsi = () => {
|
||||||
|
const corsi = getCorsiDisponibili()
|
||||||
|
const gruppi: Record<string, Corso[]> = {}
|
||||||
|
|
||||||
|
if (tipoStudente === 'triennale') {
|
||||||
|
gruppi['Primo Anno'] = corsi.filter(c => c.anno === '1')
|
||||||
|
gruppi['Secondo Anno'] = corsi.filter(c => c.anno === '2')
|
||||||
|
gruppi['Terzo Anno'] = corsi.filter(c => c.anno === '3')
|
||||||
|
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === 'M')
|
||||||
|
} else {
|
||||||
|
// Per magistrali: prima le istituzioni, poi tutto il resto come "Materie a Scelta"
|
||||||
|
gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni')
|
||||||
|
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === '3' || c.anno === 'M')
|
||||||
|
}
|
||||||
|
|
||||||
|
return gruppi
|
||||||
|
}
|
||||||
|
|
||||||
|
const cambiaTipoStudente = (nuovoTipo: TipoStudente) => {
|
||||||
|
setTipoStudente(nuovoTipo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gruppiCorsi = raggruppaCorsi()
|
||||||
|
const risultati = calcolaMediaPesata()
|
||||||
|
const totaleCfu = corsiSelezionati.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||||
|
const maxCfu = tipoStudente === 'triennale' ? 171 : 93 // 180 − 9 e 120 − 27 per la tesi
|
||||||
|
const cfuError = totaleCfu > maxCfu
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="media-pesata-app">
|
||||||
|
{/* Selezione tipo studente */}
|
||||||
|
<div class="card student-type-switcher wide">
|
||||||
|
<div class="grid-center">
|
||||||
|
<h2>Corso di Laurea</h2>
|
||||||
|
<div class="compound-button">
|
||||||
|
<button
|
||||||
|
class={tipoStudente === 'triennale' ? 'active' : ''}
|
||||||
|
onClick={() => cambiaTipoStudente('triennale')}
|
||||||
|
>
|
||||||
|
Triennale
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={tipoStudente === 'magistrale' ? 'active' : ''}
|
||||||
|
onClick={() => cambiaTipoStudente('magistrale')}
|
||||||
|
>
|
||||||
|
Magistrale
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Counter CFU */}
|
||||||
|
<div class={`cfu-counter wide ${cfuError ? 'error' : ''}`}>
|
||||||
|
<h3>
|
||||||
|
CFU Totali: {totaleCfu}/{maxCfu}
|
||||||
|
{tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}
|
||||||
|
</h3>
|
||||||
|
{cfuError && <p class="error-text">⚠️ Hai superato il limite di CFU consentiti!</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sezione selezione corsi */}
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">
|
||||||
|
<h2>Seleziona Materie</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
|
||||||
|
<div key={categoria} class="course-category">
|
||||||
|
<button onClick={() => toggleSezione(categoria)}>
|
||||||
|
<div class="h-flex">
|
||||||
|
{categoria}
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<span class={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}>▶</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sezioniAperte[categoria] && (
|
||||||
|
<div class="course-grid">
|
||||||
|
{corsi.map((corso, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
class="course-button"
|
||||||
|
onClick={() => aggiungiCorso(corso)}
|
||||||
|
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
|
||||||
|
>
|
||||||
|
<span class="course-name">{corso.nome}</span>
|
||||||
|
<span class="course-cfu">{corso.cfu} CFU</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Form per materia custom */}
|
||||||
|
<div class="custom-course">
|
||||||
|
<h3>Materia Personalizzata</h3>
|
||||||
|
{!showCustomForm ? (
|
||||||
|
<button onClick={() => setShowCustomForm(true)}>+ Aggiungi Materia Personalizzata</button>
|
||||||
|
) : (
|
||||||
|
<div class="custom-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Nome materia"
|
||||||
|
value={customCorso.nome}
|
||||||
|
onChange={e =>
|
||||||
|
setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="CFU"
|
||||||
|
min="1"
|
||||||
|
max="30"
|
||||||
|
step="1"
|
||||||
|
value={customCorso.cfu || ''}
|
||||||
|
onChange={e =>
|
||||||
|
setCustomCorso({
|
||||||
|
...customCorso,
|
||||||
|
cfu: parseInt((e.target as HTMLInputElement).value) || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button onClick={aggiungiCorsoCustom}>Aggiungi</button>
|
||||||
|
<button onClick={() => setShowCustomForm(false)}>Annulla</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sezione lista corsi selezionati */}
|
||||||
|
<div class="card">
|
||||||
|
<div class="h-flex">
|
||||||
|
<div class="title">
|
||||||
|
<h2>Materie Selezionate</h2>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
{corsiSelezionati.length > 0 && <button onClick={resetTutto}>🗑️ Cancella Tutto</button>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{corsiSelezionati.length === 0 ? (
|
||||||
|
<p>Nessuna materia selezionata</p>
|
||||||
|
) : (
|
||||||
|
<div class="courses-list">
|
||||||
|
{corsiSelezionati.map(corso => (
|
||||||
|
<div key={corso.id} class="course-item">
|
||||||
|
<span class="course-name">{corso.nome}</span>
|
||||||
|
<span class="course-cfu">{corso.cfu} CFU</span>
|
||||||
|
|
||||||
|
{!corso.passFailOnly && (
|
||||||
|
<div class="course-grade tall">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Voto"
|
||||||
|
min="18"
|
||||||
|
max="30"
|
||||||
|
step="1"
|
||||||
|
value={corso.voto || ''}
|
||||||
|
onChange={e =>
|
||||||
|
aggiornaVoto(
|
||||||
|
corso.id,
|
||||||
|
parseInt((e.target as HTMLInputElement).value) || null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
|
||||||
|
<input
|
||||||
|
class="star"
|
||||||
|
type="checkbox"
|
||||||
|
checked={corso.lode}
|
||||||
|
disabled={corso.voto !== 30}
|
||||||
|
onChange={e =>
|
||||||
|
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Lode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="actions tall">
|
||||||
|
<button
|
||||||
|
class="icon remove-btn"
|
||||||
|
onClick={() => rimuoviCorso(corso.id)}
|
||||||
|
title="Rimuovi materia"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pulsante Calcola */}
|
||||||
|
{corsiSelezionati.length > 0 && (
|
||||||
|
<div class="calculate-section wide">
|
||||||
|
<button onClick={calcolaMedia}>🧮 Calcola Media e Voto di Laurea</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Risultati */}
|
||||||
|
{risultati && mostraRisultati && (
|
||||||
|
<div class="results wide">
|
||||||
|
<h2>Risultati</h2>
|
||||||
|
{risultati.errore ? (
|
||||||
|
<div class="error-message">
|
||||||
|
<p>⚠️ {risultati.errore}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="results-grid">
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="label">Media Pesata:</span>
|
||||||
|
<span class="value">{risultati.mediaPesata}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="label">Bonus Lodi:</span>
|
||||||
|
<span class="value">+{risultati.bonusLodi}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item highlight">
|
||||||
|
<span class="label">Voto di Ammissione:</span>
|
||||||
|
<span class="value">{risultati.votoAmmissione}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item highlight">
|
||||||
|
<span class="label">Massimo Voto Di Laurea Possibile:</span>
|
||||||
|
<span class="value">
|
||||||
|
{risultati.massimoVotoLaurea}
|
||||||
|
{risultati.conLode && <span class="lode-badge">(+lode)</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Nota informativa */}
|
||||||
|
<div class="card wide">
|
||||||
|
<h3>📋 Come viene calcolata la media</h3>
|
||||||
|
<div class="info-content">
|
||||||
|
<h4>Regole di esclusione CFU:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Triennale:</strong> I 15 CFU con i voti più bassi vengono esclusi dalla media
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Magistrale:</strong> I 9 CFU con i voti più bassi vengono esclusi dalla media
|
||||||
|
</li>
|
||||||
|
<li>Se un corso ha più CFU di quelli da escludere, viene diviso proporzionalmente</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Calcolo del voto finale:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Media pesata:</strong> Somma dei (voto × CFU) diviso per i CFU totali
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Bonus lodi:</strong> +0.5 per lodi in materie > 6 CFU, +0.25 per lodi in materie
|
||||||
|
≤ 6 CFU (max +1.5 per triennale, max +2 per magistrale)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Voto di laurea:</strong> (Voto finale × 11) ÷ 3
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Note:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Le materie <strong>Pass/Fail</strong> non contribuiscono al calcolo della media
|
||||||
|
</li>
|
||||||
|
<li>Il voto finale è limitato a 30</li>
|
||||||
|
<li>Per i magistrali: massimo 3 istituzioni selezionabili</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzione per inizializzare l'app
|
||||||
|
// export function initMediaPesataApp() {
|
||||||
|
// const container = document.getElementById('media-pesata-app')
|
||||||
|
// if (container) {
|
||||||
|
// render(<MediaPesataApp />, container)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default MediaPesataApp
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||||
|
import type { JSX } from 'preact/jsx-runtime'
|
||||||
|
|
||||||
|
export const ShowMore = <T extends any>({
|
||||||
|
items,
|
||||||
|
pageSize,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
items: ReadonlySignal<T[]>
|
||||||
|
pageSize: number
|
||||||
|
children: (item: T) => JSX.Element
|
||||||
|
}) => {
|
||||||
|
const $shownItems = useSignal(pageSize)
|
||||||
|
|
||||||
|
const $paginatedItems = useComputed(() => {
|
||||||
|
return items.value.slice(0, $shownItems.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{$paginatedItems.value.map(children)}
|
||||||
|
<div class="show-more">
|
||||||
|
{$shownItems.value < items.value.length && (
|
||||||
|
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { useComputed, useSignal } from '@preact/signals'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
import { ShowMore } from './Paginate'
|
||||||
|
import { ComboBox } from './ComboBox'
|
||||||
|
import { PhosphorIcon } from './Icon'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
uid: string
|
||||||
|
gecos: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
utenti: {
|
||||||
|
icon: 'user',
|
||||||
|
label: 'Utenti',
|
||||||
|
},
|
||||||
|
macchinisti: {
|
||||||
|
icon: 'wrench',
|
||||||
|
label: 'Macchinisti',
|
||||||
|
},
|
||||||
|
rappstud: {
|
||||||
|
icon: 'bank',
|
||||||
|
label: 'Rappresentanti',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches(users: User[]) {
|
||||||
|
users.forEach(user => {
|
||||||
|
// strip ",+" from the end of the gecos field
|
||||||
|
user.gecos = user.gecos.replace(/,+$/, '')
|
||||||
|
|
||||||
|
// capitalize the first letter of each word
|
||||||
|
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
|
||||||
|
})
|
||||||
|
|
||||||
|
// reverse the order of the users
|
||||||
|
users.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
|
||||||
|
|
||||||
|
const RAPPSTUD = [
|
||||||
|
'smannella',
|
||||||
|
'lotti',
|
||||||
|
'rotolo',
|
||||||
|
'saccani',
|
||||||
|
'carbone',
|
||||||
|
'mburatti',
|
||||||
|
'ppuddu',
|
||||||
|
'marinari',
|
||||||
|
'evsilvestri',
|
||||||
|
'tateo',
|
||||||
|
'graccione',
|
||||||
|
'dilella',
|
||||||
|
'rocca',
|
||||||
|
'odetti',
|
||||||
|
'borso',
|
||||||
|
'numero',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UtentiPage = () => {
|
||||||
|
const $utentiData = useSignal<User[]>([])
|
||||||
|
|
||||||
|
const $filter = useSignal('utenti')
|
||||||
|
|
||||||
|
const $filteredData = useComputed(() =>
|
||||||
|
$filter.value === 'macchinisti'
|
||||||
|
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
|
||||||
|
: $filter.value === 'rappstud'
|
||||||
|
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
|
||||||
|
: $utentiData.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const $fuse = useComputed(
|
||||||
|
() =>
|
||||||
|
new Fuse($filteredData.value, {
|
||||||
|
keys: ['gecos', 'uid'],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const $searchText = useSignal('')
|
||||||
|
const $searchResults = useComputed(() =>
|
||||||
|
$searchText.value.trim().length > 0
|
||||||
|
? ($fuse.value?.search($searchText.value).map(result => result.item) ?? [])
|
||||||
|
: $filteredData.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
applyPatches(data)
|
||||||
|
|
||||||
|
$utentiData.value = data
|
||||||
|
|
||||||
|
$fuse.value.setCollection(data)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="search-bar">
|
||||||
|
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
|
||||||
|
{Object.fromEntries(
|
||||||
|
Object.entries(FILTERS).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
<>
|
||||||
|
{/* <span class="material-symbols-outlined">{v.icon}</span> {v.label} */}
|
||||||
|
<PhosphorIcon name={v.icon} />
|
||||||
|
{v.label}
|
||||||
|
</>,
|
||||||
|
]),
|
||||||
|
)}
|
||||||
|
</ComboBox>
|
||||||
|
<div class="search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cerca un utente Poisson..."
|
||||||
|
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||||
|
value={$searchText.value}
|
||||||
|
/>
|
||||||
|
{/* <span class="material-symbols-outlined">search</span> */}
|
||||||
|
<PhosphorIcon name="magnifying-glass" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search-results">
|
||||||
|
{$searchResults.value ? (
|
||||||
|
<ShowMore items={$searchResults} pageSize={100}>
|
||||||
|
{poissonUser => (
|
||||||
|
<div class="search-result">
|
||||||
|
<div class="icon">
|
||||||
|
{/* <span class="material-symbols-outlined">
|
||||||
|
{RAPPSTUD.includes(poissonUser.uid)
|
||||||
|
? 'account_balance'
|
||||||
|
: MACCHINISTI.includes(poissonUser.uid)
|
||||||
|
? 'construction'
|
||||||
|
: 'person'}
|
||||||
|
</span> */}
|
||||||
|
<PhosphorIcon
|
||||||
|
name={
|
||||||
|
RAPPSTUD.includes(poissonUser.uid)
|
||||||
|
? 'bank'
|
||||||
|
: MACCHINISTI.includes(poissonUser.uid)
|
||||||
|
? 'wrench'
|
||||||
|
: 'user'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text">{poissonUser.gecos}</div>
|
||||||
|
<div class="right">
|
||||||
|
<a href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`} target="_blank">
|
||||||
|
{/* <span class="material-symbols-outlined">open_in_new</span> */}
|
||||||
|
<PhosphorIcon name="arrow-square-out" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ShowMore>
|
||||||
|
) : (
|
||||||
|
<>Nessun risultato</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createAuthClient } from 'better-auth/client'
|
||||||
|
import { genericOAuthClient } from 'better-auth/client/plugins'
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [genericOAuthClient()],
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const $debugConsole = document.createElement('div')
|
||||||
|
|
||||||
|
$debugConsole.style.position = 'fixed'
|
||||||
|
$debugConsole.style.bottom = '0'
|
||||||
|
$debugConsole.style.left = '0'
|
||||||
|
$debugConsole.style.width = '100%'
|
||||||
|
$debugConsole.style.height = '25vh'
|
||||||
|
$debugConsole.style.backgroundColor = 'black'
|
||||||
|
$debugConsole.style.color = 'white'
|
||||||
|
$debugConsole.style.overflow = 'auto'
|
||||||
|
$debugConsole.style.padding = '10px'
|
||||||
|
$debugConsole.style.boxSizing = 'border-box'
|
||||||
|
$debugConsole.style.fontFamily = 'monospace'
|
||||||
|
$debugConsole.style.zIndex = '9999'
|
||||||
|
$debugConsole.style.fontSize = '15px'
|
||||||
|
$debugConsole.style.opacity = '0.8'
|
||||||
|
|
||||||
|
document.body.appendChild($debugConsole)
|
||||||
|
|
||||||
|
function logDebugConsole(...args) {
|
||||||
|
$debugConsole.innerHTML += args.join(' ') + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error = logDebugConsole
|
||||||
|
console.warn = logDebugConsole
|
||||||
|
console.log = logDebugConsole
|
||||||
|
console.debug = logDebugConsole
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// took from: https://github.com/sxyazi/marked-extended-latex
|
||||||
|
// this has a peer dependency bug
|
||||||
|
|
||||||
|
const CLASS_NAME = 'latex-b172fea480b'
|
||||||
|
|
||||||
|
const extBlock = options => ({
|
||||||
|
name: 'latex-block',
|
||||||
|
level: 'block',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\$\$[^\$]/)?.index ?? -1
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = /^\$\$([^\$]+)\$\$/.exec(src)
|
||||||
|
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
if (!options.lazy) return options.render(token.formula, true)
|
||||||
|
return `<span class="${CLASS_NAME}" block>${token.formula}</span>`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const extInline = options => ({
|
||||||
|
name: 'latex',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\$[^\$]/)?.index ?? -1
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = /^\$([^\$]+)\$/.exec(src)
|
||||||
|
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
if (!options.lazy) return options.render(token.formula, false)
|
||||||
|
return `<span class="${CLASS_NAME}">${token.formula}</span>`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let observer
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export default (options = {}) => {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (options.lazy && options.env !== 'test') {
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries, self) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = entry.target
|
||||||
|
self.unobserve(span)
|
||||||
|
|
||||||
|
Promise.resolve(options.render(span.innerText, span.hasAttribute('block'))).then(html => {
|
||||||
|
span.innerHTML = html
|
||||||
|
})
|
||||||
|
span.classList.add('latex-rendered')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
extensions: [extBlock(options), extInline(options)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const observe = () => {
|
||||||
|
if (!observer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.disconnect()
|
||||||
|
document.querySelectorAll(`span.${CLASS_NAME}:not(.latex-rendered)`).forEach(span => {
|
||||||
|
observer.observe(span)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const disconnect = () => {
|
||||||
|
observer?.disconnect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const trottleDebounce = <T extends any[], R>(
|
||||||
|
fn: (...args: T) => R,
|
||||||
|
delay: number,
|
||||||
|
options: { leading?: boolean; trailing?: boolean } = {},
|
||||||
|
): ((...args: T) => R | undefined) => {
|
||||||
|
let lastCall = 0
|
||||||
|
let lastResult: R | undefined
|
||||||
|
let lastArgs: T | undefined
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
const leading = options.leading ?? true
|
||||||
|
const trailing = options.trailing ?? true
|
||||||
|
|
||||||
|
return (...args: T): R | undefined => {
|
||||||
|
lastArgs = args
|
||||||
|
if (leading && Date.now() - lastCall >= delay) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...args)
|
||||||
|
} else {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (trailing && lastArgs) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...lastArgs)
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
return lastResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
|
||||||
|
|
||||||
|
export function clsx(...args: ClassValue[]): string {
|
||||||
|
return args
|
||||||
|
.flatMap(arg => {
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
return arg
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
return clsx(...arg)
|
||||||
|
} else if (typeof arg === 'boolean') {
|
||||||
|
return []
|
||||||
|
} else if (typeof arg === 'object') {
|
||||||
|
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMobile = () => {
|
||||||
|
const [windowWidth, setWindowWidth] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
|
||||||
|
const handleResize = () => setWindowWidth(window.innerWidth)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return windowWidth < 1024
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
import PhosphorIcon from './PhosphorIcon.astro'
|
||||||
|
|
||||||
|
const ICONS_MAP: Record<string, string> = {
|
||||||
|
github: 'github-logo',
|
||||||
|
linkedin: 'linkedin-logo',
|
||||||
|
website: 'globe',
|
||||||
|
mail: 'mailbox',
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fullName: string
|
||||||
|
description: string
|
||||||
|
|
||||||
|
image: ImageMetadata
|
||||||
|
|
||||||
|
entranceDate: number
|
||||||
|
exitDate?: number
|
||||||
|
|
||||||
|
founder?: boolean
|
||||||
|
|
||||||
|
social?: {
|
||||||
|
github?: string
|
||||||
|
linkedin?: string
|
||||||
|
website?: string
|
||||||
|
mail?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fullName, description, image, entranceDate, exitDate, founder, social } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="bubble">
|
||||||
|
<img src={image.src} alt={fullName.toLowerCase()} />
|
||||||
|
<div class="title">{fullName}</div>
|
||||||
|
<div class="date">{entranceDate}—{exitDate ?? 'Presente'}</div>
|
||||||
|
{founder && <div class="founder">Fondatore</div>}
|
||||||
|
<div class="description">{description}</div>
|
||||||
|
{
|
||||||
|
social && (
|
||||||
|
<div class="social">
|
||||||
|
{Object.entries(social).map(([key, value]) => (
|
||||||
|
<a href={value} target="_blank" rel="noopener noreferrer">
|
||||||
|
<PhosphorIcon name={ICONS_MAP[key] ?? 'question-mark'} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
large?: boolean
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { large, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['card', large && 'large']} {...props}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<footer>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
||||||
|
>macchinisti@lists.dm.unipi.it</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
const links = [
|
||||||
|
{ href: '/utenti', text: 'Utenti' },
|
||||||
|
// { href: '/macchinisti', text: 'Macchinisti' },
|
||||||
|
// { href: '/appunti', text: 'Appunti' },
|
||||||
|
{ href: '/notizie', text: 'Notizie' },
|
||||||
|
{ href: '/guide', text: 'Guide' },
|
||||||
|
{ href: '/domande-esami', text: 'Domande Orali' },
|
||||||
|
{ href: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto
|
||||||
|
{ href: '/storia', text: 'Storia' },
|
||||||
|
// { href: '/login', text: 'Login' },
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<!-- main logo on the left -->
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- hidden checkbox for mobile js-less sidebar interaction -->
|
||||||
|
<input type="checkbox" id="header-menu-toggle" />
|
||||||
|
|
||||||
|
<!-- desktop navbar links -->
|
||||||
|
<div class="links desktop-only">
|
||||||
|
{
|
||||||
|
links.map(link => (
|
||||||
|
<a role="button" href={link.href}>
|
||||||
|
{link.text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sidebar menu for mobile -->
|
||||||
|
<div class="mobile-only">
|
||||||
|
<label id="header-menu-toggle-menu" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">menu</span>
|
||||||
|
</label>
|
||||||
|
<label id="header-menu-toggle-close" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sidebar menu only visible on mobile when #header-menu-toggle is checked -->
|
||||||
|
<div class="side-menu">
|
||||||
|
<div class="links">
|
||||||
|
{
|
||||||
|
links.map(link => (
|
||||||
|
<a role="button" href={link.href}>
|
||||||
|
{link.text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { color } = Astro.props
|
||||||
|
|
||||||
|
const patternId = 'zig-zag-' + color.slice(1)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={patternId} x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill={color} d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill={`url(#${patternId})`} x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = Astro.props
|
||||||
|
|
||||||
|
const icons = Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`),
|
||||||
|
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!icons[name]) {
|
||||||
|
throw new Error(`Icon "${name}" not found`)
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Image class="phosphor-icon" src={icons[name]()} alt={name} />
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
// Card.astro
|
||||||
|
export interface Props {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
|
||||||
|
imgSrc?: string
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, imgSrc, style, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a target="_blank" href={href} style={style}>
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
|
||||||
|
</div>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<div class="description text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
const { year, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">{year} • {title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { JSDOM } from 'jsdom'
|
||||||
|
import Container from './Container.astro'
|
||||||
|
|
||||||
|
const language = Astro.props['data-language'] ?? 'text'
|
||||||
|
|
||||||
|
const html = await Astro.slots.render('default')
|
||||||
|
|
||||||
|
const rawCode = new JSDOM(html).window.document.body.textContent
|
||||||
|
---
|
||||||
|
|
||||||
|
<pre {...Astro.props}><slot /></pre>
|
||||||
|
|
||||||
|
{language === 'astro' && <Container set:html={rawCode} />}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
const { size, ...rest } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['container', size ?? 'normal']} {...rest}>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||