Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeba6356af
|
||
|
|
23757ad9b0
|
||
|
|
9d518dd4f8
|
||
|
|
3ca159e809
|
||
|
|
855bc8fa25
|
||
|
|
f7350bc6a8
|
||
|
|
4b12c3d59d
|
||
|
|
47a9d32670
|
||
|
|
6f308377ef
|
||
|
|
d7f9f2935d
|
||
|
|
ddbf06d7d0
|
||
|
|
cd0383245b
|
||
|
|
cff45dc92f
|
@@ -1,23 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
## Server, Database and File Uploads
|
||||
|
||||
- **server**
|
||||
|
||||
The server module defines all routes and extracts data from HTTP request and passes data to the **handler** module.
|
||||
|
||||
- **handler**
|
||||
|
||||
This is the main controller module that dispatches requests to various services and constructs back responses to send to the client.
|
||||
|
||||
### Services
|
||||
|
||||
...
|
||||
|
||||
- **appunti**
|
||||
|
||||
This module manages all things relating the _appunti condivisi_ pages, it talks with the **database** and **auth** modules.
|
||||
|
||||
- **database**
|
||||
|
||||
This is one of the "leaf" modules that doesn't depend on anything, its purpose is to talk to the database and return "low level data" stored there. For example instances of `time.Time` are represented as `string`s at this layer.
|
||||
@@ -4,7 +4,7 @@ Backend e frontend del nuovo sito per il PHC.
|
||||
|
||||
## Usage
|
||||
|
||||
To setup the project
|
||||
To setup the project
|
||||
|
||||
```bash shell
|
||||
# Clone the repo
|
||||
@@ -78,7 +78,7 @@ The `config/` package is just a singleton loaded when the application is started
|
||||
|
||||
#### Services
|
||||
|
||||
All other Go packages are services used by the `handler/` packages that provide authentication (`auth/`), template rendering (`templates/`), articles loading (`articles/`).
|
||||
All other Go packages are services used by the `handler/` packages that provide authentication (`auth/`), template rendering (`templates/`), articles loading (`articles/`).
|
||||
|
||||
Some very small services for now just have a single file and are `storia.go` and `utenti.go` (each provides a `-Service` interface and a default concrete implementation).
|
||||
|
||||
@@ -144,7 +144,7 @@ These files a processed by RollupJS into `out/`
|
||||
|
||||
- `HOST`
|
||||
|
||||
Indirizzo sul quale servire il sito, di default è `localhost:8000`.
|
||||
Indirizzo sul quale servire il sito, di default è `:8080`.
|
||||
|
||||
- `EMAIL`
|
||||
|
||||
@@ -177,4 +177,4 @@ These files a processed by RollupJS into `out/`
|
||||
|
||||
Questo progetto dipende dal servizio `phc/auth-service` che 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/)
|
||||
Il servizio di autenticazione di default girerà su `http://localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
["berni", "budacuferrari", "delucreziis", "butori", "cecchi", "diprisa", "gambicchia", "lamanna", "morganti", "numero", "piazza", "pignatelli", "pratali", "talluri", "tanzini"]
|
||||
@@ -8,6 +8,9 @@ macchinisti:
|
||||
- uid: delucreziis
|
||||
fullName: Antonio De Lucreziis
|
||||
entryDate: 2019/09
|
||||
- uid: serdyuk
|
||||
fullName: Illya Serdyuk
|
||||
entryDate: 2019/12
|
||||
- uid: caporali
|
||||
fullName: Francesco Caporali
|
||||
entryDate: 2018/09
|
||||
|
||||
@@ -49,7 +49,7 @@ export default defineConfig([
|
||||
plugins: [esbuild({ minify: true })],
|
||||
},
|
||||
{
|
||||
input: 'src/appunti-condivisi/main.jsx',
|
||||
input: 'src/appunti-condivisi.jsx',
|
||||
output: {
|
||||
file: 'out/appunti-condivisi.min.js',
|
||||
format: 'iife',
|
||||
@@ -68,8 +68,7 @@ export default defineConfig([
|
||||
],
|
||||
],
|
||||
}),
|
||||
// https://rollupjs.org/guide/en/#-w--watch
|
||||
!process.env.ROLLUP_WATCH && terser(),
|
||||
terser(),
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
const Counter = ({}) => {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
const increment = () => {
|
||||
setValue(value => value + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
Counter: {value} <button onClick={increment}>+</button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
<>
|
||||
<h1>Preact is Working!</h1>
|
||||
<Counter />
|
||||
</>,
|
||||
document.querySelector('#app')
|
||||
)
|
||||
@@ -1,524 +0,0 @@
|
||||
import { render } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { formatFileSize, intersperse } from './util.js'
|
||||
|
||||
function randomHex(length = 16) {
|
||||
return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista di tag "standard" disponibili nel completamento per i tags
|
||||
*/
|
||||
const availableTags = [
|
||||
// Genera una ventina di coppie come "2022/2023"
|
||||
...Array.from({ length: 20 }, (_, i) => {
|
||||
const year = new Date().getFullYear() - i
|
||||
|
||||
return { id: `${year}/${year + 1}`, label: `Anno Accademico ${year}/${year + 1}` }
|
||||
}),
|
||||
// List di tag "standard", l'id è la vera stringa del tag, mentre la label
|
||||
{ id: 'geometria-1', label: 'Geometria 1' },
|
||||
{ id: 'geometria-2', label: 'Geometria 2' },
|
||||
{ id: 'eti', label: 'Elementi di Teoria degli Insiemi' },
|
||||
{ id: 'eta', label: 'Elementi di Topologia Algebrica' },
|
||||
{ id: 'ega', label: 'Elementi di Geometria Algebrica' },
|
||||
{ id: 'gtd', label: 'Geometria e Topologia Differenziale' },
|
||||
{ id: 'ist-anal', label: 'Istituzioni di Analisi' },
|
||||
{ id: 'ist-geom', label: 'Istituzioni di Geometria' },
|
||||
{ id: 'ist-fis', label: 'Istituzioni di Fisica' },
|
||||
{ id: 'ist-prob', label: 'Istituzioni di Probabilità' },
|
||||
{ id: 'ist-alg', label: 'Istituzioni di Algebra' },
|
||||
{ id: 'ist-num', label: 'Istituzioni di Analisi Numerica' },
|
||||
{ id: 'analisi-1', label: 'Analisi 1' },
|
||||
{ id: 'analisi-2', label: 'Analisi 2' },
|
||||
{ id: 'aritmetica', label: 'Aritmetica' },
|
||||
{ id: 'programmazione', label: 'Programmazione' },
|
||||
{ id: 'fisica-1', label: 'Fisica 1' },
|
||||
{ id: 'steffe-1', label: 'Laboratorio di Comunicazione Mediante Calcolatore' },
|
||||
]
|
||||
|
||||
const useAutosizeTextarea = ({ minRows } = {}) => {
|
||||
minRows ??= 1
|
||||
|
||||
const textareaRef = useRef(null)
|
||||
|
||||
const updateTextareaHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
updateTextareaHeight()
|
||||
})
|
||||
|
||||
return {
|
||||
rows: minRows,
|
||||
ref: textareaRef,
|
||||
onInput: updateTextareaHeight,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTag = tag => {
|
||||
return tag
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\p{L}0-9\/\-]/gu, '')
|
||||
}
|
||||
|
||||
const InputTags = ({ tags, setTags, availableTags }) => {
|
||||
availableTags ??= []
|
||||
|
||||
const [id] = useState('tags-' + randomHex())
|
||||
|
||||
const nextRef = useRef()
|
||||
const [nextText, setNextText] = useState('')
|
||||
|
||||
const onFocus = e => {
|
||||
if (!e.target.closest('.tags .tag .remove')) {
|
||||
nextRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = tag => {
|
||||
setTags(tags => tags.filter(t => t !== tag))
|
||||
}
|
||||
|
||||
const addTag = tag => {
|
||||
setTags(tags => [...tags, tag])
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (e.key === 'Backspace' && nextText.length === 0) {
|
||||
removeTag(tags.at(-1))
|
||||
}
|
||||
|
||||
const trimmed = nextText.trim()
|
||||
if (e.key === 'Enter' && trimmed.length > 0) {
|
||||
addTag(normalizeTag(trimmed))
|
||||
setNextText('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-tags" onClick={onFocus}>
|
||||
{tags.map(tag => (
|
||||
<div class="tag">
|
||||
<span>{tag}</span>
|
||||
<span class="remove" onClick={() => removeTag(tag)}>
|
||||
<i class="fa-solid fa-remove"></i>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
ref={nextRef}
|
||||
list={id}
|
||||
value={nextText}
|
||||
onInput={e => setNextText(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<datalist id={id}>
|
||||
{availableTags.map(({ id, label }) => (
|
||||
<option value={id} label={label} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const withClasses = a => {
|
||||
if (Array.isArray(a)) {
|
||||
return { class: a.filter(className => !!className).join(' ') }
|
||||
} else if (typeof a === 'object') {
|
||||
return {
|
||||
class: Object.entries(a)
|
||||
.flatMap(([className, active]) => (active ? [className] : []))
|
||||
.join(' '),
|
||||
}
|
||||
} else if (typeof a === 'string') {
|
||||
return { class: a }
|
||||
} else {
|
||||
throw new Error(`Invalid class format`)
|
||||
}
|
||||
}
|
||||
|
||||
const UploadRegion = ({}) => {
|
||||
const [draggingOver, setDraggingOver] = useState(false)
|
||||
|
||||
const onDragOver = e => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
setDraggingOver(true)
|
||||
}
|
||||
|
||||
const onDragLeave = () => {
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
const onDropFiles = files => {
|
||||
if (files.length !== 1) {
|
||||
throw new Error('Must drop one file')
|
||||
}
|
||||
|
||||
const [file] = files
|
||||
console.dir(file)
|
||||
|
||||
if (file.type !== 'application/pdf') {
|
||||
console.error('The file must be a PDF')
|
||||
}
|
||||
|
||||
console.log(formatFileSize(file.size))
|
||||
}
|
||||
|
||||
const onDrop = e => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
onDropFiles(
|
||||
[...e.dataTransfer.items].flatMap(item =>
|
||||
item.kind === 'file' ? [item.getAsFile()] : []
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onDropFiles([...e.dataTransfer.files])
|
||||
}
|
||||
|
||||
setDraggingOver(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...withClasses(['upload-region', draggingOver && 'dragging-over'])}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{!draggingOver ? (
|
||||
<>
|
||||
<span>Trascina qui un PDF oppure usa il tasto sottostante</span>
|
||||
<InputFile accept="application/pdf" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div class="release-text">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
<span>Rilascia per iniziare il caricamento</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Progress = ({ value, max }) => {
|
||||
return (
|
||||
<div class="progress-bar">
|
||||
<div class="indicator" style={{ width: Math.floor((value / max) * 100) + '%' }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CancellableUpload = ({ uploadedSize, totalSize, onCancel }) => {
|
||||
return (
|
||||
<>
|
||||
<div class="progress-bytes">
|
||||
{formatFileSize(uploadedSize)} / {formatFileSize(totalSize)}
|
||||
</div>
|
||||
<div class="progress-and-action">
|
||||
<Progress value={uploadedSize} max={totalSize} />
|
||||
<button onClick={onCancel}>Annulla</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadPopup = ({}) => {
|
||||
const [shown, setShown] = useState(false)
|
||||
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
const [doneUploading, setDoneUploading] = useState(false)
|
||||
|
||||
const hash = '59e514dd50c63051'
|
||||
|
||||
return (
|
||||
<>
|
||||
{shown && (
|
||||
<div class="upload-popup">
|
||||
<div class="popup">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
Carica la dispensa "<code>file.pdf</code>"
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>
|
||||
{/* TODO: */}
|
||||
Inserisci titolo, descrizione e tag per questa dispensa, una volta
|
||||
premuto <strong>Salva</strong> la dispensa verrà aggiunta
|
||||
inizialmente non sarà visibile nell'elenco degli appunti finché non
|
||||
verrà approvata da un moderatore
|
||||
</p>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea placeholder="Descrizione..." {...descriptionTextareaProps}>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
|
||||
{!doneUploading ? (
|
||||
<CancellableUpload
|
||||
uploadedSize={34.2 * 1024 ** 2}
|
||||
totalSize={45.6 * 1024 ** 2}
|
||||
/>
|
||||
) : (
|
||||
<div class="right">
|
||||
<div class="upload-message">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
<span title={hash}>File caricato</span>
|
||||
</div>
|
||||
<button>Annulla</button>
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const InputFile = ({ accept, onFile }) => {
|
||||
const inputFileRef = useRef()
|
||||
|
||||
const [file, setFile] = useState(null)
|
||||
|
||||
const onButtonClick = e => {
|
||||
inputFileRef.current.click()
|
||||
}
|
||||
|
||||
const onInputFile = e => {
|
||||
if (e.target.files.length === 1) {
|
||||
const f = e.target.files[0]
|
||||
|
||||
setFile(f)
|
||||
onFile?.(f)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="input-file">
|
||||
<input type="file" ref={inputFileRef} accept={accept} onInput={onInputFile} />
|
||||
<button onClick={onButtonClick}>Carica File</button>
|
||||
{file && <div class="file-name">{file.name}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TabellaApprovazioni = ({ pendingApprovazioni }) => {
|
||||
return (
|
||||
<div class="table approvazioni">
|
||||
<div class="download header"></div>
|
||||
<div class="hash header">PDF</div>
|
||||
<div class="title header">Dispensa</div>
|
||||
<div class="owner header">Proprietario</div>
|
||||
<div class="actions header">Azioni</div>
|
||||
|
||||
{intersperse(
|
||||
pendingApprovazioni.map(({ id, title, owner, hash }) => (
|
||||
<>
|
||||
<div class="download">
|
||||
<a class="button icon" href={`/appunti/files/${hash}`}>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hash">
|
||||
<code>{hash}</code>
|
||||
</div>
|
||||
<div class="title">
|
||||
<a href={`/appunti/${id}`} title={id}>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
<div class="owner">
|
||||
<a href={`/u/${owner}`}>@{owner}</a>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="icon">
|
||||
<i class="fa-solid fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)),
|
||||
<div class="separator"></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const App = ({}) => {
|
||||
const descriptionTextareaProps = useAutosizeTextarea({ minRows: 2 })
|
||||
|
||||
const [tags, setTags] = useState(['geometria-1', 'fortuna', 'frigerio', '2013/2014'])
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadRegion />
|
||||
<UploadPopup />
|
||||
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>Le tue dispense</h1>
|
||||
<div class="table dispense">
|
||||
<div class="edit header"></div>
|
||||
<div class="name header">Nome</div>
|
||||
<div class="tags header">Tags</div>
|
||||
<div class="status header">Stato</div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name">Appunti di Geometria 1</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-1</div>
|
||||
<div class="tag">prof-1</div>
|
||||
<div class="tag">2016/2017</div>
|
||||
</div>
|
||||
<div class="status approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="expanded">
|
||||
<div class="edit-close">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edit-container">
|
||||
<div class="header">
|
||||
<a href="/appunti/6f82dca3d83b475c">
|
||||
<span class="title">Mezzedimi</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="label">Nome</div>
|
||||
<input type="text" placeholder="Nome" value="Mezzedimi" />
|
||||
<div class="label">Descrizione</div>
|
||||
<textarea
|
||||
placeholder="Descrizione..."
|
||||
{...descriptionTextareaProps}
|
||||
>
|
||||
Best dispensa di Geometria 1 ever written anche se non in LaTeX
|
||||
</textarea>
|
||||
<div class="label">Tags</div>
|
||||
<InputTags {...{ tags, setTags, availableTags }} />
|
||||
</div>
|
||||
<p>
|
||||
Puoi anche caricare una nuova versione del PDF ma ricorda che se
|
||||
carichi un file non precedentemente approvato inizialmente
|
||||
scomparirà dall'elenco principale in attesa di apporvazione da parte
|
||||
di un moderatore.
|
||||
</p>
|
||||
<div class="form">
|
||||
<div class="label">Stato</div>
|
||||
<div class="stato-approvazione approved">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
Approvata
|
||||
</div>
|
||||
|
||||
<div class="label">Cambia PDF</div>
|
||||
<InputFile accept="application/pdf" />
|
||||
|
||||
<div class="right">
|
||||
<button class="primary">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name pending">Appunti di Geometria 2</div>
|
||||
<div class="tags">
|
||||
<div class="tag">geometria-2</div>
|
||||
<div class="tag">prof-2</div>
|
||||
<div class="tag">2017/2018</div>
|
||||
<div class="tag">tanti-tag-1</div>
|
||||
<div class="tag">tanti-tag-2</div>
|
||||
<div class="tag">tanti-tag-3</div>
|
||||
<div class="tag">tanti-tag-4</div>
|
||||
<div class="tag">tanti-tag-5</div>
|
||||
<div class="tag">tanti-tag-6</div>
|
||||
</div>
|
||||
<div class="status pending" title="In attesa di approvazione...">
|
||||
<i class="fas fa-hourglass"></i>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="edit">
|
||||
<button class="icon flat">
|
||||
<i class="fa-solid fa-angle-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="name rejected">F1Le SuP3R LeGaLe</div>
|
||||
<div class="tags">
|
||||
<div class="tag">foo</div>
|
||||
<div class="tag">bar</div>
|
||||
</div>
|
||||
<div class="status rejected">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex col gap-1 fill-h">
|
||||
<h1>PDF da Approvare</h1>
|
||||
<TabellaApprovazioni
|
||||
pendingApprovazioni={[
|
||||
{
|
||||
id: '59e514dd50c63051',
|
||||
title: 'GAAL',
|
||||
owner: 'mezzedimi',
|
||||
hash: '2c8d593c6be289ab',
|
||||
},
|
||||
{
|
||||
id: '4c52d9726c438f3d',
|
||||
title: 'Dispensa 1',
|
||||
owner: 'persona-1',
|
||||
hash: '346ba392a3a1eb86',
|
||||
},
|
||||
{
|
||||
id: 'eecb96f04e319c4c',
|
||||
title: 'Dispensa 2',
|
||||
owner: 'persona-1',
|
||||
hash: '74f9652c28f82e7f',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render(<App />, document.querySelector('#app'))
|
||||
@@ -1,17 +0,0 @@
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`
|
||||
}
|
||||
if (bytes < 1024 ** 2) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`
|
||||
}
|
||||
if (bytes < 1024 ** 3) {
|
||||
return `${(bytes / 1024 ** 2).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
export function intersperse(list, separator) {
|
||||
return list.flatMap((el, i) => (i === 0 ? [el] : [separator, el]))
|
||||
}
|
||||
+9
-602
@@ -5,12 +5,10 @@
|
||||
--bg: #eaeaea;
|
||||
--fg: #333;
|
||||
|
||||
--fg-lighter: #444;
|
||||
|
||||
--bg-lighter: #f0f0f0;
|
||||
|
||||
--bg-dark: #ecedee;
|
||||
--bg-darker: #e4e5e7;
|
||||
--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;
|
||||
@@ -38,8 +36,6 @@
|
||||
--accent-2: #4eaa59;
|
||||
--accent-2-darker: #2e974c;
|
||||
--accent-2-darkest: #002d0d;
|
||||
|
||||
--accent-mix-bg: #dae4db;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -424,11 +420,6 @@ hr {
|
||||
background-color: var(--bg-darker-2);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
@@ -454,74 +445,6 @@ p.center {
|
||||
|
||||
/* Controls */
|
||||
|
||||
.progress-bar {
|
||||
background: var(--bg-darker-2-1);
|
||||
color: var(--fg);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 8px 0 #00000020, inset 0 0 8px 0 #00000030;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.progress-bar .indicator {
|
||||
border-radius: 4px;
|
||||
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, var(--accent-1), var(--accent-2));
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
animation: test-progressbar 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes test-progressbar {
|
||||
0% {
|
||||
width: 0%;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar .indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* background-size: 50px 50px;
|
||||
|
||||
--highlight: rgba(255, 255, 255, 0.1);
|
||||
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
transparent 0%,
|
||||
var(--highlight) 5%,
|
||||
var(--highlight) 25%,
|
||||
transparent 30%,
|
||||
transparent 50%,
|
||||
var(--highlight) 55%,
|
||||
var(--highlight) 75%,
|
||||
transparent 80%,
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
animation: move 4s linear infinite; */
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 50px 50px;
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.button) {
|
||||
color: var(--accent-1-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -541,7 +464,7 @@ a.button {
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: flex;
|
||||
display: inline-block;
|
||||
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -565,9 +488,6 @@ button,
|
||||
box-shadow: 0 4px 8px 0 #00000022;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
@@ -576,35 +496,21 @@ button:hover,
|
||||
box-shadow: 0 4px 8px 0 #00000033;
|
||||
}
|
||||
|
||||
button.primary,
|
||||
.button.primary {
|
||||
button.primary {
|
||||
border: 1px solid var(--accent-2-darker);
|
||||
background: var(--accent-2);
|
||||
color: var(--accent-2-darkest);
|
||||
}
|
||||
|
||||
button.primary:hover,
|
||||
.button.primary:hover {
|
||||
button.primary:hover {
|
||||
background: var(--accent-2-lighter);
|
||||
}
|
||||
|
||||
button.icon,
|
||||
.button.icon {
|
||||
button.icon {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
button.flat {
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.flat:hover {
|
||||
background: var(--bg-darker-2);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
select {
|
||||
font-family: var(--font-sf);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -633,14 +539,12 @@ select {
|
||||
/* Text Fields */
|
||||
|
||||
input[type='text'],
|
||||
input[type='file'],
|
||||
input[type='password'],
|
||||
textarea {
|
||||
input[type='password'] {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 2.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
@@ -656,17 +560,6 @@ textarea {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
min-height: 2rem;
|
||||
padding: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
input[type='password'] {
|
||||
font-family: caption;
|
||||
}
|
||||
@@ -684,21 +577,6 @@ input[type='password'].error {
|
||||
color: #311;
|
||||
}
|
||||
|
||||
/* Better File Input */
|
||||
|
||||
.input-file {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-file > input[type='file'] {
|
||||
position: fixed;
|
||||
top: -100px;
|
||||
}
|
||||
|
||||
/* Compound Controls */
|
||||
|
||||
.compound {
|
||||
@@ -967,458 +845,6 @@ form .field-set input {
|
||||
height: calc(var(--size) * 1rem);
|
||||
}
|
||||
|
||||
/* Appunti Condivisi */
|
||||
|
||||
.page-appunti-condivisi .main {
|
||||
padding: 0 1rem inherit;
|
||||
max-width: 95ch;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region {
|
||||
padding: 2rem;
|
||||
|
||||
width: 30rem;
|
||||
height: 30vh;
|
||||
|
||||
border: 3px dashed var(--bg-darker-4);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
transition: border-color 150ms ease-in-out, background-color 150ms ease-in-out;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region.dragging-over {
|
||||
border-color: var(--accent-1);
|
||||
color: var(--accent-1-fg);
|
||||
|
||||
background: var(--accent-mix-bg);
|
||||
}
|
||||
|
||||
/* Serve altrimenti l'evento dropleave viene emesso anche quando si passa da ".upload-region" ad uno dei figli, così invece rendiamo tutti i figli trasparenti agli eventi del mouse */
|
||||
.page-appunti-condivisi .upload-region.dragging-over * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-region.dragging-over .release-text {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-size: 22px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup {
|
||||
z-index: 100;
|
||||
background-color: #00000066;
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
/* backdrop-filter: blur(0.5rem); */
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: 0 0 32px #00000033;
|
||||
|
||||
min-width: 40ch;
|
||||
max-width: 60ch;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .block {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .form {
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .popup .form .label {
|
||||
min-width: 10ch;
|
||||
}
|
||||
|
||||
.form .progress-bytes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
|
||||
user-select: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form .progress-and-action {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup > .header {
|
||||
background: var(--bg-darker);
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
|
||||
color: var(--fg-lighter);
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .title {
|
||||
font-size: 20px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .upload-popup .popup .title code {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi #app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 3rem;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table {
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-darker-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense {
|
||||
grid-template-columns: auto 1fr minmax(auto, 50ch) auto;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .pending {
|
||||
color: royalblue;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .rejected {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table > .header {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--bg-darker);
|
||||
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table .separator {
|
||||
grid-column: 1 / -1;
|
||||
border-bottom: 1px solid var(--bg-darker-2);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit,
|
||||
.page-appunti-condivisi .table.dispense .name,
|
||||
.page-appunti-condivisi .table.dispense .tags,
|
||||
.page-appunti-condivisi .table.dispense .status {
|
||||
padding: 1rem 0 1rem 1rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .tags {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .status {
|
||||
place-self: center;
|
||||
|
||||
padding: 1rem 1.25rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .expanded {
|
||||
grid-column: span 4;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .expanded p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header code {
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container > .header .title {
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container .stato-approvazione {
|
||||
font-weight: var(--font-weight-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-container .stato-approvazione.approved {
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.dispense .edit-close {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni {
|
||||
grid-template-columns: auto auto 1fr auto auto;
|
||||
|
||||
width: 80ch;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .download,
|
||||
.page-appunti-condivisi .table.approvazioni .hash,
|
||||
.page-appunti-condivisi .table.approvazioni .title,
|
||||
.page-appunti-condivisi .table.approvazioni .owner,
|
||||
.page-appunti-condivisi .table.approvazioni .actions {
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .hash {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .table.approvazioni .actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem 2rem;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form button:not(.icon) {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .label {
|
||||
height: 2rem;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > textarea {
|
||||
resize: vertical;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .fill {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .form > .right {
|
||||
grid-column: span 2;
|
||||
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags {
|
||||
padding: 0.5rem;
|
||||
min-height: 2rem;
|
||||
|
||||
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;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag {
|
||||
user-select: none;
|
||||
|
||||
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;
|
||||
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag .remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags .tag .remove:hover {
|
||||
color: var(--card-content);
|
||||
}
|
||||
|
||||
.page-appunti-condivisi .input-tags input[type='text'] {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
height: 1.5rem;
|
||||
|
||||
min-height: unset;
|
||||
max-width: 10rem;
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Animazioni */
|
||||
|
||||
/* Pending hourglass with a bit of overshoot */
|
||||
/* @keyframes pending-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
45% {
|
||||
transform: rotate(190deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
60% {
|
||||
transform: rotate(175deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(185deg);
|
||||
}
|
||||
95% {
|
||||
transform: rotate(370deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
} */
|
||||
|
||||
@keyframes pending-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -1517,22 +943,3 @@ table td {
|
||||
table tbody tr:hover {
|
||||
background: var(--bg-darker);
|
||||
}
|
||||
|
||||
/* Utils */
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex.col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.fill-h {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ body.dark-mode {
|
||||
--bg-dark: hsl(10, 10%, 20%);
|
||||
--bg-darker: hsl(10, 10%, 17%);
|
||||
--bg-darker-2: #1d2021;
|
||||
--bg-darker-2-1: #1d5021;
|
||||
--bg-darker-3: #101111;
|
||||
|
||||
--accent-1: #154d24;
|
||||
@@ -28,7 +29,6 @@ body.dark-mode {
|
||||
--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;
|
||||
@@ -59,8 +59,7 @@ body.dark-mode {
|
||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.dark-mode input[type],
|
||||
.dark-mode textarea {
|
||||
.dark-mode input[type] {
|
||||
background: #4b4342;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
{{define "title"}}Condivisione Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
Condivisione Appunti
|
||||
</h1>
|
||||
<div id="app"></div>
|
||||
</section>
|
||||
<script src="/public/appunti-condivisi.min.js"></script>
|
||||
{{end}}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{ .Dispensa.Title }} • Appunti • PHC{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<section>
|
||||
<h1>
|
||||
<i class="fas fa-book"></i>
|
||||
{{ .Dispensa.Title }}
|
||||
</h1>
|
||||
<p>
|
||||
{{ .Dispensa.Description }}
|
||||
</p>
|
||||
{{end}}
|
||||
+18
-7
@@ -14,8 +14,18 @@
|
||||
<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>
|
||||
<p>
|
||||
Il PHC è un progetto di sperimentazione tecnologica di alcuni studenti di Matematica di Pisa che va avanti fin dal 1994.
|
||||
</p>
|
||||
<p>
|
||||
Fisicamente, occupa la stanza 106 del Dipartimento, mentre questo sito ne è la sede virtuale. Qui si trovano vari servizi offerti agli studenti, e ne si documenta lo sviluppo nella pagina <a href="/news">Notizie</a>.
|
||||
</p>
|
||||
<p>
|
||||
Nella sezione <b>Progetti</b> si trovano alcune delle attività e dei servizi del PHC, mentre <a href="/utenti">Utenti</a> contiene l'indice delle pagine personali degli studenti, che sono storicamente uno dei progetti più importanti del PHC.
|
||||
</p>
|
||||
<p>
|
||||
Per più informazioni sul PHC, si consulti la sezione <a href="/storia">Storia</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,14 +100,15 @@
|
||||
</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="title">
|
||||
Vuoi diventare un macchinista?
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
<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.
|
||||
Ti piace (o piacerebbe saper) smanettare al PC e (s)montare aggeggi tecnologici?
|
||||
Stai spesso in dipartimento? Allora fai pure un salto in PHC a parlare con noi, per diventare apprendista (e magari un giorno macchinista) o semplicemente per imparare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<a class="nav-element" href="/progetti">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Progetti
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
@@ -35,14 +35,14 @@
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<div class="name">
|
||||
<a class="nav-element" href="/risorse">
|
||||
<div class="nav-element">
|
||||
<div class="icon">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="label">
|
||||
Risorse
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-items">
|
||||
<div class="nav-item">
|
||||
@@ -85,4 +85,4 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="/public/profilo.min.js"></script>
|
||||
<section x-data="profilo">
|
||||
<h1>Profilo di <strong>@{{ .User.Username }}</strong></h1>
|
||||
<h1>Profilo di <strong>{{ .User.Name }} {{ .User.Surname }}</strong></h1>
|
||||
<p class="center">
|
||||
<a class="button" href="/logout">Logout</a>
|
||||
</p>
|
||||
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
{{template "base" .}}
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Utenti • PHC{{end}}
|
||||
{{define "body"}}
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
@@ -37,8 +37,8 @@
|
||||
<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 :
|
||||
sortMode === 'surname' ?
|
||||
entry.item.cognome + ' ' + entry.item.nome :
|
||||
entry.item.nome + ' ' + entry.item.cognome
|
||||
"></a>
|
||||
<template x-if="entry.item.tags?.includes('macchinista')">
|
||||
@@ -46,6 +46,11 @@
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="entry.item.tags?.includes('rappresentante')">
|
||||
<div class="icon" title="Rappresentante">
|
||||
<i class="fas fa-landmark"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template x-if="entry.score">
|
||||
<span x-text="entry.score"></span>
|
||||
</template> -->
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package appunti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
|
||||
// Service isola l'handler dal modulo del database e si occupa solo delle interazioni riguardanti gli appunti e le dispense
|
||||
type Service interface {
|
||||
GetDispensa(id string) (*model.Dispensa, error)
|
||||
CreateDispensa(template model.Dispensa) (string, error)
|
||||
}
|
||||
|
||||
type DefaultService struct {
|
||||
DB *database.DB
|
||||
}
|
||||
|
||||
var _ Service = &DefaultService{}
|
||||
|
||||
func (s *DefaultService) GetDispensa(id string) (*model.Dispensa, error) {
|
||||
dispensa, err := s.DB.Dispense.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := s.DB.DispensaTags.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Dispensa{
|
||||
Id: dispensa.Id,
|
||||
OwnerId: dispensa.OwnerId,
|
||||
Title: dispensa.Title,
|
||||
Description: dispensa.Description,
|
||||
Tags: tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DefaultService) CreateDispensa(template model.Dispensa) (string, error) {
|
||||
dispensaId, err := s.DB.Dispense.Create(database.Dispensa{
|
||||
OwnerId: template.OwnerId,
|
||||
Title: template.Title,
|
||||
Description: template.Description,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := s.DB.DispensaTags.Set(dispensaId, template.Tags); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
+13
-3
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
urlpkg "net/url"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
)
|
||||
@@ -44,8 +44,13 @@ func newLDAPAuthService(host string) Service {
|
||||
|
||||
// doGetRequest is a utility to make HTTP GET requests
|
||||
func (a *ldapAuthService) doGetRequest(url string, response interface{}) error {
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", path.Join(a.Host, "poisson-ldap", url), bytes.NewBuffer([]byte("")),
|
||||
"GET", u, bytes.NewBuffer([]byte("")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -74,7 +79,12 @@ func (a *ldapAuthService) doPostRequest(url string, request interface{}, respons
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), bytes.NewBuffer(jsonStr))
|
||||
u, err := urlpkg.JoinPath(a.Host, "poisson-ldap", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", u, bytes.NewBuffer(jsonStr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,32 +3,28 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"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/database/sqlite"
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
"git.phc.dm.unipi.it/phc/website/server"
|
||||
"git.phc.dm.unipi.it/phc/website/storia"
|
||||
"git.phc.dm.unipi.it/phc/website/templates"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Load()
|
||||
|
||||
auth := auth.NewDefaultService(config.AuthServiceHost)
|
||||
authService := auth.NewDefaultService(config.AuthServiceHost)
|
||||
|
||||
// Create database connection and apply pending migrations
|
||||
db := util.Must(sqlite.New("phc-server.local.db"))
|
||||
if err := db.Migrate("./database/migrations"); err != nil {
|
||||
panic(err)
|
||||
listaUtentiService, err := lista_utenti.New(authService, config.ListaUtenti)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(&handler.DefaultHandler{
|
||||
AuthService: auth,
|
||||
h := &handler.DefaultHandler{
|
||||
AuthService: authService,
|
||||
Renderer: templates.NewRenderer(
|
||||
"./_views/",
|
||||
"./_views/base.html",
|
||||
@@ -39,14 +35,13 @@ func main() {
|
||||
Storia: &storia.JsonFileStoria{
|
||||
Path: "./_content/storia.yaml",
|
||||
},
|
||||
ListaUtenti: util.Must(lista_utenti.New(auth, config.ListaUtenti)),
|
||||
Appunti: &appunti.DefaultService{
|
||||
DB: db,
|
||||
},
|
||||
})
|
||||
ListaUtenti: listaUtentiService,
|
||||
}
|
||||
|
||||
app := server.NewFiberServer(h)
|
||||
|
||||
log.Printf("Starting server on host %q", config.Host)
|
||||
if err := app.Listen(config.Host); err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sqlite.New("phc-server.local.db")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Migrate("database/migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package database
|
||||
|
||||
type DBMigrate interface {
|
||||
Migrate(migrationDir string) error
|
||||
}
|
||||
|
||||
type DBQueryAppunti interface {
|
||||
AllDispensaFile() ([]DispensaFile, error)
|
||||
}
|
||||
|
||||
type DBDispense interface {
|
||||
Create(template Dispensa) (string, error)
|
||||
Get(id string) (Dispensa, error)
|
||||
All() ([]Dispensa, error)
|
||||
Update(d Dispensa) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBUploads interface {
|
||||
Create(template Upload) (string, error)
|
||||
Get(id string) (Upload, error)
|
||||
}
|
||||
|
||||
type DBFileApprovals interface {
|
||||
Create(template FileApproval) (string, error)
|
||||
Get(id string) (FileApproval, error)
|
||||
All() ([]FileApproval, error)
|
||||
Update(d FileApproval) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DBDispensaTags interface {
|
||||
Set(dispensaId string, tags []string) error
|
||||
Get(dispensaId string) ([]string, error)
|
||||
}
|
||||
|
||||
type DBDownloads interface {
|
||||
Create(template Download) error
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
DBMigrate
|
||||
DBQueryAppunti
|
||||
|
||||
Dispense DBDispense
|
||||
Uploads DBUploads
|
||||
FileApprovals DBFileApprovals
|
||||
DispensaTags DBDispensaTags
|
||||
Downloads DBDownloads
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
-- Dispense
|
||||
CREATE TABLE IF NOT EXISTS "dispense"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Contenuto caricato
|
||||
CREATE TABLE IF NOT EXISTS "uploads"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"file" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Approvazioni contenuti caricati
|
||||
CREATE TABLE IF NOT EXISTS "file_approvals"(
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL, -- Moderatore che ha creato questa approvazione
|
||||
"upload_id" TEXT NOT NULL, -- Upload a cui si riferisce questa approvazione
|
||||
"status" TEXT NOT NULL, -- Risultato dell'approvazione: "approved" | "rejected"
|
||||
FOREIGN KEY (upload_id) REFERENCES uploads(id)
|
||||
);
|
||||
|
||||
-- Download counter
|
||||
CREATE TABLE IF NOT EXISTS "downloads"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
|
||||
-- Tags per le dispense
|
||||
CREATE TABLE IF NOT EXISTS "tags"(
|
||||
"dispensa_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
FOREIGN KEY (dispensa_id) REFERENCES dispense(id)
|
||||
);
|
||||
@@ -1,53 +0,0 @@
|
||||
package database
|
||||
|
||||
//
|
||||
// Tables
|
||||
//
|
||||
|
||||
type Dispensa struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
|
||||
type FileApproval struct {
|
||||
Id string `db:"id"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
Status string `db:"status"` // "approved" | "rejected"
|
||||
}
|
||||
|
||||
type Download struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Timestamp string `db:"timestamp"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
Tag string `db:"tag"`
|
||||
}
|
||||
|
||||
//
|
||||
// Common Joins
|
||||
//
|
||||
|
||||
// DispensaFile is a join of dispensa with the most recent upload for that "dispensa_id"
|
||||
type DispensaFile struct {
|
||||
DispensaId string `db:"dispensa_id"`
|
||||
UploadId string `db:"upload_id"`
|
||||
OwnerId string `db:"owner_id"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
File string `db:"file"`
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
Timestamp string
|
||||
Filename string
|
||||
}
|
||||
|
||||
func New(filename string) (*database.DB, error) {
|
||||
db, err := sqlx.Open("sqlite3", filename+"?_fk=1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &database.DB{
|
||||
DBMigrate: &sqliteDBMigrate{db},
|
||||
// DBQueryAppunti: ...,
|
||||
|
||||
Dispense: &sqliteDBDispense{db},
|
||||
Uploads: &sqliteDBUploads{db},
|
||||
FileApprovals: &sqliteDBFileApprovals{db},
|
||||
Downloads: &sqliteDBDownloads{db},
|
||||
DispensaTags: &sqliteDBTags{db},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBMigrate struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBMigrate = sqliteDBMigrate{}
|
||||
|
||||
func (db sqliteDBMigrate) Migrate(migrationFolder string) error {
|
||||
log.Printf(`Creating migrations table`)
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(`Loading applied migrations`)
|
||||
appliedMigrations := []migration{}
|
||||
if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(migrationFolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return fmt.Errorf("no dirs in migrations folder")
|
||||
}
|
||||
|
||||
if i < len(appliedMigrations) {
|
||||
if appliedMigrations[i].Filename != entry.Name() {
|
||||
return fmt.Errorf("misapplied migration %q with %q", appliedMigrations[i].Filename, entry.Name())
|
||||
}
|
||||
|
||||
log.Printf("Found applied migration %q", entry.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Applying new migration %q", entry.Name())
|
||||
|
||||
migrationPath := path.Join(migrationFolder, entry.Name())
|
||||
|
||||
sqlStmts, err := os.ReadFile(migrationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(sqlStmts)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO migrations VALUES (?, ?)`,
|
||||
time.Now().Format(time.RFC3339),
|
||||
entry.Name(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("All migrations applied successfully, database up to date")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDispense struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispense = sqliteDBDispense{}
|
||||
|
||||
func (d sqliteDBDispense) Create(template database.Dispensa) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
dispense(id, created_at, owner_id, title, description)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :title, :description)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Get(id string) (database.Dispensa, error) {
|
||||
var dispensa database.Dispensa
|
||||
if err := d.DB.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil {
|
||||
return database.Dispensa{}, err
|
||||
}
|
||||
|
||||
return dispensa, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) All() ([]database.Dispensa, error) {
|
||||
var dispense []database.Dispensa
|
||||
if err := d.DB.Select(&dispense, `SELECT * FROM dispense`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dispense, nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Update(dispensa database.Dispensa) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
UPDATE
|
||||
dispense
|
||||
SET
|
||||
owner_id = :owner_id
|
||||
title = :title,
|
||||
description = :description
|
||||
WHERE
|
||||
id = :id
|
||||
`, &dispensa); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d sqliteDBDispense) Delete(id string) error {
|
||||
panic("TODO: Not implemented")
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBUploads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBUploads = sqliteDBUploads{}
|
||||
|
||||
func (u sqliteDBUploads) Create(template database.Upload) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
|
||||
if _, err := u.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
uploads(id, created_at, owner_id, dispensa_id, file)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :dispensa_id, :file)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (u sqliteDBUploads) Get(id string) (database.Upload, error) {
|
||||
var upload database.Upload
|
||||
if err := u.DB.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil {
|
||||
return database.Upload{}, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"git.phc.dm.unipi.it/phc/website/util"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBFileApprovals struct{ *sqlx.DB }
|
||||
|
||||
func (f sqliteDBFileApprovals) Create(template database.FileApproval) (string, error) {
|
||||
template.Id = util.GenerateRandomString(8)
|
||||
template.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
if _, err := f.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
file_approvals(id, created_at, owner_id, upload_id, status)
|
||||
VALUES
|
||||
(:id, :created_at, :owner_id, :upload_id, :status)
|
||||
`, &template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return template.Id, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Get(id string) (database.FileApproval, error) {
|
||||
var fileApproval database.FileApproval
|
||||
if err := f.DB.Get(&fileApproval, `SELECT * FROM file_approvals WHERE id = ?`, id); err != nil {
|
||||
return database.FileApproval{}, err
|
||||
}
|
||||
|
||||
return fileApproval, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) All() ([]database.FileApproval, error) {
|
||||
var fileApprovals []database.FileApproval
|
||||
if err := f.DB.Select(&fileApprovals, `SELECT * FROM file_approvals`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileApprovals, nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Update(fileApproval database.FileApproval) error {
|
||||
if _, err := f.DB.NamedExec(`
|
||||
UPDATE
|
||||
file_approvals
|
||||
SET
|
||||
owner_id = :owner_id,
|
||||
upload_id = :upload_id,
|
||||
status = :status
|
||||
WHERE
|
||||
id = :id
|
||||
`, &fileApproval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f sqliteDBFileApprovals) Delete(id string) error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBDownloads struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDownloads = sqliteDBDownloads{}
|
||||
|
||||
func (d sqliteDBDownloads) Create(template database.Download) error {
|
||||
if _, err := d.DB.NamedExec(`
|
||||
INSERT INTO
|
||||
downloads(dispensa_id, timestamp)
|
||||
VALUES
|
||||
(:dispensa_id, :timestamp)
|
||||
`, &template); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sqliteDBTags struct{ *sqlx.DB }
|
||||
|
||||
var _ database.DBDispensaTags = sqliteDBTags{}
|
||||
|
||||
func (t sqliteDBTags) Set(dispensaId string, tags []string) error {
|
||||
tagsRows := []map[string]any{}
|
||||
for _, t := range tags {
|
||||
tagsRows = append(tagsRows, map[string]any{
|
||||
"dispensa_id": dispensaId,
|
||||
"name": t,
|
||||
})
|
||||
}
|
||||
|
||||
tx, err := t.DB.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
DELETE FROM tags WHERE dispensa_id = ?
|
||||
`, dispensaId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(`
|
||||
INSERT INTO tags(dispensa_id, name) VALUES (:dispensa_id, :name)
|
||||
`, tagsRows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t sqliteDBTags) Get(dispensaId string) ([]string, error) {
|
||||
var tags []string
|
||||
if err := t.DB.Select(&tags, `SELECT name FROM tags WHERE dispensa_id = ?`, dispensaId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -18,9 +18,7 @@ require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/gorilla/feeds v1.1.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
|
||||
@@ -15,28 +15,21 @@ github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA=
|
||||
github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U=
|
||||
github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM=
|
||||
github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
+9
-79
@@ -5,7 +5,6 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/appunti"
|
||||
"git.phc.dm.unipi.it/phc/website/articles"
|
||||
"git.phc.dm.unipi.it/phc/website/auth"
|
||||
"git.phc.dm.unipi.it/phc/website/lista_utenti"
|
||||
@@ -17,52 +16,22 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
//
|
||||
// Pages
|
||||
//
|
||||
HandleStaticPage(w io.Writer, view string, ctx Context) error
|
||||
|
||||
// Storia
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
HandleStoriaPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Appunti
|
||||
HandleAppuntiPage(w io.Writer, query string, ctx Context) error
|
||||
HandleQueryAppunti(w io.Writer, query string, ctx Context) error
|
||||
HandleAppuntiCondivisiPage(w io.Writer, ctx Context) error
|
||||
HandleDispensaPage(w io.Writer, id string, ctx Context) error
|
||||
|
||||
// News
|
||||
HandleNewsPage(w io.Writer, ctx Context) error
|
||||
|
||||
// Guide
|
||||
HandleGuidePage(w io.Writer, ctx Context) error
|
||||
|
||||
// User
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
|
||||
// Article Pages
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
|
||||
// RSS
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
// User
|
||||
HandleLogin(username, password string) (*model.Session, error)
|
||||
HandleUser(token string) *model.User
|
||||
HandleRequiredUser(ctx Context) (*model.User, error)
|
||||
|
||||
// User Listing
|
||||
HandleUtenti() ([]*model.User, error)
|
||||
HandleListaUtenti() ([]*model.ListUser, error)
|
||||
|
||||
// Appunti
|
||||
HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error)
|
||||
HandleGetDispensa(dispensaId string) (*model.Dispensa, error)
|
||||
HandleProfilePage(w io.Writer, ctx Context) error
|
||||
HandleNewsArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleGuideArticlePage(w io.Writer, articleID string, ctx Context) error
|
||||
HandleNewsFeedPage(w io.Writer) error
|
||||
HandleGuideFeedPage(w io.Writer) error
|
||||
}
|
||||
|
||||
//
|
||||
@@ -79,7 +48,6 @@ func (ctx Context) getUser() *model.User {
|
||||
// Handler holds references to abstract services for easy testing provided by every module (TODO: Make every field an interface of -Service)
|
||||
type DefaultHandler struct {
|
||||
AuthService auth.Service
|
||||
Appunti appunti.Service
|
||||
Renderer *templates.TemplateRenderer
|
||||
NewsArticlesRegistry *articles.Registry
|
||||
GuideArticlesRegistry *articles.Registry
|
||||
@@ -87,8 +55,6 @@ type DefaultHandler struct {
|
||||
Storia storia.StoriaService
|
||||
}
|
||||
|
||||
var _ Service = &DefaultHandler{}
|
||||
|
||||
func (h *DefaultHandler) HandleStaticPage(w io.Writer, view string, ctx Context) error {
|
||||
return h.Renderer.Render(w, view, util.Map{
|
||||
"User": ctx.getUser(),
|
||||
@@ -125,7 +91,7 @@ func (h *DefaultHandler) HandleStoriaPage(w io.Writer, ctx Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleAppuntiPage(w io.Writer, query string, ctx Context) error {
|
||||
func (h *DefaultHandler) HandleQueryAppunti(w io.Writer, query string, ctx Context) error {
|
||||
return h.Renderer.Render(w, "appunti.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Query": query,
|
||||
@@ -138,18 +104,6 @@ func (h *DefaultHandler) HandleAppuntiCondivisiPage(w io.Writer, ctx Context) er
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleDispensaPage(w io.Writer, id string, ctx Context) error {
|
||||
dispensa, err := h.Appunti.GetDispensa(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Renderer.Render(w, "dispensa.html", util.Map{
|
||||
"User": ctx.getUser(),
|
||||
"Dispensa": dispensa,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleNewsPage(w io.Writer, ctx Context) error {
|
||||
articles, err := h.NewsArticlesRegistry.GetArticles()
|
||||
if err != nil {
|
||||
@@ -267,27 +221,3 @@ func (h *DefaultHandler) HandleGuideFeedPage(w io.Writer) error {
|
||||
|
||||
return guideFeed.WriteRss(w)
|
||||
}
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
|
||||
func (h *DefaultHandler) HandleCreateDispensa(template model.Dispensa, ctx Context) (string, error) {
|
||||
user := ctx.getUser()
|
||||
if user == nil {
|
||||
return "", ErrNoUser
|
||||
}
|
||||
|
||||
template.OwnerId = user.Username
|
||||
|
||||
dispensaId, err := h.Appunti.CreateDispensa(template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dispensaId, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHandler) HandleGetDispensa(id string) (*model.Dispensa, error) {
|
||||
return h.Appunti.GetDispensa(id)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
type authListaUtenti struct {
|
||||
AuthService auth.Service
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
@@ -18,7 +19,12 @@ func newAuthListaUtenti(authService auth.Service) (Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti}, nil
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authListaUtenti{authService, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
@@ -38,6 +44,7 @@ func (a *authListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, a.Macchinisti)
|
||||
mergeRappresentanti(users, a.Rappresentanti)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
type jsonListaUtenti struct {
|
||||
Path string
|
||||
|
||||
Macchinisti util.Set[string]
|
||||
Macchinisti util.Set[string]
|
||||
Rappresentanti util.Set[string]
|
||||
}
|
||||
|
||||
func newJsonListaUtenti(path string) (Service, error) {
|
||||
@@ -21,7 +22,12 @@ func newJsonListaUtenti(path string) (Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti}, nil
|
||||
rappresentanti, err := loadRappresentanti()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jsonListaUtenti{path, macchinisti, rappresentanti}, nil
|
||||
}
|
||||
|
||||
func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
@@ -37,6 +43,7 @@ func (j *jsonListaUtenti) GetUtenti() ([]*model.ListUser, error) {
|
||||
}
|
||||
|
||||
mergeMacchinisti(users, j.Macchinisti)
|
||||
mergeRappresentanti(users, j.Rappresentanti)
|
||||
|
||||
log.Printf("Caricata lista di %d utenti", len(users))
|
||||
|
||||
|
||||
@@ -44,3 +44,26 @@ func mergeMacchinisti(users []*model.ListUser, macchinisti util.Set[string]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRappresentanti() (util.Set[string], error) {
|
||||
f, err := os.Open("_content/rappresentanti.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rappresentanti := []string{}
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&rappresentanti); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return util.NewSet(rappresentanti...), nil
|
||||
}
|
||||
|
||||
func mergeRappresentanti(users []*model.ListUser, rappresentanti util.Set[string]) {
|
||||
for _, user := range users {
|
||||
if _, found := rappresentanti[user.Uid]; found {
|
||||
user.Tags = append(user.Tags, "rappresentante")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package model
|
||||
|
||||
type Dispensa struct {
|
||||
Id string `json:"id"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
+5
-51
@@ -1,15 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.phc.dm.unipi.it/phc/website/config"
|
||||
"git.phc.dm.unipi.it/phc/website/handler"
|
||||
"git.phc.dm.unipi.it/phc/website/model"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/redirect/v2"
|
||||
)
|
||||
|
||||
@@ -45,7 +43,7 @@ func routes(h handler.Service, r fiber.Router) {
|
||||
//
|
||||
|
||||
r.Use(logger.New())
|
||||
// r.Use(recover.New())
|
||||
r.Use(recover.New())
|
||||
|
||||
// Remove trailing slash from URLs
|
||||
r.Use(redirect.New(redirect.Config{
|
||||
@@ -54,29 +52,9 @@ func routes(h handler.Service, r fiber.Router) {
|
||||
},
|
||||
}))
|
||||
|
||||
// Better static file serving setup
|
||||
r.Route("/public", func(r fiber.Router) {
|
||||
// Cache client side files based on checksum
|
||||
r.Use(etag.New())
|
||||
|
||||
staticConfig := fiber.Static{}
|
||||
if config.Mode == "development" {
|
||||
log.Printf("Disabling Cache-Control in development mode")
|
||||
|
||||
// if no "Cache-Control" is present the browser will cache heuristically (and we don't want that)
|
||||
r.Use(func(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "no-cache")
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
staticConfig.CacheDuration = 1 * time.Millisecond
|
||||
}
|
||||
|
||||
r.Static("/", "./_frontend/out", staticConfig)
|
||||
r.Static("/", "./_public", staticConfig)
|
||||
})
|
||||
|
||||
// Serve generated css and js files and the static "./_public" folder
|
||||
r.Static("/public/", "./_frontend/out")
|
||||
r.Static("/public/", "./_public")
|
||||
|
||||
// Process all request and add user to the request context if there is a session cookie
|
||||
r.Use(UserMiddleware(h))
|
||||
@@ -114,7 +92,7 @@ func routes(h handler.Service, r fiber.Router) {
|
||||
query := c.Query("q", "")
|
||||
|
||||
c.Type("html")
|
||||
return h.HandleAppuntiPage(c, query, CreateContext(c))
|
||||
return h.HandleQueryAppunti(c, query, CreateContext(c))
|
||||
})
|
||||
|
||||
r.Get("/appunti/condivisi", func(c *fiber.Ctx) error {
|
||||
@@ -221,28 +199,4 @@ func routesApi(h handler.Service, r fiber.Router) {
|
||||
|
||||
return c.JSON(user)
|
||||
})
|
||||
|
||||
r.Post("/appunti/dispense", func(c *fiber.Ctx) error {
|
||||
var dispensaTemplate model.Dispensa
|
||||
if err := c.BodyParser(&dispensaTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dispensaId, err := h.HandleCreateDispensa(dispensaTemplate, CreateContext(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(dispensaId)
|
||||
})
|
||||
r.Get("/appunti/dispense", func(c *fiber.Ctx) error {
|
||||
dispensaId := c.Query("id")
|
||||
|
||||
dispensa, err := h.HandleGetDispensa(dispensaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(dispensa)
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ func withSpacers(events []*GenericEvent) []*GenericEvent {
|
||||
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 "_content/storia.json" è statico
|
||||
panic(err) // Tanto nel caso si nota in fase di sviluppo visto che "_content/storia.yaml" è statico
|
||||
}
|
||||
|
||||
return year
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package util
|
||||
|
||||
import "log"
|
||||
|
||||
func Must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
+3
-3
@@ -2,11 +2,11 @@ package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
)
|
||||
|
||||
// GenerateRandomString generates a random hex string from a random array of "n" bytes
|
||||
// GenerateRandomString generates a random base64 string from a random array of "n" bytes (to prevent padding let n be a multiple of 3)
|
||||
func GenerateRandomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
|
||||
@@ -15,5 +15,5 @@ func GenerateRandomString(n int) string {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(b)
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user