Add time filter pills to RSS feed widget
Adds since query param (1h/24h/7d/30d) to the RSS API route and a matching TimeFilterPills row in the NewsFeed UI so users can narrow the feed to recent items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ export async function GET(req: NextRequest) {
|
|||||||
const q = searchParams.get('q');
|
const q = searchParams.get('q');
|
||||||
const feedId = searchParams.get('feed_id');
|
const feedId = searchParams.get('feed_id');
|
||||||
const bookmarked = searchParams.get('bookmarked');
|
const bookmarked = searchParams.get('bookmarked');
|
||||||
|
const since = searchParams.get('since');
|
||||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||||
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
||||||
|
|
||||||
@@ -27,6 +28,19 @@ export async function GET(req: NextRequest) {
|
|||||||
if (bookmarked === '1') {
|
if (bookmarked === '1') {
|
||||||
conditions.push('i.bookmarked = 1');
|
conditions.push('i.bookmarked = 1');
|
||||||
}
|
}
|
||||||
|
if (since) {
|
||||||
|
const sinceMap: Record<string, string> = {
|
||||||
|
'1h': '-1 hours',
|
||||||
|
'24h': '-24 hours',
|
||||||
|
'7d': '-7 days',
|
||||||
|
'30d': '-30 days',
|
||||||
|
};
|
||||||
|
const modifier = sinceMap[since];
|
||||||
|
if (modifier) {
|
||||||
|
conditions.push("i.pub_date >= datetime('now', ?)");
|
||||||
|
params.push(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,51 @@ function FeedFilterPills({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimeFilter = '1h' | '24h' | '7d' | '30d';
|
||||||
|
|
||||||
|
const TIME_FILTER_OPTIONS: { value: TimeFilter; label: string }[] = [
|
||||||
|
{ value: '1h', label: '1h' },
|
||||||
|
{ value: '24h', label: '24h' },
|
||||||
|
{ value: '7d', label: '7d' },
|
||||||
|
{ value: '30d', label: '30d' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function TimeFilterPills({
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
active: TimeFilter | null;
|
||||||
|
onSelect: (v: TimeFilter | null) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1.5 overflow-x-auto pb-1 scrollbar-none">
|
||||||
|
<button
|
||||||
|
onClick={() => onSelect(null)}
|
||||||
|
className={`shrink-0 px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${
|
||||||
|
active === null
|
||||||
|
? 'bg-neutral-700 text-white'
|
||||||
|
: 'bg-neutral-800/60 text-neutral-400 hover:text-neutral-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Any time
|
||||||
|
</button>
|
||||||
|
{TIME_FILTER_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
onClick={() => onSelect(opt.value)}
|
||||||
|
className={`shrink-0 px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${
|
||||||
|
active === opt.value
|
||||||
|
? 'bg-neutral-700 text-white'
|
||||||
|
: 'bg-neutral-800/60 text-neutral-400 hover:text-neutral-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AddFeedForm({ onAdded }: { onAdded: () => void }) {
|
function AddFeedForm({ onAdded }: { onAdded: () => void }) {
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -332,6 +377,7 @@ export function NewsFeed() {
|
|||||||
const [feeds, setFeeds] = useState<Feed[]>([]);
|
const [feeds, setFeeds] = useState<Feed[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeFeedId, setActiveFeedId] = useState<number | null>(null);
|
const [activeFeedId, setActiveFeedId] = useState<number | null>(null);
|
||||||
|
const [timeFilter, setTimeFilter] = useState<TimeFilter | null>(null);
|
||||||
const [showBookmarked, setShowBookmarked] = useState(false);
|
const [showBookmarked, setShowBookmarked] = useState(false);
|
||||||
const [showFeedManager, setShowFeedManager] = useState(false);
|
const [showFeedManager, setShowFeedManager] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -354,6 +400,7 @@ export function NewsFeed() {
|
|||||||
if (searchQuery) params.set('q', searchQuery);
|
if (searchQuery) params.set('q', searchQuery);
|
||||||
if (!opts?.resetFeedFilter && activeFeedId !== null) params.set('feed_id', String(activeFeedId));
|
if (!opts?.resetFeedFilter && activeFeedId !== null) params.set('feed_id', String(activeFeedId));
|
||||||
if (showBookmarked) params.set('bookmarked', '1');
|
if (showBookmarked) params.set('bookmarked', '1');
|
||||||
|
if (timeFilter) params.set('since', timeFilter);
|
||||||
params.set('limit', '50');
|
params.set('limit', '50');
|
||||||
|
|
||||||
const res = await fetch(`/api/rss?${params}`);
|
const res = await fetch(`/api/rss?${params}`);
|
||||||
@@ -373,7 +420,7 @@ export function NewsFeed() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchQuery, activeFeedId, showBookmarked]);
|
}, [searchQuery, activeFeedId, showBookmarked, timeFilter]);
|
||||||
|
|
||||||
// Initial load + refresh
|
// Initial load + refresh
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -469,6 +516,13 @@ export function NewsFeed() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Time filter pills */}
|
||||||
|
{hasFeeds && (
|
||||||
|
<div className="shrink-0 mb-2">
|
||||||
|
<TimeFilterPills active={timeFilter} onSelect={setTimeFilter} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Feed manager (collapsible) */}
|
{/* Feed manager (collapsible) */}
|
||||||
{showFeedManager && (
|
{showFeedManager && (
|
||||||
<div className="shrink-0 mb-3">
|
<div className="shrink-0 mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user