Spaces:
Sleeping
Sleeping
| // web/src/lib/reviewStar.ts | |
| export type ReviewEventType = "send_message" | "review_topic" | "review_all"; | |
| export type ReviewStarState = { | |
| lastActiveDay: string; // YYYY-MM-DD (local) | |
| todayCount: number; // today's valid review actions count | |
| streakDays: number; // consecutive active days | |
| totalDaysActive: number; // total active days | |
| }; | |
| function dayKeyLocal(d = new Date()) { | |
| const yyyy = d.getFullYear(); | |
| const mm = String(d.getMonth() + 1).padStart(2, "0"); | |
| const dd = String(d.getDate()).padStart(2, "0"); | |
| return `${yyyy}-${mm}-${dd}`; | |
| } | |
| function yesterdayKeyLocal() { | |
| return dayKeyLocal(new Date(Date.now() - 86400000)); | |
| } | |
| export function loadReviewStar(key: string): ReviewStarState | null { | |
| try { | |
| const raw = localStorage.getItem(key); | |
| if (!raw) return null; | |
| return JSON.parse(raw) as ReviewStarState; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function saveReviewStar(key: string, state: ReviewStarState) { | |
| localStorage.setItem(key, JSON.stringify(state)); | |
| } | |
| /** | |
| * Called when user enters Review section (or on workspace switch while in Review): | |
| * If day changed, reset today's count to 0 (i.e., dim star until there's real review activity). | |
| */ | |
| export function normalizeToday(key: string): ReviewStarState | null { | |
| const cur = loadReviewStar(key); | |
| if (!cur) return null; | |
| const today = dayKeyLocal(); | |
| if (cur.lastActiveDay === today) return cur; | |
| const next: ReviewStarState = { ...cur, todayCount: 0 }; | |
| saveReviewStar(key, next); | |
| return next; | |
| } | |
| /** | |
| * Mark a valid review activity. | |
| * - Same day: increment todayCount | |
| * - New day: todayCount=1, streak updates, totalDaysActive increments | |
| */ | |
| export function markReviewActive(key: string, _event: ReviewEventType): ReviewStarState { | |
| const today = dayKeyLocal(); | |
| const prev = loadReviewStar(key); | |
| if (!prev) { | |
| const next: ReviewStarState = { | |
| lastActiveDay: today, | |
| todayCount: 1, | |
| streakDays: 1, | |
| totalDaysActive: 1, | |
| }; | |
| saveReviewStar(key, next); | |
| return next; | |
| } | |
| if (prev.lastActiveDay === today) { | |
| const next: ReviewStarState = { ...prev, todayCount: prev.todayCount + 1 }; | |
| saveReviewStar(key, next); | |
| return next; | |
| } | |
| const y = yesterdayKeyLocal(); | |
| const streak = prev.lastActiveDay === y ? prev.streakDays + 1 : 1; | |
| const next: ReviewStarState = { | |
| ...prev, | |
| lastActiveDay: today, | |
| todayCount: 1, | |
| streakDays: streak, | |
| totalDaysActive: (prev.totalDaysActive ?? 0) + 1, | |
| }; | |
| saveReviewStar(key, next); | |
| return next; | |
| } | |
| /** | |
| * UI mapping: opacity/energy | |
| * - 0 activity today => dim | |
| * - 1 activity => medium | |
| * - 2+ => bright | |
| * - streak >= 3 => max | |
| */ | |
| export function starOpacity(state: ReviewStarState | null) { | |
| if (!state) return 0.15; | |
| if (state.todayCount <= 0) return 0.15; | |
| if (state.todayCount === 1) return 0.55; | |
| if (state.streakDays >= 3) return 1.0; | |
| return 0.85; | |
| } | |
| export function energyPct(state: ReviewStarState | null) { | |
| return Math.round(starOpacity(state) * 100); | |
| } | |