learning_ai_clock/web/src/components/StreakCard.tsx
saravanakumardb1 38bb2629e9 feat(web): Phase 2 — stats, categories, recurring, export/import, calendar .ics
- Statistics + streaks engine (lib/stats.ts) with daily/weekly/monthly breakdowns, on-time rate, focus time, streak tracking (23 tests)
- Categories/tags system (lib/categories.ts) with 6 built-in categories, custom tags, default urgency+cascade per category (29 tests)
- Recurring timer engine (lib/recurrence.ts) with daily/weekday/weekend/weekly/biweekly/monthly/custom rules, skip/pause, DST edge cases (37 tests)
- Timer export/import as JSON (lib/export.ts)
- Calendar .ics import (lib/calendar-import.ts) with RFC 5545 parsing, conflict detection, priority-to-urgency mapping (26 tests)
- StatsView component with Recharts (bar, line, pie charts)
- StreakCard component with milestone badges
- History page (/history) with stats, history search/filter, import/export
- Category picker in CreateTimerModal with auto urgency+cascade defaults
- Category filter chips on Dashboard + History link in header
- Installed recharts dependency
- Updated roadmap.md Phase 2 Week 4-5 with completion status
- 302 tests passing (up from 82 in Phase 1)
2026-02-27 21:59:09 -08:00

97 lines
3.3 KiB
TypeScript

'use client';
import { Flame, Trophy, Shield } from 'lucide-react';
import type { StreakInfo } from '@/lib/stats';
interface StreakCardProps {
streak: StreakInfo;
}
export function StreakCard({ streak }: StreakCardProps) {
const { currentStreak, longestStreak, streakFreezeUsed, streakFreezeAvailable } = streak;
return (
<div
className="rounded-xl border p-4"
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold flex items-center gap-2" style={{ color: 'var(--cm-text-primary)' }}>
<Flame size={16} style={{ color: currentStreak > 0 ? 'var(--cm-important)' : 'var(--cm-text-tertiary)' }} />
Streak
</h3>
{streakFreezeUsed && (
<span
className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full"
style={{ backgroundColor: 'rgba(90, 140, 255, 0.15)', color: 'var(--cm-accent)' }}
>
<Shield size={12} /> Freeze used
</span>
)}
</div>
<div className="flex items-end gap-6">
{/* Current streak */}
<div className="flex-1">
<div
className="text-3xl font-bold font-mono"
style={{ color: currentStreak > 0 ? 'var(--cm-important)' : 'var(--cm-text-tertiary)' }}
>
{currentStreak}
</div>
<div className="text-xs mt-0.5" style={{ color: 'var(--cm-text-tertiary)' }}>
{currentStreak === 1 ? 'day' : 'days'} current
</div>
</div>
{/* Longest streak */}
<div>
<div className="flex items-center gap-1">
<Trophy size={14} style={{ color: 'var(--cm-standard)' }} />
<span className="text-lg font-bold font-mono" style={{ color: 'var(--cm-text-secondary)' }}>
{longestStreak}
</span>
</div>
<div className="text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
best
</div>
</div>
{/* Freeze status */}
{streakFreezeAvailable && (
<div>
<div className="flex items-center gap-1">
<Shield size={14} style={{ color: 'var(--cm-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--cm-accent)' }}>
1 freeze
</span>
</div>
<div className="text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
available
</div>
</div>
)}
</div>
{/* Streak milestones */}
{currentStreak >= 7 && (
<div
className="mt-3 px-3 py-1.5 rounded-lg text-xs font-medium text-center"
style={{
backgroundColor: currentStreak >= 30
? 'rgba(255, 159, 67, 0.15)'
: 'rgba(46, 213, 115, 0.15)',
color: currentStreak >= 30 ? 'var(--cm-important)' : 'var(--cm-gentle)',
}}
>
{currentStreak >= 100
? '🏆 100+ day streak! Legendary!'
: currentStreak >= 30
? '🔥 30+ day streak! On fire!'
: '✨ 7+ day streak! Keep it up!'}
</div>
)}
</div>
);
}