'use strict'; import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js'; // Helper function that returns a promise that resolves after delay const Delay = (ms, val) => new Promise(resolve => setTimeout(resolve, ms, val)); export const Icons = { heart: props => html``, downArrowBox: props => html` `, upArrowBox: props => html` `, settings: props => html` `, scan: props => html` `, desktop: props => html` `, alert: props => html` `, bell: props => html` `, refresh: props => html` `, bars4: props => html` `, bars3: props => html` `, logout: props => html` `, save: props => html` `, email: props => html` `, expand: props => html` `, shrink: props => html` `, ok: props => html` `, fail: props => html` `, upload: props => html` `, download: props => html` `, bolt: props => html` `, home: props => html` `, link: props => html` `, shield: props => html` `, barsdown: props => html` `, arrowdown: props => html` `, arrowup: props => html` `, warn: props => html` `, info: props => html` `, exclamationTriangle: props => html` `, thumbUp: props => html` `, backward: props => html` `, }; export const tipColors = { green: 'bg-green-100 text-green-900', yellow: 'bg-yellow-100 text-yellow-900', red: 'bg-red-100 text-red-900', }; export function Button({title, onclick, disabled, cls, icon, ref, colors, hovercolor, disabledcolor}) { const [spin, setSpin] = useState(false); const cb = function(ev) { const res = onclick ? onclick() : null; if (res && typeof (res.catch) === 'function') { setSpin(true); res.catch(() => false).then(() => setSpin(false)); } }; if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400'; return html` ${title} <${spin ? Icons.refresh : icon} class="w-4 ${spin ? 'animate-spin' : ''}" /> />` }; export function Notification({ok, text, close}) { const closebtn = useRef(null); const from = 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2'; const to = 'translate-y-0 opacity-100 sm:translate-x-0'; const [tr, setTr] = useState(from); useEffect(function() { setTr(to); setTimeout(ev => closebtn && closebtn.current.click && closebtn.current.click(), 1500); }, []); const onclose = ev => { setTr(from); setTimeout(close, 300); }; return html` <${ok ? Icons.ok : Icons.failed} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" /> /> ${text} Anyone with a link can now view this file. /> Close /> /> /> /> /> /> /> />`; }; export function Login({loginFn, logoIcon, title, tipText}) { const [user, setUser] = useState(''); const [pass, setPass] = useState(''); const onsubmit = function(ev) { const authhdr = 'Basic ' + btoa(user + ':' + pass); const headers = {Authorization: authhdr}; return fetch('api/login', {headers}).then(loginFn).finally(r => setPass('')); }; return html` <${logoIcon} class="h-12 stroke-cyan-600 stroke-1" /> ${title || 'Login'}/> /> Username setUser(ev.target.value)} value=${user} /> /> Password setPass(ev.target.value)} value=${pass} onchange=${onsubmit} /> /> <${Button} title="Sign In" icon=${Icons.logout} onclick=${onsubmit} cls="flex w-full justify-center" /> /> ${tipText}/> /> />`; }; export function Colored({icon, text, colors}) { return html` ${icon && html`<${icon} class="w-5 h-5" />`} ${text}/> />`; }; export function Stat({title, text, tipText, tipIcon, tipColors}) { return html` ${title} /> ${text} /> <${Colored} text=${tipText} icon=${tipIcon} colors=${tipColors} /> /> /> /> />`; }; export function TextValue({value, setfn, disabled, placeholder, type, addonRight, addonLeft, attr}) { const f = type == 'number' ? x => setfn(parseInt(x)) : setfn; return html` ${ addonLeft && html`${addonLeft}>` } f(ev.target.value)} ...${attr} class="font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500" placeholder=${placeholder} value=${value} /> ${ addonRight && html`${addonRight}>` } />`; }; export function SelectValue({value, setfn, options, disabled}) { const toInt = x => x == parseInt(x) ? parseInt(x) : x; const onchange = ev => setfn(toInt(ev.target.value)); return html` ${options.map(v => html`${v[1]}/>`) } />`; }; export function SwitchValue({value, setfn}) { const onclick = ev => setfn(!value); const bg = !!value ? 'bg-blue-600' : 'bg-gray-200'; const tr = !!value ? 'translate-x-5' : 'translate-x-0'; return html` `; }; export function Setting(props) { return html` ${props.title}/> ${props.type == 'switch' ? h(SwitchValue, props) : props.type == 'select' ? h(SelectValue, props) : h(TextValue, props) } /> />`; }; export function Pagination({ totalItems, itemsPerPage, currentPage, setPageFn }) { const totalPages = Math.ceil(totalItems / itemsPerPage); const maxPageRange = 2; const lessThanSymbol = "<"; const greaterThanSymbol = ">"; const whiteSpace = " "; const itemcls = 'relative inline-flex items-center px-3 py-1 text-sm focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-blue-600'; const PageItem = ({ page, isActive }) => ( html` setPageFn(page)} class="${itemcls} ${isActive ? 'bg-blue-600 text-white' : 'cursor-pointer text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'}" > ${page} ` ); return html` showing ${(currentPage - 1) * itemsPerPage + 1} - ${Math.min(currentPage * itemsPerPage, totalItems)} of ${whiteSpace} ${totalItems} results setPageFn(Math.max(currentPage - 1, 1))} class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != 1 ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0"> ${lessThanSymbol} <${PageItem} page=${1} isActive=${currentPage === 1} /> ${currentPage > maxPageRange + 2 ? html`...` : ''} ${Array.from({length: Math.min(totalPages, maxPageRange * 2 + 1)}, (_, i) => Math.max(2, currentPage - maxPageRange) + i).map(page => page > 1 && page < totalPages && html`<${PageItem} page=${page} isActive=${currentPage === page} />`)} ${currentPage < totalPages - (maxPageRange + 1) ? html`...` : ''} ${totalPages > 1 ? html`<${PageItem} page=${totalPages} isActive=${currentPage === totalPages} />` : ''} setPageFn(Math.min(currentPage + 1, totalPages))} class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != totalPages ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0"> ${greaterThanSymbol} `; }; export function UploadFileButton(props) { const [upload, setUpload] = useState(null); // Upload promise const [status, setStatus] = useState(''); // Current upload status const btn = useRef(null); const input = useRef(null); // Send a large file chunk by chunk const sendFileData = function(url, fileName, fileData, chunkSize) { return new Promise(function(resolve, reject) { const finish = ok => { setUpload(null); const res = props.onupload ? props.onupload(ok, fileName, fileData.length) : null; if (res && typeof (res.catch) === 'function') { res.catch(() => false).then(() => ok ? resolve() : reject()); } else { ok ? resolve() : reject(); } }; const sendChunk = function(offset) { var chunk = fileData.subarray(offset, offset + chunkSize) || ''; var opts = {method: 'POST', body: chunk}; var fullUrl = url + '?offset=' + offset + '&total=' + fileData.length + '&name=' + encodeURIComponent(fileName); var ok; setStatus('Uploading ' + fileName + ', bytes ' + offset + '..' + (offset + chunk.length) + ' of ' + fileData.length); fetch(fullUrl, opts) .then(function(res) { if (res.ok && chunk.length > 0) sendChunk(offset + chunk.length); ok = res.ok; return res.text(); }) .then(function(text) { if (!ok) setStatus('Error: ' + text), finish(ok); // Fail if (chunk.length > 0) return; // More chunks to send setStatus(x => x + '. Done, resetting device...'); finish(ok); // All chunks sent }); }; //setFailed(false); sendChunk(0); }); }; const onchange = function(ev) { if (!ev.target.files[0]) return; let r = new FileReader(), f = ev.target.files[0]; r.readAsArrayBuffer(f); r.onload = function() { setUpload(sendFileData(props.url, f.name, new Uint8Array(r.result), 2048)); ev.target.value = ''; ev.preventDefault(); btn && btn.current.base.click(); }; }; const onclick = function(ev) { let fn; setUpload(x => fn = x); if (!fn) input.current.click(); // No upload in progress, show file dialog return fn; }; return html` <${Button} title=${props.title} icon=${Icons.download} onclick=${onclick} ref=${btn} /> ${status}/> />`; };
${text}
Anyone with a link can now view this file.
${title}
showing ${(currentPage - 1) * itemsPerPage + 1} - ${Math.min(currentPage * itemsPerPage, totalItems)} of ${whiteSpace} ${totalItems} results