Ulteriori aggiunte al prototipo della pagina Appunti Condivisi

feat/db
Antonio De Lucreziis 2 years ago
parent 8c4ed0b70e
commit 64200cd654

@ -49,7 +49,7 @@ export default defineConfig([
plugins: [esbuild({ minify: true })],
},
{
input: 'src/appunti-condivisi.jsx',
input: 'src/appunti-condivisi/main.jsx',
output: {
file: 'out/appunti-condivisi.min.js',
format: 'iife',
@ -68,7 +68,8 @@ export default defineConfig([
],
],
}),
terser(),
// https://rollupjs.org/guide/en/#-w--watch
!process.env.ROLLUP_WATCH && terser(),
],
},
])

@ -1,212 +0,0 @@
import { render } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
function randomHex(length = 16) {
return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('')
}
const useAutosizeTextarea = () => {
const [rows, setRows] = useState(1)
const onInput = e => {
const lines = e.target.value.split('\n')
console.log(lines)
setRows(lines.length)
}
return { rows, onInput }
}
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(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 App = ({}) => {
const descriptionTextareaProps = useAutosizeTextarea()
const [tags, setTags] = useState(['Geometria 1', 'Fortuna', 'Frigerio', '2013/2014'])
const availableTags = [
...Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i
return { id: `${year}/${year + 1}`, label: `Anno Accademico ${year}/${year + 1}` }
}),
{ id: 'G1', label: 'Geometria 1' },
{ id: 'G2', label: 'Geometria 2' },
{ id: 'ETI', label: 'Elementi di Teoria degli Insiemi' },
{ id: 'ETA', label: 'Elementi di Topologia Algebrica' },
{ id: 'Analisi 1', label: 'Analisi 1' },
{ id: 'Aritmetica', label: 'Aritmetica' },
{ id: 'Programmazione', label: 'Programmazione' },
{ id: 'Fisica 1', label: 'Fisica 1' },
{ id: 'Steffe 1', label: 'Laboratorio di Comunicazione Mediante Calcolatore' },
]
return (
<>
<div class="upload-region">
<span>Trascina qui un PDF oppure usa il tasto sottostante</span>
<button>Carica PDF</button>
</div>
<div class="dispense-table">
<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="separator"></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">
<p>
Qui puoi modificare le varie proprietà della dispensa, ricorda che se
carichi un nuovo PDF diverso dal precedente dovrà essere nuovamente
sottoposto a approvazione quindi inizialmente scomparirà dall'elenco
principale.
</p>
<div class="edit-form">
<div class="label">Nome</div>
<input type="text" placeholder="Nome" value="Mezzedimi" />
<div class="label">Descrizione</div>
<textarea
placeholder="Descrizione..."
value="Best dispensa di Geometria 1 ever written anche se non in LaTeX"
{...descriptionTextareaProps}
></textarea>
<div class="label">Tags</div>
<InputTags {...{ tags, setTags, availableTags }} />
<div class="label">PDF</div>
<input type="file" value="/mezzedimi.pdf" 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</div>
<div class="tag">Tanti Tag</div>
<div class="tag">Tanti Tag</div>
<div class="tag">Tanti Tag</div>
<div class="tag">Tanti Tag</div>
<div class="tag">Tanti Tag</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>
</>
)
}
render(<App />, document.querySelector('#app'))

@ -0,0 +1,526 @@
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 (Hash)</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}>
<i class="fa-solid fa-book"></i> {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">
<i class="fa-solid fa-book"></i> 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'))

@ -0,0 +1,17 @@
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]))
}

