- Split monolithic app.js/style.css into Alpine.js components (icons, store, fileSection, confirm)
- Add central Alpine.store('files') as single source of truth for file lists and actions
- Replace duplicated Inbox/Outbox markup with reusable fileSection component (props: dir, canShare)
- Add shared window.Icons module to remove duplicated inline SVGs
- Replace browser-native confirm() for file deletion with a styled, accessible modal dialog
- Split CSS into design tokens, base, and component layers; modernize visuals
- Update web.go embed and server static routes for the new component/style dirs
89 lines
2.7 KiB
JavaScript
89 lines
2.7 KiB
JavaScript
/**
|
|
* Global state store (Alpine.store).
|
|
*
|
|
* Single source of truth for the file lists and shared actions, analogous to a
|
|
* Pinia/Redux store in a framework app. Components read reactive getters and
|
|
* dispatch actions here instead of owning their own duplicate state.
|
|
*/
|
|
(function () {
|
|
function register() {
|
|
Alpine.store('files', {
|
|
files: { inbox: [], outbox: [] },
|
|
toast: { show: false, message: '', _timer: null },
|
|
|
|
async init() {
|
|
await this.loadFiles();
|
|
},
|
|
|
|
async loadFiles() {
|
|
try {
|
|
const [inbox, outbox] = await Promise.all([
|
|
fetch('/api/files?dir=inbox').then((r) => r.json()),
|
|
fetch('/api/files?dir=outbox').then((r) => r.json()),
|
|
]);
|
|
this.files.inbox = inbox.files || [];
|
|
this.files.outbox = outbox.files || [];
|
|
} catch (e) {
|
|
this.showToast('Failed to load files');
|
|
}
|
|
},
|
|
|
|
showToast(message) {
|
|
this.toast.message = message;
|
|
this.toast.show = true;
|
|
if (this.toast._timer) clearTimeout(this.toast._timer);
|
|
this.toast._timer = setTimeout(() => {
|
|
this.toast.show = false;
|
|
}, 2500);
|
|
},
|
|
|
|
async upload(dir, fileList) {
|
|
if (!fileList || !fileList.length) return;
|
|
for (const file of fileList) {
|
|
const form = new FormData();
|
|
form.append('file', file);
|
|
form.append('dir', dir);
|
|
await fetch('/api/upload', { method: 'POST', body: form });
|
|
}
|
|
this.showToast('File uploaded');
|
|
await this.loadFiles();
|
|
},
|
|
|
|
download(id) {
|
|
window.location.href = '/api/files/' + id + '/download';
|
|
},
|
|
|
|
async deleteFile(id) {
|
|
const confirmed = await Alpine.store('confirm').show({
|
|
title: 'Delete file',
|
|
message: 'This file will be permanently removed. Continue?',
|
|
danger: true,
|
|
});
|
|
if (!confirmed) return;
|
|
await fetch('/api/files/' + id, { method: 'DELETE' });
|
|
this.showToast('File deleted');
|
|
await this.loadFiles();
|
|
},
|
|
|
|
async share(id) {
|
|
const res = await fetch('/api/files/' + id + '/share', { method: 'POST' });
|
|
const data = await res.json();
|
|
try {
|
|
await navigator.clipboard.writeText(data.url);
|
|
this.showToast('Share link copied to clipboard');
|
|
} catch (_) {
|
|
window.prompt('Share URL:', data.url);
|
|
}
|
|
},
|
|
|
|
async logout() {
|
|
await fetch('/api/logout', { method: 'POST' });
|
|
window.location.href = '/login';
|
|
},
|
|
});
|
|
}
|
|
|
|
if (window.Alpine) register();
|
|
else document.addEventListener('alpine:init', register);
|
|
})();
|