import type { CodingQuestion } from '../data/codingQuestions'; import { getActivityDateKey } from './activityDates'; export type WeeklyContest = { id: string; startTime: Date; endTime: Date; durationMinutes: number; isLive: boolean; canJoin: boolean; }; export type WeeklyContestPrizeTier = { fromRank: number; toRank: number; label: string; coins: number; }; const WEEKLY_CONTEST_DAY = 0; const WEEKLY_CONTEST_START_HOUR = 20; const WEEKLY_CONTEST_DURATION_MINUTES = 90; const WEEKLY_CONTEST_JOIN_STORAGE_PREFIX = 'ryp.weeklyContest.joined'; const WEEKLY_CONTEST_CLAIM_STORAGE_PREFIX = 'ryp.weeklyContest.claimed'; export const WEEKLY_CONTEST_PRIZE_TIERS: WeeklyContestPrizeTier[] = [ { fromRank: 1, toRank: 1, label: '1st place', coins: 5000 }, { fromRank: 2, toRank: 2, label: '2nd place', coins: 3000 }, { fromRank: 3, toRank: 3, label: '3rd place', coins: 1000 }, { fromRank: 4, toRank: 10, label: '4th - 10th', coins: 100 }, ] as const; const contestPointsByDifficulty: Record = { Easy: 150, Medium: 300, Hard: 550, }; export function getWeeklyContest(now: Date) { const thisWeekContest = buildContestForWeek(now, 0, now); if (now >= thisWeekContest.endTime) { return buildContestForWeek(now, 1, now); } return thisWeekContest; } export function getLatestCompletedWeeklyContest(now: Date) { const thisWeekContest = buildContestForWeek(now, 0, now); if (now >= thisWeekContest.endTime) { return thisWeekContest; } return buildContestForWeek(now, -1, now); } export function getWeeklyContestQuestions(questions: CodingQuestion[]) { const buckets = { Easy: questions.filter((question) => question.difficulty === 'Easy'), Medium: questions.filter((question) => question.difficulty === 'Medium'), Hard: questions.filter((question) => question.difficulty === 'Hard'), }; const selected = [ buckets.Easy[0], buckets.Medium[0], buckets.Medium[1], buckets.Hard[0], ].filter(Boolean) as CodingQuestion[]; if (selected.length >= 4) { return selected.slice(0, 4); } const selectedIds = new Set(selected.map((question) => question.id)); for (const question of questions) { if (selectedIds.has(question.id)) { continue; } selected.push(question); selectedIds.add(question.id); if (selected.length === 4) { break; } } return selected; } export function getContestQuestionPoints(question: CodingQuestion) { return contestPointsByDifficulty[question.difficulty]; } export function getContestPrizeForRank(rank: number) { const tier = WEEKLY_CONTEST_PRIZE_TIERS.find( (entry) => rank >= entry.fromRank && rank <= entry.toRank, ); return tier?.coins ?? 0; } export function getContestPrizePool() { return WEEKLY_CONTEST_PRIZE_TIERS.reduce( (sum, tier) => sum + (tier.toRank - tier.fromRank + 1) * tier.coins, 0, ); } export function formatDurationFromNow(now: Date, target: Date) { const diffMs = Math.max(target.getTime() - now.getTime(), 0); const totalMinutes = Math.ceil(diffMs / 60000); const days = Math.floor(totalMinutes / (60 * 24)); const hours = Math.floor((totalMinutes % (60 * 24)) / 60); const minutes = totalMinutes % 60; const parts: string[] = []; if (days > 0) { parts.push(`${days}d`); } if (hours > 0) { parts.push(`${hours}h`); } if ((days === 0 && hours === 0) || minutes > 0) { parts.push(`${minutes}m`); } return parts.join(' '); } export function readJoinedContestIds(userId: string) { return readStoredContestIds(getJoinedContestStorageKey(userId)); } export function joinWeeklyContest(userId: string, contestId: string) { const nextIds = mergeStoredContestId(readJoinedContestIds(userId), contestId); writeStoredContestIds(getJoinedContestStorageKey(userId), nextIds); return nextIds; } export function readClaimedContestIds(userId: string) { return readStoredContestIds(getClaimedContestStorageKey(userId)); } export function markWeeklyContestPrizeClaimed(userId: string, contestId: string) { const nextIds = mergeStoredContestId(readClaimedContestIds(userId), contestId); writeStoredContestIds(getClaimedContestStorageKey(userId), nextIds); return nextIds; } function buildContestForWeek(now: Date, weekOffset: number, referenceNow: Date): WeeklyContest { const startTime = getContestStartForWeek(now, weekOffset); const endTime = new Date( startTime.getTime() + WEEKLY_CONTEST_DURATION_MINUTES * 60 * 1000, ); return { id: getActivityDateKey(startTime), startTime, endTime, durationMinutes: WEEKLY_CONTEST_DURATION_MINUTES, isLive: referenceNow >= startTime && referenceNow < endTime, canJoin: referenceNow < startTime, }; } function getContestStartForWeek(now: Date, weekOffset: number) { const contestStart = new Date(now); const dayDelta = WEEKLY_CONTEST_DAY - contestStart.getDay() + weekOffset * 7; contestStart.setDate(contestStart.getDate() + dayDelta); contestStart.setHours(WEEKLY_CONTEST_START_HOUR, 0, 0, 0); return contestStart; } function getJoinedContestStorageKey(userId: string) { return `${WEEKLY_CONTEST_JOIN_STORAGE_PREFIX}.${userId}`; } function getClaimedContestStorageKey(userId: string) { return `${WEEKLY_CONTEST_CLAIM_STORAGE_PREFIX}.${userId}`; } function readStoredContestIds(storageKey: string) { if (typeof window === 'undefined') { return [] as string[]; } try { const raw = window.localStorage.getItem(storageKey); if (!raw) { return []; } const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { return normalizeContestIds(parsed); } if (typeof parsed === 'string') { return normalizeContestIds([parsed]); } } catch { try { const legacyValue = window.localStorage.getItem(storageKey); return legacyValue ? normalizeContestIds([legacyValue]) : []; } catch { return []; } } return []; } function writeStoredContestIds(storageKey: string, ids: string[]) { if (typeof window === 'undefined') { return; } try { window.localStorage.setItem(storageKey, JSON.stringify(normalizeContestIds(ids))); } catch { // Ignore storage errors so the UI still works without persistence. } } function mergeStoredContestId(ids: string[], contestId: string) { return normalizeContestIds([...ids, contestId]); } function normalizeContestIds(ids: string[]) { const seen = new Set(); const normalized: string[] = []; for (const rawId of ids) { const id = String(rawId).trim(); if (!id || seen.has(id)) { continue; } seen.add(id); normalized.push(id); } return normalized; }