Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5897ac50f | ||
|
|
01982942a8 | ||
|
|
be8c35229b | ||
|
|
e36fddb2b3
|
||
|
|
e34f91fce6
|
||
|
|
064f42fb9f
|
||
|
|
a541740a29 | ||
|
|
d1fa37524d | ||
|
|
762d350898 | ||
|
|
15bc406494 |
@@ -1,3 +0,0 @@
|
|||||||
Dockerfile
|
|
||||||
node_modules
|
|
||||||
.git
|
|
||||||
@@ -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,8 @@
|
|||||||
# Environment files
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
tags
|
|
||||||
|
|
||||||
# Local files
|
|
||||||
*.local*
|
*.local*
|
||||||
|
bin/
|
||||||
|
|
||||||
# NodeJS
|
.out/
|
||||||
|
out/
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Don't version generated files
|
|
||||||
public/css/
|
|
||||||
public/js/
|
|
||||||
|
|
||||||
# Executables
|
|
||||||
phc-website-server
|
|
||||||
|
|||||||
@@ -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 +1,11 @@
|
|||||||
|
|
||||||
GO_SOURCES = $(shell find . -name '*.go')
|
|
||||||
GO_EXECUTABLE = phc-website-server
|
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: frontend go
|
all: frontend backend
|
||||||
|
|
||||||
#
|
|
||||||
# Build Frontend
|
|
||||||
#
|
|
||||||
|
|
||||||
.PHONY: setup
|
|
||||||
setup:
|
|
||||||
mkdir -p public/js
|
|
||||||
mkdir -p public/css
|
|
||||||
|
|
||||||
.PHONY: frontend
|
.PHONY: frontend
|
||||||
frontend: setup
|
frontend:
|
||||||
$(MAKE) -C frontend/
|
$(NPM) run build
|
||||||
cp frontend/dist/*.min.js public/js/
|
|
||||||
cp frontend/dist/*.css public/css/
|
|
||||||
|
|
||||||
#
|
.PHONY: backend
|
||||||
# Build Server
|
backend:
|
||||||
#
|
go build -v -o ./out/server ./cmd/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,30 @@
|
|||||||
# Nuovo Sito PHC
|
# PHC Website
|
||||||
|
|
||||||
Repo del server del nuovo sito per il PHC.
|
Sito web del PHC
|
||||||
|
|
||||||
## Dipendenze
|
## Usage
|
||||||
|
|
||||||
- **Golang**
|
Come usare questo progetto
|
||||||
|
|
||||||
- `github.com/alecthomas/chroma`
|
### Development
|
||||||
|
|
||||||
- `github.com/alecthomas/repr`
|
```sh
|
||||||
|
# Start the backend server and the frontend development server
|
||||||
|
$ go run -v ./cmd/dev-server
|
||||||
|
|
||||||
- `github.com/go-chi/chi/v5`
|
# Or using entr lo get live reload for go files
|
||||||
|
$ fd -e go | entr -r go run -v ./cmd/dev-server
|
||||||
- `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
|
|
||||||
$ git clone https://git.phc.dm.unipi.it/phc/website
|
|
||||||
$ cd frontend/
|
|
||||||
frontend/ $ npm install
|
|
||||||
frontend/ $ cd ..
|
|
||||||
$ make frontend
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
### Production
|
||||||
|
|
||||||
### Setup
|
```sh
|
||||||
|
$ go run ./cmd/server
|
||||||
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
|
### Deploy
|
||||||
|
|
||||||
Un comando comodo in fase di development che usa [`entr`](https://github.com/eradman/entr) è
|
```sh
|
||||||
|
$ make all
|
||||||
```bash shell
|
|
||||||
$ find . -type f -name '*.go' | entr -r go run .
|
|
||||||
# O anche con fd...
|
|
||||||
$ fd -e go | entr -r go run .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
Se si sta anche modificando codice dentro `frontend/`, in contemporanea serve anche fare
|
|
||||||
|
|
||||||
```bash shell
|
|
||||||
$ make frontend
|
|
||||||
# O anche con un watcher...
|
|
||||||
$ fd -e js | entr make frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- `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).
|
|
||||||
|
|
||||||
Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`.
|
|
||||||
|
|
||||||
## Altri Servizi
|
|
||||||
|
|
||||||
Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi.
|
|
||||||
|
|
||||||
Il servizio di autenticazione di default girerà su `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
|
||||||
|
|||||||
|
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,15 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
publicDir: './client/public',
|
||||||
|
srcDir: './client',
|
||||||
|
outDir: './out/client',
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://127.0.0.1:4000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="astro/client" />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from './PageLayout.astro'
|
||||||
|
|
||||||
|
const { frontmatter } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout {...frontmatter} />
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import '@fontsource/open-sans/latin.css'
|
||||||
|
import '@fontsource/source-sans-pro/latin.css'
|
||||||
|
import '@fontsource/source-code-pro/latin.css'
|
||||||
|
|
||||||
|
import '../styles/main.scss'
|
||||||
|
|
||||||
|
const { title, description, thumbnail, pageName } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta property="og:title" content={title ?? 'PHC'} />
|
||||||
|
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
||||||
|
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
|
||||||
|
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body class:list={['page-' + (pageName ?? 'unknown')]}>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<header>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||||
|
</div>
|
||||||
|
<div class="links">
|
||||||
|
<a role="button" href="#">Utenti</a>
|
||||||
|
<a role="button" href="#">Notizie</a>
|
||||||
|
<a role="button" href="#">Progetti</a>
|
||||||
|
<a role="button" href="#">About</a>
|
||||||
|
<a class="primary" role="button" href="#">Accedi</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<footer>© PHC 2023</footer>
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '../layouts/PageLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout pageName="homepage">
|
||||||
|
<section class="principal">
|
||||||
|
<div class="circuit-layer">
|
||||||
|
<canvas id="circuits-art"></canvas>
|
||||||
|
<script src="../scripts/circuits-art.ts"></script>
|
||||||
|
</div>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
|
||||||
|
</div>
|
||||||
|
<div class="whats-phc">
|
||||||
|
<div class="title">Cos'è il PHC?</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Totam vero deserunt tempore reprehenderit atque, voluptate
|
||||||
|
dolorum excepturi libero pariatur sequi?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Laboriosam soluta ab a illum mollitia quaerat quia, veniam consequatur expedita dolore autem reiciendis quae rem
|
||||||
|
excepturi optio? Maiores, hic?
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Exercitationem error atque amet. Tempora earum nemo eveniet
|
||||||
|
aspernatur quam quas, doloribus expedita quisquam dignissimos cupiditate inventore a modi optio harum veritatis,
|
||||||
|
adipisci ab ullam distinctio odio quod delectus ipsum, rerum animi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="news">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-1" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#C2A8EB" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-1)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Ultime Notizie</div>
|
||||||
|
|
||||||
|
<div class="news-list">
|
||||||
|
<div class="news">
|
||||||
|
<div class="title">Lorem ipsum dolor sit.</div>
|
||||||
|
<div class="abstract">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||||
|
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||||
|
voluptatem pariatur eius!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||||
|
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="news">
|
||||||
|
<div class="title">Tempore provident impedit libero?</div>
|
||||||
|
<div class="abstract">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||||
|
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||||
|
voluptatem pariatur eius!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||||
|
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||||
|
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||||
|
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="news">
|
||||||
|
<div class="title">Alias molestias consectetur quam</div>
|
||||||
|
<div class="abstract">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||||
|
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||||
|
voluptatem pariatur eius!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="news">
|
||||||
|
<div class="title">Inventore dignissimos sapiente nulla</div>
|
||||||
|
<div class="abstract">
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis reprehenderit porro omnis enim deleniti esse quos,
|
||||||
|
architecto adipisci veritatis, iusto perferendis aperiam recusandae exercitationem doloribus, illum commodi
|
||||||
|
voluptatem pariatur eius!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Impedit ut quod aspernatur vitae vero incidunt cupiditate perferendis explicabo sunt possimus rerum expedita dicta
|
||||||
|
nesciunt enim mollitia iure ullam aut, pariatur, at cumque. Nemo obcaecati eaque recusandae fugit sed!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="primary" href="#" role="button">Vai all'Archivio</a>
|
||||||
|
</section>
|
||||||
|
<section class="projects">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Progetti</div>
|
||||||
|
|
||||||
|
<div class="project-list">
|
||||||
|
<a href="#">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">Gitea</div>
|
||||||
|
<div class="description">
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||||
|
accusantium labore explicabo quaerat incidunt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">Zulip</div>
|
||||||
|
<div class="description">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellendus, veritatis.</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">Orario</div>
|
||||||
|
<div class="description">
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||||
|
accusantium labore explicabo quaerat incidunt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">Problemi</div>
|
||||||
|
<div class="description">
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non, hic libero beatae voluptatem incidunt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<div class="project">
|
||||||
|
<div class="image">
|
||||||
|
<div class="box"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">Cluster "Steffè"</div>
|
||||||
|
<div class="description">
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus quam iure dolor. Excepturi facere, ipsa
|
||||||
|
accusantium labore explicabo quaerat incidunt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="wanna-be-macchinista">
|
||||||
|
<div class="zig-zag">
|
||||||
|
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
<pattern id="zig-zag-3" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||||
|
<path fill="#888" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#zig-zag-3)" x="0" y="0" width="1000" height="1"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Vuoi diventare macchinista?</div>
|
||||||
|
</section>
|
||||||
|
</PageLayout>
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
@@ -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 |
@@ -0,0 +1,316 @@
|
|||||||
|
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
|
||||||
|
|
||||||
|
interface Grid<T> extends Iterable<[number, number, T]> {
|
||||||
|
has(point: [number, number]): boolean
|
||||||
|
get(point: [number, number]): T
|
||||||
|
set(point: [number, number], value: T): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrid<T>(): Grid<T> {
|
||||||
|
const cells: Record<string, T> = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
has([x, y]) {
|
||||||
|
return cells[`${x},${y}`] !== undefined
|
||||||
|
},
|
||||||
|
get([x, y]) {
|
||||||
|
return cells[`${x},${y}`]
|
||||||
|
},
|
||||||
|
set([x, y], value) {
|
||||||
|
cells[`${x},${y}`] = value
|
||||||
|
},
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (const [coord, value] of Object.entries(cells)) {
|
||||||
|
const [x, y] = coord.split(',').map(s => parseInt(s))
|
||||||
|
yield [x, y, value]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
|
||||||
|
|
||||||
|
type WireDirection = 'down' | 'down-left' | 'down-right'
|
||||||
|
|
||||||
|
type WireSteps = { position: Point2; direction: WireCell }[]
|
||||||
|
|
||||||
|
type Point2 = [number, number]
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
grid: Grid<WireCell>
|
||||||
|
queuedWire: {
|
||||||
|
index: number
|
||||||
|
steps: WireSteps
|
||||||
|
} | null
|
||||||
|
badTries: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderer = {
|
||||||
|
timer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer: Renderer | null = null
|
||||||
|
|
||||||
|
const RENDERER_FPS = 30
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
console.log('Setting up circuits art...')
|
||||||
|
|
||||||
|
$canvas.width = $canvas.clientWidth
|
||||||
|
$canvas.height = $canvas.clientHeight
|
||||||
|
|
||||||
|
const g = $canvas.getContext('2d')!
|
||||||
|
const state: State = {
|
||||||
|
grid: createGrid(),
|
||||||
|
queuedWire: null,
|
||||||
|
badTries: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = new Date()
|
||||||
|
|
||||||
|
const handle = setInterval(() => {
|
||||||
|
const time = new Date().getTime() - startTime.getTime()
|
||||||
|
|
||||||
|
update(state, g.canvas.width, g.canvas.height, time)
|
||||||
|
render(g, state, time)
|
||||||
|
}, 1000 / RENDERER_FPS)
|
||||||
|
|
||||||
|
renderer = { timer: handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(state: State, width: number, height: number, time: number) {
|
||||||
|
const w = (width / CELL_SIZE) | 0
|
||||||
|
// const h = (height / CELL_SIZE) | 0
|
||||||
|
|
||||||
|
if (state.badTries > 1000) {
|
||||||
|
console.log('finished')
|
||||||
|
clearInterval(renderer!.timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.queuedWire) {
|
||||||
|
const sx = randomInt(0, w)
|
||||||
|
if (!state.grid.has([sx, 0])) {
|
||||||
|
const steps = generateWire(state.grid, [sx, 0])
|
||||||
|
if (steps.length < 7) {
|
||||||
|
state.badTries++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.queuedWire = {
|
||||||
|
index: 0,
|
||||||
|
steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
state.grid.set(state.queuedWire.steps[0].position, 'dot')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.badTries++
|
||||||
|
} else {
|
||||||
|
const wire = state.queuedWire
|
||||||
|
|
||||||
|
const step = wire.steps[wire.index]
|
||||||
|
state.grid.set(step.position, step.direction)
|
||||||
|
|
||||||
|
if (wire.index + 1 < wire.steps.length) {
|
||||||
|
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
|
||||||
|
}
|
||||||
|
|
||||||
|
wire.index++
|
||||||
|
|
||||||
|
if (wire.index >= wire.steps.length) {
|
||||||
|
state.queuedWire = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CELL_SIZE = 27
|
||||||
|
|
||||||
|
const BACKGROUND_COLOR = '#ecffe3'
|
||||||
|
const WIRE_COLOR = '#a6ce94'
|
||||||
|
|
||||||
|
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
|
||||||
|
'down': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(0, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-left': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(-1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'down-right': g => {
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.lineTo(+1, 1)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
'dot': g => {
|
||||||
|
g.fillStyle = BACKGROUND_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.fill()
|
||||||
|
|
||||||
|
g.strokeStyle = WIRE_COLOR
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||||
|
g.stroke()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(g: CanvasRenderingContext2D, state: State, time: number) {
|
||||||
|
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
|
||||||
|
g.resetTransform()
|
||||||
|
g.scale(CELL_SIZE, CELL_SIZE)
|
||||||
|
g.lineWidth = 3 / CELL_SIZE
|
||||||
|
|
||||||
|
const w = (g.canvas.width / CELL_SIZE) | 0
|
||||||
|
const h = (g.canvas.height / CELL_SIZE) | 0
|
||||||
|
for (let y = 0; y <= h + 1; y++) {
|
||||||
|
for (let x = 0; x <= w + 1; x++) {
|
||||||
|
if (!state.grid.has([x, y])) continue
|
||||||
|
|
||||||
|
const cell = state.grid.get([x, y])
|
||||||
|
|
||||||
|
g.save()
|
||||||
|
g.translate(x, y)
|
||||||
|
|
||||||
|
// g.fillStyle = '#f008'
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
|
||||||
|
RENDER_CELL[cell](g)
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (state.queuedWire) {
|
||||||
|
// for (const step of state.queuedWire.steps) {
|
||||||
|
// const [x, y] = step.position
|
||||||
|
|
||||||
|
// g.fillStyle = '#00f8'
|
||||||
|
// g.save()
|
||||||
|
// g.translate(x, y)
|
||||||
|
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const [mx, my] = state.mouse
|
||||||
|
// g.save()
|
||||||
|
// g.fillStyle = '#0008'
|
||||||
|
// g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
|
||||||
|
// g.beginPath()
|
||||||
|
// g.rect(0, 0, 1, 1)
|
||||||
|
// g.fill()
|
||||||
|
// g.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
setup()
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (renderer) {
|
||||||
|
clearInterval(renderer.timer)
|
||||||
|
}
|
||||||
|
setup()
|
||||||
|
})
|
||||||
|
|
||||||
|
function randomInt(from: number, to: number) {
|
||||||
|
return Math.floor(Math.random() * (to - from + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomChoice<T>(choices: T[]): T {
|
||||||
|
return choices[randomInt(0, choices.length - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomWeightedChoice<T>(choices: [T, number][]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 + 4 + 5 + 6
|
||||||
|
randomWeightedChoice([
|
||||||
|
['a', 3],
|
||||||
|
['b', 4],
|
||||||
|
['c', 5],
|
||||||
|
['d', 6],
|
||||||
|
])
|
||||||
|
|
||||||
|
const DIR_TO_VEC: Record<WireDirection, Point2> = {
|
||||||
|
['down']: [0, 1],
|
||||||
|
['down-left']: [-1, 1],
|
||||||
|
['down-right']: [+1, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortCircuitBoolean = boolean | (() => boolean)
|
||||||
|
|
||||||
|
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
|
||||||
|
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
|
||||||
|
*/
|
||||||
|
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
|
||||||
|
['down']: ([x, y], grid) =>
|
||||||
|
!grid.has([x, y + 1]) &&
|
||||||
|
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
|
||||||
|
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
|
||||||
|
['down-left']: ([x, y], grid) => !grid.has([x - 1, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
|
||||||
|
['down-right']: ([x, y], grid) =>
|
||||||
|
!grid.has([x + 1, y + 1]) && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneDirections(grid: Grid<WireCell>, position: Point2, directions: WireDirection[]): WireDirection[] {
|
||||||
|
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWire(grid: Grid<WireCell>, startingPoint: Point2): { position: Point2; direction: WireCell }[] {
|
||||||
|
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
|
||||||
|
let currentPosition = startingPoint
|
||||||
|
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
|
||||||
|
|
||||||
|
const steps: { position: Point2; direction: WireCell }[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < segmentLength; i++) {
|
||||||
|
const availableDirections = pruneDirections(grid, currentPosition, ['down', 'down-left', 'down-right'])
|
||||||
|
if (availableDirections.length === 0) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
const dir =
|
||||||
|
availableDirections.includes(currentDirection) && Math.random() < 0.25
|
||||||
|
? currentDirection
|
||||||
|
: randomChoice(availableDirections)
|
||||||
|
|
||||||
|
if ((currentDirection === 'down-left' && dir === 'down-right') || (currentDirection === 'down-right' && dir === 'down-left')) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = currentPosition
|
||||||
|
const [dx, dy] = DIR_TO_VEC[dir]
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
position: [x, y],
|
||||||
|
direction: dir,
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPosition = [x + dx, y + dy]
|
||||||
|
currentDirection = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = steps.at(-1)
|
||||||
|
if (last) {
|
||||||
|
last.direction = 'dot'
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps
|
||||||
|
}
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-snap-type: y proximity;
|
||||||
|
scroll-padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Typography
|
||||||
|
//
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p + p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Controls
|
||||||
|
//
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
[role='button'] {
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
border: 3px solid #222;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 4px 4px 0 0 #222;
|
||||||
|
|
||||||
|
transition: all 64ms linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 0 #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.25rem 1.5rem;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #1e6733;
|
||||||
|
color: #f4fef7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2b8b47;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Pages
|
||||||
|
//
|
||||||
|
|
||||||
|
header {
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
height: 6rem;
|
||||||
|
border-bottom: 4px solid #222;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
min-height: 6rem;
|
||||||
|
border-top: 4px solid #222;
|
||||||
|
background: #444;
|
||||||
|
color: #fdfdfd;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-homepage {
|
||||||
|
header .logo {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
min-height: calc(100vh - 6rem);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
min-height: calc(100vh - 10rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
scroll-snap-align: start;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 60px;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zig-zag {
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.principal {
|
||||||
|
z-index: -2;
|
||||||
|
|
||||||
|
min-height: calc(100vh - 2rem);
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6rem;
|
||||||
|
|
||||||
|
padding: 6rem 0;
|
||||||
|
|
||||||
|
background: #ecffe3;
|
||||||
|
// circuit color
|
||||||
|
// background: #a6ce94;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.circuit-layer {
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding-top: 6rem;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 640px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
filter: drop-shadow(6px 6px 0 #222) drop-shadow(4px 0 0 #222) drop-shadow(0 4px 0 #222) drop-shadow(-4px 0 0 #222)
|
||||||
|
drop-shadow(0 -4px 0 #222);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.whats-phc {
|
||||||
|
background: #e4c5ff;
|
||||||
|
|
||||||
|
border: 4px solid #222;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 6px 6px 0 0 #222;
|
||||||
|
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
max-width: 37rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@extend .text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.news {
|
||||||
|
background: #c2a8eb;
|
||||||
|
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
|
.news-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
padding: 0 3rem;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.news {
|
||||||
|
background: #fffbeb;
|
||||||
|
|
||||||
|
border: 3px solid #222;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: 9px 9px 0 0 #222;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 22rem;
|
||||||
|
max-height: 27rem;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c67e14;
|
||||||
|
border: 2px solid #222;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e69419;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #c67e14;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline solid 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
// border-bottom: 2px solid #222;
|
||||||
|
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8e8b1;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
& > .abstract {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@extend .text;
|
||||||
|
}
|
||||||
|
& > .continue {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[role='button'] {
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
// background: #824ed4;
|
||||||
|
// color: #f0e6ff;
|
||||||
|
background: #f8e8b1;
|
||||||
|
color: #000d;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
// background: #8e5ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.projects {
|
||||||
|
background: #f5f2cc;
|
||||||
|
|
||||||
|
padding-bottom: 6rem;
|
||||||
|
|
||||||
|
.project-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||||
|
gap: 3rem;
|
||||||
|
|
||||||
|
padding: 0 6rem;
|
||||||
|
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.project {
|
||||||
|
// background: #fcddff;
|
||||||
|
// background: #ffa89c;
|
||||||
|
background: #a2d4f3;
|
||||||
|
color: #000e;
|
||||||
|
|
||||||
|
border: 3px solid #222;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: 9px 9px 0 0 #222;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
|
||||||
|
gap: 0.25rem 1rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
grid-row: span 2;
|
||||||
|
// place-self: center;
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background: #0003;
|
||||||
|
border: 3px solid #0006;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: all 128ms ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(0, -8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.wanna-be-macchinista {
|
||||||
|
background: #888;
|
||||||
|
color: #fdfdfd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Misc
|
||||||
|
//
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:vertical {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
border-top: 2px solid #222;
|
||||||
|
border-bottom: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:horizontal {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-top: 2px solid #222;
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
border-right: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #1e6733;
|
||||||
|
border: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #2b8b47;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
// border-left: 2px solid #222;
|
||||||
|
// border-top: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
l := sl.New()
|
||||||
|
|
||||||
|
//
|
||||||
|
// Setup the application
|
||||||
|
//
|
||||||
|
|
||||||
|
// Config
|
||||||
|
sl.ProvideFunc(l, config.Slot, config.Configure)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Server
|
||||||
|
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||||
|
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||||
|
listautenti.MountApiRoutesHook,
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start the application
|
||||||
|
//
|
||||||
|
|
||||||
|
sl.MustInvoke(l, server.Slot)
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
|
||||||
|
cfg := sl.MustUse(l, config.Slot)
|
||||||
|
|
||||||
|
cmd := exec.Command(cfg.NpmCommand, "run", "dev")
|
||||||
|
cmd.Stdout = w
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
l := sl.New()
|
||||||
|
|
||||||
|
//
|
||||||
|
// Setup the application
|
||||||
|
//
|
||||||
|
|
||||||
|
// Config
|
||||||
|
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
sl.Provide[database.Database](l, database.Slot, &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Server
|
||||||
|
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||||
|
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||||
|
listautenti.MountApiRoutesHook,
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start the application
|
||||||
|
//
|
||||||
|
|
||||||
|
err := sl.Invoke(l, server.Slot)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 +1,28 @@
|
|||||||
module git.phc.dm.unipi.it/phc/website
|
module git.phc.dm.unipi.it/phc/website
|
||||||
|
|
||||||
go 1.18
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma v0.9.4
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
github.com/gofiber/fiber/v2 v2.34.0
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/gofiber/redirect/v2 v2.1.23
|
github.com/gofiber/fiber/v2 v2.44.0 // indirect
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/yuin/goldmark v1.4.4
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
github.com/klauspost/compress v1.16.5 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
)
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
require (
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||||
github.com/klauspost/compress v1.15.6 // indirect
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/tinylib/msgp v1.1.8 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
github.com/valyala/fasthttp v1.46.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,76 +1,93 @@
|
|||||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
|
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
|
||||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
||||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
|
||||||
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
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.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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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-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/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.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/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
// This package provides a small framework to work with SQL databases using
|
||||||
|
// generics. The most important part for now is the "Ref[T]" type that lets
|
||||||
|
// us pass around typed IDs to table rows in a type safe manner.
|
||||||
|
//
|
||||||
|
// For now this library uses both generics for keeping the interface type safe
|
||||||
|
// and reflection to extract automatically some metedata from structs that
|
||||||
|
// represent tables.
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option è un valore che nel database può potenzialmente essere "NULL"
|
||||||
|
//
|
||||||
|
// NOTE/TODO: Magari si può fare una cosa fatta bene e nascondere
|
||||||
|
// l'implementazione al Go, ad esempio Option[string] può essere NULL o testo
|
||||||
|
// nel db mentre Option[time.Time] può essere codificato in json come
|
||||||
|
//
|
||||||
|
// "{ present: false }"
|
||||||
|
//
|
||||||
|
// oppure
|
||||||
|
//
|
||||||
|
// "{ present: true, value: '2023/04/...' }"
|
||||||
|
//
|
||||||
|
// in modo da semplificare leggermente la processazione in Go. Altrimenti si
|
||||||
|
// può separare in Option[T] e Nullable[T].
|
||||||
|
type Option[T any] struct {
|
||||||
|
present bool
|
||||||
|
value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Option[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
if !o.present {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(o.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Option[T]) UnmarshalJSON(v []byte) error {
|
||||||
|
if slices.Equal(v, []byte("null")) {
|
||||||
|
var zero T
|
||||||
|
o.present = false
|
||||||
|
o.value = zero
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(v, &o.value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOption[T any](value T) Option[T] {
|
||||||
|
return Option[T]{true, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmptyOption[T any]() Option[T] {
|
||||||
|
return Option[T]{present: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Option[T]) SqlType() string {
|
||||||
|
return "BLOB"
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (v *Option[T]) Scan(a any) error {
|
||||||
|
// data, ok := a.([]byte)
|
||||||
|
// if !ok {
|
||||||
|
// return fmt.Errorf(`scan expected []byte`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// m := map[string]any{}
|
||||||
|
// if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if present, _ := m["present"].(bool); present {
|
||||||
|
|
||||||
|
// m["value"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ref è un id tipato da un'entità presente nel database. È il tipo fondamentale
|
||||||
|
// esportato da questo package. Non dovrebbe essere troppo importante ma
|
||||||
|
// internamente è una stringa, inoltre è consigliato che una struct
|
||||||
|
// con _primary key_ abbia un campo di tipo Ref a se stessa.
|
||||||
|
//
|
||||||
|
// type User struct {
|
||||||
|
// Id db.Ref[User] // meglio se unico e immutabile
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
type Ref[T any] string
|
||||||
|
|
||||||
|
func (Ref[T]) SqlType() string {
|
||||||
|
return "TEXT"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Ref[T]) Scan(a any) error {
|
||||||
|
s, ok := a.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf(`scan expected string`)
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = Ref[T](s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlValue interface {
|
||||||
|
SqlType() string
|
||||||
|
sql.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
type Table[T any] struct {
|
||||||
|
Name string
|
||||||
|
PrimaryKey func(value *T, pk ...string) Ref[T]
|
||||||
|
Columns [][2]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var commonTypes = map[string]string{
|
||||||
|
"string": "TEXT",
|
||||||
|
"int": "INTEGER",
|
||||||
|
"int32": "INTEGER",
|
||||||
|
"int64": "INTEGER",
|
||||||
|
"float32": "REAL",
|
||||||
|
"float64": "REAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSqlType(typ reflect.Type) string {
|
||||||
|
st, ok := reflect.New(typ).Interface().(sqlValue)
|
||||||
|
if ok {
|
||||||
|
return st.SqlType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonTypes[typ.Name()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func AutoTable[T any](name string) Table[T] {
|
||||||
|
columns := [][2]string{}
|
||||||
|
pkIndex := -1
|
||||||
|
|
||||||
|
var zero T
|
||||||
|
typ := reflect.TypeOf(zero)
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
fieldTyp := typ.Field(i)
|
||||||
|
isPK, _, typ, col := fieldInfo(fieldTyp)
|
||||||
|
if isPK {
|
||||||
|
pkIndex = i
|
||||||
|
}
|
||||||
|
columns = append(columns, [2]string{col, toSqlType(typ)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkIndex == -1 {
|
||||||
|
panic(fmt.Sprintf("struct %T has no primary key field", zero))
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug logging
|
||||||
|
// log.Printf("[auto table] struct %T has primary key %q", zero, typ.Field(pkIndex).Name)
|
||||||
|
|
||||||
|
return Table[T]{
|
||||||
|
Name: name,
|
||||||
|
PrimaryKey: func(value *T, pk ...string) Ref[T] {
|
||||||
|
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||||
|
// they are the same type so this should be safe
|
||||||
|
ptr := reflect.ValueOf(value).Elem().Field(pkIndex).Addr().UnsafePointer()
|
||||||
|
pkPtr := (*string)(ptr)
|
||||||
|
|
||||||
|
if len(pk) > 0 {
|
||||||
|
*pkPtr = pk[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ref[T](*pkPtr)
|
||||||
|
},
|
||||||
|
Columns: columns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table Table[T]) CreateIfNotExists(dbConn *sql.DB) error {
|
||||||
|
columns := make([]string, len(table.Columns))
|
||||||
|
|
||||||
|
for i, col := range table.Columns {
|
||||||
|
columns[i] = fmt.Sprintf("%s %s", col[0], col[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s(%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(stmt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create[T any](dbConn *sql.DB, table Table[T], value T) (Ref[T], error) {
|
||||||
|
id := randomHex(15)
|
||||||
|
|
||||||
|
table.PrimaryKey(&value, id)
|
||||||
|
|
||||||
|
columns, values := structDatabaseColumnValues(&value)
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := dbConn.Exec(stmt, values...); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ref[T](id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Insert[T any](dbConn *sql.DB, table Table[T], value T) error {
|
||||||
|
columns, values := structDatabaseColumnValues(&value)
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(stmt, values...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read[T any](dbConn *sql.DB, table Table[T], id Ref[T]) (T, error) {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAll[T any](dbConn *sql.DB, table Table[T]) ([]T, error) {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update[T any](dbConn *sql.DB, table Table[T], id Ref[T], value T) error {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete[T any](dbConn *sql.DB, table Table[T], id Ref[T]) error {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package db_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockDB struct {
|
||||||
|
*testing.T
|
||||||
|
|
||||||
|
LastQuery string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Prepare(query string) (driver.Stmt, error) { return nil, nil }
|
||||||
|
func (db *mockDB) Close() error { return nil }
|
||||||
|
func (db *mockDB) Begin() (driver.Tx, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (db *mockDB) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||||
|
db.LastQuery = query
|
||||||
|
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||||
|
db.LastQuery = query
|
||||||
|
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Open(name string) (driver.Conn, error) {
|
||||||
|
return &mockDB{T: db.T}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Connect(context.Context) (driver.Conn, error) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Driver() driver.Driver {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tests
|
||||||
|
//
|
||||||
|
|
||||||
|
func TestDb1(t *testing.T) {
|
||||||
|
type Product struct {
|
||||||
|
Id db.Ref[Product] `db:"id*"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Quantity int `db:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
products := db.AutoTable[Product]("products")
|
||||||
|
|
||||||
|
mock := &mockDB{T: t}
|
||||||
|
conn := sql.OpenDB(mock)
|
||||||
|
|
||||||
|
err := products.CreateIfNotExists(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`CREATE TABLE IF NOT EXISTS products(id TEXT, title TEXT, quantity INTEGER)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref1, err := db.Create(conn, products, Product{Title: "Foo", Quantity: 27})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Assert(t, string(ref1) != "")
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := db.Insert(conn, products, Product{Id: "123", Title: "Bar", Quantity: 42}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package db
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomHex(len int) string {
|
||||||
|
buff := make([]byte, len/2+1)
|
||||||
|
rand.Read(buff)
|
||||||
|
str := hex.EncodeToString(buff)
|
||||||
|
return str[:len]
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldInfo(fieldTyp reflect.StructField) (bool, string, reflect.Type, string) {
|
||||||
|
fieldName := fieldTyp.Name
|
||||||
|
columnName, ok := fieldTyp.Tag.Lookup("db")
|
||||||
|
if !ok {
|
||||||
|
columnName = strings.ToLower(fieldName)
|
||||||
|
}
|
||||||
|
if ok && columnName[len(columnName)-1] == '*' {
|
||||||
|
return true, fieldName, fieldTyp.Type, columnName[:len(columnName)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fieldName, fieldTyp.Type, columnName
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldPrimaryKey(fieldTyp reflect.StructField) bool {
|
||||||
|
isPK, _, _, _ := fieldInfo(fieldTyp)
|
||||||
|
return isPK
|
||||||
|
}
|
||||||
|
|
||||||
|
// structDatabaseColumnValues takes a pointer to a struct and returns a pair
|
||||||
|
// of slices, one with the column names and the other with pointers to all
|
||||||
|
// its exported fields.
|
||||||
|
//
|
||||||
|
// The column name can be changed using the "db" tag attribute on that field
|
||||||
|
func structDatabaseColumnValues(s any) ([]string, []any) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
numFields := v.NumField()
|
||||||
|
|
||||||
|
names := []string{}
|
||||||
|
values := []any{}
|
||||||
|
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
fieldVal := v.Field(i)
|
||||||
|
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
key := strings.ToLower(fieldTyp.Name)
|
||||||
|
|
||||||
|
if name, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||||
|
key = name
|
||||||
|
|
||||||
|
// primary key has a "*" at the end, exclude from column name
|
||||||
|
if key[len(key)-1] == '*' {
|
||||||
|
key = key[:len(key)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names = append(names, key)
|
||||||
|
values = append(values, fieldVal.Addr().Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, values
|
||||||
|
}
|
||||||
|
|
||||||
|
func structPrimaryKeyPtr(s any) (*string, bool) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
fieldVal := v.Field(i)
|
||||||
|
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
if fieldPrimaryKey(fieldTyp) {
|
||||||
|
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||||
|
// they are the same type so this should be safe
|
||||||
|
ptr := fieldVal.Addr().UnsafePointer()
|
||||||
|
return (*string)(ptr), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// structDatabasePrimaryKeyColumn takes a pointer to a struct and returns the
|
||||||
|
// name of the field with the primary key
|
||||||
|
func structDatabasePrimaryKeyColumn(s any) (field, column string, ok bool) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
key := fieldTyp.Name
|
||||||
|
if value, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||||
|
if value[len(value)-1] == '*' {
|
||||||
|
return key, value[:len(value)-1], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func repeatSlice[T any](value T, count int) []T {
|
||||||
|
s := make([]T, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
s[i] = value
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
// The [sl] package has two main concepts, the [ServiceLocator] itself is the
|
||||||
|
// main object that one should pass around through the application. A
|
||||||
|
// [ServiceLocator] has a list of slots that can be filled with
|
||||||
|
// [ProvideFunc] and [Provide] and retrieved with [Use]. Slots should
|
||||||
|
// be unique by type and they can only be created with the [NewSlot] function.
|
||||||
|
//
|
||||||
|
// The usual way to use this module is to make slots for Go interfaces and
|
||||||
|
// then pass implementations using the [Provide] and
|
||||||
|
// [ProvideFunc] functions.
|
||||||
|
//
|
||||||
|
// Services can be of various types:
|
||||||
|
// - a service with no dependencies can be directly injected inside a
|
||||||
|
// ServiceLocator using [Provide].
|
||||||
|
// - a service with dependencies on other service should use
|
||||||
|
// [ProvideFunc]. This lets the service configure itself when needed
|
||||||
|
// and makes the developer not think about correctly ordering the
|
||||||
|
// recursive configuration of its dependencies.
|
||||||
|
// - a service can also be private, in this case the slot for a service
|
||||||
|
// should be a private field in the service package. This kind of
|
||||||
|
// services should also provide a way to inject them into a
|
||||||
|
// ServiceLocator.
|
||||||
|
// - a package can also just provide a slot with some value. This is useful
|
||||||
|
// for using the ServiceLocator to easily pass around values, effectively
|
||||||
|
// threating slots just as dynamically scoped variables.
|
||||||
|
package sl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the debug logger
|
||||||
|
//
|
||||||
|
// TODO: in the future this will be disabled and discard by default.
|
||||||
|
//
|
||||||
|
// As this is the service locator module it was meaning less to pass this
|
||||||
|
// through the ServiceLocator itself (without making the whole module more
|
||||||
|
// complex)
|
||||||
|
var Logger *log.Logger = log.New(os.Stderr, "[service locator] ", log.Lmsgprefix)
|
||||||
|
|
||||||
|
// slot is just a "typed" unique "symbol".
|
||||||
|
//
|
||||||
|
// This must be defined like so and not for example "struct{ typeName string }"
|
||||||
|
// because we might want to have more slots for the same type.
|
||||||
|
type slot[T any] *struct{}
|
||||||
|
|
||||||
|
type Hook[T any] func(*ServiceLocator, T) error
|
||||||
|
|
||||||
|
// hook is just a typed unique symbol
|
||||||
|
type hook[T any] *struct{}
|
||||||
|
|
||||||
|
// NewSlot is the only way to create instances of the slot type. Each instance
|
||||||
|
// is unique.
|
||||||
|
//
|
||||||
|
// This then lets you attach a service instance of type "T" to a
|
||||||
|
// [ServiceLocator] object.
|
||||||
|
func NewSlot[T any]() slot[T] {
|
||||||
|
return slot[T](new(struct{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHook is the only way to create instances of the hook type. Each instance
|
||||||
|
// is unique.
|
||||||
|
//
|
||||||
|
// This lets you have a service dispatch an hook
|
||||||
|
func NewHook[T any]() hook[T] {
|
||||||
|
return hook[T](new(struct{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// slotEntry represents a service that can lazily configured
|
||||||
|
// (using "configureFunc"). Once configured the instance is kept in the "value"
|
||||||
|
// field and "created" will always be "true". The field "typeName" just for
|
||||||
|
// debugging purposes.
|
||||||
|
type slotEntry struct {
|
||||||
|
// typeName is just used for debugging purposes
|
||||||
|
typeName string
|
||||||
|
|
||||||
|
// configureFunc is used by lazily provided slot values to tell how to
|
||||||
|
// configure them self when required
|
||||||
|
configureFunc func(*ServiceLocator) (any, error)
|
||||||
|
|
||||||
|
// configured tells if this slot is already configured
|
||||||
|
configured bool
|
||||||
|
|
||||||
|
// value for this slot
|
||||||
|
value any
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConfigured tries to call configure on this slot entry if not already configured
|
||||||
|
func (s *slotEntry) checkConfigured(l *ServiceLocator) error {
|
||||||
|
if !s.configured {
|
||||||
|
v, err := s.configureFunc(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] configured service of type %T`, s.typeName, v)
|
||||||
|
|
||||||
|
s.configured = true
|
||||||
|
s.value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type hookEntry struct {
|
||||||
|
// typeName is just used for debugging purposes
|
||||||
|
typeName string
|
||||||
|
|
||||||
|
// listeners is a list of functions to call when this hook is called
|
||||||
|
listeners []func(*ServiceLocator, any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLocator is the main context passed around to retrive service
|
||||||
|
// instances, the interface uses generics so to inject and retrive service
|
||||||
|
// instances you should use the functions [Provide], [ProvideFunc] and [Use].
|
||||||
|
// This is essentially a dictionary indexed by slots that are them self just
|
||||||
|
// typed unique symbols
|
||||||
|
type ServiceLocator struct {
|
||||||
|
providers map[any]*slotEntry
|
||||||
|
hooks map[any]*hookEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new [ServiceLocator] context to pass around in the application.
|
||||||
|
func New() *ServiceLocator {
|
||||||
|
return &ServiceLocator{
|
||||||
|
providers: map[any]*slotEntry{},
|
||||||
|
hooks: map[any]*hookEntry{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide will inject a concrete instance inside the ServiceLocator "l" for
|
||||||
|
// the given "slotKey". This should be used for injecting "static" services, for
|
||||||
|
// instances whose construction depend on other services you should use the
|
||||||
|
// [ProvideFunc] function.
|
||||||
|
//
|
||||||
|
// This is generic over "T" to check that instances for the given slot type
|
||||||
|
// check as "T" can also be an interface.
|
||||||
|
func Provide[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
||||||
|
typeName := getTypeName[T]()
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] provided value of type %T`, typeName, value)
|
||||||
|
|
||||||
|
l.providers[slotKey] = &slotEntry{
|
||||||
|
typeName: typeName,
|
||||||
|
configured: true,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideFunc will inject an instance inside the given ServiceLocator
|
||||||
|
// and "slotKey" that is created only when requested with a call to the
|
||||||
|
// [Use] function.
|
||||||
|
//
|
||||||
|
// This is generic over "T" to check that instances for the given slot type
|
||||||
|
// check as "T" can also be an interface.
|
||||||
|
func ProvideFunc[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
||||||
|
typeName := getTypeName[T]()
|
||||||
|
Logger.Printf(`[slot: %s] inject lazy provider`, typeName)
|
||||||
|
|
||||||
|
l.providers[slotKey] = &slotEntry{
|
||||||
|
typeName: typeName,
|
||||||
|
configureFunc: func(l *ServiceLocator) (any, error) { return createFunc(l) },
|
||||||
|
configured: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use retrieves the value of type T associated with the given slot key from
|
||||||
|
// the provided ServiceLocator instance.
|
||||||
|
//
|
||||||
|
// If the ServiceLocator does not have a value for the slot key, or if the
|
||||||
|
// value wasn't correctly configured (in the case of a lazy slot), an error
|
||||||
|
// is returned.
|
||||||
|
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
slot, ok := l.providers[slotKey]
|
||||||
|
if !ok {
|
||||||
|
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := slot.checkConfigured(l)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := slot.value.(T)
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] using slot with value of type %T`, getTypeName[T](), v)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUse[T any](l *ServiceLocator, slotKey slot[T]) T {
|
||||||
|
v, err := Use(l, slotKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func Invoke[T any](l *ServiceLocator, slotKey slot[T]) error {
|
||||||
|
slot, ok := l.providers[slotKey]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := slot.checkConfigured(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := slot.value.(T)
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] invoked slot with value of type %T`, getTypeName[T](), v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustInvoke[T any](l *ServiceLocator, slotKey slot[T]) {
|
||||||
|
if err := Invoke(l, slotKey); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideHook[T any](l *ServiceLocator, hookKey hook[T], listeners ...Hook[T]) {
|
||||||
|
typeName := getTypeName[T]()
|
||||||
|
Logger.Printf(`[hook: %s] injecting hooks`, typeName)
|
||||||
|
|
||||||
|
// cast type safe listeners to internal untyped version to put inside the hook map
|
||||||
|
anyListeners := make([]func(*ServiceLocator, any) error, len(listeners))
|
||||||
|
for i, l := range listeners {
|
||||||
|
ll := l
|
||||||
|
anyListeners[i] = func(l *ServiceLocator, a any) error {
|
||||||
|
t, ok := a.(T)
|
||||||
|
if !ok {
|
||||||
|
panic(`illegal state`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ll(l, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.hooks[hookKey] = &hookEntry{
|
||||||
|
typeName: typeName,
|
||||||
|
listeners: anyListeners,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseHook[T any](l *ServiceLocator, hookKey hook[T], value T) error {
|
||||||
|
hookEntry, ok := l.hooks[hookKey]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf(`no injected hooks for hook of type %s`, hookEntry.typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Printf(`[hook: %s] calling hook with value of type %T`, hookEntry.typeName, value)
|
||||||
|
for _, hookFunc := range hookEntry.listeners {
|
||||||
|
if err := hookFunc(l, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustUseHook[T any](l *ServiceLocator, hookKey hook[T], value T) {
|
||||||
|
if err := UseHook(l, hookKey, value); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTypeName is a trick to get the name of a type (even if it is an
|
||||||
|
// interface type)
|
||||||
|
func getTypeName[T any]() string {
|
||||||
|
var zero T
|
||||||
|
return fmt.Sprintf(`%T`, &zero)[1:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompactIndentedLines removes all indentation from a string and also removes all newlines, usefull in tests
|
||||||
|
func CompactIndentedLines(s string) string {
|
||||||
|
return regexp.MustCompile(`(?m)\n\s+`).ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
@@ -1,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?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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,23 @@
|
|||||||
|
{
|
||||||
|
"name": "website-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Frontend for the PHC website",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/open-sans": "^4.5.14",
|
||||||
|
"@fontsource/source-code-pro": "^4.5.14",
|
||||||
|
"@fontsource/source-sans-pro": "^4.5.11",
|
||||||
|
"astro": "^2.3.1",
|
||||||
|
"sass": "^1.62.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Mode string
|
||||||
|
Host string
|
||||||
|
|
||||||
|
NpmCommand string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Slot = sl.NewSlot[Config]()
|
||||||
|
|
||||||
|
func setFromEnvOrDefault(target *string, m map[string]string, key string, defaultValue string) {
|
||||||
|
v, ok := m[key]
|
||||||
|
if ok {
|
||||||
|
*target = v
|
||||||
|
} else {
|
||||||
|
*target = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Configure(l *sl.ServiceLocator) (Config, error) {
|
||||||
|
env, err := godotenv.Read(".env")
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
|
||||||
|
setFromEnvOrDefault(&cfg.Mode, env, "MODE", "production")
|
||||||
|
setFromEnvOrDefault(&cfg.Host, env, "HOST", ":4000")
|
||||||
|
setFromEnvOrDefault(&cfg.NpmCommand, env, "NPM_COMMAND", "npm")
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ExampleProductionConfig = Config{
|
||||||
|
Mode: "production",
|
||||||
|
Host: ":4000",
|
||||||
|
|
||||||
|
NpmCommand: "npm",
|
||||||
|
}
|
||||||
|
|
||||||
|
var ExampleDevelopmentConfig = Config{
|
||||||
|
Mode: "development",
|
||||||
|
Host: ":4000",
|
||||||
|
|
||||||
|
NpmCommand: "npm",
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Slot = sl.NewSlot[Database]()
|
||||||
|
|
||||||
|
// Database is the main database service interface
|
||||||
|
type Database interface {
|
||||||
|
// User
|
||||||
|
|
||||||
|
CreateUser(user model.User) (db.Ref[model.User], error)
|
||||||
|
ReadUser(id db.Ref[model.User]) (model.User, error)
|
||||||
|
ReadUsers() ([]model.User, error)
|
||||||
|
UpdateUser(user model.User) error
|
||||||
|
DeleteUser(id db.Ref[model.User]) error
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
|
||||||
|
CreateAccount(account model.Account) (db.Ref[model.Account], error)
|
||||||
|
ReadAccount(id db.Ref[model.Account]) (model.Account, error)
|
||||||
|
UpdateAccount(user model.Account) error
|
||||||
|
DeleteAccount(id db.Ref[model.Account]) error
|
||||||
|
|
||||||
|
// Users & Accounts
|
||||||
|
|
||||||
|
ReadUserAccounts(user db.Ref[model.User]) ([]model.Account, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory is an in-memory implementation of [Database] that can be used for testing
|
||||||
|
type Memory struct {
|
||||||
|
Database // TODO: Per ora almeno così facciamo compilare il codice
|
||||||
|
|
||||||
|
Users []model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateUser(user model.User) (db.Ref[model.User], error) {
|
||||||
|
m.Users = append(m.Users, user)
|
||||||
|
return db.Ref[model.User](user.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ReadUser(id db.Ref[model.User]) (model.User, error) {
|
||||||
|
for _, u := range m.Users {
|
||||||
|
if u.Id == id {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ReadUsers() ([]model.User, error) {
|
||||||
|
return m.Users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateUser(user model.User) error {
|
||||||
|
for i, u := range m.Users {
|
||||||
|
if u.Id == user.Id {
|
||||||
|
m.Users[i] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`no user with id "%s"`, user.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteUser(id db.Ref[model.User]) error {
|
||||||
|
for i, u := range m.Users {
|
||||||
|
if u.Id == id {
|
||||||
|
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`no user with id "%s"`, id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Users = db.AutoTable[model.User]("users")
|
||||||
|
var Accounts = db.AutoTable[model.Account]("accounts")
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package listautenti
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MountApiRoutesHook(l *sl.ServiceLocator, api fiber.Router) error {
|
||||||
|
db, err := sl.Use(l, database.Slot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Get("/lista-utenti", func(c *fiber.Ctx) error {
|
||||||
|
users, err := db.ReadUsers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pre-process data to strip server only fields...
|
||||||
|
return c.JSON(users)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package listautenti_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
"github.com/valyala/fasthttp/fasthttputil"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/util"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApiListaUtenti(t *testing.T) {
|
||||||
|
|
||||||
|
memDB := &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := sl.New()
|
||||||
|
|
||||||
|
// Config
|
||||||
|
sl.Provide(l, config.Slot, config.ExampleProductionConfig)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
sl.Provide[database.Database](l, database.Slot, memDB)
|
||||||
|
|
||||||
|
// Server
|
||||||
|
sl.ProvideFunc(l, server.Slot, server.Configure)
|
||||||
|
sl.ProvideHook(l, server.ApiRoutesHook,
|
||||||
|
listautenti.MountApiRoutesHook,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize server instance
|
||||||
|
srv, err := sl.Use(l, server.Slot)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Try doing the request
|
||||||
|
//
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
ln := fasthttputil.NewInmemoryListener()
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := fasthttp.Serve(ln, srv.Router.Handler())
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return ln.Dial()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, string(body), util.CompactIndentedLines(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Id":"e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
"Username":"claire-doe",
|
||||||
|
"FullName":"Claire Doe",
|
||||||
|
"Email":"claire.doe@example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id":"9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
"Username":"john-smith",
|
||||||
|
"FullName":"John Smith",
|
||||||
|
"Email":"john.smith@example.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`))
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
// Id è l'id unico di questo utente, non è modificabile una volta creato
|
||||||
|
// l'utente.
|
||||||
|
Id db.Ref[User] `db:"id"`
|
||||||
|
|
||||||
|
// Username è il nome leggibile di questo utente utilizzato anche per le
|
||||||
|
// route per singolo utente, deve essere unico nel sito.
|
||||||
|
//
|
||||||
|
// NOTE: Quando un utente accede per la prima volta di default gli viene
|
||||||
|
// chiesto se usare quello dell'account che sta usando o se cambiarlo
|
||||||
|
// (in teoria non dovrebbe essere un problema poterlo modificare
|
||||||
|
// successivamente).
|
||||||
|
Username string `db:"username"`
|
||||||
|
|
||||||
|
// FullName da mostrare in giro per il sito
|
||||||
|
FullName db.Option[string] `db:"full_name"`
|
||||||
|
|
||||||
|
// Email per eventuale contatto
|
||||||
|
Email db.Option[string] `db:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderType string
|
||||||
|
|
||||||
|
const AteneoProvider ProviderType = "ateneo"
|
||||||
|
const PoissonProvider ProviderType = "poisson"
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Id db.Ref[Account] `db:"id"` // Id of this entity
|
||||||
|
UserId db.Ref[User] `db:"user_id"` // UserId tells the owner of this account binding
|
||||||
|
Provider ProviderType `db:"provider"` // Provider is the name of the authentication method
|
||||||
|
|
||||||
|
Token string `db:"token"` // Token to use to make requests on behalf of this user
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct{ Router *fiber.App }
|
||||||
|
|
||||||
|
var Slot = sl.NewSlot[*Server]()
|
||||||
|
|
||||||
|
var ApiRoutesHook = sl.NewHook[fiber.Router]()
|
||||||
|
|
||||||
|
func Configure(l *sl.ServiceLocator) (*Server, error) {
|
||||||
|
cfg, err := sl.Use(l, config.Slot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := fiber.New(fiber.Config{})
|
||||||
|
r.Static("/assets", "./out/frontend/assets")
|
||||||
|
|
||||||
|
api := r.Group("/api")
|
||||||
|
if err := sl.UseHook(l, ApiRoutesHook, api); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Fatal(r.Listen(cfg.Host))
|
||||||
|
}()
|
||||||
|
|
||||||
|
return &Server{r}, nil
|
||||||
|
}
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type byEventDateDescending []*GenericEvent
|
|
||||||
|
|
||||||
func (m byEventDateDescending) Len() int {
|
|
||||||
return len(m)
|
|
||||||
}
|
|
||||||
func (m byEventDateDescending) Swap(i, j int) {
|
|
||||||
m[i], m[j] = m[j], m[i]
|
|
||||||
}
|
|
||||||
func (m byEventDateDescending) Less(i, j int) bool {
|
|
||||||
return m[i].Date > m[j].Date
|
|
||||||
}
|
|
||||||
|
|
||||||
type Macchinista struct {
|
|
||||||
Uid string `json:"uid"`
|
|
||||||
FullName string `json:"fullName"`
|
|
||||||
EntryDate string `json:"entryDate"`
|
|
||||||
ExitDate string `json:"exitDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenericEvent struct {
|
|
||||||
// Campi principali
|
|
||||||
Type string `json:"type"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
|
|
||||||
// Altri campi utilizzati ogni tanto
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
|
|
||||||
// Se il tipo di evento è riferito ad un utente usiamo anche questo campo apposta
|
|
||||||
Uid string `json:"uid"`
|
|
||||||
FullName string `json:"fullName"`
|
|
||||||
|
|
||||||
// Se il tipo è "spacer" questa è la sua dimensione
|
|
||||||
Size int `json:"size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HistoryDB struct {
|
|
||||||
Macchinisti []*Macchinista `json:"macchinisti"`
|
|
||||||
GenericEvent []*GenericEvent `json:"eventi"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRawHistory() (*HistoryDB, error) {
|
|
||||||
var rawHistory HistoryDB
|
|
||||||
|
|
||||||
historyDB, err := ioutil.ReadFile("./storia.json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(historyDB, &rawHistory); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &rawHistory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDateYear(date string) int {
|
|
||||||
year, err := strconv.Atoi(strings.Split(date, "/")[0])
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // Tanto nel caso si nota in fase di sviluppo visto che "storia.json" è statico
|
|
||||||
}
|
|
||||||
|
|
||||||
return year
|
|
||||||
}
|
|
||||||
|
|
||||||
func withSpacers(events []*GenericEvent) []*GenericEvent {
|
|
||||||
// crea un nuovo slice con capacità già almeno "len(events)"
|
|
||||||
newEvents := make([]*GenericEvent, 0, len(events))
|
|
||||||
|
|
||||||
for i := 0; i < len(events)-1; i++ {
|
|
||||||
newEvents = append(newEvents, events[i])
|
|
||||||
|
|
||||||
delta := util.Abs(getDateYear(events[i+1].Date) - getDateYear(events[i].Date))
|
|
||||||
if delta > 1 {
|
|
||||||
logDelta := int(math.Log(float64(delta)))
|
|
||||||
newEvents = append(newEvents, &GenericEvent{
|
|
||||||
Type: "spacer",
|
|
||||||
Size: logDelta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newEvents = append(newEvents, events[len(events)-1])
|
|
||||||
|
|
||||||
return newEvents
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStoria() ([]*GenericEvent, error) {
|
|
||||||
history, err := GetRawHistory()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
events := history.GenericEvent[:]
|
|
||||||
for _, m := range history.Macchinisti {
|
|
||||||
if m.ExitDate != "" {
|
|
||||||
events = append(events, &GenericEvent{
|
|
||||||
Type: "exit-macchinista",
|
|
||||||
Uid: m.Uid,
|
|
||||||
FullName: m.FullName,
|
|
||||||
Date: m.ExitDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if m.EntryDate != "" {
|
|
||||||
events = append(events, &GenericEvent{
|
|
||||||
Type: "entry-macchinista",
|
|
||||||
Uid: m.Uid,
|
|
||||||
FullName: m.FullName,
|
|
||||||
Date: m.EntryDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(byEventDateDescending(events))
|
|
||||||
|
|
||||||
return withSpacers(events), nil
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"macchinisti": [
|
|
||||||
{
|
|
||||||
"uid": "minnocci",
|
|
||||||
"fullName": "Francesco Minnocci",
|
|
||||||
"entryDate": "2022/02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uid": "manicastri",
|
|
||||||
"fullName": "Francesco Manicastri",
|
|
||||||
"entryDate": "2022/05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uid": "delucreziis",
|
|
||||||
"fullName": "Antonio De Lucreziis",
|
|
||||||
"entryDate": "2019/??"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uid": "caporali",
|
|
||||||
"fullName": "Francesco Caporali",
|
|
||||||
"entryDate": "2018/??",
|
|
||||||
"exitDate": "2022/03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uid": "dachille",
|
|
||||||
"fullName": "Letizia D'Achille",
|
|
||||||
"entryDate": "2018/??",
|
|
||||||
"exitDate": "2022/03"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"eventi": [
|
|
||||||
{
|
|
||||||
"type": "simple",
|
|
||||||
"title": "Nuovo sito del PHC",
|
|
||||||
"description": "TBA",
|
|
||||||
"date": "2022/??"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "simple",
|
|
||||||
"title": "Apertura del PHC",
|
|
||||||
"date": "1994"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package templates
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/website/config"
|
|
||||||
"git.phc.dm.unipi.it/phc/website/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CachedTemplate holds a reference to the list of patterns to preload and a reference to the already parsed template
|
|
||||||
type CachedTemplate struct {
|
|
||||||
PreLoadTemplatePatterns []string
|
|
||||||
cachedTmpl *template.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCacheTemplate creates a new template from a list of patterns a loads it
|
|
||||||
func NewCacheTemplate(loadPatterns ...string) *CachedTemplate {
|
|
||||||
cachedTemplate := &CachedTemplate{
|
|
||||||
PreLoadTemplatePatterns: loadPatterns,
|
|
||||||
cachedTmpl: nil,
|
|
||||||
}
|
|
||||||
template.Must(cachedTemplate.Load(template.New("")))
|
|
||||||
return cachedTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load returns the cached template or loads it from disk using the list of preload (glob) patterns.
|
|
||||||
func (ct *CachedTemplate) Load(t *template.Template) (*template.Template, error) {
|
|
||||||
if ct.cachedTmpl != nil {
|
|
||||||
return ct.cachedTmpl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.cachedTmpl = t
|
|
||||||
|
|
||||||
for _, pattern := range ct.PreLoadTemplatePatterns {
|
|
||||||
var err error
|
|
||||||
ct.cachedTmpl, err = ct.cachedTmpl.ParseGlob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ct.cachedTmpl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload the pattern from disk (used for developing)
|
|
||||||
func (ct *CachedTemplate) Reload() {
|
|
||||||
ct.cachedTmpl = nil
|
|
||||||
template.Must(ct.Load(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template returns the cached template
|
|
||||||
func (ct *CachedTemplate) Template() *template.Template {
|
|
||||||
log.Printf("preloadPatterns: %v", ct.PreLoadTemplatePatterns)
|
|
||||||
return ct.cachedTmpl
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add a render function to CachedTemplate instead of returning the template reference. In this way the Template Renderer would not have a "direct" dependency on "html/template"...
|
|
||||||
|
|
||||||
// TemplateRenderer holds cached templates for rendering
|
|
||||||
type TemplateRenderer struct {
|
|
||||||
viewsDir string
|
|
||||||
preloadTemplatePatterns []string
|
|
||||||
templateCache map[string]*CachedTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRenderer constructs a template renderer with a base file
|
|
||||||
func NewRenderer(rootPath string, loadPatterns ...string) *TemplateRenderer {
|
|
||||||
return &TemplateRenderer{
|
|
||||||
rootPath,
|
|
||||||
loadPatterns,
|
|
||||||
map[string]*CachedTemplate{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TemplateRenderer) Load(name string) *CachedTemplate {
|
|
||||||
cachedTemplate, present := r.templateCache[name]
|
|
||||||
|
|
||||||
if !present {
|
|
||||||
loaders := []string{}
|
|
||||||
loaders = append(loaders, r.preloadTemplatePatterns...)
|
|
||||||
loaders = append(loaders, path.Join(r.viewsDir, name))
|
|
||||||
cachedTemplate = NewCacheTemplate(loaders...)
|
|
||||||
r.templateCache[name] = cachedTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the template, also injects "Page" and "Config" values in the template
|
|
||||||
func (r *TemplateRenderer) Render(w io.Writer, name string, data util.H) error {
|
|
||||||
cachedTemplate := r.Load(name)
|
|
||||||
|
|
||||||
if config.Mode == "development" {
|
|
||||||
cachedTemplate.Reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
newData := util.H{}
|
|
||||||
newData.Apply(data)
|
|
||||||
newData["Page"] = util.H{
|
|
||||||
// Used to inject a page specific class on <body>
|
|
||||||
"Name": strings.TrimSuffix(path.Base(name), ".html"),
|
|
||||||
}
|
|
||||||
newData["Config"] = config.Object()
|
|
||||||
|
|
||||||
return cachedTemplate.Template().ExecuteTemplate(w, "base", newData)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable top-level await, and other modern ESM features.
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
// Enable node-style module resolution, for things like npm package imports.
|
||||||
|
"moduleResolution": "node",
|
||||||
|
// Enable JSON imports.
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
// Enable stricter transpilation for better output.
|
||||||
|
"isolatedModules": true,
|
||||||
|
// Astro directly run TypeScript code, no transpilation needed.
|
||||||
|
"noEmit": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
// Report an error when importing a file using a casing different from the casing on disk.
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
// Properly support importing CJS modules in ESM
|
||||||
|
"esModuleInterop": true,
|
||||||
|
// Skip typechecking libraries and .d.ts files
|
||||||
|
"skipLibCheck": true,
|
||||||
|
// Add alias for assets folder for easy reference to assets
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {},
|
||||||
|
// TypeScript 5.0 changed how `isolatedModules` and `importsNotUsedAsValues` works, deprecating the later
|
||||||
|
// Until the majority of users are on TypeScript 5.0, we'll have to supress those deprecation errors
|
||||||
|
"ignoreDeprecations": "5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserInfo struct {
|
|
||||||
Uid string `json:"uid"`
|
|
||||||
Nome string `json:"nome"`
|
|
||||||
Cognome string `json:"cognome"`
|
|
||||||
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
IsMacchinista bool `json:"macchinista,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUtenti() ([]UserInfo, error) {
|
|
||||||
history, err := GetRawHistory()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creo un set per velocizzare l'utilizzo dopo
|
|
||||||
macchinisti := map[string]bool{}
|
|
||||||
for _, m := range history.Macchinisti {
|
|
||||||
macchinisti[m.Uid] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []UserInfo
|
|
||||||
|
|
||||||
usersJsonData, err := ioutil.ReadFile("./utenti-poisson-2022.local.json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(usersJsonData, &users); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package util
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Numeric interface {
|
|
||||||
constraints.Float | constraints.Integer
|
|
||||||
}
|
|
||||||
|
|
||||||
func Abs[T Numeric](a T) T {
|
|
||||||
if a < 0 {
|
|
||||||
return -a
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
// H is a shortcut for creating a json-like object
|
|
||||||
type H map[string]interface{}
|
|
||||||
|
|
||||||
// Apply is like Object.apply from JS and merges the given objects on top of target
|
|
||||||
func (target H) Apply(sources ...H) H {
|
|
||||||
for _, source := range sources {
|
|
||||||
for k, v := range source {
|
|
||||||
target[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateRandomString(n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64.URLEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Appunti • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<section>
|
|
||||||
<h1>
|
|
||||||
<i class="fas fa-book"></i>
|
|
||||||
Raccolta degli Appunti
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
Questa è la raccolta degli appunti presenti su Poisson. Cerca il titolo della dispensa, il nome e cognome o l'username dell'autore oppure scrivi il nome del corso rispetto a cui filtrare. Altrimenti in cima compariranno gli appunti più "gettonati".
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Puoi aggiungere le tue dispense dalla <a href="/appunti/condivisi">pagina personale di condivisione</a>.
|
|
||||||
</p>
|
|
||||||
<div class="search">
|
|
||||||
<div class="compound">
|
|
||||||
<input type="text" id="search-field" placeholder="Prova con "Geometria 1" o "Mezzedimi"..." autocomplete="off" {{ if .Query }}value="{{ .Query }}"{{ end }}>
|
|
||||||
<button class="icon">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="appunti-list">
|
|
||||||
<code>TODO: Lista work in progress</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Aggiorna automaticamente la barra dell'URL con la query corrente
|
|
||||||
document.querySelector('#search-field').addEventListener('input', function() {
|
|
||||||
const q = this.value.trim();
|
|
||||||
history.replaceState({}, 'Appunti',
|
|
||||||
q.length === 0 ? '/appunti' : '/appunti?q=' + encodeURIComponent(q)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{end}}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
{{define "base"}}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ template "title" . }}</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
|
|
||||||
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
|
|
||||||
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
|
|
||||||
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
renderMathInElement(document.body, {
|
|
||||||
delimiters: [
|
|
||||||
{ left: "$", right: "$", display: false },
|
|
||||||
{ left: "$$", right: "$$", display: true },
|
|
||||||
{ left: "\\(", right: "\\)", display: false },
|
|
||||||
{ left: "\\[", right: "\\]", display: true }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const $toggle = document.querySelector('#toggle-dark-mode');
|
|
||||||
const $toggleIcon = document.querySelector('#toggle-dark-mode i');
|
|
||||||
|
|
||||||
// Loads prefered dark from from localStorage or defaults to media query.
|
|
||||||
let prefersDarkMode = localStorage.getItem('theme') !== undefined ? localStorage.getItem('theme') === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
function storePrefersDarkMode(mode) {
|
|
||||||
prefersDarkMode = mode;
|
|
||||||
localStorage.setItem('theme', mode ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayToggle() {
|
|
||||||
document.body.classList.toggle('dark-mode', prefersDarkMode);
|
|
||||||
$toggleIcon.classList.toggle('fa-moon', prefersDarkMode);
|
|
||||||
$toggleIcon.classList.toggle('fa-sun', !prefersDarkMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
$toggle.addEventListener('click', () => {
|
|
||||||
storePrefersDarkMode(!prefersDarkMode);
|
|
||||||
displayToggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
displayToggle();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/public/style.css">
|
|
||||||
<link rel="stylesheet" href="/public/theme-dark.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body {{ if .Page.Name }}class="page-{{ .Page.Name }}"{{ end }}>
|
|
||||||
{{ template "navbar" . }}
|
|
||||||
|
|
||||||
<div class="main">
|
|
||||||
{{ template "body" . }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="footer-item">
|
|
||||||
<a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Home • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<canvas id="circuit-pattern"></canvas>
|
|
||||||
|
|
||||||
<!-- Non avevo molta fantasia per il nome di questa classe -->
|
|
||||||
<section class="super">
|
|
||||||
<div class="block image">
|
|
||||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
|
||||||
</div>
|
|
||||||
<div class="block text">
|
|
||||||
<h1>
|
|
||||||
Cos'è il PHC?
|
|
||||||
</h1>
|
|
||||||
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum vel maiores dolorum dicta, repellat nulla repellendus amet fuga voluptas molestiae, magni</p>
|
|
||||||
<p>Voluptatibus sequi praesentium similique, quos ex illo autem quidem!</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Notizie Importanti</h2>
|
|
||||||
<div class="card-list">
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="/news/notizia-super-wow-1">News 1</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">yyyy-mm-dd</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
much doge, 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...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="/news/notizia-super-wow-2">News 2</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">yyyy-mm-dd</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
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...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="/news/notizia-super-wow-3">News 3</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">yyyy-mm-dd</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
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...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<!-- TODO: Progetti/Servizi/Cose fornite -->
|
|
||||||
<h2>Progetti</h2>
|
|
||||||
<div class="card-list">
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">Project title</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">Project title</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">Project title</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem assumenda voluptatum harum quo nisi alias ab cupiditate corrupti est, illum culpa, excepturi, fugiat dolore doloribus minima placeat facilis enim quaerat!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<hr>
|
|
||||||
<section>
|
|
||||||
<!-- <h2>Altro...</h2> -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">Vuoi diventare un macchinista?</div>
|
|
||||||
<!-- <h3 style="font-weight: 500;">Vuoi diventare un macchinista?</h3> -->
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Ti interessa (o interesserebbe) smanettare al PC, montare e smontare i cose? Stai spesso in dipartimento? Allora
|
|
||||||
puoi venire a parlare con noi per diventare un apprendista macchinista per poi entrare nel PHC.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="/public/home.js"></script>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Link Utili • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<section>
|
|
||||||
<h1>
|
|
||||||
<i class="fas fa-link"></i>
|
|
||||||
Link Utili
|
|
||||||
</h1>
|
|
||||||
<p class="center">
|
|
||||||
Questo è un elenco di alcuni indirizzi potenzialmente utili
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<div class="card-list">
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="https://linktr.ee/aulastudenti">Aula Studenti</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="https://credenziali.phc.dm.unipi.it/">Credenziali Poisson</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="http://detexify.kirelabs.org/classify.html">Non trovi un simbolo Latex?</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="https://git.phc.dm.unipi.it/phc/dm-scripts/src/branch/main/printa4">Script per stampare in dipartimento dal proprio PC</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="https://github.com/SuzanneSoy/WiTeX">Wikipedia+LaTex??</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i <a href="https://en.wikipedia.org/wiki/Abstract_nonsense">tuoi</a> <a href="https://en.wikipedia.org/wiki/New_Math">articoli</a> <a href="https://en.wikipedia.org/wiki/%C3%89variste_Galois#Final_days">preferiti</a> di Wikipedia con un typesetting in stile LaTex!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="https://www.dm.unipi.it">CdS Matematica</a>
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>
|
|
||||||
La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{end}}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Accedi • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<section>
|
|
||||||
<h1>
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
Account di Poisson
|
|
||||||
</h1>
|
|
||||||
<div class="card-list">
|
|
||||||
<form class="card" action="/login" method="POST">
|
|
||||||
<div class="title">
|
|
||||||
<i class="fas fa-sign-in-alt"></i>
|
|
||||||
Accedi
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Inserisci le tue credenziali di Poisson per accedere
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="field-set">
|
|
||||||
<!-- <label for="login-provider">Provider:</label> -->
|
|
||||||
<input type="hidden" name="provider" id="login-provider" value="poisson-ldap">
|
|
||||||
|
|
||||||
<label for="login-username">Username:</label>
|
|
||||||
<input type="text" name="username" id="login-username">
|
|
||||||
|
|
||||||
<label for="login-password">Password:</label>
|
|
||||||
<input type="password" name="password" id="login-password">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button class="primary">Accedi</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Ottenere un account</h2>
|
|
||||||
<p>
|
|
||||||
Dal 2022 in nuovi utenti hanno bisogno di compilare un modulo se vogliono ottenere un account<sup>1</sup>, scarica il <a href="#">modulo di richiesta</a> e portacelo in PHC o inviacelo via email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
1: In realtà il modulo ancora non esiste
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Recupero credenziali</h2>
|
|
||||||
<p>
|
|
||||||
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una
|
|
||||||
email all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}{{ .Article.Title }} • News • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<section class="news-content">
|
|
||||||
<h1>{{ .Article.Title }}</h1>
|
|
||||||
<div class="date">
|
|
||||||
{{ .Article.PublishDate.Format "2006/01/02" }}
|
|
||||||
</div>
|
|
||||||
<div class="tags">
|
|
||||||
{{ range .Article.Tags }}
|
|
||||||
<span class="tag">{{ . }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ .ContentHTML }}
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
|
|
||||||
{{define "title"}}News • PHC{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<section>
|
|
||||||
<h1>
|
|
||||||
<i class="far fa-newspaper"></i>
|
|
||||||
Notizie Importanti
|
|
||||||
</h1>
|
|
||||||
<div class="card-list">
|
|
||||||
{{ range .Articles }}
|
|
||||||
{{ if .HasTag "important" }}
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="/news/{{ .Id }}">
|
|
||||||
{{ .Title }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>{{ .Description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="tags">
|
|
||||||
{{ range .Tags }}
|
|
||||||
<span class="tag">{{ . }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>
|
|
||||||
<i class="fas fa-history"></i>
|
|
||||||
Archivio notizie
|
|
||||||
</h2>
|
|
||||||
<div class="card-list">
|
|
||||||
{{ range .Articles }}
|
|
||||||
{{ if not (.HasTag "important") }}
|
|
||||||
<div class="card">
|
|
||||||
<div class="title">
|
|
||||||
<a href="/news/{{ .Id }}">
|
|
||||||
{{ .Title }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">{{ .PublishDate.Format "2006/01/02" }}</div>
|
|
||||||
<div class="description">
|
|
||||||
<p>{{ .Description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="tags">
|
|
||||||
{{ range .Tags }}
|
|
||||||
<span class="tag">{{ . }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
{{ define "navbar" }}
|
|
||||||
<nav>
|
|
||||||
<!-- Site -->
|
|
||||||
<div class="nav-logo">
|
|
||||||
<a class="nav-element" href="/">
|
|
||||||
<img src="/public/images/logo-circuit-board.svg" alt="phc-logo">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-main">
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/utenti">Utenti</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item dropdown">
|
|
||||||
<div class="name">
|
|
||||||
<a class="nav-element" href="/progetti">
|
|
||||||
<div class="icon">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</div>
|
|
||||||
<div class="label">
|
|
||||||
Progetti
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-items">
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="{{ .Config.GitUrl }}">Gitea</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="{{ .Config.ChatUrl }}">Zulip</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/seminari">Seminari</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item dropdown">
|
|
||||||
<div class="name">
|
|
||||||
<a class="nav-element" href="/risorse">
|
|
||||||
<div class="icon">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</div>
|
|
||||||
<div class="label">
|
|
||||||
Risorse
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-items">
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/news">News</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/appunti">Appunti</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/guide">Guide</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/link">Link Utili</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/storia">Storia</a>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="nav-item">
|
|
||||||
<a class="nav-element" href="/about">About</a>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="nav-item filler"></div>
|
|
||||||
|
|
||||||
<!-- User Related -->
|
|
||||||
<div class="nav-item">
|
|
||||||
<div id="toggle-dark-mode" class="nav-button">
|
|
||||||
<i class="fas fa-moon"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{if .User}}
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/profilo">@{{ .User.Username }}</a>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="nav-item">
|
|
||||||
<a class="nav-element" href="/login">Accedi</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
{{define "title"}}Profilo di @{{ .User.Username }} • PHC{{end}}
|
|
||||||
{{define "body"}}
|
|
||||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
<script src="/public/js/profilo.min.js"></script>
|
|
||||||
<section x-data="profilo">
|
|
||||||
<h1>Profilo di @{{ .User.Username }}</h1>
|
|
||||||
<p class="center">
|
|
||||||
<a class="button" href="/logout">Logout</a>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Appunti e Dispense</h2>
|
|
||||||
<p>
|
|
||||||
Per gestire i tuoi appunti e le tue dispense caricate nella sezione <a href="/appunti">Appunti</a> del sito vai
|
|
||||||
<a href="/appunti/condivisi">alla pagina appunti condivisi</a>.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Impostazioni</h2>
|
|
||||||
<div class="card-list">
|
|
||||||
<form class="card" action="/profile/impostazioni" method="POST">
|
|
||||||
<div class="title">
|
|
||||||
Sito PHC
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Aggiorna i campi relativi al sito del PHC, l'immagine del profilo verrà visualizzata sulla tua pagina utente <a href="#">phc.dm.unipi.it/u/{{ .User.Username }}</a> e nella lista di <a href="/utenti">tutti gli utenti</a>.
|
|
||||||
</p>
|
|
||||||
<div class="field-set">
|
|
||||||
<label for="website-nickname">Nickname</label>
|
|
||||||
<input type="text" name="nickname" id="website-nickname" placeholder="{{ .User.Username }}">
|
|
||||||
<label for="website-profile-picture">Immagine Profilo</label>
|
|
||||||
<input type="file" name="picture" id="website-profile-picture">
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Puoi anche aggiungere link ad altri tuoi profili su siti esterni (quando riempi l'ultimo campo se ne crea uno nuovo sotto)
|
|
||||||
</p>
|
|
||||||
<div class="field-set">
|
|
||||||
<label for="website-link-1">Link 1</label>
|
|
||||||
<input type="text" name="link" id="website-link-1"
|
|
||||||
placeholder="https://github.com/...">
|
|
||||||
<label for="website-link-2">Link 2</label>
|
|
||||||
<input type="text" name="link" id="website-link-2"
|
|
||||||
placeholder="https://twitter.com/...">
|
|
||||||
<label for="website-link-3">Link 3</label>
|
|
||||||
<input type="text" name="link" id="website-link-3"
|
|
||||||
placeholder="https://news.ycombinator.com/user?id=...">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button class="primary">Aggiorna</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form class="card" action="/profile/impostazioni" method="POST">
|
|
||||||
<div class="title">
|
|
||||||
Informazioni Utente
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Aggiorna i campi modificabili dell'account Poisson
|
|
||||||
</p>
|
|
||||||
<div class="field-set">
|
|
||||||
<label for="user-username">Username</label>
|
|
||||||
<input type="text" readonly name="username" id="user-username" value="{{ .User.Username }}">
|
|
||||||
<label for="user-name">Nome</label>
|
|
||||||
<input type="text" readonly name="name" id="user-name" value="{{ .User.Name }}">
|
|
||||||
<label for="user-surname">Cognome</label>
|
|
||||||
<input type="text" readonly name="surname" id="user-surname" value="{{ .User.Surname }}">
|
|
||||||
<label for="user-email">Email</label>
|
|
||||||
<input type="text" readonly name="email" id="user-email" value="{{ .User.Email }}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button class="primary">Aggiorna</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form x-data="passwordForm" class="card" action="/profile/impostazioni" method="POST">
|
|
||||||
<div class="title">
|
|
||||||
Modifica Password
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Aggiorna la password di Poisson
|
|
||||||
</p>
|
|
||||||
<div class="field-set">
|
|
||||||
<label for="pass-username">Username</label>
|
|
||||||
<input type="text" readonly name="username" id="pass-username" value="{{ .User.Username }}">
|
|
||||||
<label for="pass-password">Password</label>
|
|
||||||
<input type="password" name="password" id="pass-password" :class="!passwordSame ? 'error' : ''"
|
|
||||||
x-model="password" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
|
||||||
<label for="pass-password-2">Ripeti Password</label>
|
|
||||||
<input type="password" name="password-2" id="pass-password-2" :class="!passwordSame ? 'error' : ''"
|
|
||||||
x-model="passwordAgain" @input="onUpdate" placeholder="In realtà anche questo non funge...">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button class="primary">Aggiorna Password</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Recupero Credenziali Poisson</h2>
|
|
||||||
<p>
|
|
||||||
Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una email
|
|
||||||
all'indirizzo <a href="mailto:{{ .Config.Email }}">{{ .Config.Email }}</a> e poi recuperare le nuove credenziali sul
|
|
||||||
sito <a href="https://credenziali.phc.dm.unipi.it/">credenziali.phc.dm.unipi.it</a>.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
{{define "title"}}Utenti • PHC{{end}}
|
|
||||||
{{define "body"}}
|
|
||||||
<section>
|
|
||||||
<h1>
|
|
||||||
<i class="fas fa-clock"></i>
|
|
||||||
Storia del PHC
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
In questa pagina è raccontata la storia del PHC con alcuni degli eventi principali. Al momento è <i>work in progress</i> ma pianifichiamo di aggiungere almeno le date di ingresso di tutti i macchinisti.
|
|
||||||
</p>
|
|
||||||
<div class="history-container">
|
|
||||||
<div class="timeline-bar"></div>
|
|
||||||
<div class="events">
|
|
||||||
{{ range .Storia }}
|
|
||||||
{{ if eq .Type "simple" }}
|
|
||||||
<div class="event">
|
|
||||||
<div class="title">
|
|
||||||
{{ .Title }}
|
|
||||||
</div>
|
|
||||||
<div class="date">{{ .Date }}</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if eq .Type "entry-macchinista" }}
|
|
||||||
<div class="event">
|
|
||||||
<div class="title">
|
|
||||||
Ingresso di
|
|
||||||
<i class="fa-solid fa-sign-in"></i>
|
|
||||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">{{ .Date }}</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if eq .Type "exit-macchinista" }}
|
|
||||||
<div class="event">
|
|
||||||
<div class="title">
|
|
||||||
Uscita di
|
|
||||||
<i class="fa-solid fa-sign-out"></i>
|
|
||||||
<a href="{{ $.Config.UserPagesBaseUrl }}{{ .Uid }}">{{ .FullName }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="date">{{ .Date }}</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if eq .Type "spacer" }}
|
|
||||||
<div class="spacer" style="--size: {{ .Size }}"></div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{{template "base" .}}
|
|
||||||
{{define "title"}}Utenti • PHC{{end}}
|
|
||||||
{{define "body"}}
|
|
||||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
|
|
||||||
<script src="/public/js/utenti.min.js"></script>
|
|
||||||
<section x-data="utenti">
|
|
||||||
<h1>
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
Lista degli Utenti
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
Questa è la lista di tutti gli utenti con un account su Poisson. Scrivi nome, cognome o
|
|
||||||
username di un utente per filtrare la lista in tempo reale. Altrimenti di base in cima
|
|
||||||
compariranno gli utenti con più "follower".
|
|
||||||
</p>
|
|
||||||
<div class="search">
|
|
||||||
<div class="compound padded">
|
|
||||||
<div class="icon" title="Ordina per">
|
|
||||||
<i class="fas fa-sort"></i>
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<select x-model="sortMode" @click="updateSortMode()">
|
|
||||||
<option value="chronological">Cronologico</option>
|
|
||||||
<option value="name">Nome</option>
|
|
||||||
<option value="surname">Cognome</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="compound">
|
|
||||||
<input type="text" x-model="searchField" @input="updateSearch()" placeholder="Cerca..." autocomplete="off" />
|
|
||||||
<button class="icon" title="In realtà questo tasto è finto">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-list card-list">
|
|
||||||
<template x-for="entry in searchResults" :key="entry.item.uid">
|
|
||||||
<div class="user-item card">
|
|
||||||
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
|
|
||||||
sortMode === 'surname' ?
|
|
||||||
entry.item.cognome + ' ' + entry.item.nome :
|
|
||||||
entry.item.nome + ' ' + entry.item.cognome
|
|
||||||
"></a>
|
|
||||||
<template x-if="entry.item.macchinista">
|
|
||||||
<div class="icon" title="Macchinista">
|
|
||||||
<i class="fas fa-wrench"></i>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- <template x-if="entry.score">
|
|
||||||
<span x-text="entry.score"></span>
|
|
||||||
</template> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="spinner" x-ref="spinner" x-show="searchResults.length < searchResultsBuffer.length">
|
|
||||||
<i class="fas fa-hourglass"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||