File size: 6,724 Bytes
f91a684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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;
}