feat(web): componentize frontend and replace native delete confirm
- 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
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user