Overhaul RSS feed widget: persistence, multi-feed management, search, bookmarks

- Add rss_feeds + rss_items tables with indexes and HN default seed
- Add 5-min background RSS sync loop in monitor.js with 90-day prune
- New /api/rss/feeds route for feed CRUD with immediate fetch on add
- Rewrite /api/rss route with search, feed filter, pagination, read/bookmark PATCH
- Full NewsFeed component rewrite: feed manager, search bar, filter pills,
  read/unread tracking, bookmarks, favicons, auto-refresh with new items badge
- Remove placeholder widget, NewsFeed now spans 4 cols / 3 rows
- Add rss-parser deps to Dockerfile for standalone monitor
This commit is contained in:
Shivam Patel
2026-02-09 04:50:06 -05:00
parent e47a719d79
commit f95e28202d
7 changed files with 795 additions and 64 deletions

View File

@@ -1,23 +1,76 @@
import { NextResponse } from 'next/server';
import Parser from 'rss-parser';
import { NextRequest, NextResponse } from 'next/server';
import { getDb } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET() {
const parser = new Parser();
try {
const feed = await parser.parseURL('https://news.ycombinator.com/rss');
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const q = searchParams.get('q');
const feedId = searchParams.get('feed_id');
const bookmarked = searchParams.get('bookmarked');
const limit = parseInt(searchParams.get('limit') || '50', 10);
const offset = parseInt(searchParams.get('offset') || '0', 10);
const items = feed.items.slice(0, 10).map(item => ({
title: item.title,
link: item.link,
pubDate: item.pubDate,
creator: item.creator || 'Unknown',
contentSnippet: item.contentSnippet,
}));
const db = await getDb();
return NextResponse.json(items);
} catch (error) {
return NextResponse.json({ error: 'Failed to parse RSS' }, { status: 500 });
const conditions: string[] = [];
const params: (string | number)[] = [];
if (q) {
conditions.push('(i.title LIKE ? OR i.snippet LIKE ?)');
params.push(`%${q}%`, `%${q}%`);
}
if (feedId) {
conditions.push('i.feed_id = ?');
params.push(parseInt(feedId, 10));
}
if (bookmarked === '1') {
conditions.push('i.bookmarked = 1');
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const items = await db.all(
`SELECT i.*, f.name as feed_name, f.url as feed_url
FROM rss_items i
LEFT JOIN rss_feeds f ON f.id = i.feed_id
${where}
ORDER BY i.pub_date DESC
LIMIT ? OFFSET ?`,
...params, limit, offset
);
return NextResponse.json(items);
}
export async function PATCH(req: NextRequest) {
const body = await req.json();
const { id, read, bookmarked } = body;
if (!id) {
return NextResponse.json({ error: 'Item id is required' }, { status: 400 });
}
const db = await getDb();
const updates: string[] = [];
const params: (number)[] = [];
if (read !== undefined) {
updates.push('read = ?');
params.push(read ? 1 : 0);
}
if (bookmarked !== undefined) {
updates.push('bookmarked = ?');
params.push(bookmarked ? 1 : 0);
}
if (updates.length === 0) {
return NextResponse.json({ error: 'Nothing to update' }, { status: 400 });
}
params.push(id);
await db.run(`UPDATE rss_items SET ${updates.join(', ')} WHERE id = ?`, ...params);
const item = await db.get('SELECT * FROM rss_items WHERE id = ?', id);
return NextResponse.json(item);
}