v2.0: Major feature update

New Features:
- API key authentication for external access
- Apple Shortcuts integration endpoint (/api/v1/add)
- Full-text search across all articles
- Folders for organizing articles
- Highlights and notes on articles
- Reading stats with streaks
- Reading goals (daily/weekly/monthly)
- Import from Pocket/Instapaper
- RSS feed output
- PWA support for mobile
- Auto theme scheduling (day/night)
- Settings page with all configuration

API Endpoints:
- /api/v1/add - Add articles via API key
- /api/keys - Manage API keys
- /api/search - Full-text search
- /api/folders - Folder management
- /api/highlights - Highlights/notes
- /api/stats - Reading statistics
- /api/goals - Reading goals
- /api/import - Pocket/Instapaper import
- /api/rss - RSS feed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-17 12:19:57 +00:00
parent 27963af055
commit 513576b90e
22 changed files with 2431 additions and 30 deletions

129
src/app/api/stats/route.ts Normal file
View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { eq, desc, gte, sql } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
// GET /api/stats - Get reading stats
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const days = parseInt(searchParams.get("days") || "30");
// Get date range
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const startDateStr = startDate.toISOString().split("T")[0];
// Get daily stats
const dailyStats = await db
.select()
.from(schema.readingStats)
.where(gte(schema.readingStats.date, startDateStr))
.orderBy(desc(schema.readingStats.date));
// Calculate totals
const totals = dailyStats.reduce(
(acc, day) => ({
articlesRead: acc.articlesRead + (day.articlesRead || 0),
articlesAdded: acc.articlesAdded + (day.articlesAdded || 0),
wordsRead: acc.wordsRead + (day.wordsRead || 0),
timeSpentSeconds: acc.timeSpentSeconds + (day.timeSpentSeconds || 0),
}),
{ articlesRead: 0, articlesAdded: 0, wordsRead: 0, timeSpentSeconds: 0 }
);
// Calculate current streak
let streak = 0;
const today = new Date().toISOString().split("T")[0];
const sortedDays = [...dailyStats].sort((a, b) => b.date.localeCompare(a.date));
for (const day of sortedDays) {
if (day.articlesRead && day.articlesRead > 0) {
streak++;
} else if (day.date !== today) {
break;
}
}
// Get article counts
const articleCounts = await db
.select({
total: sql<number>`count(*)`,
favorites: sql<number>`sum(case when is_favorite = 1 then 1 else 0 end)`,
archived: sql<number>`sum(case when is_archived = 1 then 1 else 0 end)`,
unread: sql<number>`sum(case when reading_progress < 100 and is_archived = 0 then 1 else 0 end)`,
})
.from(schema.articles);
return NextResponse.json({
period: { days, startDate: startDateStr },
totals,
streak,
daily: dailyStats,
library: articleCounts[0],
});
} catch (error) {
console.error("Error getting stats:", error);
return NextResponse.json({ error: "Failed to get stats" }, { status: 500 });
}
}
// POST /api/stats - Record reading activity
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { articleId, wordsRead, timeSpentSeconds, completed } = body;
const today = new Date().toISOString().split("T")[0];
// Get or create today's stats
const existing = await db
.select()
.from(schema.readingStats)
.where(eq(schema.readingStats.date, today))
.limit(1);
if (existing.length > 0) {
await db
.update(schema.readingStats)
.set({
articlesRead: (existing[0].articlesRead || 0) + (completed ? 1 : 0),
wordsRead: (existing[0].wordsRead || 0) + (wordsRead || 0),
timeSpentSeconds: (existing[0].timeSpentSeconds || 0) + (timeSpentSeconds || 0),
})
.where(eq(schema.readingStats.date, today));
} else {
await db.insert(schema.readingStats).values({
id: uuidv4(),
date: today,
articlesRead: completed ? 1 : 0,
wordsRead: wordsRead || 0,
timeSpentSeconds: timeSpentSeconds || 0,
});
}
// Update article reading time if articleId provided
if (articleId && timeSpentSeconds) {
const article = await db
.select()
.from(schema.articles)
.where(eq(schema.articles.id, articleId))
.limit(1);
if (article.length > 0) {
await db
.update(schema.articles)
.set({
readingTimeSeconds: (article[0].readingTimeSeconds || 0) + timeSpentSeconds,
finishedAt: completed ? new Date() : article[0].finishedAt,
})
.where(eq(schema.articles.id, articleId));
}
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error recording stats:", error);
return NextResponse.json({ error: "Failed to record stats" }, { status: 500 });
}
}