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