Add study activity tracking and refresh class header UI
All checks were successful
Automated Container Build / build-and-push (push) Successful in 54s

This commit is contained in:
Elijah 2026-07-12 11:44:26 -07:00
parent 95af276d2e
commit dad62c9914
21 changed files with 1598 additions and 44 deletions

View file

@ -0,0 +1,104 @@
import { prisma } from "@/lib/db";
import type { StudyActivityType } from "@/lib/validation/activitySchemas";
const ARIZONA_OFFSET_MS = 7 * 60 * 60 * 1000;
const WEEKS_TO_DISPLAY = 53;
const DAYS_TO_DISPLAY = WEEKS_TO_DISPLAY * 7;
const STREAK_THRESHOLD = 20;
export interface DailyActivity {
date: string;
flashcards: number;
questions: number;
total: number;
level: 0 | 1 | 2 | 3 | 4;
}
export interface ActivitySummary {
days: DailyActivity[];
today: string;
activeDays: number;
flashcards: number;
questions: number;
currentStreak: number;
}
function toArizonaDateKey(date: Date): string {
return new Date(date.getTime() - ARIZONA_OFFSET_MS).toISOString().slice(0, 10);
}
function dateKeyToUtcDate(dateKey: string): Date {
return new Date(`${dateKey}T00:00:00.000Z`);
}
function addDays(dateKey: string, days: number): string {
const date = dateKeyToUtcDate(dateKey);
date.setUTCDate(date.getUTCDate() + days);
return date.toISOString().slice(0, 10);
}
function getLevel(total: number): 0 | 1 | 2 | 3 | 4 {
if (total === 0) return 0;
if (total < 20) return 1;
if (total < 50) return 2;
if (total < 100) return 3;
return 4;
}
function emptyDay(date: string): DailyActivity {
return { date, flashcards: 0, questions: 0, total: 0, level: 0 };
}
export async function recordActivity(type: StudyActivityType) {
return prisma.studyActivity.create({ data: { type } });
}
export async function getActivitySummary(now = new Date()): Promise<ActivitySummary> {
const today = toArizonaDateKey(now);
const todayDate = dateKeyToUtcDate(today);
const dayOfWeek = todayDate.getUTCDay();
const startDate = addDays(today, -(dayOfWeek + (WEEKS_TO_DISPLAY - 1) * 7));
const endDate = addDays(startDate, DAYS_TO_DISPLAY - 1);
const activities = await prisma.studyActivity.findMany({
select: { type: true, occurredAt: true },
orderBy: { occurredAt: "asc" },
});
const totals = new Map<string, DailyActivity>();
for (const activity of activities) {
const date = toArizonaDateKey(activity.occurredAt);
const day = totals.get(date) ?? emptyDay(date);
if (activity.type === "FLASHCARD") day.flashcards += 1;
if (activity.type === "QUIZ_QUESTION") day.questions += 1;
day.total += 1;
day.level = getLevel(day.total);
totals.set(date, day);
}
const days = Array.from({ length: DAYS_TO_DISPLAY }, (_, index) => {
const date = addDays(startDate, index);
return totals.get(date) ?? emptyDay(date);
});
const visibleDays = days.filter((day) => day.date <= today && day.date <= endDate);
const flashcards = visibleDays.reduce((sum, day) => sum + day.flashcards, 0);
const questions = visibleDays.reduce((sum, day) => sum + day.questions, 0);
let streakDate = (totals.get(today)?.total ?? 0) >= STREAK_THRESHOLD
? today
: addDays(today, -1);
let currentStreak = 0;
while ((totals.get(streakDate)?.total ?? 0) >= STREAK_THRESHOLD) {
currentStreak += 1;
streakDate = addDays(streakDate, -1);
}
return {
days,
today,
activeDays: visibleDays.filter((day) => day.total > 0).length,
flashcards,
questions,
currentStreak,
};
}