@ -5,10 +5,12 @@
--bg: #eaeaea;
--fg: #333;
--fg-lighter: #444;
--bg-lighter: #f0f0f0;
--bg-dark: hsl(220, 5%, 93%);
--bg-darker: hsl(220, 5%, 90%);
--bg-dark: #ecedee;
--bg-darker: #e4e5e7;
--bg-darker-2: #d5d5d5;
--bg-darker-2-1: #c8c8c8;
--bg-darker-3: #c0c0c0;
@ -36,6 +38,8 @@
--accent-2: #4eaa59;
--accent-2-darker: #2e974c;
--accent-2-darkest: #002d0d;
--accent-mix-bg: #dae4db;
}
* {
@ -420,6 +424,11 @@ hr {
background-color: var(--bg-darker-2);
}
sub,
sup {
font-size: 12px;
}
pre {
margin: 0.5rem 0;
@ -445,6 +454,74 @@ 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);
@ -464,7 +541,7 @@ a.button {
button,
.button {
display: inline-block;
display: flex;
font-family: var(--font-sf);
font-weight: var(--font-weight-medium);
@ -488,6 +565,9 @@ button,
box-shadow: 0 4px 8px 0 #00000022;
cursor: pointer;
align-items: center;
justify-content: center;
}
button:hover,
@ -496,17 +576,20 @@ 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;
}
@ -556,8 +639,8 @@ textarea {
border: none;
background: none;
height: 2rem;
padding: 0 0.5rem;
height: 2.25rem;
padding: 0.25rem 0.5rem;
font-size: 17px;
@ -574,15 +657,14 @@ textarea {
}
textarea {
height: unset;
height: auto;
min-height: 2rem;
padding: 0.5rem;
line-height: 1.5;
}
input[type='file'] {
padding: 0.25rem 0.5rem;
height: unset;
padding: 0.5rem;
}
input[type='password'] {
@ -602,6 +684,21 @@ 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 {
@ -873,16 +970,17 @@ form .field-set input {
/* Appunti Condivisi */
.page-appunti-condivisi .main {
padding: 0 1rem 6rem;
max-width: 100ch;
padding: 0 1rem inherit;
max-width: 95ch;
}
.page-appunti-condivisi .upload-region {
padding: 2rem;
width: 30rem;
height: 30vh;
border: 2px dashed var(--bg-darker-4);
border: 3px dashed var(--bg-darker-4);
border-radius: 0.5rem;
display: flex;
@ -892,6 +990,128 @@ form .field-set input {
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 {
@ -900,64 +1120,80 @@ form .field-set input {
align-items: center;
gap: 3rem;
width: 100%;
}
.page-appunti-condivisi .dispense-table {
.page-appunti-condivisi .table {
width: 100%;
display: grid;
grid-template-columns: auto 1fr minmax(auto, 50ch) auto;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--bg-darker-2);
box-shadow: var(--shadow-1);
overflow: hidden;
}
.page-appunti-condivisi .dispense-table .pending {
.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 .dispense-table .rejected {
.page-appunti-condivisi .table.dispense .rejected {
color: darkred;
}
.page-appunti-condivisi .dispense-table .header {
.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 .dispense-table .separator {
grid-column: span 4;
.page-appunti-condivisi .table .separator {
grid-column: 1 / -1;
border-bottom: 1px solid var(--bg-darker-2);
}
.page-appunti-condivisi .dispense-table .edit,
.page-appunti-condivisi .dispense-table .name,
.page-appunti-condivisi .dispense-table .tags,
.page-appunti-condivisi .dispense-table .status {
.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 .dispense-table .tags {
.page-appunti-condivisi .table.dispense .tags {
flex-wrap: wrap;
gap: 0.25rem;
gap: 0.5rem;
}
.page-appunti-condivisi .dispense-table .status {
.page-appunti-condivisi .table.dispense .status {
place-self: center;
padding: 1rem 1.25rem 1rem 2rem;
}
.page-appunti-condivisi .dispense-table .expanded {
.page-appunti-condivisi .table.dispense .expanded {
grid-column: span 4;
display: grid;
grid-template-columns: auto 1fr;
}
.page-appunti-condivisi .dispense-table .edit-container {
.page-appunti-condivisi .table.dispense .expanded p {
width: 100%;
}
.page-appunti-condivisi .table.dispense .edit-container {
display: flex;
flex-direction: column;
gap: 1rem;
@ -965,20 +1201,85 @@ form .field-set input {
padding: 1rem 1rem 1rem 0;
}
.page-appunti-condivisi .dispense-table .edit-close {
.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 .dispense-table .edit-form {
.page-appunti-condivisi .form {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1rem;
gap: 1rem 2rem;
align-items: center;
}
.page-appunti-condivisi .dispense-table .edit-form > .label {
.page-appunti-condivisi .form button:not(.icon) {
min-width: 6rem;
}
.page-appunti-condivisi .form > .label {
height: 2rem;
align-self: flex-start;
@ -989,18 +1290,22 @@ form .field-set input {
font-weight: var(--font-weight-medium);
}
.page-appunti-condivisi .dispense-table .edit-form > textarea {
.page-appunti-condivisi .form > textarea {
resize: vertical;
overflow-y: hidden;
}
.page-appunti-condivisi .dispense-table .edit-form > .right {
.page-appunti-condivisi .form > .fill {
grid-column: span 2;
}
.page-appunti-condivisi .form > .right {
grid-column: span 2;
display: flex;
justify-content: end;
padding-top: 0.5rem;
gap: 1rem;
}
.page-appunti-condivisi .input-tags {
@ -1212,3 +1517,22 @@ 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%;
}

@ -1,6 +1,7 @@
package server
import (
"log"
"time"
"git.phc.dm.unipi.it/phc/website/config"
@ -61,14 +62,15 @@ func routes(h handler.Service, r fiber.Router) {
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
staticConfig.MaxAge = -1
staticConfig.CacheDuration = 1 * time.Millisecond
}
r.Static("/", "./_frontend/out", staticConfig)

Loading…
Cancel
Save