feat(flashcards): révision SM-2, génération IA et page /revision
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>
This commit is contained in:
69
memento-note/components/flashcards/revision-heatmap.tsx
Normal file
69
memento-note/components/flashcards/revision-heatmap.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface HeatmapDay {
|
||||
date: string
|
||||
count: number
|
||||
}
|
||||
|
||||
interface RevisionHeatmapProps {
|
||||
data: HeatmapDay[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
function intensityClass(count: number, max: number): string {
|
||||
if (count <= 0) return 'bg-black/[0.04] dark:bg-white/[0.06]'
|
||||
const ratio = count / Math.max(max, 1)
|
||||
if (ratio >= 0.75) return 'bg-brand-accent'
|
||||
if (ratio >= 0.5) return 'bg-brand-accent/70'
|
||||
if (ratio >= 0.25) return 'bg-brand-accent/40'
|
||||
return 'bg-brand-accent/20'
|
||||
}
|
||||
|
||||
export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||
const { t } = useLanguage()
|
||||
|
||||
const { cells, maxCount } = useMemo(() => {
|
||||
const map = new Map(data.map((d) => [d.date, d.count]))
|
||||
const today = new Date()
|
||||
const cells: { date: string; count: number; label: string }[] = []
|
||||
for (let i = 89; i >= 0; i--) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
cells.push({
|
||||
date: key,
|
||||
count: map.get(key) || 0,
|
||||
label: d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }),
|
||||
})
|
||||
}
|
||||
const maxCount = Math.max(1, ...cells.map((c) => c.count))
|
||||
return { cells, maxCount }
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-concrete">
|
||||
{t('flashcards.heatmapTitle')}
|
||||
</p>
|
||||
<span className="text-[10px] text-concrete/60">{t('flashcards.heatmapLast90')}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(15,minmax(0,1fr))] gap-1 sm:grid-cols-[repeat(18,minmax(0,1fr))]">
|
||||
{cells.map((cell) => (
|
||||
<div
|
||||
key={cell.date}
|
||||
title={`${cell.label}: ${cell.count}`}
|
||||
className={cn(
|
||||
'aspect-square rounded-[3px] transition-colors',
|
||||
intensityClass(cell.count, maxCount),
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user