RYP / src /lib /weeklyContest.ts
Soumya79's picture
Upload 1361 files
f91a684 verified
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<CodingQuestion['difficulty'], number> = {
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<string>();
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;
}