mirror of
https://github.com/Tony0410/readlater.git
synced 2026-05-25 06:11:40 +08:00
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:
129
src/app/api/stats/route.ts
Normal file
129
src/app/api/stats/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user