From 89f471809a08d3d3af049dcef43093543c11fca0 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Tue, 17 Feb 2026 12:01:53 -0800 Subject: Redesign Settings page: grid layout and extended import/export options --- frontend-vanilla/src/main.ts | 169 +++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 80 deletions(-) (limited to 'frontend-vanilla/src/main.ts') diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 6a605c4..49b63b7 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -349,93 +349,111 @@ if (typeof window !== 'undefined') { }, 1000); } +// ... (add this variable at module level or inside renderSettings if possible, but module level is safer for persistence across clicks if renderSettings re-runs? No, event flow is synchronous: click button -> click file input. User selects file. Change event fires. +// Actually, file input click is async in terms of user action. renderSettings won't run in between unless something else triggers it. +// But to be safe, I'll update the function signature of importOPML to importData. + export function renderSettings() { const contentArea = document.getElementById('content-area'); if (!contentArea) return; + contentArea.innerHTML = `

Settings

-
-

Add Feed

-
- - -
-
- -
-

Appearance

-
- -
- - +
+
+

Add Feed

+
+ +
-
-
- - -
-
+ + +
+

Data

+
+ +
+ OPML + TEXT + JSON +
+
+
+ +
+ + + +
+ +
+
+ +
+

Theme

+
+
+ + +
+
+
+ + +
+
+

Manage Feeds

-
- -
-

Data Management

-
- -
- - -
-
-
`; - // Attach settings listeners + // --- Listeners --- + + // Theme document.getElementById('theme-options')?.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('button'); if (btn) { - const theme = btn.getAttribute('data-theme')!; - store.setTheme(theme); + store.setTheme(btn.getAttribute('data-theme')!); renderSettings(); } }); + // Font document.getElementById('font-selector')?.addEventListener('change', (e) => { store.setFontTheme((e.target as HTMLSelectElement).value); }); + // Add Feed document.getElementById('add-feed-btn')?.addEventListener('click', async () => { const input = document.getElementById('new-feed-url') as HTMLInputElement; const url = input.value.trim(); if (url) { - const success = await addFeed(url); - if (success) { + if (await addFeed(url)) { input.value = ''; alert('Feed added successfully!'); fetchFeeds(); @@ -445,40 +463,36 @@ export function renderSettings() { } }); - document.getElementById('export-opml-btn')?.addEventListener('click', () => { - window.location.href = '/api/export/opml'; + // Import Logic + let pendingImportFormat = 'opml'; + const fileInput = document.getElementById('import-file') as HTMLInputElement; + + document.querySelectorAll('.import-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + pendingImportFormat = (e.currentTarget as HTMLElement).getAttribute('data-format') || 'opml'; + fileInput.click(); + }); }); - document.getElementById('import-opml-file')?.addEventListener('change', async (e) => { + fileInput?.addEventListener('change', async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { - const success = await importOPML(file); - if (success) { - alert('OPML imported successfully! Crawling started.'); + if (await importData(file, pendingImportFormat)) { + alert(`Import (${pendingImportFormat}) started! check logs.`); fetchFeeds(); } else { - alert('Failed to import OPML.'); + alert('Failed to import.'); } } + fileInput.value = ''; // Reset }); - // Feed Management Listeners + // Manage Feeds document.querySelectorAll('.delete-feed-btn').forEach(btn => { btn.addEventListener('click', async (e) => { const id = parseInt((e.target as HTMLElement).getAttribute('data-id')!); - if (confirm('Are you sure you want to delete this feed?')) { + if (confirm('Delete this feed?')) { await deleteFeed(id); - fetchFeeds(); - // re-render settings to remove the deleted feed from list - // delay slightly to allow feed fetch? No, fetchFeeds is async. - // We should await fetchFeeds before re-rendering? - // But fetchFeeds updates store, and store emits 'feeds-updated'. - // Does 'feeds-updated' re-render settings? - // No, 'feeds-updated' calls renderFeeds (the sidebar list). - // So we need to explicitly call renderSettings() to update the management list. - // But we should wait for fetchFeeds() to complete so store is updated. - // wait... fetchFeeds() is async but we don't await result in the listener above? - // Ah, fetchFeeds() returns Promise. await fetchFeeds(); renderSettings(); } @@ -489,12 +503,10 @@ export function renderSettings() { btn.addEventListener('click', async (e) => { const id = parseInt((e.target as HTMLElement).getAttribute('data-id')!); const input = document.querySelector(`.feed-tag-input[data-id="${id}"]`) as HTMLInputElement; - const category = input.value.trim(); - await updateFeed(id, { category }); - // updateFeed returns boolean, assuming success + await updateFeed(id, { category: input.value.trim() }); await fetchFeeds(); await fetchTags(); - renderSettings(); // Update list to show persistence + renderSettings(); alert('Feed updated'); }); }); @@ -514,25 +526,22 @@ async function addFeed(url: string): Promise { } } -async function importOPML(file: File): Promise { +async function importData(file: File, format: string): Promise { try { const formData = new FormData(); formData.append('file', file); - formData.append('format', 'opml'); + formData.append('format', format); - // We need to handle CSRF manually since apiFetch expects JSON or simple body const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrf_token='))?.split('=')[1]; const res = await fetch('/api/import', { method: 'POST', - headers: { - 'X-CSRF-Token': csrfToken || '' - }, + headers: { 'X-CSRF-Token': csrfToken || '' }, body: formData }); return res.ok; } catch (err) { - console.error('Failed to import OPML', err); + console.error('Failed to import', err); return false; } } -- cgit v1.2.3