Ulteriori aggiunte al prototipo della pagina Appunti Condivisi
parent
8c4ed0b70e
commit
64200cd654
@ -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]))
|
||||||
|
}
|
Loading…
Reference in New Issue