Compare commits
No commits in common. 'main-old' and 'main' have entirely different histories.
@ -1,3 +0,0 @@
|
|||||||
Dockerfile
|
|
||||||
node_modules
|
|
||||||
.git
|
|
@ -0,0 +1,50 @@
|
|||||||
|
# This file defines a Drone pipeline that builds a static website with "npm run build". This
|
||||||
|
# pipeline must be marked as "Trusted" in the Drone project settings.
|
||||||
|
#
|
||||||
|
# We mount the target directory of the project at "/var/www/{project}" to the container
|
||||||
|
# "dist/" directory and the run the build. A caveat is that the container builds files
|
||||||
|
# with "root" permissions, so we need to fix those after each build with a second pipeline.
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: deploy
|
||||||
|
image: node:latest
|
||||||
|
volumes:
|
||||||
|
- name: host-website-dist
|
||||||
|
path: /mnt/website
|
||||||
|
commands:
|
||||||
|
- npm install
|
||||||
|
- npm run build
|
||||||
|
- cp -rT ./dist /mnt/website
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: host-website-dist
|
||||||
|
host: # this volume is mounted on the host machine
|
||||||
|
path: /var/www/website
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: exec # this job is executed on the host machine
|
||||||
|
name: caddy-permissions
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: chown
|
||||||
|
commands:
|
||||||
|
- chown -R caddy:caddy /var/www/website
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
@ -1,15 +0,0 @@
|
|||||||
# 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,19 +1,23 @@
|
|||||||
# Environment files
|
# build output
|
||||||
.env
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
# Miscellaneous
|
# dependencies
|
||||||
tags
|
node_modules/
|
||||||
|
|
||||||
# Local files
|
# logs
|
||||||
*.local*
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
# NodeJS
|
# local data
|
||||||
dist/
|
*.local*
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Don't version generated files
|
# environment variables
|
||||||
public/css/
|
.env
|
||||||
public/js/
|
.env.production
|
||||||
|
|
||||||
# Executables
|
# macOS-specific files
|
||||||
phc-website-server
|
.DS_Store
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 110,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"npm.packageManager": "bun"
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
FROM golang:1.18-alpine AS backend-builder
|
|
||||||
WORKDIR /backend
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download -x
|
|
||||||
COPY . .
|
|
||||||
RUN go build -buildvcs=false -o website-server -v .
|
|
||||||
|
|
||||||
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"]
|
|
@ -1,37 +0,0 @@
|
|||||||
|
|
||||||
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,99 +1,42 @@
|
|||||||
# Nuovo Sito PHC
|
# PHC Website
|
||||||
|
|
||||||
Repo del server del nuovo sito per il PHC.
|
Questo è il repository del sito web del PHC. Il sito è costruito utilizzando Astro, un framework statico per la generazione di siti web.
|
||||||
|
|
||||||
## Dipendenze
|
## Installazione
|
||||||
|
|
||||||
- **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
|
```bash
|
||||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
bun install
|
||||||
$ cd frontend/
|
|
||||||
frontend/ $ npm install
|
|
||||||
frontend/ $ cd ..
|
|
||||||
$ make frontend
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Sviluppo
|
||||||
|
|
||||||
### Setup
|
```bash
|
||||||
|
bun dev
|
||||||
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 .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server
|
## Build
|
||||||
|
|
||||||
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
|
|
||||||
|
|
||||||
```bash shell
|
```bash
|
||||||
$ find . -type f -name '*.go' | entr -r go run .
|
bun build
|
||||||
# O anche con fd...
|
|
||||||
$ fd -e go | entr -r go run .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
## Deploy [TODO]
|
||||||
|
|
||||||
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
|
Il progetto contiene un `Dockerfile` che viene usato per il deploy (del server prodotto da Astro).
|
||||||
|
|
||||||
```bash shell
|
```bash
|
||||||
$ make frontend
|
docker build -t phc-website .
|
||||||
# O anche con un watcher...
|
docker run -p 3000:3000 phc-website
|
||||||
$ fd -e js | entr make frontend
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
C'è anche un `.drone.yml` che viene usato per il deploy su un server remoto utilizzando Drone per il CD.
|
||||||
|
|
||||||
- `MODE`
|
|
||||||
|
|
||||||
Può essere `production` (default) o `development`.
|
|
||||||
|
|
||||||
- `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).
|
## Come Contribuire
|
||||||
|
|
||||||
Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`.
|
**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ì.
|
||||||
|
|
||||||
## Altri Servizi
|
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.
|
||||||
|
|
||||||
Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi.
|
### Cose da fare
|
||||||
|
|
||||||
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/)
|
- Aggiungere guide in [src/content/guides/](./src/content/guides/)
|
Before Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 620 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.2 MiB |
@ -1,113 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
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(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import preact from '@astrojs/preact'
|
||||||
|
|
||||||
|
import mdx from '@astrojs/mdx'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'github-light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: [preact(), mdx()],
|
||||||
|
output: 'static'
|
||||||
|
})
|
@ -1,52 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
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,23 +0,0 @@
|
|||||||
|
|
||||||
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)"
|
|
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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()],
|
|
||||||
},
|
|
||||||
])
|
|
@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
},
|
|
||||||
}))
|
|
@ -1,69 +0,0 @@
|
|||||||
/* 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';
|
|
@ -1,78 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
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')
|
|
||||||
},
|
|
||||||
}))
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@ -1,76 +0,0 @@
|
|||||||
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=
|
|
@ -1,208 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
---
|
|
||||||
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?
|
|
||||||
|
|
||||||
![testing](https://picsum.photos/400/300)
|
|
||||||
|
|
||||||
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$ |
|
|
||||||
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
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?
|
|
||||||
|
|
||||||
![testing](https://picsum.photos/200/300)
|
|
||||||
|
|
||||||
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,37 +0,0 @@
|
|||||||
---
|
|
||||||
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?
|
|
||||||
|
|
||||||
![testing](https://picsum.photos/200/300)
|
|
||||||
|
|
||||||
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,44 @@
|
|||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "run-s astro:sync astro:dev",
|
||||||
|
"build": "run-s astro:build",
|
||||||
|
"astro:sync": "astro sync",
|
||||||
|
"astro:dev": "astro dev",
|
||||||
|
"astro:build": "astro check && astro build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/node": "^8.3.4",
|
||||||
|
"@astrojs/preact": "^3.5.3",
|
||||||
|
"@fontsource-variable/material-symbols-outlined": "^5.1.1",
|
||||||
|
"@fontsource/iosevka": "^5.0.11",
|
||||||
|
"@fontsource/mononoki": "^5.0.11",
|
||||||
|
"@fontsource/open-sans": "^5.0.24",
|
||||||
|
"@fontsource/source-code-pro": "^5.0.16",
|
||||||
|
"@fontsource/source-sans-pro": "^5.0.8",
|
||||||
|
"@fontsource/space-mono": "^5.0.20",
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"astro": "^4.15.11",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
|
"preact": "^10.19.6",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/mdx": "^3.1.7",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"jsdom": "^24.1.1",
|
||||||
|
"linkedom": "^0.18.4",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"tsx": "^4.7.1"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
<svg width="1000" height="500" viewBox="0 0 1000 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="80" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="160" y="50" width="150" height="60" fill="#1E6733" />
|
||||||
|
<rect x="140" y="90" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="140" y="200" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="140" y="410" width="10" height="20" fill="#ECC333" />
|
||||||
|
<rect x="140" y="350" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="240" y="110" width="70" height="10" fill="#ECC333" />
|
||||||
|
<rect x="250" y="130" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="340" y="50" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="340" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="590" y="190" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="690" y="180" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="690" y="310" width="60" height="140" fill="#1E6733" />
|
||||||
|
<rect x="690" y="50" width="60" height="120" fill="#1E6733" />
|
||||||
|
<rect x="590" y="320" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="590" y="50" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="420" y="240" width="150" height="60" fill="#1E6733" />
|
||||||
|
<rect x="340" y="320" width="60" height="130" fill="#1E6733" />
|
||||||
|
<rect x="240" y="140" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="350" y="170" width="40" height="10" fill="#ECC333" />
|
||||||
|
<rect x="330" y="330" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="160" y="200" width="80" height="60" fill="#1E6733" />
|
||||||
|
<rect x="650" y="200" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="750" y="330" width="10" height="60" fill="#ECC333" />
|
||||||
|
<rect x="800" y="450" width="40" height="10" fill="#ECC333" />
|
||||||
|
<rect x="850" y="450" width="30" height="10" fill="#ECC333" />
|
||||||
|
<rect x="750" y="90" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="810" y="110" width="60" height="10" fill="#ECC333" />
|
||||||
|
<rect x="580" y="330" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="580" y="60" width="10" height="50" fill="#ECC333" />
|
||||||
|
<rect x="710" y="420" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="710" y="390" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="100" y="280" width="20" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="100" y="260" width="20" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="110" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="350" y="430" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="350" y="410" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="350" y="390" width="40" height="10" fill="#303030" />
|
||||||
|
<rect x="700" y="70" width="20" height="40" fill="#303030" />
|
||||||
|
<rect x="700" y="120" width="20" height="40" fill="#303030" />
|
||||||
|
<rect x="610" y="280" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="600" y="240" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="430" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="280" width="40" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="475" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="475" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="520" y="265" width="10" height="15" fill="#C4C4C4" />
|
||||||
|
<rect x="505" y="280" width="25" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="535" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="445" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="460" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="490" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="490" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="520" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="250" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="265" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="550" y="280" width="10" height="10" fill="#C4C4C4" />
|
||||||
|
<rect x="620" y="210" width="20" height="20" fill="#C4C4C4" />
|
||||||
|
<rect x="370" y="70" width="20" height="30" fill="#303030" />
|
||||||
|
<rect x="370" y="110" width="20" height="30" fill="#303030" />
|
||||||
|
<rect x="870" y="440" width="40" height="10" transform="rotate(-90 870 440)" fill="#303030" />
|
||||||
|
<rect x="890" y="440" width="40" height="10" transform="rotate(-90 890 440)" fill="#303030" />
|
||||||
|
<rect x="810" y="440" width="40" height="10" transform="rotate(-90 810 440)" fill="#303030" />
|
||||||
|
<rect x="790" y="440" width="40" height="10" transform="rotate(-90 790 440)" fill="#303030" />
|
||||||
|
<rect x="270" y="100" width="40" height="10" transform="rotate(-90 270 100)" fill="#303030" />
|
||||||
|
<rect x="290" y="100" width="40" height="10" transform="rotate(-90 290 100)" fill="#303030" />
|
||||||
|
<rect x="190" y="100" width="40" height="10" transform="rotate(-90 190 100)" fill="#303030" />
|
||||||
|
<rect x="170" y="100" width="40" height="10" transform="rotate(-90 170 100)" fill="#303030" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M140 60V170C134.477 170 130 174.477 130 180H90C90 174.477 85.5228 170 80 170V60C85.5228 60 90 55.5228 90 50H130C130 55.5228 134.477 60 140 60Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M130 320H90C90 325.523 85.5229 330 80 330V440C85.5229 440 90 444.477 90 450H130C130 444.477 134.477 440 140 440V330C134.477 330 130 325.523 130 320Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M770 60C775.523 60 780 55.5228 780 50H910C910 55.5228 914.477 60 920 60V100C914.477 100 910 104.477 910 110H780C780 104.477 775.523 100 770 100V60Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M770 400C775.523 400 780 395.523 780 390H910C910 395.523 914.477 400 920 400V440C914.477 440 910 444.477 910 450H780C780 444.477 775.523 440 770 440V400Z"
|
||||||
|
fill="#1E6733" />
|
||||||
|
<rect x="750" y="190" width="10" height="40" fill="#ECC333" />
|
||||||
|
<rect x="750" y="240" width="10" height="20" fill="#ECC333" />
|
||||||
|
<rect x="400" y="200" width="10" height="40" fill="#ECC333" />
|
||||||
|
<rect x="400" y="250" width="10" height="20" fill="#ECC333" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
@ -1,342 +0,0 @@
|
|||||||
|
|
||||||
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);
|
|
||||||
})
|
|
Before Width: | Height: | Size: 790 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="100%" height="2rem" viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag" x="0" y="0" width="2" height="1">
|
||||||
|
<path fill="#C2A8EB" d="M 0,0 L 1,1 L 2,0 L 0,0"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag)" x="0" y="0" width="100%" height="1" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 344 B |
@ -1,945 +0,0 @@
|
|||||||
/* 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);
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* image?: string,
|
||||||
|
* course?: string,
|
||||||
|
* title?: string,
|
||||||
|
* author: string,
|
||||||
|
* courseYear: string
|
||||||
|
* }} AppuntiCardProps
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AppuntiCardProps} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const AppuntiCard = ({ course, title, author, courseYear }) => {
|
||||||
|
return (
|
||||||
|
<div class="appunti-item">
|
||||||
|
<div class="thumbnail"></div>
|
||||||
|
{title && <div class="title">{title}</div>}
|
||||||
|
{course && <div class="course">{course}</div>}
|
||||||
|
<div class="author">@{author}</div>
|
||||||
|
<div class="course-year">{courseYear}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppuntiList = ({ children }) => {
|
||||||
|
return <div class="appunti-list">{children}</div>
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { type ComponentChildren } from 'preact'
|
||||||
|
import { useState, useRef, useEffect } from 'preact/hooks'
|
||||||
|
import { clsx, isMobile } from './lib/util'
|
||||||
|
|
||||||
|
export const ComboBox = ({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
setValue: (s: string) => void
|
||||||
|
children: Record<string, ComponentChildren>
|
||||||
|
}) => {
|
||||||
|
const [cloak, setCloak] = useState(true)
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const comboRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (comboRef.current && !comboRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [itemWidth, setItemWidth] = useState<number>(200)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false)
|
||||||
|
setCloak(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="combobox"
|
||||||
|
ref={comboRef}
|
||||||
|
style={{ width: isMobile() ? undefined : itemWidth + 48 + 'px' }}
|
||||||
|
>
|
||||||
|
<div class="selected" onClick={() => setOpen(!open)}>
|
||||||
|
<div class="content">{children[value]}</div>
|
||||||
|
<span class="material-symbols-outlined">expand_more</span>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
class={clsx('dropdown', cloak && 'invisible')}
|
||||||
|
ref={el => el && setItemWidth(el.offsetWidth)}
|
||||||
|
>
|
||||||
|
{Object.keys(children).map(key => (
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
onClick={() => {
|
||||||
|
setValue(key)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children[key]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const Counter = ({}) => {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="counter">
|
||||||
|
<button onClick={() => setCount(value => value - 1)}>-</button>
|
||||||
|
<div class="value">{count}</div>
|
||||||
|
<button onClick={() => setCount(value => value + 1)}>+</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||||
|
import type { JSX } from 'preact/jsx-runtime'
|
||||||
|
|
||||||
|
export const ShowMore = <T extends any>({
|
||||||
|
items,
|
||||||
|
pageSize,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
items: ReadonlySignal<T[]>
|
||||||
|
pageSize: number
|
||||||
|
children: (item: T) => JSX.Element
|
||||||
|
}) => {
|
||||||
|
const $shownItems = useSignal(pageSize)
|
||||||
|
|
||||||
|
const $paginatedItems = useComputed(() => {
|
||||||
|
return items.value.slice(0, $shownItems.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{$paginatedItems.value.map(children)}
|
||||||
|
<div class="show-more">
|
||||||
|
{$shownItems.value < items.value.length && (
|
||||||
|
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
import { useComputed, useSignal } from '@preact/signals'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
import { ShowMore } from './Paginate'
|
||||||
|
import { ComboBox } from './ComboBox'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
uid: string
|
||||||
|
gecos: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
utenti: {
|
||||||
|
icon: 'person',
|
||||||
|
label: 'Utenti',
|
||||||
|
},
|
||||||
|
macchinisti: {
|
||||||
|
icon: 'construction',
|
||||||
|
label: 'Macchinisti',
|
||||||
|
},
|
||||||
|
rappstud: {
|
||||||
|
icon: 'account_balance',
|
||||||
|
label: 'Rappresentanti',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches(users: User[]) {
|
||||||
|
users.forEach(user => {
|
||||||
|
// strip ",+" from the end of the gecos field
|
||||||
|
user.gecos = user.gecos.replace(/,+$/, '')
|
||||||
|
|
||||||
|
// capitalize the first letter of each word
|
||||||
|
user.gecos = user.gecos.replace(/\b\w/g, c => c.toUpperCase())
|
||||||
|
})
|
||||||
|
|
||||||
|
// reverse the order of the users
|
||||||
|
users.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MACCHINISTI = ['delucreziis', 'minnocci', 'baldino', 'manicastri', 'llombardo', 'serdyuk']
|
||||||
|
|
||||||
|
const RAPPSTUD = [
|
||||||
|
'smannella',
|
||||||
|
'lotti',
|
||||||
|
'rotolo',
|
||||||
|
'saccani',
|
||||||
|
'carbone',
|
||||||
|
'mburatti',
|
||||||
|
'ppuddu',
|
||||||
|
'marinari',
|
||||||
|
'evsilvestri',
|
||||||
|
'tateo',
|
||||||
|
'graccione',
|
||||||
|
'dilella',
|
||||||
|
'rocca',
|
||||||
|
'odetti',
|
||||||
|
'borso',
|
||||||
|
'numero',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UtentiPage = () => {
|
||||||
|
const $utentiData = useSignal<User[]>([])
|
||||||
|
|
||||||
|
const $filter = useSignal('utenti')
|
||||||
|
|
||||||
|
const $filteredData = useComputed(() =>
|
||||||
|
$filter.value === 'macchinisti'
|
||||||
|
? $utentiData.value.filter(user => MACCHINISTI.includes(user.uid))
|
||||||
|
: $filter.value === 'rappstud'
|
||||||
|
? $utentiData.value.filter(user => RAPPSTUD.includes(user.uid))
|
||||||
|
: $utentiData.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const $fuse = useComputed(
|
||||||
|
() =>
|
||||||
|
new Fuse($filteredData.value, {
|
||||||
|
keys: ['gecos', 'uid'],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const $searchText = useSignal('')
|
||||||
|
const $searchResults = useComputed(() =>
|
||||||
|
$searchText.value.trim().length > 0
|
||||||
|
? $fuse.value?.search($searchText.value).map(result => result.item) ?? []
|
||||||
|
: $filteredData.value
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
applyPatches(data)
|
||||||
|
|
||||||
|
$utentiData.value = data
|
||||||
|
|
||||||
|
$fuse.value.setCollection(data)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="search-bar">
|
||||||
|
<ComboBox value={$filter.value} setValue={s => ($filter.value = s)}>
|
||||||
|
{Object.fromEntries(
|
||||||
|
Object.entries(FILTERS).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
<>
|
||||||
|
<span class="material-symbols-outlined">{v.icon}</span> {v.label}
|
||||||
|
</>,
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
</ComboBox>
|
||||||
|
<div class="search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cerca un utente Poisson..."
|
||||||
|
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||||
|
value={$searchText.value}
|
||||||
|
/>
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="search-results">
|
||||||
|
{$searchResults.value ? (
|
||||||
|
<ShowMore items={$searchResults} pageSize={100}>
|
||||||
|
{poissonUser => (
|
||||||
|
<div class="search-result">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">
|
||||||
|
{RAPPSTUD.includes(poissonUser.uid)
|
||||||
|
? 'account_balance'
|
||||||
|
: MACCHINISTI.includes(poissonUser.uid)
|
||||||
|
? 'construction'
|
||||||
|
: 'person'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text">{poissonUser.gecos}</div>
|
||||||
|
<div class="right">
|
||||||
|
<a
|
||||||
|
href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">open_in_new</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ShowMore>
|
||||||
|
) : (
|
||||||
|
<>Nessun risultato</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const trottleDebounce = <T extends any[], R>(
|
||||||
|
fn: (...args: T) => R,
|
||||||
|
delay: number,
|
||||||
|
options: { leading?: boolean; trailing?: boolean } = {}
|
||||||
|
): ((...args: T) => R | undefined) => {
|
||||||
|
let lastCall = 0
|
||||||
|
let lastResult: R | undefined
|
||||||
|
let lastArgs: T | undefined
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
const leading = options.leading ?? true
|
||||||
|
const trailing = options.trailing ?? true
|
||||||
|
|
||||||
|
return (...args: T): R | undefined => {
|
||||||
|
lastArgs = args
|
||||||
|
if (leading && Date.now() - lastCall >= delay) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...args)
|
||||||
|
} else {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (trailing && lastArgs) {
|
||||||
|
lastCall = Date.now()
|
||||||
|
lastResult = fn(...lastArgs)
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
return lastResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClassValue = string | ClassValue[] | Record<string, boolean> | false | undefined
|
||||||
|
|
||||||
|
export function clsx(...args: ClassValue[]): string {
|
||||||
|
return args
|
||||||
|
.flatMap(arg => {
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
return arg
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
return clsx(...arg)
|
||||||
|
} else if (typeof arg === 'boolean') {
|
||||||
|
return []
|
||||||
|
} else if (typeof arg === 'object') {
|
||||||
|
return Object.entries(arg).flatMap(([key, value]) => (value ? key : []))
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMobile = () => {
|
||||||
|
const [windowWidth, setWindowWidth] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
|
||||||
|
const handleResize = () => setWindowWidth(window.innerWidth)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return windowWidth < 1024
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
import type { AstroBuiltinProps } from 'astro'
|
||||||
|
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
large?: boolean
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { large, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['card', large && 'large']} {...props}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
<footer>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
© PHC 2024 • <a href="mailto:macchinisti@lists.dm.unipi.it"
|
||||||
|
>macchinisti@lists.dm.unipi.it</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
@ -0,0 +1,32 @@
|
|||||||
|
<header>
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
|
||||||
|
</a>
|
||||||
|
<input type="checkbox" id="header-menu-toggle" />
|
||||||
|
<div class="links desktop-only">
|
||||||
|
<a role="button" href="/utenti">Utenti</a>
|
||||||
|
<!-- <a role="button" href="/appunti">Appunti</a> -->
|
||||||
|
<a role="button" href="/notizie">Notizie</a>
|
||||||
|
<a role="button" href="/guide">Guide</a>
|
||||||
|
<a role="button" href="/storia">Storia</a>
|
||||||
|
<!-- <a class="primary" role="button" href="/login">Login</a> -->
|
||||||
|
</div>
|
||||||
|
<div class="mobile-only">
|
||||||
|
<label id="header-menu-toggle-menu" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">menu</span>
|
||||||
|
</label>
|
||||||
|
<label id="header-menu-toggle-close" role="button" class="flat icon" for="header-menu-toggle">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="side-menu">
|
||||||
|
<div class="links">
|
||||||
|
<a role="button" href="/utenti">Utenti</a>
|
||||||
|
<!-- <a role="button" href="/appunti">Appunti</a> -->
|
||||||
|
<a role="button" href="/notizie">Notizie</a>
|
||||||
|
<a role="button" href="/guide">Guide</a>
|
||||||
|
<a role="button" href="/storia">Storia</a>
|
||||||
|
<!-- <a class="primary" role="button" href="/login">Login</a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { color } = Astro.props
|
||||||
|
|
||||||
|
const patternId = 'zig-zag-' + color.slice(1)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="2rem"
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={patternId} x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill={color} d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill={`url(#${patternId})`} x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
// Card.astro
|
||||||
|
export interface Props {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
|
||||||
|
imgSrc?: string
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, imgSrc, style, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a target="_blank" href={href} style={style}>
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
{imgSrc ? <img src={imgSrc} alt={'logo for ' + title.toLowerCase()} /> : <div class="box" />}
|
||||||
|
</div>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<div class="description">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
const { year, title } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">{year} • {title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { JSDOM } from 'jsdom'
|
||||||
|
import Container from './Container.astro'
|
||||||
|
|
||||||
|
const language = Astro.props['data-language'] ?? 'text'
|
||||||
|
|
||||||
|
const html = await Astro.slots.render('default')
|
||||||
|
|
||||||
|
const rawCode = new JSDOM(html).window.document.body.textContent
|
||||||
|
---
|
||||||
|
|
||||||
|
<pre {...Astro.props}><slot /></pre>
|
||||||
|
|
||||||
|
{language === 'astro' && <Container set:html={rawCode} />}
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
const { size, ...rest } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['container', size ?? 'normal']} {...rest}>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
colors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { colors } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="palette">
|
||||||
|
{
|
||||||
|
colors.map(value => (
|
||||||
|
<>
|
||||||
|
<div class="color">
|
||||||
|
<div class="region" style={{ backgroundColor: value }} />
|
||||||
|
</div>
|
||||||
|
<div class="label">{value}</div>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
@ -0,0 +1,52 @@
|
|||||||
|
import { z, defineCollection } from 'astro:content'
|
||||||
|
|
||||||
|
// Una notizia ha una data di pubblicazione ma non ha un autore in quanto sono
|
||||||
|
// notizie generiche sul PHC e non sono scritte da un autore specifico
|
||||||
|
const newsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
image: z
|
||||||
|
.object({
|
||||||
|
url: z.string(),
|
||||||
|
alt: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Una guida ha un autore ma non ha una data di pubblicazione in quanto è un
|
||||||
|
// contenuto statico e non è importante sapere quando è stata pubblicata
|
||||||
|
const guidesCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
series: z.string().optional(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Per ora sono su un sito a parte ma prima o poi verranno migrati qui
|
||||||
|
const seminariettiCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
publishDate: z.date(),
|
||||||
|
eventDate: z.date(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Export a single `collections` object to register your collection(s)
|
||||||
|
export const collections = {
|
||||||
|
news: newsCollection,
|
||||||
|
guides: guidesCollection,
|
||||||
|
seminarietti: seminariettiCollection,
|
||||||
|
}
|
@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
id: git-101
|
||||||
|
title: Git 101
|
||||||
|
description: Una guida introduttiva alle basi di Git
|
||||||
|
author: Luca Lombardo
|
||||||
|
tags: [git, gitea]
|
||||||
|
---
|
||||||
|
|
||||||
|
Git è un sistema di controllo di versione distribuito creato per gestire progetti di qualsiasi dimensione, mantenendo traccia delle modifiche al codice sorgente. Questa guida ci accompagnerà dai concetti di base fino alle funzionalità avanzate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Introduzione a Git**
|
||||||
|
|
||||||
|
### **Cos'è Git?**
|
||||||
|
|
||||||
|
- **Sistema di controllo di versione**: Gestisce le modifiche al codice sorgente nel tempo.
|
||||||
|
|
||||||
|
- **Distribuito**: Ogni sviluppatore ha una copia del repository.
|
||||||
|
|
||||||
|
- **Veloce e leggero**: Ottimizzato per la velocità e le prestazioni.
|
||||||
|
|
||||||
|
### **Perché usare Git?**
|
||||||
|
|
||||||
|
- **Tracciabilità**: Ogni modifica è tracciata e reversibile.
|
||||||
|
|
||||||
|
- **Collaborazione**: Più persone possono lavorare sullo stesso progetto.
|
||||||
|
|
||||||
|
- **Backup**: Repository remoto per il backup del codice.
|
||||||
|
|
||||||
|
- **Branching**: Lavoriamo su nuove funzionalità senza influenzare il codice principale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Installazione**
|
||||||
|
|
||||||
|
> Se ci troviamo al dipartimento di matematica a Pisa, è già installato su tutte le macchine dell'aula 3 ed aula 4!
|
||||||
|
|
||||||
|
### **Windows**
|
||||||
|
|
||||||
|
1. Scarichiamo [Git for Windows](https://git-scm.com/download/win).
|
||||||
|
|
||||||
|
2. Seguiamo il wizard di installazione.
|
||||||
|
|
||||||
|
3. Durante l'installazione:
|
||||||
|
|
||||||
|
- Selezioniamo "Git Bash" come terminale.
|
||||||
|
|
||||||
|
- Configuriamo un editor di testo (es. Vim o Nano).
|
||||||
|
|
||||||
|
### **macOS**
|
||||||
|
|
||||||
|
1. Usiamo `brew` per installare Git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install git
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Linux**
|
||||||
|
|
||||||
|
1. Installiamo Git usando il nostro gestore di pacchetti:
|
||||||
|
|
||||||
|
- **Debian/Ubuntu**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install git
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Arch Linux**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S git
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Configurazione iniziale**
|
||||||
|
|
||||||
|
Una volta installato, configuriamo Git con il nostro nome e indirizzo email:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global user.name "Il Nostro Nome"
|
||||||
|
git config --global user.email "nostro@email.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verifica configurazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **4. Concetti fondamentali**
|
||||||
|
|
||||||
|
### **Repository**
|
||||||
|
|
||||||
|
- **Repository locale**: Una cartella sul nostro computer che contiene il nostro progetto.
|
||||||
|
|
||||||
|
- **Repository remoto**: Una versione del progetto ospitata su un server (es. GitHub, GitLab).
|
||||||
|
|
||||||
|
### **Branch**
|
||||||
|
|
||||||
|
Un ramo permette di lavorare su modifiche isolate rispetto al codice principale (branch `main` o `master`).
|
||||||
|
|
||||||
|
### **Commit**
|
||||||
|
|
||||||
|
Una snapshot del nostro codice in un determinato momento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **5. Creazione e gestione di un repository**
|
||||||
|
|
||||||
|
### **Inizializzare un nuovo repository**
|
||||||
|
|
||||||
|
Se stiamo iniziando un nuovo progetto, possiamo creare un nuovo repository con il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Clonare un repository esistente**
|
||||||
|
|
||||||
|
Se invece vogliamo lavorare su un progetto esistente, possiamo clonare da remoto con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **6. Lavorare con Git**
|
||||||
|
|
||||||
|
### **Aggiungere file**
|
||||||
|
|
||||||
|
Aggiungiamo file allo stage per includerli nel prossimo commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <nome-file>
|
||||||
|
# Oppure, per aggiungere tutti i file:
|
||||||
|
git add .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Commit**
|
||||||
|
|
||||||
|
Il comando **`git commit`** è utilizzato per registrare le modifiche nel repository locale. Ogni commit è una snapshot del progetto, contenente tutte le modifiche che sono state aggiunte tramite `git add`.
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. **Aggiungiamo modifiche all'area di staging**:
|
||||||
|
Prima di fare un commit, dobbiamo aggiungere i file che vogliamo includere al prossimo commit usando `git add`. Questo comando prepara i file per essere salvati nella cronologia del repository.
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <nome-file>
|
||||||
|
# Oppure per aggiungere tutti i file modificati
|
||||||
|
git add .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Effettuiamo il commit**:
|
||||||
|
Una volta che i file sono nell'area di staging, possiamo fare un commit. Ogni commit dovrebbe avere un messaggio descrittivo che spieghi cosa è stato cambiato nel progetto.
|
||||||
|
|
||||||
|
Comando per fare un commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "Descrizione chiara del cambiamento"
|
||||||
|
```
|
||||||
|
|
||||||
|
L'opzione `-m` permette di aggiungere il messaggio direttamente dalla linea di comando. Se omettiamo `-m`, Git aprirà un editor di testo per scrivere il messaggio di commit.
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- Git salva lo stato dei file nell'area di staging in un commit, che viene aggiunto alla cronologia del repository locale.
|
||||||
|
|
||||||
|
- Ogni commit ha un identificatore unico (hash) che consente di risalire facilmente alle modifiche in qualsiasi momento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Push**
|
||||||
|
|
||||||
|
**`git push`** è il comando che ci permette di inviare le modifiche dal nostro repository locale a un repository remoto (ad esempio su GitHub, GitLab, Bitbucket, ecc.).
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. Dopo aver fatto uno o più commit locali, dobbiamo inviare queste modifiche al repository remoto.
|
||||||
|
|
||||||
|
2. Per fare questo, usiamo il comando **`git push`** seguito dal nome del remoto (di solito `origin` per il repository remoto di default) e dal nome del branch (di solito `main` o `master`, ma potrebbe essere qualsiasi altro nome di branch che stiamo utilizzando).
|
||||||
|
|
||||||
|
Comando per inviare le modifiche:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- Git confronta il nostro branch locale con il branch remoto. Se ci sono nuovi commit nel branch remoto che non sono ancora nel nostro branch locale, ci verrà richiesto di fare un **pull** per aggiornare prima di fare il push.
|
||||||
|
|
||||||
|
- Il nostro repository locale viene sincronizzato con il remoto, rendendo le modifiche visibili a tutti gli altri che hanno accesso al repository remoto.
|
||||||
|
|
||||||
|
#### Errori comuni:
|
||||||
|
|
||||||
|
- Se il repository remoto è stato aggiornato nel frattempo da qualcun altro (ad esempio, con un altro push), riceveremo un errore che ci avvisa che dobbiamo fare prima un `git pull` per sincronizzare il nostro lavoro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Pull**
|
||||||
|
|
||||||
|
**`git pull`** è il comando che ci permette di scaricare e integrare le modifiche dal repository remoto al nostro repository locale. È una combinazione di due comandi: **`git fetch`** (scarica i cambiamenti dal remoto) e **`git merge`** (integra questi cambiamenti nel nostro branch attuale).
|
||||||
|
|
||||||
|
#### Come funziona:
|
||||||
|
|
||||||
|
1. Se altri collaboratori hanno fatto modifiche al repository remoto, possiamo ottenere queste modifiche con **`git pull`**. Questo comando aggiorna il nostro branch locale con le modifiche più recenti dal repository remoto.
|
||||||
|
|
||||||
|
2. Eseguiamo il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo caso, `origin` è il nome del repository remoto (il nome predefinito quando cloni un repository), e `main` è il branch che vogliamo aggiornare.
|
||||||
|
|
||||||
|
#### Cosa succede dietro le quinte:
|
||||||
|
|
||||||
|
- **`git fetch`** scarica tutte le modifiche dal repository remoto, ma non le integra ancora nel nostro codice.
|
||||||
|
|
||||||
|
- **`git merge`** unisce le modifiche scaricate al nostro branch attuale, risolvendo eventuali conflitti, se necessario.
|
||||||
|
|
||||||
|
#### Errori comuni:
|
||||||
|
|
||||||
|
- Se ci sono conflitti tra il nostro lavoro e quello degli altri, Git ci avviserà che dovremo risolverli manualmente. Dopo aver risolto i conflitti, dovremo aggiungere i file risolti (`git add`) e completare il merge con un commit.
|
||||||
|
|
||||||
|
## **7. Lavorare con branch**
|
||||||
|
|
||||||
|
### Creare un nuovo branch
|
||||||
|
|
||||||
|
Per creare un nuovo branch in Git, utilizziamo il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituiamo `<nome-branch>` con il nome desiderato per il nuovo branch.
|
||||||
|
|
||||||
|
### Spostarsi su un branch
|
||||||
|
|
||||||
|
Per spostarci su un branch esistente, usiamo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Oppure, per creare e spostarci su un nuovo branch in un solo comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch -c <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unire un branch nel branch principale
|
||||||
|
|
||||||
|
Per unire un branch nel branch principale (di solito chiamato `main`):
|
||||||
|
|
||||||
|
1. Spostiamoci sul branch principale:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Eseguiamo il merge del branch desiderato:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituiamo `<nome-branch>` con il nome del branch che vogliamo unire.
|
||||||
|
|
||||||
|
### Risoluzione dei conflitti
|
||||||
|
|
||||||
|
Quando due persone modificano lo stesso file, Git può generare un conflitto. Ecco come risolverlo:
|
||||||
|
|
||||||
|
1. Identifichiamo il file in conflitto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Modifichiamo manualmente il file per risolvere il conflitto. Cerchiamo i segni di conflitto (`<<<<<<<`, `=======`, `>>>>>>>`) e scegliamo quali modifiche mantenere.
|
||||||
|
|
||||||
|
3. Aggiungiamo il file risolto allo stage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Concludiamo con un commit per salvare le modifiche risolte:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **9. Comandi utili**
|
||||||
|
|
||||||
|
### **Visualizzare le differenze**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Annullare modifiche**
|
||||||
|
|
||||||
|
1. **Prima del commit**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -- <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Dopo il commit**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git reset --soft HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Eliminare un branch**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch -d <nome-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **10. Best practices**
|
||||||
|
|
||||||
|
- Scriviamo messaggi di commit chiari e descrittivi.
|
||||||
|
|
||||||
|
- Creiamo branch per nuove funzionalità o bugfix.
|
||||||
|
|
||||||
|
- Sincronizziamo frequentemente il nostro repository locale con quello remoto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **11. Risorse aggiuntive**
|
||||||
|
|
||||||
|
- [Documentazione ufficiale di Git](https://git-scm.com/doc)
|
||||||
|
|
||||||
|
- [Guida interattiva Learn Git Branching](https://learngitbranching.js.org/)
|
||||||
|
|
||||||
|
- [GitHub Docs](https://docs.github.com/)
|
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
id: pagina-poisson-con-astro
|
||||||
|
title: Pagina Poisson con Astro
|
||||||
|
description: Vediamo come creare una pagina Poisson moderna con Astro
|
||||||
|
author: Antonio De Lucreziis
|
||||||
|
tags: [astro, website]
|
||||||
|
---
|
||||||
|
|
||||||
|
In questa guida vedremo come creare una pagina Poisson moderna utilizzando Astro, un nuovo framework di sviluppo web statico. Per prima cosa installeremo NodeJS sul nostro computer, poi creeremo un nuovo progetto Astro e infine dopo averlo generato, lo caricheremo su Poisson.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Se siete sul vostro pc installate VSCode ed il plugin di Astro.
|
||||||
|
|
||||||
|
Poi installiamo NodeJS (se siete su Windows è consigliato [installare WSL con `wsl --install`](https://learn.microsoft.com/en-us/windows/wsl/install) e poi installare i seguenti pacchetti nell'ambiente Linux)
|
||||||
|
|
||||||
|
> NodeJS: https://nodejs.org/en/download/package-manager
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://fnm.vercel.app/install | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
fnm use --install-if-missing 20
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creazione di un nuovo progetto Astro
|
||||||
|
|
||||||
|
Per prima cosa dobbiamo creare un nuovo progetto di Astro sul nostro computer, possiamo scegliere [uno dei tanti temi disponibili per Astro](https://astro.build/themes/) o partire da un blog di esempio con il seguente comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create astro@latest -- --template blog
|
||||||
|
cd nome-del-progetto
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Se ad esempio volessimo usare un tema come "[Astro Nano](https://github.com/markhorn-dev/astro-nano)" possiamo fare così:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/markhorn-dev/astro-nano sito-poisson
|
||||||
|
cd sito-poisson
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ultima cosa importante che c'è da cambiare è che le pagine Poisson sono hostate su `https://poisson.phc.dm.unipi.it/~nomeutente/` che non è la radice del dominio del sito. Quindi dobbiamo cambiare il file `astro.config.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default defineConfig({
|
||||||
|
...
|
||||||
|
base: '/~nomeutente/',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lavorare con Astro
|
||||||
|
|
||||||
|
Per vedere il nostro progetto in locale possiamo eseguire il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
A questo punto in base al tema scelto possiamo modificare i file dentro `src/pages` per cambiare il contenuto delle pagine. Molti temi sono preimpostati per scrivere contenuti in Markdown, ad esempio per il _template blog_ possiamo scrivere gli articoli per il nostro blog in `src/content/blog/{nome_post}.md`.
|
||||||
|
|
||||||
|
## Appunti
|
||||||
|
|
||||||
|
Una volta creato il progetto possiamo caricare appunti e dispense nella cartella `/public`
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Per caricare il nostro sito su Poisson possiamo usare il comando `rsync`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
rsync -avz dist/ username@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove `username` è il nostro username Poisson. Da notare che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore.
|
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
id: deploy-with-github-actions
|
||||||
|
title: Deploy automatico per Poisson da GitHub
|
||||||
|
description: Come impostare il deploy automatico per la propria pagina Poisson utilizzando le GitHub Actions
|
||||||
|
author: Antonio De Lucreziis
|
||||||
|
tags: [github, deploy, poisson]
|
||||||
|
---
|
||||||
|
|
||||||
|
Supponiamo di avere un sito web statico che vogliamo caricare su Poisson, ad esempio un progetto NodeJS che genera in `dist/` o `out/` i file da caricare. Come possiamo automatizzare il processo di deploy su Poisson?
|
||||||
|
|
||||||
|
Vediamo come automatizzare questo processo utilizzando le GitHub Actions.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Manualmente, possiamo costruire il nostro progetto in locale e poi caricare i file su Poisson utilizzando `rsync`, ad esempio come segue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm run build
|
||||||
|
$ rsync -avz dist/ <username>@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
(osserviamo che gli `/` alla fine di `dist/` e `public_html/` sono importanti per evitare di creare delle cartelle per errore)
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
Per automatizzare questo processo possiamo caricare il nostro progetto su GitHub ed aggiungere un _workflow_ che esegue il build e il deploy ogni volta che facciamo un push sul branch `main`. Ad esempio, possiamo creare un file `.github/workflows/deploy-poison.yaml` con quanto segue:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy to Poisson
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Write SSH keys
|
||||||
|
run: |
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/known_hosts
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/id_ed25519
|
||||||
|
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '23'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: rsync -cavz dist/ ${{ secrets.SSH_USER }}@poisson.phc.dm.unipi.it:public_html/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comando rsync
|
||||||
|
|
||||||
|
Il comando `rsync` ha le seguenti opzioni:
|
||||||
|
|
||||||
|
- `-c` per controllare i file tramite checksum invece che per data e dimensione (che sono sempre diverse visto che stiamo ricosruendo il sito ogni volta con le GitHub Actions)
|
||||||
|
|
||||||
|
- `-a` per copiare ricorsivamente i file e mantenere i permessi
|
||||||
|
|
||||||
|
- `-v` per mostrare i file copiati
|
||||||
|
|
||||||
|
- `-z` per comprimere i file durante il trasferimento
|
||||||
|
|
||||||
|
## SSH Segreti
|
||||||
|
|
||||||
|
Per stabilire una connessione SSH a Poisson dalle GitHub Actions in modo sicuro, dobbiamo aggiungere alcuni segreti alla nostra repository. Vediamo meglio il workflow:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Write SSH keys
|
||||||
|
run: |
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/known_hosts
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/id_ed25519
|
||||||
|
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
Questa è la parte più importante del workflow, che permette di autenticarsi su Poisson senza dover inserire la password ogni volta (cosa che non possiamo materialmente fare dall GitHub Actions).
|
||||||
|
|
||||||
|
Per farlo, creiamo in locale una coppia di chiavi SSH apposta per le GitHub Actions e aggiungiamo la chiave pubblica su Poisson. Per farlo, possiamo seguire questi passaggi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ssh-keygen -t ed25519 -C "deploy@github-actions" -f actions-deploy-key
|
||||||
|
$ ssh-copy-id -i actions-deploy-key <username>@poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
Qui generiamo una chiave ssh utilizzando l'algoritmo `ed25519` (leggermente più consigliato rispetto a `rsa`, in particolare ha anche chiavi più corte), `-C` aggiunge semplicemente un commento alla chiave e `-f` specifica il file in cui salvare la chiave.
|
||||||
|
|
||||||
|
Poi eseguendo `cat actions-deploy-key` possiamo copiare il contenuto della chiave privata ed aggiungiamo il contenuto in un segreto chiamato `SSH_PRIVATE_KEY` nella nostra repository.
|
||||||
|
|
||||||
|
Poi, per evitare che la connessione venga rifiutata, eseguiamo in locale anche uno scan delle chiavi SSH di Poisson:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ssh-keyscan poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
(se l'output è vuoto riprovare con `ssh-keyscan -4 ...`) e copiamo l'output in un segreto della nostra repository chiamato `SSH_KNOWN_HOSTS`.
|
||||||
|
|
||||||
|
Infine possiamo aggiungere anche un segreto `SSH_USER` con il nostro username o modificare anche direttamente il workflow ed inserire l'username direttamente nel file.
|
||||||
|
|
||||||
|
Ora ogni volta che facciamo un push sul branch `main` il nostro sito verrà automaticamente costruito e caricato su Poisson!
|
@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
id: attivazione-poisson
|
||||||
|
title: Come attivare il proprio account Poisson
|
||||||
|
description: Guida per l'attivazione dell'account Poisson, con le istruzioni per il primo accesso e la configurazione del proprio spazio web
|
||||||
|
author: Luca Lombardo
|
||||||
|
tags: [poisson]
|
||||||
|
---
|
||||||
|
|
||||||
|
Poisson è un server autogestito dalla comunità studentesca di matematica, da sempre gestito con cura dai membri del PHC. Ogni studentə ha la possibilità di attivare un account personale, che consente l'accesso alla macchina via SSH e la creazione di uno spazio web personale.
|
||||||
|
|
||||||
|
## Come richiedere un account
|
||||||
|
|
||||||
|
Se non si è mai creato un account Poisson, è necessario inviare una richiesta via email a **macchinisti@lists.dm.unipi.it** includendo:
|
||||||
|
|
||||||
|
- Nome
|
||||||
|
|
||||||
|
- Cognome
|
||||||
|
|
||||||
|
- Username di ateneo (quello associato alla propria email istituzionale)
|
||||||
|
|
||||||
|
Nella mail è sufficiente specificare che si desidera attivare un account Poisson. I "macchinisti" si occuperanno di attivare l'account il prima possibile.
|
||||||
|
|
||||||
|
### Come ottenere le credenziali
|
||||||
|
|
||||||
|
Dopo l'attivazione, le credenziali del proprio account saranno disponibili accedendo al seguente sito:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://credenziali.phc.dm.unipi.it/">https://credenziali.phc.dm.unipi.it/</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Assicuriamoci di accedere con le credenziali di ateneo per recuperare username e password assegnati.
|
||||||
|
|
||||||
|
## Primo accesso al server
|
||||||
|
|
||||||
|
Per accedere a Poisson via SSH, è necessario:
|
||||||
|
|
||||||
|
1. Aprire un terminale (su Linux/Mac) o utilizzare un client SSH come PuTTY (su Windows).
|
||||||
|
|
||||||
|
2. Eseguire il comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh <username>@poisson.phc.dm.unipi.it
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove `<username>` è il proprio username che è stato fornito con le credenziali.
|
||||||
|
|
||||||
|
## Configurazione della pagina web
|
||||||
|
|
||||||
|
Dopo aver effettuato il primo accesso, è possibile configurare la propria pagina web personale. Nella home directory del proprio account, è presente una cartella `public_html` in cui è possibile inserire i file necessari per la propria pagina web.
|
||||||
|
|
||||||
|
Vediamo un piccolo esempio di file `index.html` che possiamo creare:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Sergio Steffè</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Sergio Steffè</h1>
|
||||||
|
<img
|
||||||
|
src="https://people.cs.dm.unipi.it/steffe/sergio.jpg"
|
||||||
|
alt="Foto di Sergio Steffè"
|
||||||
|
style="max-width: 300px; border-radius: 10px;"
|
||||||
|
/>
|
||||||
|
<p>Ciao! Sono Sergio Steffè.</p>
|
||||||
|
<p>Email: <a href="mailto:sergio.steffe@example.com">sergio.steffe@example.com</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Una volta salvato il file `index.html` nella cartella `public_html`, sarà possibile visualizzarlo accedendo al seguente indirizzo:
|
||||||
|
|
||||||
|
https://poisson.phc.dm.unipi.it/~<username>
|
||||||
|
|
||||||
|
Dove `<username>` è il proprio username.
|
||||||
|
|
||||||
|
### Creazione di pagine web più complesse
|
||||||
|
|
||||||
|
Per creare pagine web più complesse, suggeriamo di utilizzare il framework [Astro](https://astro.build/), che permette di creare siti web statici in modo semplice e veloce. [Abbiamo scritto una guida](/guide/pagina-poisson-con-astro) su come iniziare a utilizzare Astro per creare la nostra pagina web e caricarla su Poisson.
|
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
id: recupero-password
|
||||||
|
title: MI SONO SCORDATO LA PASSWORD DI POISSON COME FACCIO ORA?
|
||||||
|
description: Hai dimenticato la tua password Poisson? Niente paura, ecco cosa fare!
|
||||||
|
author: Luca Lombardo
|
||||||
|
tags: [poisson, password]
|
||||||
|
---
|
||||||
|
|
||||||
|
Se per qualche strano motivo hai dimenticato la tua password Poisson, niente paura! La procedura è super semplice:
|
||||||
|
|
||||||
|
1. Scrivi una mail a **macchinisti@lists.dm.unipi.it**.
|
||||||
|
|
||||||
|
2. Nella mail, specifica il tuo nome, cognome, username di ateneo (quello della tua email istituzionale) e se lo ricordi, anche il tuo username Poisson.
|
||||||
|
|
||||||
|
3. Attendi pazientemente la risposta dei macchinisti, che ti resetteranno la password e ti invieranno una nuova via email.
|
||||||
|
|
||||||
|
E voilà, in un batter d'occhio sarai di nuovo pronto a entrare nel fantastico mondo di Poisson.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Nota:** Non preoccuparti, capita a tutti di dimenticare una password ogni tanto. Se vuoi evitare che succeda di nuovo, prova ad usare un password manager come [Bitwarden](https://bitwarden.com/) 😉
|
@ -0,0 +1,209 @@
|
|||||||
|
import Container from '../../components/meta/Container.astro'
|
||||||
|
import Palette from '../../components/meta/Palette.astro'
|
||||||
|
|
||||||
|
# Meta > Design
|
||||||
|
|
||||||
|
In questa pagina tento di spiegare come funziona il design di questo sito. I blocchi di codice con sfondo arancione chiaro sono esempi di codice Astro e sotto hanno un'anteprima del risultato _generata automaticamente_. Ad esempio
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<p>Questo è un paragrafo</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Molti di questi esempi hanno alcuni stili impostati in `style` per mostrare come funzionano. Nella pratica invece è consigliato create una classe per ogni tipo di componente ed impostare le proprietà via CSS, ad esempio
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-custom-form {
|
||||||
|
--card-base: var(--palette-red);
|
||||||
|
max-width: 25rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="my-custom-form card">
|
||||||
|
<p>Questo è un paragrafo</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Card
|
||||||
|
|
||||||
|
Le card sono uno dei componenti più importanti di questo sito. Sono utilizzate per mostrare i post, i progetti e le pagine. Ecco alcuni esempi per dare un'idea
|
||||||
|
|
||||||
|
### Esempio di base
|
||||||
|
|
||||||
|
Una card semplice ha un titolo ed una descrizione.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="--card-base: var(--guide-base); max-width: 25rem;">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Varianti
|
||||||
|
|
||||||
|
#### Grande
|
||||||
|
|
||||||
|
Le card possono essere di dimensioni diverse. Questa è una card grande.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card large" style="--card-base: lightgreen; max-width: 25rem;">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Low Level: Mixin SCSS
|
||||||
|
|
||||||
|
Non dovrebbe essere mai necessario usarlo direttamente ma l'effetto di ombra delle card è ottenuto con questo mixin SCSS (che si trova in `src/styles/mixins.scss`).
|
||||||
|
|
||||||
|
```scss
|
||||||
|
@mixin neo-brutalist-card($size: 3px, $offset: $size + 1, $shadow: true, $hoverable: false) {
|
||||||
|
border: $size solid #222;
|
||||||
|
border-radius: $size * 2;
|
||||||
|
|
||||||
|
@if $shadow {
|
||||||
|
box-shadow: $offset $offset 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $hoverable {
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: $offset + 1 $offset + 1 0 0 #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ad esempio tutti i bottoni utilizzano direttamente questo mixin senza cambiare i parametri di default.
|
||||||
|
|
||||||
|
### Sotto-componenti
|
||||||
|
|
||||||
|
#### Titolo
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">Titolo</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testo & Modificatori
|
||||||
|
|
||||||
|
Se c'è poco testo, può essere inserito direttamente nella card.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="text">
|
||||||
|
Descrizione lorem ipsum dolor sit amet consectetur
|
||||||
|
adipisicing elit. Aspernatur, labore?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Altrimenti può essere inserito in un tag `<p>`.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card">
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
|
||||||
|
Distinctio, vel! Veritatis est sit beatae eveniet.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Error, minus, asperiores quaerat nulla cumque, nisi ipsam
|
||||||
|
assumenda consectetur accusamus tempore consequatur quae. Fugit?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Quos sapiente amet numquam quis, libero odit eum, eius
|
||||||
|
perspiciatis repellat nesciunt cupiditate asperiores maiores?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
C'è anche il modificatore `small` e `dimmed` per ridurre la grandezza del testo e renderlo grigio rispettivamente.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="max-width: 25rem;">
|
||||||
|
<div class="text">
|
||||||
|
Some normal text, this is a very long
|
||||||
|
text that should wrap on the next line
|
||||||
|
</div>
|
||||||
|
<div class="text small">
|
||||||
|
This is some small text
|
||||||
|
</div>
|
||||||
|
<div class="text dimmed">
|
||||||
|
This is some dimmed text
|
||||||
|
</div>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
This is some small dimmed text
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
I tag sono una lista di link con `display: flex` e `flex-wrap: wrap`.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div class="card" style="max-width: 25rem;">
|
||||||
|
<div class="tags">
|
||||||
|
<a href="#">#tag1</a>
|
||||||
|
<a href="#">#tagg2</a>
|
||||||
|
<a href="#">#tag3</a>
|
||||||
|
<a href="#">#taggg4</a>
|
||||||
|
<a href="#">#tagg5</a>
|
||||||
|
<a href="#">#taggg6</a>
|
||||||
|
<a href="#">#tag7</a>
|
||||||
|
<a href="#">#taggg8</a>
|
||||||
|
<a href="#">#tag9</a>
|
||||||
|
<a href="#">#taggggg10</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Palette
|
||||||
|
|
||||||
|
Varie sezioni del sito utilizzano diverse palette di colori. Questa è la palette di base.
|
||||||
|
|
||||||
|
### Guide
|
||||||
|
|
||||||
|
<Palette
|
||||||
|
colors={[
|
||||||
|
'var(--guide-base)',
|
||||||
|
'var(--guide-darkest)',
|
||||||
|
'var(--guide-darker)',
|
||||||
|
'var(--guide-dark)',
|
||||||
|
'var(--guide-light)',
|
||||||
|
'var(--guide-lighter)',
|
||||||
|
'var(--guide-lightest)',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Combo Box
|
||||||
|
|
||||||
|
I combo box sono un componente per fare dropdown scritto in Preact. Questo è un esempio di come funzionano.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { ComboBox } from '@/lib/components/ComboBox'
|
||||||
|
|
||||||
|
const [value, setValue] = useState('option-1')
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ComboBox value={value} setValue={setValue}>
|
||||||
|
{{
|
||||||
|
'option-1': <>Option 1</>
|
||||||
|
'option-2': <>Option 2</>
|
||||||
|
'option-3': <>Option 3</>
|
||||||
|
}}
|
||||||
|
</ComboBox>
|
||||||
|
```
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
# Questo documento è utilizzato nella homepage del sito nella card principale
|
||||||
|
title: Cos'è il PHC?
|
||||||
|
---
|
||||||
|
|
||||||
|
Il <span title="Pisa Happy Computing">**PHC**</span> è un laboratorio informatico, gestito dagli studenti del **Dipartimento di Matematica** di Pisa e nato nel 1999, che offre vari servizi agli studenti come [Poisson](https://poisson.phc.dm.unipi.it), che ospita le pagine degli studenti.
|
||||||
|
|
||||||
|
La sede del PHC è la [stanza 106](https://www.dm.unipi.it/mappa/?sel=638cd24b50cf34e03924a00c) del Dipartimento, dove si trovano i **macchinisti** per discutere e realizzare progetti [hardware](http://steffe.cs.dm.unipi.it/) e [software](https://lab.phc.dm.unipi.it/orario), e occuparsi di server autogestiti.
|
||||||
|
|
||||||
|
Le macchine del PHC girano principalmente Linux/Unix come sistemi operativi e i macchinisti sono grandi sostenitori di software [FOSS](https://it.wikipedia.org/wiki/Free_and_Open_Source_Software) (che loro stessi sviluppano sull'[istanza Gitea del PHC](https://git.phc.dm.unipi.it/phc)).
|
||||||
|
|
||||||
|
La lista dei vari macchinisti e di altri eventi notevoli si trova nella [pagina della storia](/storia) del PHC.
|
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: Il Nuovo Sito del PHC
|
||||||
|
description: Benvenuti nel nuovo sito web del PHC, realizzato in Astro. Vediamo le feature principali e cosa abbiamo in serbo per il futuro.
|
||||||
|
publishDate: 2024-10-05
|
||||||
|
---
|
||||||
|
|
||||||
|
# Il Nuovo Sito del PHC
|
||||||
|
|
||||||
|
Questo è il nuovo sito del PHC, realizzato in [Astro](https://astro.build/), un nuovo framework per realizzare siti web.
|
||||||
|
|
||||||
|
## Perché Astro?
|
||||||
|
|
||||||
|
Astro è un framework statico che ci consente di creare siti web veloci e performanti. Per ora il sito è completamente statico, ma Astro ci permetterà di passare facilmente a un modello di sito ibrido, con funzionalità dinamiche e interattive.
|
||||||
|
|
||||||
|
A differenza di altri framework, di base Astro non richiede JavaScript per funzionare, il che lo rende una scelta ideale per siti web leggeri e veloci.
|
||||||
|
|
||||||
|
## Caratteristiche del Nuovo Sito
|
||||||
|
|
||||||
|
Il nuovo sito unifica varie funzionalità che prima erano sparse in diversi siti web. I contenuti del sito dei Seminarietti verranno integrati nella sezione guide che ospiterà anche molte altre risorse utili.
|
||||||
|
|
||||||
|
- **Lista Utenti**: La lista degli utenti di Poisson precedentemente presente sul sito era ordinata per nome e non era facile cercare un utente specifico. Ora la lista è ordinata in base alla data di creaizone dell'account e c'è una barra di ricerca fuzzy per cercare un utente e la sua pagina Poisson.
|
||||||
|
|
||||||
|
- **Guide**: La sezione guide ospiterà articoli, tutorial e risorse utili per gli studenti. Questa sezione sarà in costante aggiornamento con nuovi contenuti.
|
||||||
|
|
||||||
|
- **News**: La sezione news ospiterà gli novità e annunci importanti riguardanti il PHC.
|
||||||
|
|
||||||
|
- **Storia**: Questa sezione raccoglie la storia del PHC fin dalla creazione di Poisson nel lontano 1994, includendo tutti i vari macchinisti nel corso della tempo.
|
||||||
|
|
||||||
|
## Cooming Soon
|
||||||
|
|
||||||
|
- **Appunti**: Stiamo lavorando ad una nuova sezione appunti unificata che raccoglierà tutte le dispense presenti su Poisson. Gli utenti potranno cercare gli appunti per corso e per professore e scaricarli in formato PDF.
|
||||||
|
|
||||||
|
- **Account Utente**: Prossimamente sarà possibile accedere al sito del PHC sia con un account Poisson che con un account di Ateneo. Gli utenti potranno recuperare le proprie credenziali Poisson (precedentemente esisteva il sito separato https://credenziali.phc.dm.unipi.it/ per questo scopo), modificare la propria password e visualizzare i propri appunti caricati.
|
||||||
|
|
||||||
|
## Conclusioni
|
||||||
|
|
||||||
|
Speriamo che il nuovo sito vi piaccia e vi sia utile. Se avete suggerimenti o richieste, non esitate a contattarci. Buona navigazione, magari iniziando dalla [pagina sulla storia del PHC](/storia)!
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<Header />
|
||||||
|
<main class="article card large">
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
import '@fontsource/open-sans/latin.css'
|
||||||
|
import '@fontsource/source-sans-pro/latin.css'
|
||||||
|
import '@fontsource/source-code-pro/latin.css'
|
||||||
|
import '@fontsource/space-mono/latin.css'
|
||||||
|
import '@fontsource/iosevka/latin.css'
|
||||||
|
|
||||||
|
import '@fontsource-variable/material-symbols-outlined/full.css'
|
||||||
|
|
||||||
|
import '../styles/main.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
thumbnail?: string
|
||||||
|
|
||||||
|
/** Tags for the page, used for styling */
|
||||||
|
pageTags?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
import phcIcon from '../assets/icon.png'
|
||||||
|
|
||||||
|
const { title, description, thumbnail, pageTags } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta property="og:title" content={title ?? 'PHC'} />
|
||||||
|
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
||||||
|
<meta name="description" content={description ?? 'Sito web del PHC'} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://phc.dm.unipi.it/" />
|
||||||
|
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href={phcIcon.src} />
|
||||||
|
<link rel="apple-touch-icon" href={phcIcon.src} />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import renderMathInElement from 'katex/contrib/auto-render'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
renderMathInElement(document.body, {
|
||||||
|
delimiters: [
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '$', right: '$', display: false },
|
||||||
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
|
{ left: '\\[', right: '\\]', display: true },
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
window.goatcounter = {
|
||||||
|
path(p) {
|
||||||
|
return location.host + p
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
is:inline
|
||||||
|
async
|
||||||
|
src="//analytics.phc.dm.unipi.it/count.js"
|
||||||
|
data-goatcounter="https://analytics.phc.dm.unipi.it/count"></script>
|
||||||
|
|
||||||
|
<title>{title ?? 'PHC'}</title>
|
||||||
|
</head>
|
||||||
|
<body class:list={typeof pageTags === 'string' ? [pageTags] : pageTags}>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
import { AppuntiList, AppuntiCard } from '@client/Appunti'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Appunti | PHC" pageTags="appunti">
|
||||||
|
<h1>Appunti & Dispense</h1>
|
||||||
|
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" />
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>In primo piano</h2>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable center">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="someuser" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load course="Geometria 1" author="exampleuser" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load course="Algebra 1" author="anotheruser" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <h2>Categorie</h2>
|
||||||
|
|
||||||
|
<h3>Analisi</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Algebra Lineare</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Geometria</h3>
|
||||||
|
|
||||||
|
<div class="appunti-scrollable">
|
||||||
|
<AppuntiList>
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
<AppuntiCard client:load title="Appunti 1" author="example" courseYear="2023/2024" />
|
||||||
|
</AppuntiList>
|
||||||
|
</div> -->
|
||||||
|
</PageLayout>
|
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import ArticleLayout from '@/layouts/ArticleLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
|
||||||
|
return guides.map(entry => ({
|
||||||
|
params: { id: entry.data.id },
|
||||||
|
props: { entry },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
const { Content } = await entry.render()
|
||||||
|
---
|
||||||
|
|
||||||
|
<ArticleLayout
|
||||||
|
{...entry.data}
|
||||||
|
title={entry.data.title + ' | Guide | PHC'}
|
||||||
|
pageTags={['guida', entry.data.id, entry.data.series && 'series']}
|
||||||
|
>
|
||||||
|
<h1>{entry.data.title}</h1>
|
||||||
|
|
||||||
|
{entry.data.series && <div class="series">Serie: {entry.data.series}</div>}
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<strong>Autore</strong>
|
||||||
|
{entry.data.author}
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<strong>Tags</strong>
|
||||||
|
<div class="tags">
|
||||||
|
{entry.data.tags.map(tag => <a href={`/guide/tags/${tag}`}>#{tag}</a>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ArticleLayout>
|
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Guide | PHC" pageTags="guide">
|
||||||
|
<h1>
|
||||||
|
<a href="/guide">Guide</a>
|
||||||
|
</h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
guides.toReversed().map(guide => (
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">
|
||||||
|
<a href={`/guide/${guide.data.id}`}>{guide.data.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text">{guide.data.description}</div>
|
||||||
|
<div class="tags">
|
||||||
|
{guide.data.tags.map((tag: string) => (
|
||||||
|
<a href={`/guide/tags/${tag}`}>#{tag}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import PageLayout from '@/layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('guides')
|
||||||
|
|
||||||
|
const tags: string[] = []
|
||||||
|
|
||||||
|
guides.forEach(post => {
|
||||||
|
post.data.tags.forEach((tag: string) => {
|
||||||
|
tags.push(tag)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(new Set(tags)).map(tag => {
|
||||||
|
return {
|
||||||
|
params: { tag },
|
||||||
|
props: {
|
||||||
|
tag,
|
||||||
|
guides: guides.filter(post => post.data.tags.includes(tag)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tag: string
|
||||||
|
guides: CollectionEntry<'guides'>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag, guides } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={`#${tag} | Guide | PHC`} pageTags="guide tag">
|
||||||
|
<h1><a href="/guide">Guide</a> > <a href={`/guide/tags/${tag}`}>#{tag}</a></h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
guides.map(guide => (
|
||||||
|
<div class="card">
|
||||||
|
<div class="title">
|
||||||
|
<a href={`/guide/${guide.data.id}`}>{guide.data.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text">{guide.data.description}</div>
|
||||||
|
<div class="tags">
|
||||||
|
{guide.data.tags.map((tag: string) => (
|
||||||
|
<a href={`/guide/tags/${tag}`}>#{tag}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
import PageLayout from '@/layouts/PageLayout.astro'
|
||||||
|
import { Content as WhatPhcContent, frontmatter as whatsPhcFrontmatter } from '@/content/meta/whats-phc.md'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import ProjectCard from '@/components/ProjectCard.astro'
|
||||||
|
import HomepageZigZag from '@/components/HomepageZigZag.astro'
|
||||||
|
import Card from '@/components/Card.astro'
|
||||||
|
|
||||||
|
const news = await getCollection('news')
|
||||||
|
|
||||||
|
const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="PHC" pageTags="homepage">
|
||||||
|
<section class="principal">
|
||||||
|
<div class="circuit-layer">
|
||||||
|
<canvas id="circuits-art"></canvas>
|
||||||
|
<script src="../scripts/circuits-art.ts"></script>
|
||||||
|
</div>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/phc-logo-2024-11@x8.png" alt="phc logo" />
|
||||||
|
</div>
|
||||||
|
<div class="whats-phc card large">
|
||||||
|
<input type="checkbox" class="hide" id="mobile-whats-phc-read-more" checked />
|
||||||
|
<div class="title">{whatsPhcFrontmatter.title}</div>
|
||||||
|
<div class="text">
|
||||||
|
<WhatPhcContent />
|
||||||
|
</div>
|
||||||
|
<div class="mobile-only">
|
||||||
|
<label for="mobile-whats-phc-read-more">
|
||||||
|
<div class="button">
|
||||||
|
<span>Mostra di più</span>
|
||||||
|
<span>Mostra meno</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="news">
|
||||||
|
<HomepageZigZag color="#C2A8EB" />
|
||||||
|
|
||||||
|
<div class="title">Ultime Notizie</div>
|
||||||
|
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
news.map(newsItem => (
|
||||||
|
<Card>
|
||||||
|
<a href={`/notizie/${newsItem.slug}`} class="title">
|
||||||
|
{newsItem.data.title}
|
||||||
|
</a>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="text">{newsItem.data.description}</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="primary" href="/notizie" role="button">Vai all'Archivio</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="projects" id="projects">
|
||||||
|
<HomepageZigZag color="#f5f2cc" />
|
||||||
|
|
||||||
|
<div class="title">Progetti</div>
|
||||||
|
|
||||||
|
<div class="project-list">
|
||||||
|
<ProjectCard
|
||||||
|
title="Gitea"
|
||||||
|
href="https://git.phc.dm.unipi.it/"
|
||||||
|
imgSrc="https://upload.wikimedia.org/wikipedia/commons/b/bb/Gitea_Logo.svg"
|
||||||
|
style="--card-bg: rgb(150, 197, 150); --masonry-height: 2;"
|
||||||
|
>
|
||||||
|
<p>Gitea è un servizio di hosting per progetti software, come GitHub ma autogestito.</p>
|
||||||
|
<p>
|
||||||
|
Qui puoi trovare i progetti del PHC, e accedendo con un account di Ateneo potrai crearne
|
||||||
|
di nuovi.
|
||||||
|
</p>
|
||||||
|
</ProjectCard>
|
||||||
|
<!-- <ProjectCard
|
||||||
|
title="PHC-Bot"
|
||||||
|
href="#"
|
||||||
|
style="--card-bg: #c55; --masonry-height: 1;"
|
||||||
|
>
|
||||||
|
<p>Un bot con cui chattare per chiedere informazioni o supporto tecnico.</p>
|
||||||
|
</ProjectCard> -->
|
||||||
|
<ProjectCard
|
||||||
|
title="Orario"
|
||||||
|
href="https://lab.phc.dm.unipi.it/orario/"
|
||||||
|
style="--card-bg: #75ca75; --masonry-height: 1;"
|
||||||
|
imgSrc="https://lab.phc.dm.unipi.it/orario/icon.png"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Questo sito permette di visualizzare il proprio orario delle lezioni, con informazioni sui
|
||||||
|
docenti e le aule.
|
||||||
|
</p>
|
||||||
|
</ProjectCard>
|
||||||
|
<ProjectCard
|
||||||
|
title="Problemi"
|
||||||
|
href="https://lab.phc.dm.unipi.it/problemi/"
|
||||||
|
style="--card-bg: #aa88c0; --masonry-height: 2;"
|
||||||
|
imgSrc="https://lab.phc.dm.unipi.it/problemi/favicon/android-chrome-512x512.png"
|
||||||
|
>
|
||||||
|
<p>Bacheca di problemi di Matematica da risolvere, inviandone le soluzioni in LaTeX.</p>
|
||||||
|
<p>Di sera, puoi leggere i problemi sullo schermo del Dipartimento in atrio.</p>
|
||||||
|
</ProjectCard>
|
||||||
|
<ProjectCard
|
||||||
|
title='Cluster "Steffè"'
|
||||||
|
href="http://steffe.cs.dm.unipi.it/"
|
||||||
|
style="--masonry-height: 2;"
|
||||||
|
imgSrc="https://steffe.lb.cs.dm.unipi.it/assets/img/logo.png"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cluster progettato ed assemblato durante il progetto speciale per la didattica "Calcolo
|
||||||
|
Parallelo dall'Infrastruttura alla Matematica".
|
||||||
|
</p>
|
||||||
|
</ProjectCard>
|
||||||
|
<ProjectCard
|
||||||
|
title="Seminarietti"
|
||||||
|
href="https://seminarietti.phc.dm.unipi.it/"
|
||||||
|
style="--card-bg: #bd9fec; --masonry-height: 2;"
|
||||||
|
imgSrc="https://seminarietti.phc.dm.unipi.it/favicon.png"
|
||||||
|
>
|
||||||
|
<p>Varie conferenze del PHC su argomenti di informatica, matematica e tecnologia.</p>
|
||||||
|
</ProjectCard>
|
||||||
|
<ProjectCard
|
||||||
|
title="Tutorato"
|
||||||
|
href="https://tutorato.phc.dm.unipi.it/"
|
||||||
|
style="--card-bg: #c55; --masonry-height: 2;"
|
||||||
|
imgSrc="https://tutorato.phc.dm.unipi.it/favicon.svg"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Un sito con tutte le informazioni sui tutorati di Matematica, con tanto di archivio degli
|
||||||
|
anni passati.
|
||||||
|
</p>
|
||||||
|
</ProjectCard>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="wanna-be-macchinista">
|
||||||
|
<HomepageZigZag color="#888" />
|
||||||
|
|
||||||
|
<div class="title"></div>
|
||||||
|
|
||||||
|
<Card large style="--card-base: #ddd;">
|
||||||
|
<div class="title" title="recentemente, anche il caffè">E cosa si fa in PHC?</div>
|
||||||
|
<div class="text">
|
||||||
|
<p>Nessuno lo sa di preciso, ma facciamo molte cose:</p>
|
||||||
|
<ul>
|
||||||
|
<li>amministrazione di sistemi Linux/Unix e macchine virtuali</li>
|
||||||
|
<li>supporto tecnico per installare e usare Linux sul proprio portatile</li>
|
||||||
|
<li>costruiamo, smontiamo ed aggiustamo computer (antichi e moderni)</li>
|
||||||
|
<li>sviluppo di software per backend e web development</li>
|
||||||
|
<li>
|
||||||
|
organizzazione di seminari di divulgazione (vedi <a href="#projects">sopra</a>)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Inoltre, il PHC è prima di tutto un luogo dove imparare, trasmettere le proprie conoscenze
|
||||||
|
e condividere la passione per la tecnologia.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<div class="gallery-collage">
|
||||||
|
{
|
||||||
|
galleryCollage.map((module, i) => {
|
||||||
|
const src = module.default.src
|
||||||
|
const filename = src.split('/').at(-1).split('?').at(0).split('.').at(0)
|
||||||
|
|
||||||
|
const [rows, cols] = filename.includes('@')
|
||||||
|
? filename
|
||||||
|
.split('@')
|
||||||
|
.at(-1)
|
||||||
|
.split('x')
|
||||||
|
.map((s: string) => parseInt(s))
|
||||||
|
: [1, 1]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={`--rows: ${rows}; --cols: ${cols};`}>
|
||||||
|
<Image src={module.default} alt={`gallery image ${i}`} />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card large style="--card-base: #ddd;">
|
||||||
|
<div class="title">Vuoi diventare macchinista?</div>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
<strong>Macchinista non si nasce, si diventa:</strong> se sei uno studente di Matematica e
|
||||||
|
vuoi diventare un macchinista, vienici a trovare!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
L'unico prerequisito è la voglia di imparare! Di solito, c'è un periodo di "apprendistato"
|
||||||
|
nel quale si apprendono le basi; una volta superato potrai diventare un macchinista a
|
||||||
|
tutti gli effetti.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Login | PHC" pageTags="login">
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<!-- form with username and password, and a button for oauth login -->
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<h3 class="center">Accedi con Poisson</h3>
|
||||||
|
<input type="text" id="username" placeholder="Username" name="username" required />
|
||||||
|
<input type="password" id="password" placeholder="Password" name="password" required />
|
||||||
|
|
||||||
|
<button class="primary center" type="submit">Login</button>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3 class="center">Accedi con Ateneo</h3>
|
||||||
|
<a href="/auth/ateneo" class="primary center" role="button">Login</a>
|
||||||
|
</form>
|
||||||
|
<!-- <span class="material-symbols-outlined">person</span> -->
|
||||||
|
</PageLayout>
|
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../../components/Header.astro'
|
||||||
|
import Footer from '../../components/Footer.astro'
|
||||||
|
|
||||||
|
import CodeBlockPreview from '../../components/meta/CodeBlockPreview.astro'
|
||||||
|
|
||||||
|
import { Content, getHeadings } from '../../content/meta/design.mdx'
|
||||||
|
|
||||||
|
const headings = getHeadings()
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props} pageTags="design">
|
||||||
|
<Header />
|
||||||
|
<aside>
|
||||||
|
<nav>
|
||||||
|
<h3>Indice</h3>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
headings.map(heading => (
|
||||||
|
<li style={{ '--depth': heading.depth }}>
|
||||||
|
<a href={`#${heading.slug}`}>{heading.text}</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="text">
|
||||||
|
<Content components={{ pre: CodeBlockPreview }} />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import ArticleLayout from '../../layouts/ArticleLayout.astro'
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const guides = await getCollection('news')
|
||||||
|
return guides.map(entry => ({
|
||||||
|
params: { id: entry.slug },
|
||||||
|
props: { entry },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props
|
||||||
|
const { Content } = await entry.render()
|
||||||
|
---
|
||||||
|
|
||||||
|
<ArticleLayout {...entry.data} title={entry.data.title + ' | Notizie | PHC'} pageTags={['notizia']}>
|
||||||
|
<Content />
|
||||||
|
</ArticleLayout>
|
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
import PageLayout from '@layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
const news = await getCollection('news')
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Notizie | PHC" pageTags="notizie">
|
||||||
|
<h1><a href="/notizie">Notizie</a></h1>
|
||||||
|
<div class="card-list">
|
||||||
|
{
|
||||||
|
news.map(newsItem => (
|
||||||
|
<div class="card">
|
||||||
|
<a href={`/notizie/${newsItem.slug}`} class="title">
|
||||||
|
{newsItem.data.title}
|
||||||
|
</a>
|
||||||
|
<div class="text small dimmed">
|
||||||
|
{new Date(newsItem.data.publishDate).toLocaleDateString('it-IT', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="text">{newsItem.data.description}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
|
|
||||||
|
import Header from '../components/Header.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
import Timeline from '../components/Timeline.astro'
|
||||||
|
|
||||||
|
import imgPulizie from '@/assets/gallery/006-pulizie@3x4.jpg'
|
||||||
|
import imgCluster from '@/assets/gallery/001-cluster-fra-luca@4x3.jpg'
|
||||||
|
import WebSite from '@/assets/gallery/005-website-development@3x4.jpg'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Storia | PHC" pageTags={'storia'}>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<div class="card large" style={{ '--card-base': '#ffd3a0' }}>
|
||||||
|
<div class="title">Storia</div>
|
||||||
|
<div class="text">
|
||||||
|
<p>
|
||||||
|
Il PHC nasce quasi venti anni fa, nel lontano 1999 ed ha perciò una ricca storia. Qui
|
||||||
|
proveremo ad annoverare la storia del PHC, in una timeline con gli eventi più salienti del
|
||||||
|
progetto.
|
||||||
|
</p>
|
||||||
|
<img
|
||||||
|
class="small flat"
|
||||||
|
src="https://media.tenor.com/MRCIli40TYoAAAAj/under-construction90s-90s.gif"
|
||||||
|
alt="Work in progress"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Per delle note storiche un po' più dettagliate, si legga l'ottima pagina sul <a
|
||||||
|
href="http://betti.dm.unipi.it/servizi/PHC.html">sito del dipartimento</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline">
|
||||||
|
<Timeline title="Un nuovo look" year="Nov 2024">
|
||||||
|
<p>
|
||||||
|
Dopo mesi di lavoro e di progettazione, il sito del PHC viene riscritto da zero in Astro.
|
||||||
|
Il progetto è stato voluto principalmente da <strong>Antonio De Lucreziis</strong> (in foto,
|
||||||
|
in fase di sviluppo), con il supporto di <strong>Francesco Minnocci</strong>.
|
||||||
|
</p>
|
||||||
|
<img class="fill" src={WebSite.src} alt="Sviluppo del sito" />
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Luca e le grandi pulizie" year="Apr 2024">
|
||||||
|
<p>
|
||||||
|
Nel 2024, <strong>Luca Lombardo</strong> entra a far parte del PHC. Questo stesso anno vengono
|
||||||
|
effettuate delle grandi pulizie nella stanza del PHC, come non si faceva dal lontano 2006.
|
||||||
|
</p>
|
||||||
|
<img class="fill" src={imgPulizie.src} alt="pulizie phc" />
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Il cluster di Raspberry Pi" year="2023">
|
||||||
|
<p>
|
||||||
|
Il dipartimento acquista in due fasi diverse 34 (cloni) di Raspberry Pi 4, che vengono
|
||||||
|
assemblati in un cluster per il progetto "High Performance Mathematics". Il cluster è
|
||||||
|
stato assemblato, configurato e messo in funzione con la forte collaborazione macchinisti
|
||||||
|
del PHC, che ancora oggi ne curano la manutenzione.
|
||||||
|
</p>
|
||||||
|
<img class="fill" src={imgCluster.src} alt="cluster di raspberry pi" />
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Tanti Francesco" year="2022">
|
||||||
|
<p>
|
||||||
|
Nel 2022, entrano a far parte del PHC <strong>Francesco Minnocci</strong>, <strong
|
||||||
|
>Francesco Manicastri</strong
|
||||||
|
> e <strong>Francesco Baldino</strong>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Nuovi macchinisti" year="2019">
|
||||||
|
<p>
|
||||||
|
Nel 2019, entrano a far parte del PHC <strong>Antonio De Lucreziis</strong> e <strong
|
||||||
|
>Illya Serdyuk</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Nuovi macchinisti" year="2018">
|
||||||
|
<p>
|
||||||
|
Nel 2018, entrano a far parte del PHC <strong>Francesco Caporali</strong> e <strong
|
||||||
|
>Letizia D'Achille</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Rinnovo del sito" year="2004">
|
||||||
|
<p>
|
||||||
|
Dopo un periodo di inattività del progetto, il sito del PHC viene riscritto in PHP e
|
||||||
|
trasferito sul dominio <a
|
||||||
|
href="https://web.archive.org/web/20040823112401/http://poisson.phc.unipi.it/"
|
||||||
|
>poisson.phc.unipi.it</a
|
||||||
|
>, il cui design è caratterizzato da un <a
|
||||||
|
href="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
|
||||||
|
>logo</a
|
||||||
|
> creato da <strong>Michele Cerulli</strong>.
|
||||||
|
</p>
|
||||||
|
<!-- TODO: Pensare a come mostrare questa immagine su mobile -->
|
||||||
|
<img
|
||||||
|
class="fill"
|
||||||
|
src="https://web.archive.org/web/20060609003904im_/http://poisson.phc.unipi.it/logo_studenti.orig.png"
|
||||||
|
alt="Logo"
|
||||||
|
/>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Seminari del PHC" year="2000">
|
||||||
|
<p>
|
||||||
|
Nel primo semestre del 2000, vengono tenuti diversi <a
|
||||||
|
href="https://web.archive.org/web/20010430151939/http://www.phc.unipi.it/seminari2000/"
|
||||||
|
>seminari</a
|
||||||
|
> su temi quali: installazione di programmi e sistemi operativi (come Linux), sicurezza informatica,
|
||||||
|
esperimenti con matematica e musica e creazione di pagine web.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Rete del PHC e DNS" year="1999">
|
||||||
|
<p>
|
||||||
|
Nel maggio del 1999 viene attivata la rete 131.114.10.0, con tanto di nameserver sul
|
||||||
|
dominio <a href="https://web.archive.org/web/20010410215451/http://www.phc.unipi.it/"
|
||||||
|
>phc.unipi.it</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Fondazione del PHC" year="1999">
|
||||||
|
<p>
|
||||||
|
In seguito alla proposta del prof. <strong>Sergio Steffè</strong>, in data 26 febbraio
|
||||||
|
1999 il Dipartimento di Matematica approva una delibera per stanziare la stanza 106 ed
|
||||||
|
alcuni computer ad uso di un gruppo di studenti, così da avere un luogo in cui
|
||||||
|
"smanettare", dare supporto informatica agli studenti e gestire il sito Poisson.
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
<Timeline title="Apertura di Poisson" year="~1994">
|
||||||
|
<p>
|
||||||
|
Nell'attuale Aula 4, allora semplice Aula studenti, nasce il sito web <strong
|
||||||
|
>poisson.dm.unipi.it</strong
|
||||||
|
>
|
||||||
|
su dei computer messi a disposizione agli studenti da Vinicio Villani. Una versione del 1996
|
||||||
|
di tale sito si trova nel <a
|
||||||
|
href="https://web.archive.org/web/19971017065805/http://poisson.dm.unipi.it/"
|
||||||
|
>Web Archive</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</Timeline>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
|
||||||
|
import { UtentiPage } from '../client/UtentiPage.tsx'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Utenti | PHC" pageTags="utenti">
|
||||||
|
<h1>Utenti</h1>
|
||||||
|
<UtentiPage client:load />
|
||||||
|
</PageLayout>
|
@ -0,0 +1,334 @@
|
|||||||
|
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia('(max-width: 1024px)').matches
|
||||||
|
|
||||||
|
const CELL_SIZE = !isMobile ? 27 : 19
|
||||||
|
|
||||||
|
interface Grid<T> extends Iterable<[number, number, T]> {
|
||||||
|
has(point: [number, number]): boolean
|
||||||
|
get(point: [number, number]): T
|
||||||
|
set(point: [number, number], value: T): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrid<T>(): Grid<T> {
|
||||||
|
const cells: Record<string, T> = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
has([x, y]) {
|
||||||
|
return cells[`${x},${y}`] !== undefined
|
||||||
|
},
|
||||||
|
get([x, y]) {
|
||||||
|
return cells[`${x},${y}`]
|
||||||
|
},
|
||||||
|
set([x, y], value) {
|
||||||
|
cells[`${x},${y}`] = value
|
||||||
|
},
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (const [coord, value] of Object.entries(cells)) {
|
||||||
|
const [x, y] = coord.split(',').map(s => parseInt(s))
|
||||||
|
yield [x, y, value]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
|
||||||
|
|
||||||
|
type WireDirection = 'down' | 'down-left' | 'down-right'
|
||||||
|
|
||||||
|
type WireSteps = { position: Point2; direction: WireCell }[]
|
||||||
|
|
||||||
|
type Point2 = [number, number]
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
grid: Grid<WireCell>
|
||||||
|
queuedWire: {
|
||||||
|
index: number
|
||||||
|
steps: WireSteps
|
||||||
|
} | null
|
||||||
|
badTries: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderer = {
|
||||||
|
timer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer: Renderer | null = null
|
||||||
|
|
||||||
|
const RENDERER_FPS = 30
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
console.log('Setting up circuits art...')
|
||||||
|
|
||||||
|
$canvas.width = $canvas.clientWidth * window.devicePixelRatio
|
||||||
|
$canvas.height = $canvas.clientHeight * window.devicePixelRatio
|
||||||
|
|
||||||
|
const g = $canvas.getContext('2d')!
|
||||||
|
const state: State = {
|
||||||
|
grid: createGrid(),
|
||||||
|
queuedWire: null,
|
||||||
|
badTries: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = new Date()
|
||||||
|
|
||||||
|
const handle = window.setInterval(() => {
|
||||||
|
const time = new Date().getTime() - startTime.getTime()
|
||||||
|
|
||||||
|
update(
|
||||||
|
state,
|
||||||
|
g.canvas.width / window.devicePixelRatio,
|
||||||
|
g.canvas.height / window.devicePixelRatio,
|
||||||
|
time
|
||||||
|
)
|
||||||
|
render(g, state, time)
|
||||||
|
}, 1000 / RENDERER_FPS)
|
||||||
|
|
||||||
|
renderer = { timer: handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(state: State, width: number, _height: number, _time: number) {
|
||||||
|
const w = (width / CELL_SIZE) | 0
|
||||||
|
// const h = (height / CELL_SIZE) | 0
|
||||||
|
|
||||||
|
if (state.badTries > 1000) {
|
||||||
|
console.log('finished')
|
||||||
|
clearInterval(renderer!.timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.queuedWire) {
|
||||||
|
const sx = randomInt(0, w)
|
||||||
|
if (!state.grid.has([sx, 0])) {
|
||||||
|
const steps = generateWire(state.grid, [sx, 0])
|
||||||
|
if (steps.length < 7) {
|
||||||
|
state.badTries++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.queuedWire = {
|
||||||
|
index: 0,
|
||||||
|
steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
state.grid.set(state.queuedWire.steps[0].position, 'dot')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.badTries++
|
||||||
|
} else {
|
||||||
|
const wire = state.queuedWire
|
||||||
|
|
||||||
|
const step = wire.steps[wire.index]
|
||||||
|
state.grid.set(step.position, step.direction)
|
||||||
|
|
||||||
|
if (wire.index + 1 < wire.steps.length) {
|
||||||
|
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
|
||||||
|
}
|
||||||
|
|
||||||
|
wire.index++
|
||||||
|
|
||||||
|
if (wire.index >= wire.steps.length) {
|
||||||
|
state.queuedWire = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKGROUND_COLOR = '#ecffe3'
|
||||||
|
const WIRE_COLOR = '#a6ce94'
|
||||||
|
|
||||||
|
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
|
||||||
|
'down': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(0, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-left': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(-1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-right': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(+1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'dot': g => {
|
||||||
|
g.fillStyle = BACKGROUND_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.fill()
|
||||||
|
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(g: CanvasRenderingContext2D, state: State, _time: number) {
|
||||||
|
const WIDTH = g.canvas.width / window.devicePixelRatio
|
||||||
|
const HEIGHT = g.canvas.height / window.devicePixelRatio
|
||||||
|
|
||||||
|
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
|
||||||
|
|
||||||
|
g.resetTransform()
|
||||||
|
g.scale(window.devicePixelRatio, window.devicePixelRatio)
|
||||||
|
g.scale(CELL_SIZE, CELL_SIZE)
|
||||||
|
|
||||||
|
g.lineWidth = 3 / CELL_SIZE
|
||||||
|
|
||||||
|
const w = (WIDTH / CELL_SIZE) | 0
|
||||||
|
const h = (HEIGHT / CELL_SIZE) | 0
|
||||||
|
for (let y = 0; y <= h + 1; y++) {
|
||||||
|
for (let x = 0; x <= w + 1; x++) {
|
||||||
|
if (!state.grid.has([x, y])) continue
|
||||||
|
|
||||||
|
const cell = state.grid.get([x, y])
|
||||||
|
|
||||||
|
g.save()
|
||||||
|
g.translate(x, y)
|
||||||
|
|
||||||
|
// g.fillStyle = '#f008'
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
|
||||||
|
RENDER_CELL[cell](g)
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (state.queuedWire) {
|
||||||
|
// for (const step of state.queuedWire.steps) {
|
||||||
|
// const [x, y] = step.position
|
||||||
|
|
||||||
|
// g.fillStyle = '#00f8'
|
||||||
|
// g.save()
|
||||||
|
// g.translate(x, y)
|
||||||
|
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const [mx, my] = state.mouse
|
||||||
|
// g.save()
|
||||||
|
// g.fillStyle = '#0008'
|
||||||
|
// g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(0, 0, 1, 1)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
setup()
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (renderer) {
|
||||||
|
clearInterval(renderer.timer)
|
||||||
|
}
|
||||||
|
setup()
|
||||||
|
})
|
||||||
|
|
||||||
|
function randomInt(from: number, to: number) {
|
||||||
|
return Math.floor(Math.random() * (to - from + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomChoice<T>(choices: T[]): T {
|
||||||
|
return choices[randomInt(0, choices.length - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIR_TO_VEC: Record<WireDirection, Point2> = {
|
||||||
|
['down']: [0, 1],
|
||||||
|
['down-left']: [-1, 1],
|
||||||
|
['down-right']: [+1, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortCircuitBoolean = boolean | (() => boolean)
|
||||||
|
|
||||||
|
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
|
||||||
|
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
|
||||||
|
*/
|
||||||
|
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
|
||||||
|
['down']: ([x, y], grid) =>
|
||||||
|
!grid.has([x, y + 1]) &&
|
||||||
|
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
|
||||||
|
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
|
||||||
|
['down-left']: ([x, y], grid) =>
|
||||||
|
!grid.has([x - 1, y + 1]) &&
|
||||||
|
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
|
||||||
|
['down-right']: ([x, y], grid) =>
|
||||||
|
!grid.has([x + 1, y + 1]) &&
|
||||||
|
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneDirections(
|
||||||
|
grid: Grid<WireCell>,
|
||||||
|
position: Point2,
|
||||||
|
directions: WireDirection[]
|
||||||
|
): WireDirection[] {
|
||||||
|
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWire(
|
||||||
|
grid: Grid<WireCell>,
|
||||||
|
startingPoint: Point2
|
||||||
|
): { position: Point2; direction: WireCell }[] {
|
||||||
|
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
|
||||||
|
let currentPosition = startingPoint
|
||||||
|
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
|
||||||
|
|
||||||
|
const steps: { position: Point2; direction: WireCell }[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < segmentLength; i++) {
|
||||||
|
const availableDirections = pruneDirections(grid, currentPosition, [
|
||||||
|
'down',
|
||||||
|
'down-left',
|
||||||
|
'down-right',
|
||||||
|
])
|
||||||
|
if (availableDirections.length === 0) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
const dir =
|
||||||
|
availableDirections.includes(currentDirection) && Math.random() < 0.25
|
||||||
|
? currentDirection
|
||||||
|
: randomChoice(availableDirections)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(currentDirection === 'down-left' && dir === 'down-right') ||
|
||||||
|
(currentDirection === 'down-right' && dir === 'down-left')
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = currentPosition
|
||||||
|
const [dx, dy] = DIR_TO_VEC[dir]
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
position: [x, y],
|
||||||
|
direction: dir,
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPosition = [x + dx, y + dy]
|
||||||
|
currentDirection = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = steps.at(-1)
|
||||||
|
if (last) {
|
||||||
|
last.direction = 'dot'
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps
|
||||||
|
}
|
@ -0,0 +1,821 @@
|
|||||||
|
// $news-bg: #fffbeb;
|
||||||
|
// $news-accent-bg: #f8e8b1;
|
||||||
|
|
||||||
|
@import './mixins.scss';
|
||||||
|
|
||||||
|
@layer component {
|
||||||
|
//
|
||||||
|
// Components - for complex parts of the UI like search bars or compound buttons
|
||||||
|
//
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-family: 'Material Symbols Outlined Variable';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
|
||||||
|
display: inline-grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||||||
|
|
||||||
|
max-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Iosevka', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover input[type='text'] {
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just to know for reference
|
||||||
|
.fake-masonry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--columns), 1fr);
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-row: span var(--masonry-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr auto;
|
||||||
|
grid-template-areas: 'icon text . right';
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .icon {
|
||||||
|
grid-area: icon;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
grid-area: text;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .right {
|
||||||
|
grid-area: right;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-scrollable {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.appunti-list {
|
||||||
|
padding: 2px;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appunti-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
color: #444;
|
||||||
|
|
||||||
|
& > .thumbnail {
|
||||||
|
width: 10rem;
|
||||||
|
aspect-ratio: 10 / 14;
|
||||||
|
background: #d0d0d0;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($hoverable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .thumbnail + * {
|
||||||
|
font-weight: 700;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .title,
|
||||||
|
& > .course,
|
||||||
|
& > .author,
|
||||||
|
& > .course-year {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
|
||||||
|
.article {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
|
||||||
|
background: var(--card-bg, var(--project-card-bg));
|
||||||
|
color: #000e;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
// .news-item {
|
||||||
|
// background: $news-bg;
|
||||||
|
// color: #111;
|
||||||
|
|
||||||
|
// @include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
|
||||||
|
// width: 22rem;
|
||||||
|
// max-height: 27rem;
|
||||||
|
|
||||||
|
// overflow: hidden;
|
||||||
|
|
||||||
|
// ::-webkit-scrollbar {
|
||||||
|
// width: 10px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ::-webkit-scrollbar-thumb {
|
||||||
|
// background-color: #c67e14;
|
||||||
|
// border: 2px solid #222;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// background-color: #e69419;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// a {
|
||||||
|
// font-weight: 600;
|
||||||
|
// text-decoration: none;
|
||||||
|
// color: #c67e14;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// text-decoration: underline solid 2px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .title {
|
||||||
|
// padding: 1rem;
|
||||||
|
// background: $news-accent-bg;
|
||||||
|
// line-height: 1;
|
||||||
|
// font-size: 26px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// a.title {
|
||||||
|
// color: #83530c;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .abstract {
|
||||||
|
// flex-grow: 1;
|
||||||
|
|
||||||
|
// padding: 1rem;
|
||||||
|
|
||||||
|
// overflow-y: auto;
|
||||||
|
|
||||||
|
// @extend .text;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .content {
|
||||||
|
// display: flex;
|
||||||
|
// padding: 1rem;
|
||||||
|
// flex-direction: column;
|
||||||
|
// gap: 0.5rem;
|
||||||
|
|
||||||
|
// background: #fff8da;
|
||||||
|
|
||||||
|
// & > .continue {
|
||||||
|
// padding: 1rem;
|
||||||
|
|
||||||
|
// display: grid;
|
||||||
|
// align-items: end;
|
||||||
|
// justify-content: end;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .description {
|
||||||
|
// font-size: 16px;
|
||||||
|
// line-height: 1.5;
|
||||||
|
|
||||||
|
// flex-grow: 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .tags {
|
||||||
|
// display: flex;
|
||||||
|
// gap: 0.5rem;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
// font-size: 14px;
|
||||||
|
// color: #555;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .date {
|
||||||
|
// font-size: 14px;
|
||||||
|
// font-style: italic;
|
||||||
|
// font-weight: 600;
|
||||||
|
// color: #0008;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & > .author {
|
||||||
|
// font-weight: 600;
|
||||||
|
// font-size: 15px;
|
||||||
|
// color: #555;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
--timeline-track-size: 6rem;
|
||||||
|
--timeline-thickness: 6px;
|
||||||
|
--timeline-color: #333;
|
||||||
|
|
||||||
|
max-width: 120ch;
|
||||||
|
grid-template-columns: 1fr var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
--timeline-track-size: 3rem;
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .timeline-item {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding: 1rem 1rem 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .date {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeline vertical line
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: var(--timeline-thickness);
|
||||||
|
|
||||||
|
background: var(--timeline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child::before {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child::before {
|
||||||
|
bottom: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeline circle
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background: var(--timeline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
&:nth-child(odd)::before {
|
||||||
|
grid-column: 2 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even)::before {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd)::after {
|
||||||
|
grid-column: 2 / span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even)::after {
|
||||||
|
grid-column: 1 / span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
max-width: 15rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(-10rem - 1px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(calc(-50% - var(--timeline-track-size) / 2));
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: $screen-desktop-min) {
|
||||||
|
&:nth-child(odd) {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
grid-template-columns: 1fr var(--timeline-track-size);
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
left: auto;
|
||||||
|
right: 50%;
|
||||||
|
transform: translateX(calc(50% - var(--timeline-track-size) / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
grid-column: 2 / span 2;
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(calc(-50% + var(--timeline-track-size) / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
|
||||||
|
grid-template-columns: var(--timeline-track-size) 1fr;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Cards
|
||||||
|
//
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
--card-base-internal: var(--card-base, #f8f8f8);
|
||||||
|
|
||||||
|
background: var(--card-base-internal);
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
// Variants
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 4px, $offset: 8px);
|
||||||
|
|
||||||
|
row-gap: 1rem;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child Items
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 75%);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dimmed {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: color-mix(in srgb, var(--card-base-internal), #000 60%);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline solid 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline solid 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.title {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline solid 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
padding: 0.9rem;
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
padding: 1.25rem;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Card List
|
||||||
|
//
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(25rem, auto));
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& > .card {
|
||||||
|
max-width: 25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
place-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
& > .search {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .filter-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover select {
|
||||||
|
background: var(--filter-bg-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
.combobox {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0.25rem 0 0.25rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
--filter-bg-color-hover: color-mix(in srgb, var(--filter-bg-color, #ddd), #000 10%);
|
||||||
|
|
||||||
|
background: var(--filter-bg-color, #ddd);
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
height: 100%;
|
||||||
|
gap: 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: -3px;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.5rem 0 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
&:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
.dropdown {
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
|
||||||
|
top: calc(100% + 9px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-collage {
|
||||||
|
// display: flex;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
|
||||||
|
// width: 64rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
--cell-size: 6rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, calc(var(--cell-size)));
|
||||||
|
grid-auto-rows: calc(var(--cell-size));
|
||||||
|
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
place-content: center;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .card {
|
||||||
|
padding: 3px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
grid-column: span var(--cols, 1);
|
||||||
|
grid-row: span var(--rows, 1);
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
grid-column: span 1;
|
||||||
|
grid-row: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
@media screen and (max-width: $screen-desktop-min) {
|
||||||
|
max-height: none;
|
||||||
|
|
||||||
|
width: 25rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
@import './mixins.scss';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Controls - for things like buttons, input, select
|
||||||
|
//
|
||||||
|
|
||||||
|
@layer common {
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
[role='button'] {
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: 5px 5px 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
& {
|
||||||
|
padding: 0.25rem 1.5rem;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #1e6733;
|
||||||
|
color: #f4fef7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2b8b47;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flat {
|
||||||
|
background: transparent;
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0002;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='password'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@include neo-brutalist-card;
|
||||||
|
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fdfdfd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
background: #38adc1;
|
||||||
|
|
||||||
|
min-width: 40ch;
|
||||||
|
|
||||||
|
@include neo-brutalist-card($size: 3px, $offset: 9px);
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
[role='button'] {
|
||||||
|
padding-left: 3rem;
|
||||||
|
padding-right: 3rem;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #1c7f90;
|
||||||
|
color: #f4fef7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4ea2b1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #0003;
|
||||||
|
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|