aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/Settings.tsx
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 07:13:18 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 07:13:18 -0800
commita5cd9538b0db731a0d0e10e58804ef8ad32211b7 (patch)
tree0b46b0f43965de9df03b023abc40d869224e38d8 /frontend/src/components/Settings.tsx
parente31b68197ec16d2805ec14c2bf532a03f4739e92 (diff)
downloadneko-a5cd9538b0db731a0d0e10e58804ef8ad32211b7.tar.gz
neko-a5cd9538b0db731a0d0e10e58804ef8ad32211b7.tar.bz2
neko-a5cd9538b0db731a0d0e10e58804ef8ad32211b7.zip
Implement Frontend Settings with tests
Diffstat (limited to 'frontend/src/components/Settings.tsx')
-rw-r--r--frontend/src/components/Settings.tsx121
1 files changed, 121 insertions, 0 deletions
diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx
new file mode 100644
index 0000000..def8ffe
--- /dev/null
+++ b/frontend/src/components/Settings.tsx
@@ -0,0 +1,121 @@
+import React, { useEffect, useState } from 'react';
+import type { Feed } from '../types';
+import './Settings.css';
+
+export default function Settings() {
+ const [feeds, setFeeds] = useState<Feed[]>([]);
+ const [newFeedUrl, setNewFeedUrl] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ useEffect(() => {
+ fetchFeeds();
+ }, []);
+
+ const fetchFeeds = () => {
+ setLoading(true);
+ fetch('/api/feed/')
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to fetch feeds');
+ return res.json();
+ })
+ .then((data) => {
+ setFeeds(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ setError(err.message);
+ setLoading(false);
+ });
+ };
+
+ const handleAddFeed = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newFeedUrl) return;
+
+ setLoading(true);
+ fetch('/api/feed/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: newFeedUrl }),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to add feed');
+ return res.json();
+ })
+ .then(() => {
+ setNewFeedUrl('');
+ fetchFeeds(); // Refresh list (or we could append if server returns full feed object)
+ })
+ .catch((err) => {
+ setError(err.message);
+ setLoading(false);
+ });
+ };
+
+ const handleDeleteFeed = (id: number) => {
+ if (!globalThis.confirm('Are you sure you want to delete this feed?')) return;
+
+ setLoading(true);
+ fetch(`/api/feed/${id}`, {
+ method: 'DELETE',
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to delete feed');
+ setFeeds(feeds.filter((f) => f._id !== id));
+ setLoading(false);
+ })
+ .catch((err) => {
+ setError(err.message);
+ setLoading(false);
+ });
+ };
+
+ return (
+ <div className="settings-page">
+ <h2>Settings</h2>
+
+ <div className="add-feed-section">
+ <h3>Add New Feed</h3>
+ <form onSubmit={handleAddFeed} className="add-feed-form">
+ <input
+ type="url"
+ value={newFeedUrl}
+ onChange={(e) => setNewFeedUrl(e.target.value)}
+ placeholder="https://example.com/feed.xml"
+ required
+ className="feed-input"
+ disabled={loading}
+ />
+ <button type="submit" disabled={loading}>
+ Add Feed
+ </button>
+ </form>
+ {error && <p className="error-message">{error}</p>}
+ </div>
+
+ <div className="feed-list-section">
+ <h3>Manage Feeds</h3>
+ {loading && <p>Loading...</p>}
+ <ul className="settings-feed-list">
+ {feeds.map((feed) => (
+ <li key={feed._id} className="settings-feed-item">
+ <div className="feed-info">
+ <span className="feed-title">{feed.title || '(No Title)'}</span>
+ <span className="feed-url">{feed.url}</span>
+ </div>
+ <button
+ onClick={() => handleDeleteFeed(feed._id)}
+ className="delete-btn"
+ disabled={loading}
+ title="Delete Feed"
+ >
+ Delete
+ </button>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ );
+}