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>
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
'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>
|
|
)
|
|
}
|