import { useEffect, useMemo, useState } from 'react'; import { useAuth, useCollection, exportCSV, exportJSON } from '@deplixo/sdk'; import { FeedTimeline } from './components/FeedTimeline.jsx'; import { FeedManager } from './components/FeedManager.jsx'; function App() { const { user, loading, login, logout } = useAuth(); const [activeTab, setActiveTab] = useState('timeline'); const [keywordInput, setKeywordInput] = useState(''); const [digestEnabled, setDigestEnabled] = useState(true); const [matchedOnly, setMatchedOnly] = useState(false); const categoryOptions = useMemo( () => ['All', 'Tech', 'Sports', 'Business', 'World', 'Science', 'Entertainment'], [] ); const feedsCollection = useCollection('rss_feeds', { personal: true }); const savedArticlesCollection = useCollection('saved_articles', { personal: true }); const filtersCollection = useCollection('rss_filters', { personal: true }); const filterStatsCollection = useCollection('rss_filter_stats', { personal: true }); const feeds = feedsCollection.items || []; const savedArticles = savedArticlesCollection.items || []; const filtersItems = filtersCollection.items || []; const filterStatsItems = filterStatsCollection.items || []; const keywordFilters = filtersItems[0] || null; const filterStats = filterStatsItems[0] || null; const [keywords, setKeywords] = useState([]); useEffect(() => { const rawKeywords = keywordFilters?.keywords; const normalized = Array.isArray(rawKeywords) ? rawKeywords : typeof rawKeywords === 'string' ? rawKeywords.split(',') : []; setKeywords( normalized .map((item) => String(item).trim()) .filter(Boolean) ); setDigestEnabled(keywordFilters?.digestEnabled !== false); setMatchedOnly(Boolean(keywordFilters?.matchedOnly)); }, [keywordFilters]); const cleanedKeywords = useMemo( () => keywords.map((keyword) => keyword.trim()).filter(Boolean), [keywords] ); const keywordPreview = cleanedKeywords.slice(0, 6); const handleAddKeyword = async () => { const next = keywordInput.trim(); if (!next || cleanedKeywords.some((keyword) => keyword.toLowerCase() === next.toLowerCase())) { setKeywordInput(''); return; } const updatedKeywords = [...cleanedKeywords, next]; setKeywords(updatedKeywords); setKeywordInput(''); const payload = { keywords: updatedKeywords, digestEnabled, matchedOnly, updatedAt: new Date().toISOString(), }; if (keywordFilters?.id) { await filtersCollection.update(keywordFilters.id, payload); } else { await filtersCollection.add(payload); } }; const handleRemoveKeyword = async (keywordToRemove) => { const updatedKeywords = cleanedKeywords.filter( (keyword) => keyword.toLowerCase() !== keywordToRemove.toLowerCase() ); setKeywords(updatedKeywords); const payload = { keywords: updatedKeywords, digestEnabled, matchedOnly, updatedAt: new Date().toISOString(), }; if (keywordFilters?.id) { await filtersCollection.update(keywordFilters.id, payload); } else { await filtersCollection.add(payload); } }; const handleToggleSetting = async (field, nextValue) => { if (field === 'digestEnabled') setDigestEnabled(nextValue); if (field === 'matchedOnly') setMatchedOnly(nextValue); const payload = { keywords: cleanedKeywords, digestEnabled: field === 'digestEnabled' ? nextValue : digestEnabled, matchedOnly: field === 'matchedOnly' ? nextValue : matchedOnly, updatedAt: new Date().toISOString(), }; if (keywordFilters?.id) { await filtersCollection.update(keywordFilters.id, payload); } else { await filtersCollection.add(payload); } }; const handleExportBookmarksJSON = () => { exportJSON(savedArticles, { filename: 'bookmarks.json' }); }; const handleExportBookmarksCSV = () => { const rows = savedArticles.map((item) => ({ title: item.title || '', source: item.source || item.feedName || '', url: item.url || item.link || '', date: item.date || item.createdAt || '', summary: item.summary || item.snippet || '', })); exportCSV(rows, { filename: 'bookmarks.csv', columns: ['title', 'source', 'url', 'date', 'summary'], }); }; const statusLabel = digestEnabled ? 'Daily digest enabled' : 'Daily digest paused'; const matcherLabel = matchedOnly ? 'Hourly matcher: keyword matches only' : 'Hourly matcher: all feeds'; const lastUpdated = keywordFilters?.updatedAt || keywordFilters?.createdAt || 'Not configured yet'; if (loading) { return
Signing in...
; } if (!user) { return (

Feedstream

Sign in to save your feeds and bookmarks

Connect with Google to keep your RSS feed list, keyword filters, and bookmarked articles personal and synced to your account.

); } return (

Feedstream

{user.name} {feeds.length} saved feeds {savedArticles.length} saved articles
{activeTab === 'timeline' && } {activeTab === 'feeds' && } {activeTab === 'filters' && (

Keyword Filters

Manage the keywords used by the hourly matcher and the daily digest.

{cleanedKeywords.length} keywords
Digest status {statusLabel}
Matcher status {matcherLabel}
Last updated {lastUpdated}
setKeywordInput(e.target.value)} placeholder="Add keyword, e.g. AI, startups, cybersecurity" onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddKeyword(); } }} />
{keywordPreview.length > 0 ? ( keywordPreview.map((keyword) => ( )) ) : (

No keyword filters yet. Add a few terms to tune your digest.

)}

Notification & Digest Status

Track what the background RSS jobs can use for filtering and delivery.

{filterStats?.matchedCount || 0} matched
Current feed count {feeds.length}
Saved article count {savedArticles.length}
Configured keywords {cleanedKeywords.length ? cleanedKeywords.join(', ') : 'None'}
Delivery mode {digestEnabled ? 'Digest active' : 'Digest disabled'}
Use these controls to configure the filters that your hourly matcher and daily digest should respect. The background jobs can read this collection to determine which articles to surface and summarize.
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();