- 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)
97 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|