Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0520e7641f | ||
|
|
45d41d714b | ||
|
|
cb76d0cdf2 | ||
|
|
dc53ee66cb | ||
|
|
41b08b86e0 | ||
|
|
1d61efeda0 | ||
|
|
39ad130e81 | ||
|
|
6a692fdf5f | ||
|
|
b0f4eb069e | ||
|
|
0d7d24f1b9 | ||
|
|
e6042c4e99
|
||
|
|
751298f4c6 | ||
|
|
6cf6e54bbb | ||
|
|
b2a9355d1d | ||
|
|
e3b71962e5
|
||
|
|
2e2f15874b
|
||
|
|
5d3f082d62 | ||
|
|
80ef1f11c2 | ||
|
|
cdb5d2ef07 | ||
|
|
9393fa73b9 | ||
|
|
4c87ade7a0
|
||
|
|
9aea9bc3a8 | ||
|
|
c8e9ee964f | ||
|
|
e6c46d75dc | ||
|
|
58db3e3901 | ||
|
|
91998e8924 | ||
|
|
1ccb71d3a1 | ||
|
|
a53ee26e78 | ||
|
|
5b0fb7fa8a
|
||
|
|
f775634fd3
|
||
|
|
ce2ec3e5ad | ||
|
|
163a6f8a2c
|
||
|
|
7aed41dd0c
|
||
|
|
57f0a97c68
|
||
|
|
349fb9bcf1 | ||
|
|
57f5764608 | ||
|
|
8bd5f83fc0 | ||
|
|
280c3aaef4 | ||
|
|
447e87b691 | ||
|
|
99371a1688 | ||
|
|
695720e44c | ||
|
|
1a2e2341d3 | ||
|
|
783bf20e21 | ||
|
|
650278828c | ||
|
|
b79eb41438 | ||
|
|
847f588e87 | ||
|
|
bfb5a61e14 | ||
|
|
106531603c | ||
|
|
1a27b2d91e | ||
|
|
b9444b4687 | ||
|
|
a2582c884b | ||
|
|
e727b2f71f | ||
|
|
54f5d798e8 | ||
|
|
e7cf338a8c | ||
|
|
cf21b9a143 | ||
|
|
c04a161ccc | ||
|
|
8bbfc57ea8 | ||
|
|
bdff6aadd0 | ||
|
|
41c654daea | ||
|
|
9a868120b8 | ||
|
|
b0f4a58221 | ||
|
|
4c0a6ed1c0 | ||
|
|
79c7cd9642 | ||
|
|
97266879e0 | ||
|
|
b9ac8a61f7 | ||
|
|
4344c397f6 | ||
|
|
1f099d4d53 | ||
|
|
b442af689c | ||
|
|
9ddcc9b0f5 | ||
|
|
7a7fef0580 | ||
|
|
9890bd2c58 | ||
|
|
8a5e1a54c0 | ||
|
|
8e141af5df | ||
|
|
5d6d4bd4d4 | ||
|
|
514973852c | ||
|
|
647d192ee7 | ||
|
|
219a966860 | ||
|
|
a5f724d167 | ||
|
|
d98dc0bc3e | ||
|
|
6c36b803e9 | ||
|
|
26268329d9 | ||
|
|
5043cc2593 | ||
|
|
ef1183b35e | ||
|
|
9db584aab6 | ||
|
|
f3c45b469c | ||
|
|
5f570cca7b | ||
|
|
eea1291887 | ||
|
|
646a078131 | ||
|
|
c7a3c2a153 | ||
|
|
77c73713d4 | ||
|
|
168ef97460 | ||
|
|
4f1e72a39d | ||
|
|
0e8bee0520 | ||
|
|
5b088cdb8b | ||
|
|
c61287f412 | ||
|
|
246b82c5b0 | ||
|
|
33c15ffaad | ||
|
|
d0c9fdc3ef | ||
|
|
95b7939960 | ||
|
|
8885f40c43 | ||
|
|
935278357b | ||
|
|
1c3040ee79 | ||
|
|
ae8b2b2d51 | ||
|
|
764b569e31 | ||
|
|
b7d65edbc9 | ||
|
|
3a81824019 | ||
|
|
6f0b1467e8 | ||
|
|
b19400f3ee | ||
|
|
1510163d7b | ||
|
|
aa21c64880 | ||
|
|
bffe3056f7 | ||
|
|
744d7e0860 | ||
|
|
f7cba43f7b | ||
|
|
55711c1f0e | ||
|
|
a7c5bee55b |
@@ -1,4 +1,3 @@
|
||||
Dockerfile
|
||||
node_modules
|
||||
.git
|
||||
*.local*
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# This file defines a Drone pipeline that builds a static website with "npm run build". This
|
||||
# pipeline must be marked as "Trusted" in the Drone project settings.
|
||||
#
|
||||
# We mount the target directory of the project at "/var/www/{project}" to the container
|
||||
# "dist/" directory and the run the build. A caveat is that the container builds files
|
||||
# with "root" permissions, so we need to fix those after each build with a second pipeline.
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
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,15 @@
|
||||
# Server config
|
||||
MODE=development
|
||||
HOST=localhost:8000
|
||||
|
||||
# Separate services
|
||||
GIT_URL=https://git.phc.dm.unipi.it
|
||||
CHAT_URL=https://chat.phc.dm.unipi.it
|
||||
|
||||
# Other
|
||||
EMAIL=macchinisti@lists.dm.unipi.it
|
||||
|
||||
USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~
|
||||
|
||||
# AuthService
|
||||
AUTH_SERVICE_HOST=:memory:
|
||||
@@ -1,11 +0,0 @@
|
||||
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,23 +1,19 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
# Miscellaneous
|
||||
tags
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local data
|
||||
# Local files
|
||||
*.local*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
# NodeJS
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
# Don't version generated files
|
||||
public/css/
|
||||
public/js/
|
||||
|
||||
# Executables
|
||||
phc-website-server
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
printWidth: 120,
|
||||
singleQuote: true,
|
||||
quoteProps: 'consistent',
|
||||
tabWidth: 4,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
arrowParens: 'avoid',
|
||||
|
||||
plugins: ['prettier-plugin-astro'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
options: {
|
||||
parser: 'astro',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: '*.{yml,yaml,json}',
|
||||
excludeFiles: 'package-lock.json',
|
||||
options: {
|
||||
tabWidth: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"npm.packageManager": "bun",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.tabSize": 2,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,25 @@
|
||||
# https://dev.to/code42cate/how-to-dockerize-and-deploy-astro-6ll
|
||||
FROM node:18 AS frontend-builder
|
||||
WORKDIR /frontend
|
||||
COPY ./frontend/package.json ./package.json
|
||||
COPY ./frontend/package-lock.json ./package-lock.json
|
||||
RUN npm install
|
||||
COPY ./frontend .
|
||||
RUN make
|
||||
|
||||
# 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
|
||||
FROM golang:1.18-alpine AS backend-builder
|
||||
WORKDIR /backend
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
RUN go build -buildvcs=false -o website-server -v .
|
||||
|
||||
# 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
|
||||
FROM alpine:latest AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=frontend-builder /frontend/dist ./frontend/dist
|
||||
COPY --from=backend-builder /backend/website-server ./website-server
|
||||
COPY ./views ./views
|
||||
COPY ./news ./news
|
||||
COPY ./public ./public
|
||||
COPY ./.env ./.env
|
||||
EXPOSE 8000
|
||||
CMD ["./website-server"]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
GO_SOURCES = $(shell find . -name '*.go')
|
||||
GO_EXECUTABLE = phc-website-server
|
||||
|
||||
.PHONY: all
|
||||
all: frontend go
|
||||
|
||||
#
|
||||
# Build Frontend
|
||||
#
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
mkdir -p public/js
|
||||
mkdir -p public/css
|
||||
|
||||
.PHONY: frontend
|
||||
frontend: setup
|
||||
$(MAKE) -C frontend/
|
||||
cp frontend/dist/*.min.js public/js/
|
||||
cp frontend/dist/*.css public/css/
|
||||
|
||||
#
|
||||
# Build Server
|
||||
#
|
||||
|
||||
.PHONY: go
|
||||
go: $(GO_EXECUTABLE)
|
||||
@echo "Compiled Server"
|
||||
|
||||
$(GO_EXECUTABLE): $(GO_SOURCES)
|
||||
go build -o $(GO_EXECUTABLE) .
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
@echo "GO_SOURCES = $(GO_SOURCES)"
|
||||
@echo "GO_EXECUTABLE = $(GO_EXECUTABLE)"
|
||||
@@ -1,35 +1,99 @@
|
||||
# PHC Website
|
||||
# Nuovo Sito PHC
|
||||
|
||||
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||
Repo del server del nuovo sito per il PHC.
|
||||
|
||||
## Installazione
|
||||
## Dipendenze
|
||||
|
||||
- **Golang**
|
||||
|
||||
- `github.com/alecthomas/chroma`
|
||||
|
||||
- `github.com/alecthomas/repr`
|
||||
|
||||
- `github.com/go-chi/chi/v5`
|
||||
|
||||
- `github.com/joho/godotenv`
|
||||
|
||||
- `github.com/litao91/goldmark-mathjax`
|
||||
|
||||
- `github.com/yuin/goldmark`
|
||||
|
||||
- `github.com/yuin/goldmark-highlighting`
|
||||
|
||||
- `gopkg.in/yaml.v3`
|
||||
|
||||
- **NodeJS**
|
||||
|
||||
- AlpineJS
|
||||
|
||||
- FuseJS
|
||||
|
||||
## Setup
|
||||
|
||||
Per ottenere il progetto basta fare
|
||||
|
||||
```bash
|
||||
bun install
|
||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
||||
$ cd frontend/
|
||||
frontend/ $ npm install
|
||||
frontend/ $ cd ..
|
||||
$ make frontend
|
||||
```
|
||||
|
||||
## Sviluppo
|
||||
## Development
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
### Setup
|
||||
|
||||
Copiare il file `.env.dev` in `.env` per dire al server di lavorare in modalità di development e su quale indirizzo servire il sito, poi avviare il server.
|
||||
|
||||
```bash shell
|
||||
$ cp .env.dev .env
|
||||
$ go run .
|
||||
```
|
||||
|
||||
## Build
|
||||
### Server
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
|
||||
|
||||
```bash shell
|
||||
$ find . -type f -name '*.go' | entr -r go run .
|
||||
# O anche con fd...
|
||||
$ fd -e go | entr -r go run .
|
||||
```
|
||||
|
||||
## Deploy
|
||||
### Frontend
|
||||
|
||||
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.
|
||||
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
|
||||
|
||||
## Come Contribuire
|
||||
```bash shell
|
||||
$ make frontend
|
||||
# O anche con un watcher...
|
||||
$ fd -e js | entr make frontend
|
||||
```
|
||||
|
||||
**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ì.
|
||||
### Environment Variables
|
||||
|
||||
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.
|
||||
- `MODE`
|
||||
|
||||
### Cose da fare
|
||||
Può essere `production` (default) o `development`.
|
||||
|
||||
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
||||
- `HOST`
|
||||
|
||||
Indirizzo (locale) sul quale servire il sito, di default è `localhost:8000`.
|
||||
|
||||
- `MAIL`
|
||||
|
||||
Indirizzo di posta elettronica per contattare gli ammistratori del sito,
|
||||
che compare nel footer di ogni pagina.
|
||||
|
||||
- `<SERVIZIO>_URL`
|
||||
|
||||
Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale).
|
||||
|
||||
Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`.
|
||||
|
||||
## Altri Servizi
|
||||
|
||||
Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi.
|
||||
|
||||
Il servizio di autenticazione di default girerà su `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
||||
|
||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,113 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
ArticlePath string
|
||||
markdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
|
||||
func (article *Article) HasTag(tag string) bool {
|
||||
for _, t := range article.Tags {
|
||||
if t == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewArticle(articlePath string) (*Article, error) {
|
||||
article := &Article{
|
||||
ArticlePath: articlePath,
|
||||
}
|
||||
|
||||
err := article.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func trimAll(vs []string) []string {
|
||||
r := []string{}
|
||||
for _, v := range vs {
|
||||
r = append(r, strings.TrimSpace(v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (article *Article) load() error {
|
||||
fileBytes, err := os.ReadFile(article.ArticlePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source := string(fileBytes)
|
||||
|
||||
// TODO: Ehm bugia pare che esista "https://github.com/yuin/goldmark-meta" però non penso valga la pena aggiungerlo
|
||||
parts := strings.SplitN(source, "---", 3)[1:]
|
||||
|
||||
frontMatterSource := parts[0]
|
||||
markdownSource := parts[1]
|
||||
|
||||
var frontMatter struct {
|
||||
Id string `yaml:"id"`
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Tags string `yaml:"tags"`
|
||||
PublishDate string `yaml:"publish_date"`
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
article.Id = frontMatter.Id
|
||||
article.Title = frontMatter.Title
|
||||
article.Description = frontMatter.Description
|
||||
article.Tags = trimAll(strings.Split(frontMatter.Tags, ","))
|
||||
article.PublishDate = publishDate
|
||||
|
||||
article.markdownSource = markdownSource
|
||||
article.renderedHTML = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (article *Article) Render() (string, error) {
|
||||
if config.Mode == "development" {
|
||||
article.load()
|
||||
}
|
||||
|
||||
if article.renderedHTML == "" {
|
||||
var buf bytes.Buffer
|
||||
if err := articleMarkdown.Convert([]byte(article.markdownSource), &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
article.renderedHTML = buf.String()
|
||||
}
|
||||
|
||||
return article.renderedHTML, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
RootPath string
|
||||
ArticleCache map[string]*Article
|
||||
}
|
||||
|
||||
func NewRegistry(rootPath string) *Registry {
|
||||
return &Registry{
|
||||
rootPath,
|
||||
map[string]*Article{},
|
||||
}
|
||||
}
|
||||
|
||||
func (registry *Registry) loadArticles() error {
|
||||
entries, err := os.ReadDir(registry.RootPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
article, err := NewArticle(path.Join(registry.RootPath, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry.ArticleCache[article.Id] = article
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticle(id string) (*Article, error) {
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
article, present := registry.ArticleCache[id]
|
||||
if !present {
|
||||
return nil, fmt.Errorf(`no article with id "%s"`, id)
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
return article, nil
|
||||
}
|
||||
|
||||
func (registry *Registry) GetArticles() ([]*Article, error) {
|
||||
err := registry.loadArticles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
articles := []*Article{}
|
||||
for _, article := range registry.ArticleCache {
|
||||
articles = append(articles, article)
|
||||
}
|
||||
|
||||
sort.Slice(articles, func(i, j int) bool {
|
||||
return articles[i].PublishDate.After(articles[j].PublishDate)
|
||||
})
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package articles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/formatters/html"
|
||||
mathjax "github.com/litao91/goldmark-mathjax"
|
||||
highlighting "github.com/yuin/goldmark-highlighting"
|
||||
)
|
||||
|
||||
var articleMarkdown goldmark.Markdown
|
||||
|
||||
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
|
||||
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
lang, ok := c.Language()
|
||||
if entering {
|
||||
if ok {
|
||||
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
|
||||
return
|
||||
}
|
||||
w.WriteString("<pre><code>")
|
||||
} else {
|
||||
if ok {
|
||||
w.WriteString("</pre></code>")
|
||||
return
|
||||
}
|
||||
w.WriteString("</pre></code>")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
articleMarkdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
extension.Typographer,
|
||||
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
|
||||
mathjax.NewMathJax(),
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("github"),
|
||||
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import preact from '@astrojs/preact'
|
||||
|
||||
import mdx from '@astrojs/mdx'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import yaml from '@rollup/plugin-yaml'
|
||||
|
||||
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',
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ErrInvalidSession is thrown when an AuthenticatorService is given a missing
|
||||
// or invalid session token
|
||||
var ErrInvalidSession = fmt.Errorf(`invalid session token`)
|
||||
|
||||
// Service is an authentication service abstraction. When a user is logged in a
|
||||
// new session token is returned, this can be used to read and modify user
|
||||
// properties without having to re-send the user password. (TODO: implement
|
||||
// token renewal)
|
||||
type Service interface {
|
||||
// GetUser retrieves the user info given the username
|
||||
GetUser(username string) (*model.User, error)
|
||||
// GetUsers retrieves the full user list from the authentication service
|
||||
GetUsers() ([]*model.User, error)
|
||||
// GetSession retrieves the user session associated to a session token
|
||||
GetSession(token string) (*model.Session, error)
|
||||
// Login tries to log in a user given username and password and if successful returns a new user session
|
||||
Login(username, password string) (*model.Session, error)
|
||||
}
|
||||
|
||||
// UserForSession returns the user linked to the given session token, this is just a shortcut for calling [AuthenticatorService.GetSession] and then [AuthenticatorService.GetUser]
|
||||
func UserForSession(as Service, token string) (*model.User, error) {
|
||||
session, err := as.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := as.GetUser(session.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// NewDefaultService create an AuthenticatorService given an "host" string,
|
||||
// If host is ":memory:" then this uses the [auth.Memory] implementation,
|
||||
// otherwise for now this defaults to [auth.LDAPAuthService]
|
||||
func NewDefaultService(host string) Service {
|
||||
if host == ":memory:" {
|
||||
return exampleMemoryUsers
|
||||
}
|
||||
|
||||
return &LDAPAuthService{host}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// ldapUser represents an LDAP User, most fields are inherited from [auth.User]
|
||||
type ldapUser struct {
|
||||
Uid string `json:"username"`
|
||||
NumericId int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Gecos string `json:"gecos"`
|
||||
}
|
||||
|
||||
// AsUser converts an [ldapUser] to an instance of [auth.User]
|
||||
func (u ldapUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Uid,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
FullName: u.Gecos,
|
||||
}
|
||||
}
|
||||
|
||||
// LDAPAuthService just holds the remote host of the HTTP LDAP service to make requests to
|
||||
type LDAPAuthService struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func NewLDAPAuthService(host string) Service {
|
||||
return &LDAPAuthService{host}
|
||||
}
|
||||
|
||||
// doGetRequest is a utility to make HTTP GET requests
|
||||
func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error {
|
||||
req, err := http.NewRequest(
|
||||
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(response); err != nil {
|
||||
log.Printf(`GET %q resulted in %v`, url, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doPostRequest is a utility to make HTTP POST requests
|
||||
func (a *LDAPAuthService) doPostRequest(url string, request interface{}, response interface{}) error {
|
||||
jsonStr, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), bytes.NewBuffer(jsonStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.NewDecoder(res.Body).Decode(response)
|
||||
}
|
||||
|
||||
func (a *LDAPAuthService) GetUser(username string) (*model.User, error) {
|
||||
var user ldapUser
|
||||
if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (a *LDAPAuthService) GetUsers() ([]*model.User, error) {
|
||||
ldapUsers := []*ldapUser{}
|
||||
if err := a.doGetRequest("/users", &ldapUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*model.User, len(ldapUsers))
|
||||
for i, u := range ldapUsers {
|
||||
users[i] = u.AsUser()
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *LDAPAuthService) GetSession(token string) (*model.Session, error) {
|
||||
var response model.Session
|
||||
if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (a *LDAPAuthService) Login(username, password string) (*model.Session, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
var response model.Session
|
||||
if err := a.doPostRequest("/login", reqBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
var exampleMemoryUsers = &Memory{
|
||||
Users: map[string]*memoryUser{
|
||||
"aziis98": {
|
||||
User: model.User{
|
||||
Username: "aziis98",
|
||||
Name: "Antonio",
|
||||
Surname: "De Lucreziis",
|
||||
Email: "aziis98@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "123",
|
||||
},
|
||||
"bachoseven": {
|
||||
User: model.User{
|
||||
Username: "bachoseven",
|
||||
Name: "Francesco",
|
||||
Surname: "Minnocci",
|
||||
Email: "bachoseven@example.org",
|
||||
}.WithDefaultFullName(),
|
||||
Password: "234",
|
||||
},
|
||||
},
|
||||
Sessions: map[string]*memorySession{},
|
||||
}
|
||||
|
||||
type memoryUser struct {
|
||||
model.User
|
||||
Password string `json:"-"`
|
||||
}
|
||||
|
||||
func (u memoryUser) AsUser() *model.User {
|
||||
return &model.User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
FullName: u.FullName,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
type memorySession struct {
|
||||
Username string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (s memorySession) AsSession() *model.Session {
|
||||
return &model.Session{
|
||||
Username: s.Username,
|
||||
Token: s.Token,
|
||||
}
|
||||
}
|
||||
|
||||
type Memory struct {
|
||||
Users map[string]*memoryUser
|
||||
Sessions map[string]*memorySession
|
||||
}
|
||||
|
||||
func (m *Memory) GetUser(username string) (*model.User, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
return user.AsUser(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetUsers() ([]*model.User, error) {
|
||||
users := make([]*model.User, len(m.Users))
|
||||
i := 0
|
||||
for _, u := range m.Users {
|
||||
users[i] = u.AsUser()
|
||||
i++
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetSession(token string) (*model.Session, error) {
|
||||
session, ok := m.Sessions[token]
|
||||
if !ok {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
||||
|
||||
func (m *Memory) Login(username string, password string) (*model.Session, error) {
|
||||
user, ok := m.Users[username]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`no user with that username`)
|
||||
}
|
||||
|
||||
if user.Password != password {
|
||||
return nil, fmt.Errorf(`invalid credentials`)
|
||||
}
|
||||
|
||||
session := &memorySession{username, util.GenerateRandomString(15)}
|
||||
m.Sessions[session.Token] = session
|
||||
|
||||
return session.AsSession(), nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var Mode string
|
||||
var Host string
|
||||
|
||||
var GitUrl string
|
||||
var ChatUrl string
|
||||
var Email string
|
||||
|
||||
var UserPagesBaseUrl string
|
||||
|
||||
var AuthServiceHost string
|
||||
|
||||
func loadEnv(target *string, name, defaultValue string) {
|
||||
value := os.Getenv(name)
|
||||
if len(strings.TrimSpace(value)) == 0 {
|
||||
*target = defaultValue
|
||||
} else {
|
||||
*target = value
|
||||
}
|
||||
|
||||
log.Printf("%s = %v", name, *target)
|
||||
}
|
||||
|
||||
func Load() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
||||
// Production
|
||||
loadEnv(&Mode, "MODE", "production")
|
||||
loadEnv(&Host, "HOST", ":8080")
|
||||
|
||||
// Services
|
||||
loadEnv(&GitUrl, "GIT_URL", "https://git.example.org")
|
||||
loadEnv(&ChatUrl, "CHAT_URL", "https://chat.example.org")
|
||||
loadEnv(&Email, "EMAIL", "mail@example.org")
|
||||
|
||||
// Poisson
|
||||
loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~")
|
||||
|
||||
// AuthService
|
||||
loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535")
|
||||
}
|
||||
|
||||
func Object() util.H {
|
||||
return util.H{
|
||||
"Mode": Mode,
|
||||
"Host": Host,
|
||||
|
||||
"GitUrl": GitUrl,
|
||||
"ChatUrl": ChatUrl,
|
||||
"Email": Email,
|
||||
|
||||
"UserPagesBaseUrl": UserPagesBaseUrl,
|
||||
|
||||
"AuthServiceHost": AuthServiceHost,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
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!,
|
||||
},
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
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
|
||||
);
|
||||
@@ -1,555 +0,0 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1749233567397,
|
||||
"tag": "0000_violet_agent_zero",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
STYLES_SOURCES = $(wildcard src/styles/*.scss)
|
||||
SCRIPTS_SOURCES = $(wildcard src/*.js)
|
||||
|
||||
.PHONY: all
|
||||
all: scripts styles
|
||||
|
||||
.PHONY: styles
|
||||
styles: $(STYLES_SOURCES)
|
||||
npm run build:styles
|
||||
|
||||
.PHONY: scripts
|
||||
scripts: $(SCRIPTS_SOURCES)
|
||||
npm run build:scripts
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf dist/
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
@echo "STYLES_SOURCES = $(STYLES_SOURCES)"
|
||||
@echo "SCRIPTS_SOURCES = $(SCRIPTS_SOURCES)"
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Package to generate frontend files for phc/website",
|
||||
"scripts": {
|
||||
"build:scripts": "rollup -c",
|
||||
"build:styles": "sass -s compressed --no-source-map src/styles/main.scss dist/main.css"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"rollup": "^2.75.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.52.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.10.2",
|
||||
"fuse.js": "^6.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'rollup'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: 'src/utenti.js',
|
||||
external: ['alpinejs', 'fuse.js'], // libraries to not bundle
|
||||
output: {
|
||||
file: 'dist/utenti.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
// map library names to global constants
|
||||
alpinejs: 'Alpine',
|
||||
'fuse.js': 'Fuse',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
{
|
||||
input: 'src/profilo.js',
|
||||
external: ['alpinejs'],
|
||||
output: {
|
||||
file: 'dist/profilo.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
alpinejs: 'Alpine',
|
||||
},
|
||||
},
|
||||
plugins: [terser()],
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,16 @@
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
Alpine.data('profilo', () => ({
|
||||
init() {
|
||||
console.log('Profilo!')
|
||||
},
|
||||
}))
|
||||
|
||||
Alpine.data('passwordForm', () => ({
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
passwordSame: true,
|
||||
onUpdate() {
|
||||
this.passwordSame = this.password === this.passwordAgain
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,69 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--fg-300: #777;
|
||||
--fg-400: #666;
|
||||
--fg-500: #333;
|
||||
--fg-600: #222;
|
||||
|
||||
--bg-000: #f0f0f0;
|
||||
--bg-100: #f0f0f0;
|
||||
--bg-500: #eaeaea;
|
||||
--bg-550: #ecedee;
|
||||
--bg-600: #e4e5e7;
|
||||
--bg-700: #d5d5d5;
|
||||
--bg-750: #c8c8c8;
|
||||
--bg-800: #c0c0c0;
|
||||
--bg-850: #b8b8b8;
|
||||
|
||||
--accent-300: #5cc969;
|
||||
--accent-400: #4eaa59;
|
||||
--accent-500: #278542;
|
||||
--accent-600: #2e974c;
|
||||
--accent-700: #154d24;
|
||||
--accent-800: #002d0d;
|
||||
|
||||
--ft-ss: 'Inter', sans-serif;
|
||||
--ft-ss-wt-light: 300;
|
||||
--ft-ss-wt-normal: 400;
|
||||
--ft-ss-wt-medium: 500;
|
||||
--ft-ss-wt-bold: 700;
|
||||
|
||||
--shadow-500: 0 0 16px 0 #00000018;
|
||||
|
||||
// Components
|
||||
--text-input-bg: var(--bg-000);
|
||||
--text-input-readonly-bg: var(--bg-600);
|
||||
--text-input-readonly-fg: var(--fg-300);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-500);
|
||||
color: var(--fg-500);
|
||||
|
||||
font-family: var(--ft-ss);
|
||||
font-size: 17px;
|
||||
font-weight: var(--ft-ss-wt-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@import './typography.scss';
|
||||
@@ -0,0 +1,78 @@
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import Alpine from 'alpinejs'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
const SHOW_MORE_INCREMENT = 15
|
||||
const FUSE_OPTIONS = {
|
||||
includeScore: true,
|
||||
keys: [
|
||||
'nome',
|
||||
'cognome',
|
||||
'tags',
|
||||
{ name: 'nomeCompleto', getFn: user => `${user.nome} ${user.cognome}` },
|
||||
],
|
||||
}
|
||||
|
||||
const SORT_MODES = {
|
||||
chronological: () => 0,
|
||||
name: (a, b) => (a.nome < b.nome ? -1 : 1),
|
||||
surname: (a, b) => (a.cognome < b.cognome ? -1 : 1),
|
||||
}
|
||||
|
||||
function getSortedUserList(original, mode) {
|
||||
return [...original].sort(SORT_MODES[mode])
|
||||
}
|
||||
|
||||
Alpine.data('utenti', () => ({
|
||||
searchField: '', // two-way binding for the search input field
|
||||
sortMode: 'chronological', // two-way binding for the sorting mode
|
||||
fetchedUsers: [], // hold complete user list
|
||||
sortedUserBuffer: [], // Yet another buffer of the user list for the sort mode
|
||||
fuse: new Fuse([], FUSE_OPTIONS), // current fuse instance, used to filter the list above
|
||||
searchResultsBuffer: [], // stores the full current search
|
||||
searchResults: [], // list to renderer on screen with a subset of the whole search results buffer
|
||||
async init() {
|
||||
// Get user list from server
|
||||
const response = await fetch('/api/utenti')
|
||||
this.fetchedUsers = await response.json()
|
||||
|
||||
// This will call the function "showMore()" when the user is near the end of the list
|
||||
new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
console.log('Near the bottom of the page')
|
||||
this.showMore()
|
||||
}
|
||||
})
|
||||
}).observe(this.$refs.spinner)
|
||||
|
||||
// Initialize with an empty query
|
||||
this.updateSortMode()
|
||||
this.updateSearch()
|
||||
},
|
||||
showMore() {
|
||||
// setTimeout(() => {
|
||||
// Updates the final "searchResults" list with more items from the previous buffer
|
||||
const newCount = this.searchResults.length + SHOW_MORE_INCREMENT
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, newCount)
|
||||
// }, 250) // For fun
|
||||
},
|
||||
setResults(list) {
|
||||
this.searchResultsBuffer = list.filter(
|
||||
entry => entry.score === undefined || entry.score <= 0.25
|
||||
)
|
||||
this.searchResults = this.searchResultsBuffer.slice(0, SHOW_MORE_INCREMENT)
|
||||
},
|
||||
updateSortMode() {
|
||||
this.sortedUserBuffer = getSortedUserList(this.fetchedUsers, this.sortMode)
|
||||
this.fuse.setCollection(this.sortedUserBuffer)
|
||||
this.updateSearch()
|
||||
},
|
||||
updateSearch() {
|
||||
console.time('search')
|
||||
if (this.searchField.trim().length === 0) {
|
||||
// Reset the result list
|
||||
this.setResults(this.sortedUserBuffer.map(user => ({ item: user })))
|
||||
} else {
|
||||
// Update the result list with the new results
|
||||
this.setResults(this.fuse.search(this.searchField))
|
||||
}
|
||||
console.timeEnd('search')
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,25 @@
|
||||
module git.phc.dm.unipi.it/phc/website
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.9.4
|
||||
github.com/gofiber/fiber/v2 v2.34.0
|
||||
github.com/gofiber/redirect/v2 v2.1.23
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f
|
||||
github.com/yuin/goldmark v1.4.4
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
|
||||
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
|
||||
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE=
|
||||
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
|
||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/redirect/v2"
|
||||
)
|
||||
|
||||
func UserMiddleware(as auth.Service) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Cookies("session-token")
|
||||
user, _ := auth.UserForSession(as, token)
|
||||
c.Locals("user", user)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(logger.New())
|
||||
app.Use(recover.New())
|
||||
|
||||
// Remove trailing slash from URLs
|
||||
app.Use(redirect.New(redirect.Config{
|
||||
Rules: map[string]string{
|
||||
"/*/": "/$1",
|
||||
},
|
||||
}))
|
||||
|
||||
// Serve content statically from "./public", mounted on the "/public/" route
|
||||
app.Static("/public/", "./public")
|
||||
|
||||
authService := auth.NewDefaultService(config.AuthServiceHost)
|
||||
app.Use(UserMiddleware(authService))
|
||||
|
||||
// Templates & Renderer
|
||||
renderer := templates.NewRenderer(
|
||||
"./views/",
|
||||
"./views/base.html",
|
||||
"./views/partials/*.html",
|
||||
)
|
||||
|
||||
newsArticlesRegistry := articles.NewRegistry("./news")
|
||||
|
||||
// Routes
|
||||
|
||||
actuallyStaticRoutes := map[string]string{
|
||||
"/": "home.html",
|
||||
"/link": "link.html",
|
||||
"/login": "login.html",
|
||||
"/utenti": "utenti.html",
|
||||
}
|
||||
|
||||
for route, view := range actuallyStaticRoutes {
|
||||
localView := view
|
||||
app.Get(route, func(c *fiber.Ctx) error {
|
||||
c.Type("html")
|
||||
return renderer.Render(c, localView, util.H{
|
||||
"User": c.Locals("user"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
app.Get("/api/utenti", func(c *fiber.Ctx) error {
|
||||
utenti, err := authService.GetUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(utenti)
|
||||
})
|
||||
|
||||
app.Get("/api/profilo", func(c *fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
return fmt.Errorf(`user not logged in`)
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
})
|
||||
|
||||
app.Get("/storia", func(c *fiber.Ctx) error {
|
||||
storia, err := GetStoria()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html")
|
||||
return renderer.Render(c, "storia.html", util.H{
|
||||
"User": c.Locals("user"),
|
||||
"Storia": storia,
|
||||
})
|
||||
})
|
||||
|
||||
app.Get("/appunti", func(c *fiber.Ctx) error {
|
||||
searchQuery := c.Query("q", "")
|
||||
|
||||
c.Type("html")
|
||||
return renderer.Render(c, "appunti.html", util.H{
|
||||
"User": c.Locals("user"),
|
||||
"Query": searchQuery,
|
||||
})
|
||||
})
|
||||
|
||||
app.Get("/news", func(c *fiber.Ctx) error {
|
||||
articles, err := newsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html")
|
||||
return renderer.Render(c, "news.html", util.H{
|
||||
"User": c.Locals("user"),
|
||||
"Articles": articles,
|
||||
})
|
||||
})
|
||||
|
||||
app.Post("/login", func(c *fiber.Ctx) error {
|
||||
var loginForm struct {
|
||||
Provider string `form:"provider"`
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&loginForm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := authService.Login(loginForm.Username, loginForm.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inThreeDays := time.Now().Add(3 * 24 * time.Hour)
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: session.Token,
|
||||
Expires: inThreeDays,
|
||||
})
|
||||
|
||||
return c.Redirect("/profilo")
|
||||
})
|
||||
|
||||
app.Get("/profilo", func(c *fiber.Ctx) error {
|
||||
user, ok := c.Locals("user").(*model.User)
|
||||
if !ok || user == nil {
|
||||
return fmt.Errorf(`user not logged in`)
|
||||
}
|
||||
|
||||
c.Type("html")
|
||||
return renderer.Render(c, "profilo.html", util.H{
|
||||
"User": c.Locals("user"),
|
||||
})
|
||||
})
|
||||
|
||||
app.Get("/logout", func(c *fiber.Ctx) error {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session-token",
|
||||
Path: "/",
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
})
|
||||
return c.Redirect("/")
|
||||
})
|
||||
|
||||
app.Get("/news/:article", func(c *fiber.Ctx) error {
|
||||
articleID := c.Params("article")
|
||||
|
||||
article, err := newsArticlesRegistry.GetArticle(articleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html, err := article.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html")
|
||||
return renderer.Render(c, "news-base.html", util.H{
|
||||
"User": c.Locals("user"),
|
||||
"Article": article,
|
||||
"ContentHTML": template.HTML(html),
|
||||
})
|
||||
})
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
err := app.Listen(config.Host)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
// User represents a user returned from AuthenticatorService
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Surname string `json:"surname"`
|
||||
// FullName is a separate field from Name and Surname because for example
|
||||
// ldap stores them all as separate fields.
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// WithDefaultFullName is a utility that returns a copy of the given user with the full name set to the concatenation of the name and surname of the user.
|
||||
func (u User) WithDefaultFullName() User {
|
||||
return User{
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Surname: u.Surname,
|
||||
Email: u.Email,
|
||||
|
||||
FullName: u.Username + " " + u.Surname,
|
||||
}
|
||||
}
|
||||
|
||||
// Session represents a session returned from AuthenticatorService
|
||||
type Session struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
id: 2021-12-22-notizia-1
|
||||
title: "Notizia 1"
|
||||
tags: important, prova, test, foo, bar
|
||||
publish_date: 2021/12/22 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur "adipisicing" elit. Repudiandae -- optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora $1 + 1$ ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
$$
|
||||
\int_0^1 x^2 \mathrm d x
|
||||
$$
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
```
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
- bar with some `code`
|
||||
```go
|
||||
type Article struct {
|
||||
Id string
|
||||
Title string
|
||||
Description string
|
||||
Tags []string
|
||||
PublishDate time.Time
|
||||
|
||||
MarkdownSource string
|
||||
renderedHTML string
|
||||
}
|
||||
```
|
||||
|
||||
#### Tables
|
||||
|
||||
<https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#tables>
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
|
||||
| Expression | Derivative | Integral |
|
||||
| :---: | :---: | :---: |
|
||||
| $x^a$ | $a x^{a-1}$ | $\displaystyle \frac{1}{a+1} x^{a+1} + c$ se $a \neq -1$ |
|
||||
| $\sin x$ | $\cos x$ | $\displaystyle -\cos x + c$ |
|
||||
| $e^x$ | $e^x$ | $\displaystyle e^x + c$ |
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: 2021-12-23-notizia-2
|
||||
title: "Notizia 2"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/23 22:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: 2021-12-24-notizia-3
|
||||
title: "Notizia 3"
|
||||
tags: prova, test, foo, bar
|
||||
publish_date: 2021/12/24 18:00
|
||||
description: |
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur...
|
||||
---
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur **distinctio possimus** laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus _laudantium molestias similique placeat_, dolore omnis et aperiam rem [delectus tempora ea,](#) cupiditate explicabo vel! Porro?
|
||||
|
||||

|
||||
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repudiandae optio ad, consequatur distinctio possimus laudantium molestias similique placeat, dolore omnis et aperiam rem delectus tempora ea, cupiditate explicabo vel! Porro?
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Item 4
|
||||
- Item 5
|
||||
- Item 5
|
||||
- Item 5
|
||||
|
||||
- foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "website",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "run-s astro:sync astro:dev",
|
||||
"build": "run-s astro:build",
|
||||
"astro:server": "bun run ./dist/server/entry.mjs",
|
||||
"drizzle:migrate": "drizzle-kit migrate",
|
||||
"drizzle:generate": "drizzle-kit generate",
|
||||
"start": "run-s drizzle:migrate astro:server",
|
||||
"astro:sync": "astro sync",
|
||||
"astro:dev": "astro dev",
|
||||
"astro:build": "astro check && astro build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/node": "9.0.0",
|
||||
"@astrojs/preact": "4.0.0",
|
||||
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||
"@fontsource/iosevka": "^5.0.11",
|
||||
"@fontsource/mononoki": "^5.0.11",
|
||||
"@fontsource/open-sans": "^5.0.24",
|
||||
"@fontsource/source-code-pro": "^5.0.16",
|
||||
"@fontsource/source-sans-pro": "^5.0.8",
|
||||
"@fontsource/space-mono": "^5.0.20",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="80" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="160" y="50" width="150" height="60" fill="#1E6733" />
|
||||
<rect x="140" y="90" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="140" y="200" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="140" y="410" width="10" height="20" fill="#ECC333" />
|
||||
<rect x="140" y="350" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="240" y="110" width="70" height="10" fill="#ECC333" />
|
||||
<rect x="250" y="130" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="340" y="50" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="340" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="590" y="190" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="690" y="180" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="690" y="310" width="60" height="140" fill="#1E6733" />
|
||||
<rect x="690" y="50" width="60" height="120" fill="#1E6733" />
|
||||
<rect x="590" y="320" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="590" y="50" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="420" y="240" width="150" height="60" fill="#1E6733" />
|
||||
<rect x="340" y="320" width="60" height="130" fill="#1E6733" />
|
||||
<rect x="240" y="140" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="350" y="170" width="40" height="10" fill="#ECC333" />
|
||||
<rect x="330" y="330" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="160" y="200" width="80" height="60" fill="#1E6733" />
|
||||
<rect x="650" y="200" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="750" y="330" width="10" height="60" fill="#ECC333" />
|
||||
<rect x="800" y="450" width="40" height="10" fill="#ECC333" />
|
||||
<rect x="850" y="450" width="30" height="10" fill="#ECC333" />
|
||||
<rect x="750" y="90" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="810" y="110" width="60" height="10" fill="#ECC333" />
|
||||
<rect x="580" y="330" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="580" y="60" width="10" height="50" fill="#ECC333" />
|
||||
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4" />
|
||||
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4" />
|
||||
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="350" y="430" width="40" height="10" fill="#303030" />
|
||||
<rect x="350" y="410" width="40" height="10" fill="#303030" />
|
||||
<rect x="350" y="390" width="40" height="10" fill="#303030" />
|
||||
<rect x="700" y="70" width="20" height="40" fill="#303030" />
|
||||
<rect x="700" y="120" width="20" height="40" fill="#303030" />
|
||||
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4" />
|
||||
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4" />
|
||||
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||
<rect x="370" y="70" width="20" height="30" fill="#303030" />
|
||||
<rect x="370" y="110" width="20" height="30" fill="#303030" />
|
||||
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030" />
|
||||
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030" />
|
||||
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030" />
|
||||
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030" />
|
||||
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030" />
|
||||
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030" />
|
||||
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030" />
|
||||
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z"
|
||||
fill="#1E6733" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z"
|
||||
fill="#1E6733" />
|
||||
<rect x="750" y="190" width="10" height="40" fill="#ECC333" />
|
||||
<rect x="750" y="240" width="10" height="20" fill="#ECC333" />
|
||||
<rect x="400" y="200" width="10" height="40" fill="#ECC333" />
|
||||
<rect x="400" y="250" width="10" height="20" fill="#ECC333" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,342 @@
|
||||
|
||||
function pointInInterval(a, b, x) {
|
||||
return a <= x && x <= b;
|
||||
}
|
||||
|
||||
function intervalIntersectsInterval(a, b, c, d) {
|
||||
if (c <= a) {
|
||||
return a <= d;
|
||||
}
|
||||
|
||||
return pointInInterval(a, b, c);
|
||||
}
|
||||
|
||||
function rectIntersectRect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
return intervalIntersectsInterval(x1, x2, x3, x4) && intervalIntersectsInterval(y1, y2, y3, y4);
|
||||
}
|
||||
|
||||
function intersects(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
const a_minx = Math.min(x1, x2);
|
||||
const a_maxx = Math.max(x1, x2);
|
||||
const a_miny = Math.min(y1, y2);
|
||||
const a_maxy = Math.max(y1, y2);
|
||||
|
||||
const b_minx = Math.min(x3, x4);
|
||||
const b_maxx = Math.max(x3, x4);
|
||||
const b_miny = Math.min(y3, y4);
|
||||
const b_maxy = Math.max(y3, y4);
|
||||
|
||||
if (!rectIntersectRect(a_minx, a_miny, a_maxx, a_maxy, b_minx, b_miny, b_maxx, b_maxy))
|
||||
return false;
|
||||
|
||||
const a_dx = x2 - x1;
|
||||
const a_dy = y2 - y1;
|
||||
|
||||
const b_dx = x4 - x3;
|
||||
const b_dy = y4 - y3;
|
||||
|
||||
const det = -b_dx * a_dy + a_dx * b_dy;
|
||||
|
||||
if (Math.abs(det) === 0 && -a_dy * (x3 - x1) + a_dx * (y3 - y1) === 0) {
|
||||
return y1 <= y3 <= y2 || y1 <= y4 <= y2;
|
||||
}
|
||||
|
||||
const s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / det;
|
||||
const t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / det;
|
||||
|
||||
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
|
||||
}
|
||||
|
||||
const $canvas = document.querySelector('#circuit-pattern');
|
||||
let g = $canvas.getContext('2d');
|
||||
|
||||
let WIDTH = $canvas.offsetWidth;
|
||||
let HEIGHT = $canvas.offsetHeight;
|
||||
|
||||
const TS = 20;
|
||||
|
||||
let ROWS = HEIGHT / TS;
|
||||
let COLS = WIDTH / TS;
|
||||
|
||||
function updateCanvasDimensions() {
|
||||
g = $canvas.getContext('2d');
|
||||
|
||||
WIDTH = $canvas.offsetWidth;
|
||||
HEIGHT = $canvas.offsetHeight;
|
||||
|
||||
ROWS = HEIGHT / TS;
|
||||
COLS = WIDTH / TS;
|
||||
|
||||
$canvas.width = WIDTH * devicePixelRatio;
|
||||
$canvas.height = HEIGHT * devicePixelRatio;
|
||||
g.scale(devicePixelRatio, devicePixelRatio);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => updateCanvasDimensions());
|
||||
updateCanvasDimensions();
|
||||
|
||||
const state = {
|
||||
wires: [], // :: [(Int, Int)]
|
||||
nextWire: null,
|
||||
nextWireIndex: 0,
|
||||
nextSegmentLen: 0,
|
||||
t: 0,
|
||||
};
|
||||
|
||||
function randomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
function randomChoice(list) {
|
||||
return list[randomInt(0, list.length - 1)];
|
||||
}
|
||||
|
||||
const DIRS = {
|
||||
left: [-1, 1],
|
||||
center: [0, 1],
|
||||
right: [+1, 1],
|
||||
};
|
||||
|
||||
const NEXT_DIRS = {
|
||||
left: ['center'],
|
||||
center: ['left', 'right'],
|
||||
right: ['center'],
|
||||
};
|
||||
|
||||
function canPlaceSegment([[p1x, p1y], [p2x, p2y]]) {
|
||||
return state.wires.every(wire => {
|
||||
return everyWireSegment(wire, ([[sx, sy], [lx, ly]]) => {
|
||||
return !intersects(sx, sy, lx, ly, p1x, p1y, p2x, p2y);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function everyWireSegment(wire, segmentFn) {
|
||||
let [start, ...rest] = wire;
|
||||
|
||||
return rest.every((pt) => {
|
||||
const r = segmentFn([start, pt]);
|
||||
start = pt;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
function generateWire() {
|
||||
const pieceCount = Math.pow(randomFloat(1, 2.5), 2) | 0;
|
||||
const startPoint = [
|
||||
randomInt(0, COLS),
|
||||
Math.pow(randomFloat(0, ROWS / 12), 2) | 0,
|
||||
];
|
||||
const sizes = Array(pieceCount).fill(0).map(() => randomInt(2, 4) | 0);
|
||||
|
||||
const dirs = [randomChoice(Object.keys(DIRS))];
|
||||
for (let i = 0; i < pieceCount - 1; i++) {
|
||||
const last = dirs[dirs.length - 1];
|
||||
dirs.push(randomChoice(NEXT_DIRS[last]));
|
||||
}
|
||||
|
||||
const wire = [startPoint];
|
||||
for (let i = 0; i < pieceCount; i++) {
|
||||
const [lpx, lpy] = wire[wire.length - 1];
|
||||
const [dx, dy] = DIRS[dirs[i]];
|
||||
const s = sizes[i];
|
||||
|
||||
const s_bonus = (dirs[i] === 'center' ? s * 2 : s) | 0;
|
||||
|
||||
wire.push([lpx + dx * s_bonus, lpy + dy * s_bonus]);
|
||||
}
|
||||
|
||||
const ok = everyWireSegment(wire, segment => {
|
||||
return canPlaceSegment(segment);
|
||||
});
|
||||
|
||||
if (!ok)
|
||||
return null;
|
||||
|
||||
return wire;
|
||||
}
|
||||
|
||||
// // LinearintERPolation
|
||||
function lerp(a, b, t) {
|
||||
return t * (b - a) + a;
|
||||
}
|
||||
function lerpPoint([x1, y1], [x2, y2], t) {
|
||||
return [lerp(x1, x2, t), lerp(y1, y2, t)];
|
||||
}
|
||||
|
||||
const CELLS_PER_SECOND = 15;
|
||||
|
||||
let lastTime = new Date().getTime();
|
||||
|
||||
function update() {
|
||||
const now = new Date().getTime();
|
||||
const delta = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
|
||||
if (state.nextWire) {
|
||||
state.t += CELLS_PER_SECOND * delta;
|
||||
|
||||
if (state.t >= 1.0) {
|
||||
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
|
||||
const dx = Math.abs(x2 - x1);
|
||||
const dy = Math.abs(y2 - y1);
|
||||
state.t = 0;
|
||||
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
state.nextWireIndex++;
|
||||
}
|
||||
|
||||
if (state.nextWireIndex > state.nextWire.length) {
|
||||
state.wires.push(state.nextWire);
|
||||
state.nextWire = null;
|
||||
}
|
||||
} else {
|
||||
const wire = generateWire();
|
||||
if (wire) {
|
||||
state.nextWire = wire;
|
||||
state.nextWireIndex = 2;
|
||||
|
||||
const [[x1, y1], [x2, y2]] = state.nextWire.slice(state.nextWireIndex - 2, state.nextWireIndex);
|
||||
const dx = Math.abs(x2 - x1);
|
||||
const dy = Math.abs(y2 - y1);
|
||||
state.t = 0;
|
||||
state.nextSegmentLen = Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getColors() {
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
return {
|
||||
BACKGROUND: '#282828',
|
||||
CIRCUIT: '#38302e',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
BACKGROUND: '#eaeaea',
|
||||
CIRCUIT: '#d4d4d4',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const { BACKGROUND, CIRCUIT } = getColors();
|
||||
|
||||
g.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
g.strokeStyle = CIRCUIT;
|
||||
g.lineWidth = 3;
|
||||
|
||||
state.wires.forEach(wire => {
|
||||
const [[sx, sy], ...rest] = wire;
|
||||
|
||||
function renderDot(x, y) {
|
||||
if (y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g.beginPath();
|
||||
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
|
||||
g.stroke();
|
||||
}
|
||||
|
||||
renderDot(sx, sy);
|
||||
|
||||
g.beginPath();
|
||||
g.moveTo(sx * TS, sy * TS);
|
||||
rest.forEach(([x, y]) => {
|
||||
g.lineTo(x * TS, y * TS);
|
||||
});
|
||||
g.stroke();
|
||||
|
||||
const [lx, ly] = wire[wire.length - 1];
|
||||
|
||||
renderDot(lx, ly);
|
||||
|
||||
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
|
||||
if (y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g.fillStyle = BACKGROUND;
|
||||
g.beginPath();
|
||||
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
});
|
||||
});
|
||||
|
||||
if (state.nextWire) {
|
||||
const wirePart = state.nextWire.slice(0, state.nextWireIndex);
|
||||
const wire = [
|
||||
...wirePart.slice(0, wirePart.length - 1),
|
||||
lerpPoint(
|
||||
wirePart[wirePart.length - 2],
|
||||
wirePart[wirePart.length - 1],
|
||||
state.t
|
||||
)
|
||||
];
|
||||
const [[sx, sy], ...rest] = wire;
|
||||
|
||||
function renderDot(x, y) {
|
||||
if (y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g.beginPath();
|
||||
g.ellipse(x * TS, y * TS, TS / 5, TS / 5, 0, 0, Math.PI * 2);
|
||||
g.stroke();
|
||||
}
|
||||
|
||||
renderDot(sx, sy);
|
||||
|
||||
g.beginPath();
|
||||
g.moveTo(sx * TS, sy * TS);
|
||||
rest.forEach(([x, y]) => {
|
||||
g.lineTo(x * TS, y * TS);
|
||||
});
|
||||
g.stroke();
|
||||
|
||||
const [lx, ly] = wire[wire.length - 1];
|
||||
|
||||
renderDot(lx, ly);
|
||||
|
||||
[wire[0], wire[wire.length - 1]].forEach(([x, y]) => {
|
||||
if (y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g.fillStyle = BACKGROUND;
|
||||
g.beginPath();
|
||||
g.ellipse(x * TS, y * TS, TS / 5 - 1, TS / 5 - 1, 0, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
});
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
// g.translate(0.5, 0.5);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// $canvas.classList.remove('hide');
|
||||
|
||||
render();
|
||||
|
||||
let i = 20;
|
||||
while (i > 0) {
|
||||
const wire = generateWire();
|
||||
|
||||
if (wire) {
|
||||
state.wires.push(wire);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
update();
|
||||
}, 1000 / 30);
|
||||
})
|
||||
|
After Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
||||
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 344 B |
@@ -0,0 +1,945 @@
|
||||
/* TODO: Don't use CDN and serve as static files */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #eaeaea;
|
||||
--fg: #333;
|
||||
|
||||
--bg-lighter: #f0f0f0;
|
||||
|
||||
--bg-dark: hsl(220, 5%, 93%);
|
||||
--bg-darker: hsl(220, 5%, 90%);
|
||||
--bg-darker-2: #d5d5d5;
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--bg-darker-3: #c0c0c0;
|
||||
--bg-darker-4: #b8b8b8;
|
||||
|
||||
--accent-1: #278542;
|
||||
--accent-1-fg: #154d24;
|
||||
|
||||
--card-date: #666;
|
||||
--card-content: #222;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #00000018;
|
||||
|
||||
--text-input-bg: #fff;
|
||||
--text-input-readonly-bg: #e4e4e4;
|
||||
--text-input-readonly-fg: #777;
|
||||
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 70ch;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 8rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
color: var(--accent-1-fg);
|
||||
}
|
||||
|
||||
/* Tutti i link dentro la navbar sono speciali e non sembrano link */
|
||||
nav .nav-element {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--accent-1-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav .nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
transition: transform 150ms ease-in-out;
|
||||
}
|
||||
|
||||
nav .nav-logo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
nav .nav-logo img {
|
||||
width: 150px;
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
nav .nav-main {
|
||||
display: flex;
|
||||
/* grid-template-columns: repeat(5, 1fr) 1fr auto 1fr; */
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-item.filler {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element {
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-button {
|
||||
margin: 0 0.5rem;
|
||||
/* padding: 0.5rem; */
|
||||
|
||||
aspect-ratio: 1;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
/* background: var(--bg-darker-2); */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-radius: 1rem;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
user-select: none;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-element:hover,
|
||||
nav .nav-main .nav-button:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown {
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items .nav-item:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: var(--bg-dark);
|
||||
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
nav .nav-main .nav-item.dropdown .nav-items {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: none;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
/*
|
||||
nav .nav-main .nav-item.dropdown .nav-items::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: -2px;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
|
||||
background: var(--bg-darker);
|
||||
} */
|
||||
|
||||
nav .nav-main .nav-item.dropdown .name:hover + .nav-items,
|
||||
nav .nav-main .nav-item.dropdown .name + .nav-items:hover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* .nav-main borders */
|
||||
.nav-main > .nav-item:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.nav-main > .nav-item:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 3rem;
|
||||
|
||||
width: 80ch;
|
||||
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
border-bottom: none;
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
max-width: 60ch;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card .title {
|
||||
font-size: 22px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
font-weight: var(--font-weight-light);
|
||||
color: var(--card-content);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0 0.5rem;
|
||||
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tags .tag {
|
||||
height: 1.5rem;
|
||||
border-radius: calc(1.5rem / 2);
|
||||
padding: 0 calc(1.5rem / 2);
|
||||
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--card-date);
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
width: 70ch;
|
||||
max-width: 100%;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 50ch;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
background: var(--bg-lighter);
|
||||
border: 1px solid #cbcbcb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px 0 #00000033;
|
||||
|
||||
font-size: 90%;
|
||||
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
a:not(.button) {
|
||||
color: var(--accent-1-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not(.button):hover {
|
||||
color: var(--accent-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 17px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 1rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
background: var(--bg-darker-2-1);
|
||||
box-shadow: 0 4px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border: 1px solid var(--accent-2-darker);
|
||||
background: var(--accent-2);
|
||||
color: var(--accent-2-darkest);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--accent-2-lighter);
|
||||
}
|
||||
|
||||
button.icon {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
select {
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 16px;
|
||||
|
||||
/* gray variant #b3b3b3 */
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
/* gray variant #bfbfbf */
|
||||
background: var(--bg-darker-2);
|
||||
/* gray variant #333333 */
|
||||
color: var(--fg);
|
||||
|
||||
height: 2rem;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
padding: 0 0.25rem;
|
||||
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Text Fields */
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'] {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
background: var(--text-input-bg);
|
||||
color: var(--fg);
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-size: 17px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
input[type='password'] {
|
||||
font-family: caption;
|
||||
}
|
||||
|
||||
input[type='text']:read-only,
|
||||
input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
input[type='text'].error,
|
||||
input[type='password'].error {
|
||||
background: #faa;
|
||||
color: #311;
|
||||
}
|
||||
|
||||
/* Compound Controls */
|
||||
|
||||
.compound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 8px 0 #00000022;
|
||||
|
||||
border: 1px solid var(--bg-darker-3);
|
||||
background: var(--bg-darker-2);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.compound > .divider {
|
||||
height: 2rem;
|
||||
width: 1px;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.compound .icon {
|
||||
width: 2.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.compound > select {
|
||||
background: none;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.compound > button,
|
||||
.compound > .button,
|
||||
.compound > input,
|
||||
.compound > select {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.compound > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.compound > :not(:last-child) {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
form .field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form .field-set {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-auto-rows: auto;
|
||||
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
form .field-set .fill {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
form .field-set label {
|
||||
grid-column: 1 / 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
form .field-set input {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
|
||||
.page-home {
|
||||
/* TODO: Sarebbe meglio se si riuscisse a capire come farlo senza */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-home .nav-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-home .super {
|
||||
position: absolute;
|
||||
left: 50vw;
|
||||
top: 50vh;
|
||||
|
||||
width: 90vw;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 2vmin;
|
||||
}
|
||||
|
||||
.page-home canvas {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
|
||||
transition: opacity 1000ms ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.page-home canvas.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-home .super .block.text {
|
||||
max-width: 40ch;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.page-home .super .block.text h1 {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-home .super .block.text p {
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.page-home .super .block.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-home .super .block.image img {
|
||||
max-width: 80ch;
|
||||
max-height: 50vh;
|
||||
filter: drop-shadow(0 0 64px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 8px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.page-home .main {
|
||||
padding-top: calc(100vh - 6rem);
|
||||
}
|
||||
|
||||
.page-utenti .user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-utenti .user-item .icon {
|
||||
width: 1.75rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.page-utenti .spinner {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 5rem;
|
||||
|
||||
color: var(--fg);
|
||||
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.page-storia .history-container {
|
||||
--bar-size: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
margin: 5rem 0;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -2rem;
|
||||
width: var(--bar-size);
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .timeline-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
top: -3rem;
|
||||
bottom: -3rem;
|
||||
left: 0;
|
||||
border-left: var(--bar-size) dashed var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event::before {
|
||||
--size: 1rem;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(-2rem + var(--bar-size) / 2 - var(--size) / 2);
|
||||
transform: translate(0, 1rem);
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
border-radius: 100%;
|
||||
|
||||
background: var(--bg-darker-4);
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .title {
|
||||
font-size: 22px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .event .date {
|
||||
color: var(--card-date);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-storia .history-container .events .spacer {
|
||||
height: calc(var(--size) * 1rem);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 2rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search input[type='text'] {
|
||||
width: 50ch;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
/* Rendered Markdown */
|
||||
|
||||
.news-content p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-content h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.news-content img {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 2px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
.news-content .date {
|
||||
font-size: 15px;
|
||||
color: var(--card-date);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content .tags,
|
||||
.news-content .date {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-content table {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
/* Math */
|
||||
|
||||
.katex-display {
|
||||
margin: 1rem 0;
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
table td:not(:first-child),
|
||||
table th:not(:first-child) {
|
||||
border-left: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table td {
|
||||
border-top: 1px solid var(--bg-darker-3);
|
||||
}
|
||||
|
||||
table tbody tr:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
body.dark-mode {
|
||||
--bg: #282828;
|
||||
--fg: #a6cc92;
|
||||
/* --fg: #6eac4d; */
|
||||
/* Magari questo: */
|
||||
/* --fg: #a3b09c; */
|
||||
|
||||
--bg-dark: hsl(10, 10%, 20%);
|
||||
--bg-darker: hsl(10, 10%, 17%);
|
||||
--bg-darker-2: #1d2021;
|
||||
--bg-darker-3: #101111;
|
||||
|
||||
--accent-1: #154d24;
|
||||
--accent-1-fg: #278542;
|
||||
|
||||
--card-date: #928374;
|
||||
--card-content: #d4be98;
|
||||
|
||||
--font-sf: 'Inter', sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--shadow-1: 0 0 16px 0 #16182077;
|
||||
|
||||
--text-input-bg: var(--bg-darker);
|
||||
--text-input-readonly-bg: hsl(10, 10%, 22%);
|
||||
--text-input-readonly-fg: hsl(10, 10%, 40%);
|
||||
|
||||
--bg-darker-2-1: #c8c8c8;
|
||||
--accent-2-lighter: #5cc969;
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
}
|
||||
|
||||
.dark-mode button {
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
background: var(--bg-darker);
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
.dark-mode button:hover {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.dark-mode button.primary {
|
||||
border: 1px solid #113a1c;
|
||||
background: #1e6732;
|
||||
color: #b7e3c3;
|
||||
}
|
||||
|
||||
.dark-mode button.primary:hover {
|
||||
background: #23773a;
|
||||
}
|
||||
|
||||
.dark-mode nav .nav-logo img {
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.dark-mode input[type] {
|
||||
background: #4b4342;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text']:read-only,
|
||||
.dark-mode input[type='password']:read-only {
|
||||
background: var(--text-input-readonly-bg);
|
||||
color: var(--text-input-readonly-fg);
|
||||
box-shadow: 0 0 8px 0 #00000010;
|
||||
}
|
||||
|
||||
.dark-mode input[type='text'].error,
|
||||
.dark-mode input[type='password'].error {
|
||||
background: #633;
|
||||
color: #faa;
|
||||
}
|
||||
|
||||
.dark-mode pre {
|
||||
background: var(--bg-darker);
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
/* Trick molto molto malvagio per non dover rendere il colorscheme dei blocchi di codice dinamici rispetto alla dark mode */
|
||||
.dark-mode pre > code > span {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.dark-mode table td,
|
||||
.dark-mode table th {
|
||||
border-color: var(--fg);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 295 KiB |
@@ -1,28 +0,0 @@
|
||||
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!,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* image?: string,
|
||||
* course?: string,
|
||||
* title?: string,
|
||||
* author: string,
|
||||
* courseYear: string
|
||||
* }} AppuntiCardProps
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AppuntiCardProps} param0
|
||||
* @returns
|
||||
*/
|
||||
export const AppuntiCard = ({ course, title, author, courseYear }) => {
|
||||
return (
|
||||
<div class="appunti-item">
|
||||
<div class="thumbnail"></div>
|
||||
{title && <div class="title">{title}</div>}
|
||||
{course && <div class="course">{course}</div>}
|
||||
<div class="author">@{author}</div>
|
||||
<div class="course-year">{courseYear}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const AppuntiList = ({ children }) => {
|
||||
return <div class="appunti-list">{children}</div>
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type ComponentChildren } from 'preact'
|
||||
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||
import { clsx, isMobile } from './lib/util'
|
||||
import { PhosphorIcon } from './Icon'
|
||||
|
||||
export const ComboBox = ({
|
||||
value,
|
||||
setValue,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
setValue: (s: string) => void
|
||||
children: Record<string, ComponentChildren>
|
||||
}) => {
|
||||
const [cloak, setCloak] = useState(true)
|
||||
const [open, setOpen] = useState(true)
|
||||
const comboRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
const [itemWidth, setItemWidth] = useState<number>(200)
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false)
|
||||
setCloak(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div class="combobox" ref={comboRef} style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}>
|
||||
<div class="selected" onClick={() => setOpen(!open)}>
|
||||
<div class="content">{children[value]}</div>
|
||||
{/* <span class="material-symbols-outlined">expand_more</span> */}
|
||||
<PhosphorIcon name="caret-down" />
|
||||
</div>
|
||||
{open && (
|
||||
<div class={clsx('dropdown', cloak && 'invisible')} ref={el => el && setItemWidth(el.offsetWidth)}>
|
||||
{Object.keys(children).map(key => (
|
||||
<div
|
||||
class="option"
|
||||
onClick={() => {
|
||||
setValue(key)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export const Counter = ({}) => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div class="counter">
|
||||
<button onClick={() => setCount(value => value - 1)}>-</button>
|
||||
<div class="value">{count}</div>
|
||||
<button onClick={() => setCount(value => value + 1)}>+</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
const icons = Object.fromEntries(
|
||||
Object.entries(
|
||||
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`, {
|
||||
eager: true,
|
||||
}),
|
||||
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||
)
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const PhosphorIcon = ({ name }: Props) => {
|
||||
const icon = icons[name]
|
||||
|
||||
if (!icon) {
|
||||
throw new Error(`Icon "${name}" not found`)
|
||||
}
|
||||
|
||||
return <img class="phosphor-icon" src={icon.default.src} alt={name} />
|
||||
}
|
||||
@@ -1,766 +0,0 @@
|
||||
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
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||
import type { JSX } from 'preact/jsx-runtime'
|
||||
|
||||
export const ShowMore = <T extends any>({
|
||||
items,
|
||||
pageSize,
|
||||
children,
|
||||
}: {
|
||||
items: ReadonlySignal<T[]>
|
||||
pageSize: number
|
||||
children: (item: T) => JSX.Element
|
||||
}) => {
|
||||
const $shownItems = useSignal(pageSize)
|
||||
|
||||
const $paginatedItems = useComputed(() => {
|
||||
return items.value.slice(0, $shownItems.value)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{$paginatedItems.value.map(children)}
|
||||
<div class="show-more">
|
||||
{$shownItems.value < items.value.length && (
|
||||
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import { useComputed, useSignal } from '@preact/signals'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { ShowMore } from './Paginate'
|
||||
import { ComboBox } from './ComboBox'
|
||||
import { PhosphorIcon } from './Icon'
|
||||
|
||||
type User = {
|
||||
uid: string
|
||||
gecos: string
|
||||
}
|
||||
|
||||
const FILTERS = {
|
||||
utenti: {
|
||||
icon: 'user',
|
||||
label: 'Utenti',
|
||||
},
|
||||
macchinisti: {
|
||||
icon: 'wrench',
|
||||
label: 'Macchinisti',
|
||||
},
|
||||
rappstud: {
|
||||
icon: 'bank',
|
||||
label: 'Rappresentanti',
|
||||
},
|
||||
}
|
||||
|
||||
function applyPatches(users: User[]) {
|
||||
users.forEach(user => {
|
||||
// strip ",+" from the end of the gecos field
|
||||
user.gecos = user.gecos.replace(/,+$/, '')
|
||||
|
||||
// capitalize the first letter of each word
|
||||
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
|
||||
})
|
||||
|
||||
// reverse the order of the users
|
||||
users.reverse()
|
||||
}
|
||||
|
||||
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
|
||||
|
||||
const RAPPSTUD = [
|
||||
'smannella',
|
||||
'lotti',
|
||||
'rotolo',
|
||||
'saccani',
|
||||
'carbone',
|
||||
'mburatti',
|
||||
'ppuddu',
|
||||
'marinari',
|
||||
'evsilvestri',
|
||||
'tateo',
|
||||
'graccione',
|
||||
'dilella',
|
||||
'rocca',
|
||||
'odetti',
|
||||
'borso',
|
||||
'numero',
|
||||
]
|
||||
|
||||
export const UtentiPage = () => {
|
||||
const $utentiData = useSignal<User[]>([])
|
||||
|
||||
const $filter = useSignal('utenti')
|
||||
|
||||
const $filteredData = useComputed(() =>
|
||||
$filter.value === 'macchinisti'
|
||||
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
|
||||
: $filter.value === 'rappstud'
|
||||
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
|
||||
: $utentiData.value,
|
||||
)
|
||||
|
||||
const $fuse = useComputed(
|
||||
() =>
|
||||
new Fuse($filteredData.value, {
|
||||
keys: ['gecos', 'uid'],
|
||||
}),
|
||||
)
|
||||
|
||||
const $searchText = useSignal('')
|
||||
const $searchResults = useComputed(() =>
|
||||
$searchText.value.trim().length > 0
|
||||
? ($fuse.value?.search($searchText.value).map(result => result.item) ?? [])
|
||||
: $filteredData.value,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
applyPatches(data)
|
||||
|
||||
$utentiData.value = data
|
||||
|
||||
$fuse.value.setCollection(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="search-bar">
|
||||
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
|
||||
{Object.fromEntries(
|
||||
Object.entries(FILTERS).map(([k, v]) => [
|
||||
k,
|
||||
<>
|
||||
{/* <span class="material-symbols-outlined">{v.icon}</span> {v.label} */}
|
||||
<PhosphorIcon name={v.icon} />
|
||||
{v.label}
|
||||
</>,
|
||||
]),
|
||||
)}
|
||||
</ComboBox>
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cerca un utente Poisson..."
|
||||
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||
value={$searchText.value}
|
||||
/>
|
||||
{/* <span class="material-symbols-outlined">search</span> */}
|
||||
<PhosphorIcon name="magnifying-glass" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results">
|
||||
{$searchResults.value ? (
|
||||
<ShowMore items={$searchResults} pageSize={100}>
|
||||
{poissonUser => (
|
||||
<div class="search-result">
|
||||
<div class="icon">
|
||||
{/* <span class="material-symbols-outlined">
|
||||
{RAPPSTUD.includes(poissonUser.uid)
|
||||
? 'account_balance'
|
||||
: MACCHINISTI.includes(poissonUser.uid)
|
||||
? 'construction'
|
||||
: 'person'}
|
||||
</span> */}
|
||||
<PhosphorIcon
|
||||
name={
|
||||
RAPPSTUD.includes(poissonUser.uid)
|
||||
? 'bank'
|
||||
: MACCHINISTI.includes(poissonUser.uid)
|
||||
? 'wrench'
|
||||
: 'user'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="text">{poissonUser.gecos}</div>
|
||||
<div class="right">
|
||||
<a href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`} target="_blank">
|
||||
{/* <span class="material-symbols-outlined">open_in_new</span> */}
|
||||
<PhosphorIcon name="arrow-square-out" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ShowMore>
|
||||
) : (
|
||||
<>Nessun risultato</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createAuthClient } from 'better-auth/client'
|
||||
import { genericOAuthClient } from 'better-auth/client/plugins'
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [genericOAuthClient()],
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
const $debugConsole = document.createElement('div')
|
||||
|
||||
$debugConsole.style.position = 'fixed'
|
||||
$debugConsole.style.bottom = '0'
|
||||
$debugConsole.style.left = '0'
|
||||
$debugConsole.style.width = '100%'
|
||||
$debugConsole.style.height = '25vh'
|
||||
$debugConsole.style.backgroundColor = 'black'
|
||||
$debugConsole.style.color = 'white'
|
||||
$debugConsole.style.overflow = 'auto'
|
||||
$debugConsole.style.padding = '10px'
|
||||
$debugConsole.style.boxSizing = 'border-box'
|
||||
$debugConsole.style.fontFamily = 'monospace'
|
||||
$debugConsole.style.zIndex = '9999'
|
||||
$debugConsole.style.fontSize = '15px'
|
||||
$debugConsole.style.opacity = '0.8'
|
||||
|
||||
document.body.appendChild($debugConsole)
|
||||
|
||||
function logDebugConsole(...args) {
|
||||
$debugConsole.innerHTML += args.join(' ') + '<br>'
|
||||
}
|
||||
|
||||
console.error = logDebugConsole
|
||||
console.warn = logDebugConsole
|
||||
console.log = logDebugConsole
|
||||
console.debug = logDebugConsole
|
||||
@@ -1,83 +0,0 @@
|
||||
// took from: https://github.com/sxyazi/marked-extended-latex
|
||||
// this has a peer dependency bug
|
||||
|
||||
const CLASS_NAME = 'latex-b172fea480b'
|
||||
|
||||
const extBlock = options => ({
|
||||
name: 'latex-block',
|
||||
level: 'block',
|
||||
start(src) {
|
||||
return src.match(/\$\$[^\$]/)?.index ?? -1
|
||||
},
|
||||
tokenizer(src, tokens) {
|
||||
const match = /^\$\$([^\$]+)\$\$/.exec(src)
|
||||
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
|
||||
},
|
||||
renderer(token) {
|
||||
if (!options.lazy) return options.render(token.formula, true)
|
||||
return `<span class="${CLASS_NAME}" block>${token.formula}</span>`
|
||||
},
|
||||
})
|
||||
|
||||
const extInline = options => ({
|
||||
name: 'latex',
|
||||
level: 'inline',
|
||||
start(src) {
|
||||
return src.match(/\$[^\$]/)?.index ?? -1
|
||||
},
|
||||
tokenizer(src, tokens) {
|
||||
const match = /^\$([^\$]+)\$/.exec(src)
|
||||
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
|
||||
},
|
||||
renderer(token) {
|
||||
if (!options.lazy) return options.render(token.formula, false)
|
||||
return `<span class="${CLASS_NAME}">${token.formula}</span>`
|
||||
},
|
||||
})
|
||||
|
||||
let observer
|
||||
/* istanbul ignore next */
|
||||
export default (options = {}) => {
|
||||
/* istanbul ignore next */
|
||||
if (options.lazy && options.env !== 'test') {
|
||||
observer = new IntersectionObserver(
|
||||
(entries, self) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) {
|
||||
continue
|
||||
}
|
||||
|
||||
const span = entry.target
|
||||
self.unobserve(span)
|
||||
|
||||
Promise.resolve(options.render(span.innerText, span.hasAttribute('block'))).then(html => {
|
||||
span.innerHTML = html
|
||||
})
|
||||
span.classList.add('latex-rendered')
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
extensions: [extBlock(options), extInline(options)],
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const observe = () => {
|
||||
if (!observer) {
|
||||
return
|
||||
}
|
||||
|
||||
observer.disconnect()
|
||||
document.querySelectorAll(`span.${CLASS_NAME}:not(.latex-rendered)`).forEach(span => {
|
||||
observer.observe(span)
|
||||
})
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const disconnect = () => {
|
||||
observer?.disconnect()
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
export const trottleDebounce = <T extends any[], R>(
|
||||
fn: (...args: T) => R,
|
||||
delay: number,
|
||||
options: { leading?: boolean; trailing?: boolean } = {},
|
||||
): ((...args: T) => R | undefined) => {
|
||||
let lastCall = 0
|
||||
let lastResult: R | undefined
|
||||
let lastArgs: T | undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
|
||||
const leading = options.leading ?? true
|
||||
const trailing = options.trailing ?? true
|
||||
|
||||
return (...args: T): R | undefined => {
|
||||
lastArgs = args
|
||||
if (leading && Date.now() - lastCall >= delay) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...args)
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
if (trailing && lastArgs) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...lastArgs)
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
return lastResult
|
||||
}
|
||||
}
|
||||
|
||||
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
|
||||
|
||||
export function clsx(...args: ClassValue[]): string {
|
||||
return args
|
||||
.flatMap(arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return arg
|
||||
} else if (Array.isArray(arg)) {
|
||||
return clsx(...arg)
|
||||
} else if (typeof arg === 'boolean') {
|
||||
return []
|
||||
} else if (typeof arg === 'object') {
|
||||
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const isMobile = () => {
|
||||
const [windowWidth, setWindowWidth] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setWindowWidth(window.innerWidth)
|
||||
|
||||
const handleResize = () => setWindowWidth(window.innerWidth)
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
return windowWidth < 1024
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
import PhosphorIcon from './PhosphorIcon.astro'
|
||||
|
||||
const ICONS_MAP: Record<string, string> = {
|
||||
github: 'github-logo',
|
||||
linkedin: 'linkedin-logo',
|
||||
website: 'globe',
|
||||
mail: 'mailbox',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
fullName: string
|
||||
description: string
|
||||
|
||||
image: ImageMetadata
|
||||
|
||||
entranceDate: number
|
||||
exitDate?: number
|
||||
|
||||
founder?: boolean
|
||||
|
||||
social?: {
|
||||
github?: string
|
||||
linkedin?: string
|
||||
website?: string
|
||||
mail?: string
|
||||
}
|
||||
}
|
||||
|
||||
const { fullName, description, image, entranceDate, exitDate, founder, social } = Astro.props
|
||||
---
|
||||
|
||||
<div class="bubble">
|
||||
<img src={image.src} alt={fullName.toLowerCase()} />
|
||||
<div class="title">{fullName}</div>
|
||||
<div class="date">{entranceDate}—{exitDate ?? 'Presente'}</div>
|
||||
{founder && <div class="founder">Fondatore</div>}
|
||||
<div class="description">{description}</div>
|
||||
{
|
||||
social && (
|
||||
<div class="social">
|
||||
{Object.entries(social).map(([key, value]) => (
|
||||
<a href={value} target="_blank" rel="noopener noreferrer">
|
||||
<PhosphorIcon name={ICONS_MAP[key] ?? 'question-mark'} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
type Props = {
|
||||
large?: boolean
|
||||
style?: string
|
||||
}
|
||||
|
||||
const { large, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<div class:list={['card', large && 'large']} {...props}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
<footer>
|
||||
<div class="text">
|
||||
<p>
|
||||
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
||||
>macchinisti@lists.dm.unipi.it</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
const links = [
|
||||
{ href: '/utenti', text: 'Utenti' },
|
||||
// { href: '/macchinisti', text: 'Macchinisti' },
|
||||
// { href: '/appunti', text: 'Appunti' },
|
||||
{ href: '/notizie', text: 'Notizie' },
|
||||
{ href: '/guide', text: 'Guide' },
|
||||
{ href: '/domande-esami', text: 'Domande Orali' },
|
||||
{ href: '/media-pesata', text: 'Calcolo Media' }, // Beta testing - solo URL diretto
|
||||
{ href: '/storia', text: 'Storia' },
|
||||
// { href: '/login', text: 'Login' },
|
||||
]
|
||||
---
|
||||
|
||||
<header>
|
||||
<!-- main logo on the left -->
|
||||
<a href="/" class="logo">
|
||||
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
|
||||
</a>
|
||||
|
||||
<!-- hidden checkbox for mobile js-less sidebar interaction -->
|
||||
<input type="checkbox" id="header-menu-toggle" />
|
||||
|
||||
<!-- desktop navbar links -->
|
||||
<div class="links desktop-only">
|
||||
{
|
||||
links.map(link => (
|
||||
<a role="button" href={link.href}>
|
||||
{link.text}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- sidebar menu for mobile -->
|
||||
<div class="mobile-only">
|
||||
<label id="header-menu-toggle-menu" role="button" class="flat icon" for="header-menu-toggle">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
</label>
|
||||
<label id="header-menu-toggle-close" role="button" class="flat icon" for="header-menu-toggle">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- sidebar menu only visible on mobile when #header-menu-toggle is checked -->
|
||||
<div class="side-menu">
|
||||
<div class="links">
|
||||
{
|
||||
links.map(link => (
|
||||
<a role="button" href={link.href}>
|
||||
{link.text}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
type Props = {
|
||||
color: string
|
||||
}
|
||||
|
||||
const { color } = Astro.props
|
||||
|
||||
const patternId = 'zig-zag-' + color.slice(1)
|
||||
---
|
||||
|
||||
<div class="zig-zag">
|
||||
<svg
|
||||
width="100%"
|
||||
height="2rem"
|
||||
viewBox="0 0 1 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
>
|
||||
<defs>
|
||||
<pattern id={patternId} x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||
<path fill={color} d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill={`url(#${patternId})`} x="0" y="0" width="1000" height="1"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const { name } = Astro.props
|
||||
|
||||
const icons = Object.fromEntries(
|
||||
Object.entries(
|
||||
import.meta.glob<{ default: ImageMetadata }>(`node_modules/@phosphor-icons/core/assets/light/*.svg`),
|
||||
).map(([path, module]) => [path.split('/').pop()!.split('.')[0].replace('-light', ''), module]),
|
||||
)
|
||||
|
||||
if (!icons[name]) {
|
||||
throw new Error(`Icon "${name}" not found`)
|
||||
}
|
||||
---
|
||||
|
||||
<Image class="phosphor-icon" src={icons[name]()} alt={name} />
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
// Card.astro
|
||||
export interface Props {
|
||||
title: string
|
||||
href: string
|
||||
|
||||
imgSrc?: string
|
||||
style?: string
|
||||
}
|
||||
|
||||
const { href, imgSrc, style, title } = Astro.props
|
||||
---
|
||||
|
||||
<a target="_blank" href={href} style={style}>
|
||||
<div class="project">
|
||||
<div class="image">
|
||||
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
|
||||
</div>
|
||||
<div class="title">{title}</div>
|
||||
<div class="description text">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
const { year, title } = Astro.props
|
||||
---
|
||||
|
||||
<div class="timeline-item">
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<div class="title">{year} • {title}</div>
|
||||
<div class="text">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
import { JSDOM } from 'jsdom'
|
||||
import Container from './Container.astro'
|
||||
|
||||
const language = Astro.props['data-language'] ?? 'text'
|
||||
|
||||
const html = await Astro.slots.render('default')
|
||||
|
||||
const rawCode = new JSDOM(html).window.document.body.textContent
|
||||
---
|
||||
|
||||
<pre {...Astro.props}><slot /></pre>
|
||||
|
||||
{language === 'astro' && <Container set:html={rawCode} />}
|
||||