Livre US-FLASHCARDS avec decks, session de révision, stats et migration Prisma. Finalise le Web Clipper (i18n 15 langues) et corrige les erreurs ESLint bloquant la CI. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { auth } from '@/auth'
|
|
import prisma from '@/lib/prisma'
|
|
import { isCardMastered } from '@/lib/flashcards/sm2'
|
|
|
|
export async function GET() {
|
|
try {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const userId = session.user.id
|
|
const now = new Date()
|
|
|
|
const [reviews, allCards] = await Promise.all([
|
|
prisma.flashcardReview.findMany({
|
|
where: { card: { deck: { userId } } },
|
|
select: { reviewedAt: true, grade: true },
|
|
orderBy: { reviewedAt: 'desc' },
|
|
take: 500,
|
|
}),
|
|
prisma.flashcard.findMany({
|
|
where: { deck: { userId } },
|
|
select: { id: true, interval: true, easinessFactor: true, front: true, deck: { select: { name: true } } },
|
|
}),
|
|
])
|
|
|
|
const heatmapMap = new Map<string, number>()
|
|
for (const r of reviews) {
|
|
const day = r.reviewedAt.toISOString().slice(0, 10)
|
|
heatmapMap.set(day, (heatmapMap.get(day) || 0) + 1)
|
|
}
|
|
const heatmap = Array.from(heatmapMap.entries())
|
|
.map(([date, count]) => ({ date, count }))
|
|
.sort((a, b) => a.date.localeCompare(b.date))
|
|
.slice(-90)
|
|
|
|
const totalCards = allCards.length
|
|
const masteredCount = allCards.filter((c) => isCardMastered(c.interval)).length
|
|
const retentionRate = totalCards > 0 ? Math.round((masteredCount / totalCards) * 100) : 0
|
|
|
|
const weekMs = 7 * 24 * 60 * 60 * 1000
|
|
const retentionByWeek: { week: string; rate: number }[] = []
|
|
for (let i = 7; i >= 0; i--) {
|
|
const weekEnd = new Date(now.getTime() - i * weekMs)
|
|
const weekStart = new Date(weekEnd.getTime() - weekMs)
|
|
const cardsAtWeek = allCards.filter((c) => c.interval >= 7 * (8 - i))
|
|
const masteredAtWeek = cardsAtWeek.filter((c) => isCardMastered(c.interval)).length
|
|
const rate = cardsAtWeek.length > 0
|
|
? Math.round((masteredAtWeek / cardsAtWeek.length) * 100)
|
|
: retentionRate
|
|
retentionByWeek.push({
|
|
week: weekStart.toISOString().slice(0, 10),
|
|
rate,
|
|
})
|
|
}
|
|
|
|
const difficultCards = [...allCards]
|
|
.sort((a, b) => a.easinessFactor - b.easinessFactor)
|
|
.slice(0, 5)
|
|
.map((c) => ({
|
|
id: c.id,
|
|
front: c.front.slice(0, 120),
|
|
easinessFactor: c.easinessFactor,
|
|
deckName: c.deck.name,
|
|
}))
|
|
|
|
return NextResponse.json({
|
|
heatmap,
|
|
retentionRate,
|
|
retentionByWeek,
|
|
difficultCards,
|
|
totalReviews: reviews.length,
|
|
})
|
|
} catch (error) {
|
|
console.error('[flashcards/stats]', error)
|
|
return NextResponse.json({ error: 'Failed to load stats' }, { status: 500 })
|
|
}
|
|
}
|