diff --git a/src/app/api/articles/route.ts b/src/app/api/articles/route.ts index f93b88d..492f50e 100644 --- a/src/app/api/articles/route.ts +++ b/src/app/api/articles/route.ts @@ -2,15 +2,37 @@ import { NextRequest, NextResponse } from "next/server"; import { db, schema } from "@/lib/db"; import { extractArticle } from "@/lib/utils/extract"; import { v4 as uuidv4 } from "uuid"; -import { desc, eq } from "drizzle-orm"; +import { desc, eq, inArray } from "drizzle-orm"; -// GET /api/articles - List all articles +// GET /api/articles - List all articles (optimized - excludes content fields) export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; const filter = searchParams.get("filter"); // all, favorites, archived - let query = db.select().from(schema.articles); + // Select only fields needed for list view (exclude large content/textContent) + const listFields = { + id: schema.articles.id, + url: schema.articles.url, + title: schema.articles.title, + author: schema.articles.author, + siteName: schema.articles.siteName, + excerpt: schema.articles.excerpt, + leadImage: schema.articles.leadImage, + wordCount: schema.articles.wordCount, + readingProgress: schema.articles.readingProgress, + readingTimeSeconds: schema.articles.readingTimeSeconds, + isFavorite: schema.articles.isFavorite, + isArchived: schema.articles.isArchived, + folderId: schema.articles.folderId, + tags: schema.articles.tags, + createdAt: schema.articles.createdAt, + updatedAt: schema.articles.updatedAt, + readAt: schema.articles.readAt, + finishedAt: schema.articles.finishedAt, + }; + + let query = db.select(listFields).from(schema.articles); if (filter === "favorites") { query = query.where(eq(schema.articles.isFavorite, true)) as typeof query; @@ -33,6 +55,49 @@ export async function GET(request: NextRequest) { } } +// PATCH /api/articles - Bulk update articles +export async function PATCH(request: NextRequest) { + try { + const body = await request.json(); + const { ids, updates } = body; + + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return NextResponse.json({ error: "Article IDs required" }, { status: 400 }); + } + + // Only allow certain fields to be bulk updated + const allowedUpdates: Partial = {}; + if (typeof updates.isArchived === "boolean") { + allowedUpdates.isArchived = updates.isArchived; + } + if (typeof updates.isFavorite === "boolean") { + allowedUpdates.isFavorite = updates.isFavorite; + } + if (updates.folderId !== undefined) { + allowedUpdates.folderId = updates.folderId; + } + + if (Object.keys(allowedUpdates).length === 0) { + return NextResponse.json({ error: "No valid updates provided" }, { status: 400 }); + } + + allowedUpdates.updatedAt = new Date(); + + await db + .update(schema.articles) + .set(allowedUpdates) + .where(inArray(schema.articles.id, ids)); + + return NextResponse.json({ success: true, updated: ids.length }); + } catch (error) { + console.error("Error bulk updating articles:", error); + return NextResponse.json( + { error: "Failed to update articles" }, + { status: 500 } + ); + } +} + // POST /api/articles - Save a new article export async function POST(request: NextRequest) { try { diff --git a/src/app/page.tsx b/src/app/page.tsx index 7e17c07..4a39eeb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -17,8 +17,9 @@ import { Search, FolderIcon, Settings, - BarChart3, X, + CheckSquare, + ArchiveRestore, } from "lucide-react"; import Link from "next/link"; @@ -36,6 +37,9 @@ export default function Home() { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isLoading, setIsLoading] = useState(true); const [stats, setStats] = useState<{ streak: number; todayCount: number } | null>(null); + const [isSelectMode, setIsSelectMode] = useState(false); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [isBulkUpdating, setIsBulkUpdating] = useState(false); const [readerSettings, setReaderSettings] = useReaderSettings(); const [ttsSettings, setTTSSettings] = useTTSSettings(); @@ -194,6 +198,77 @@ export default function Home() { } }; + // Fetch full article (with content) when opening for reading + const handleSelectArticle = async (article: Article) => { + // If article already has content, use it directly + if (article.content && article.textContent) { + setSelectedArticle(article); + return; + } + // Otherwise fetch full article + try { + const response = await fetch(`/api/articles/${article.id}`); + if (response.ok) { + const fullArticle = await response.json(); + setSelectedArticle(fullArticle); + } + } catch (error) { + console.error("Failed to fetch article:", error); + } + }; + + // Toggle article selection + const handleToggleSelect = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + // Select all visible articles + const handleSelectAll = () => { + if (selectedIds.size === articles.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(articles.map((a) => a.id))); + } + }; + + // Bulk archive/unarchive + const handleBulkArchive = async (archive: boolean) => { + if (selectedIds.size === 0) return; + setIsBulkUpdating(true); + try { + await fetch("/api/articles", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ids: Array.from(selectedIds), + updates: { isArchived: archive }, + }), + }); + await fetchArticles(); + setSelectedIds(new Set()); + setIsSelectMode(false); + fetchStats(); + } catch (error) { + console.error("Bulk update failed:", error); + } finally { + setIsBulkUpdating(false); + } + }; + + // Exit select mode + const exitSelectMode = () => { + setIsSelectMode(false); + setSelectedIds(new Set()); + }; + // Reading view if (selectedArticle) { return ( @@ -268,7 +343,7 @@ export default function Home() {