From 6fa13a06411048f3217397f4285b3e64e7b9ee58 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 09:42:14 -0800 Subject: feature: implement full OPML and Text import/export (fixing NK-r6nhj0) --- frontend/src/components/Settings.css | 74 ++++++++++++++++++++++++++++++++++++ frontend/src/components/Settings.tsx | 64 +++++++++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 4 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/components/Settings.css b/frontend/src/components/Settings.css index 171dcad..ec6fc83 100644 --- a/frontend/src/components/Settings.css +++ b/frontend/src/components/Settings.css @@ -84,4 +84,78 @@ .delete-btn:disabled { background: #ffcdd2; cursor: not-allowed; +} + +.import-export-section { + display: flex; + gap: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 600px) { + .import-export-section { + flex-direction: column; + } +} + +.import-section, +.export-section { + flex: 1; + background: var(--sidebar-bg); + padding: 1.5rem; + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.import-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.file-input { + font-size: 0.9rem; + max-width: 100%; +} + +.export-buttons { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.export-btn { + display: inline-block; + padding: 0.5rem 1rem; + background: var(--bg-color); + color: var(--link-color); + text-decoration: none; + border: 1px solid var(--border-color); + border-radius: 4px; + font-weight: bold; + text-align: center; + min-width: 70px; +} + +.export-btn:hover { + background: var(--sidebar-bg); +} + +button { + cursor: pointer; + padding: 0.5rem 1rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + font-weight: bold; +} + +button:hover:not(:disabled) { + background: var(--sidebar-bg); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; } \ No newline at end of file diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index 3f508e9..6b6dab1 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -9,6 +9,8 @@ export default function Settings() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [importFile, setImportFile] = useState(null); + const fetchFeeds = () => { setLoading(true); apiFetch('/api/feed/') @@ -30,8 +32,6 @@ export default function Settings() { fetchFeeds(); }, []); - - const handleAddFeed = (e: React.FormEvent) => { e.preventDefault(); if (!newFeedUrl) return; @@ -48,7 +48,7 @@ export default function Settings() { }) .then(() => { setNewFeedUrl(''); - fetchFeeds(); // Refresh list (or we could append if server returns full feed object) + fetchFeeds(); }) .catch((err) => { setError(err.message); @@ -74,6 +74,34 @@ export default function Settings() { }); }; + const handleImport = (e: React.FormEvent) => { + e.preventDefault(); + if (!importFile) return; + + setLoading(true); + const formData = new FormData(); + formData.append('file', importFile); + formData.append('format', 'opml'); + + apiFetch('/api/import', { + method: 'POST', + body: formData, + }) + .then((res) => { + if (!res.ok) throw new Error('Failed to import feeds'); + return res.json(); + }) + .then(() => { + setImportFile(null); + fetchFeeds(); + alert('Import successful!'); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; + return (

Settings

@@ -94,9 +122,37 @@ export default function Settings() { Add Feed - {error &&

{error}

}
+
+
+

Import Feeds (OPML)

+
+ setImportFile(e.target.files?.[0] || null)} + className="file-input" + disabled={loading} + /> + +
+
+ +
+

Export Feeds

+
+ OPML + Text + JSON +
+
+
+ + {error &&

{error}

} +

Manage Feeds

{loading &&

Loading...

} -- cgit v1.2.3