From 464f93a6aa0a8298ecbcaae08058eed23e1f83e1 Mon Sep 17 00:00:00 2001 From: Gemini Agent Date: Mon, 19 Jan 2026 13:42:49 +0000 Subject: [PATCH] Add sorting and client-side caching for instant switching - Add sort dropdown: Newest/Oldest/Title A-Z/Z-A - Client-side cache with 30s TTL for instant section switching - Re-sorting uses cached data for immediate response - Cache clears on data modifications (add/archive/delete) Co-Authored-By: Claude Opus 4.5 --- src/app/page.tsx | 116 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4a39eeb..fe94a03 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { Article, Folder } from "@/lib/types"; import { useReaderSettings, useTTSSettings } from "@/hooks/useSettings"; import { useTTS } from "@/hooks/useTTS"; @@ -20,10 +20,16 @@ import { X, CheckSquare, ArchiveRestore, + ArrowUpDown, } from "lucide-react"; import Link from "next/link"; type FilterType = "all" | "favorites" | "archived" | "folder" | "search"; +type SortType = "newest" | "oldest" | "title-asc" | "title-desc"; + +// Simple client-side cache for instant section switching +const articleCache = new Map(); +const CACHE_TTL = 30000; // 30 seconds export default function Home() { const [articles, setArticles] = useState([]); @@ -40,6 +46,8 @@ export default function Home() { const [isSelectMode, setIsSelectMode] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [isBulkUpdating, setIsBulkUpdating] = useState(false); + const [sortBy, setSortBy] = useState("newest"); + const [showSortMenu, setShowSortMenu] = useState(false); const [readerSettings, setReaderSettings] = useReaderSettings(); const [ttsSettings, setTTSSettings] = useTTSSettings(); @@ -61,8 +69,37 @@ export default function Home() { } }, [readerSettings]); - // Fetch articles - const fetchArticles = useCallback(async () => { + // Sort articles client-side + const sortArticles = useCallback((articles: Article[], sort: SortType): Article[] => { + const sorted = [...articles]; + switch (sort) { + case "newest": + return sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + case "oldest": + return sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + case "title-asc": + return sorted.sort((a, b) => a.title.localeCompare(b.title)); + case "title-desc": + return sorted.sort((a, b) => b.title.localeCompare(a.title)); + default: + return sorted; + } + }, []); + + // Fetch articles with caching + const fetchArticles = useCallback(async (skipCache = false) => { + const cacheKey = filter === "folder" ? `folder-${selectedFolderId}` : filter; + + // Check cache first for instant display + if (!skipCache) { + const cached = articleCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + setArticles(sortArticles(cached.data, sortBy)); + setIsLoading(false); + return; + } + } + try { let url = `/api/articles?filter=${filter}`; if (filter === "folder" && selectedFolderId) { @@ -71,14 +108,16 @@ export default function Home() { const response = await fetch(url); if (response.ok) { const data = await response.json(); - setArticles(data); + // Cache the raw data + articleCache.set(cacheKey, { data, timestamp: Date.now() }); + setArticles(sortArticles(data, sortBy)); } } catch (error) { console.error("Failed to fetch articles:", error); } finally { setIsLoading(false); } - }, [filter, selectedFolderId]); + }, [filter, selectedFolderId, sortBy, sortArticles]); // Fetch folders const fetchFolders = useCallback(async () => { @@ -116,6 +155,15 @@ export default function Home() { fetchStats(); }, [fetchArticles, fetchFolders, fetchStats]); + // Re-sort when sort changes (use cached data for instant response) + useEffect(() => { + const cacheKey = filter === "folder" ? `folder-${selectedFolderId}` : filter; + const cached = articleCache.get(cacheKey); + if (cached) { + setArticles(sortArticles(cached.data, sortBy)); + } + }, [sortBy, filter, selectedFolderId, sortArticles]); + // Search const handleSearch = async () => { if (!searchQuery.trim()) { @@ -151,7 +199,8 @@ export default function Home() { throw new Error(data.error || "Failed to add article"); } - await fetchArticles(); + articleCache.clear(); + await fetchArticles(true); fetchStats(); }; @@ -180,7 +229,8 @@ export default function Home() { body: JSON.stringify({ isArchived }), }); - await fetchArticles(); + articleCache.clear(); + await fetchArticles(true); fetchStats(); if (selectedArticle?.id === id) { @@ -191,7 +241,8 @@ export default function Home() { // Delete article const handleDelete = async (id: string) => { await fetch(`/api/articles/${id}`, { method: "DELETE" }); - await fetchArticles(); + articleCache.clear(); + await fetchArticles(true); if (selectedArticle?.id === id) { setSelectedArticle(null); @@ -252,7 +303,9 @@ export default function Home() { updates: { isArchived: archive }, }), }); - await fetchArticles(); + // Clear cache to force refresh + articleCache.clear(); + await fetchArticles(true); setSelectedIds(new Set()); setIsSelectMode(false); fetchStats(); @@ -489,6 +542,51 @@ export default function Home() { {articles.length} articles + {/* Sort dropdown */} +
+ + {showSortMenu && ( + <> +
setShowSortMenu(false)} + /> +
+ {[ + { value: "newest", label: "Newest first" }, + { value: "oldest", label: "Oldest first" }, + { value: "title-asc", label: "Title A-Z" }, + { value: "title-desc", label: "Title Z-A" }, + ].map((option) => ( + + ))} +
+ + )} +
+ {articles.length > 0 && (