feat: Add Past Contests submission and verification#142
Conversation
|
@hansikareddy29 is attempting to deploy a commit to the aviralsaxena16's projects Team on Vercel. A member of the Team first needs to authorize it. |
🎉 Thanks for Your Contribution to CanonForces!
|
WalkthroughIntroduces a complete contest submission system with past contest listing, problem viewing, and code submission capabilities. Adds API routes for fetching contests from CLIST, submitting solutions with coin rewards, and verifying Codeforces submissions. Includes React components for contest browsing and solution submission with Monaco editor support. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Browser
participant API as Next.js API
participant CLIST as CLIST API
participant Firestore
User->>Browser: Navigate to Past Contests
Browser->>API: GET /api/past-contests
API->>CLIST: Fetch contests (30-day window)
CLIST-->>API: Contest data
API->>API: Filter platforms & language
API-->>Browser: Return contest list
Browser->>User: Display contests with filter tabs
User->>Browser: Click "View Problems"
Browser->>Browser: Fetch generated problem metadata
Browser->>User: Show contest problems modal
sequenceDiagram
actor User
participant Browser
participant SolutionModal as Solution Modal
participant API as Next.js API
participant Firestore as Firestore DB
participant CoinSystem as Coin System
User->>SolutionModal: Select language & write code
User->>SolutionModal: Click "Submit Solution"
SolutionModal->>API: POST /submit-solution (code, contest, problem)
API->>Firestore: Check if first submission for this problem
API->>API: Compute coinsEarned (10 if first, 0 if repeat)
API->>Firestore: Start transaction
Firestore->>Firestore: Create submission document
alt First Submission
Firestore->>Firestore: Increment user coins by 10
end
Firestore-->>API: Commit transaction
API-->>SolutionModal: Success response + coinsEarned
SolutionModal->>User: Show success modal with coins awarded
sequenceDiagram
actor User
participant Modal as Solution Modal
participant API as Next.js API
participant CFApi as Codeforces API
participant Firestore
User->>Modal: Click "Verify Codeforces Submission"
Modal->>API: POST /verify-codeforces (handle, contestId, problemIndex)
API->>CFApi: Fetch user.status (submissions)
CFApi-->>API: Recent submissions
API->>API: Search for matching submission (verdict=OK)
alt Found Match
API-->>Modal: {verified: true}
Modal->>Firestore: Save verified submission
Modal->>User: Show verification success
else No Match
API-->>Modal: {verified: false}
Modal->>User: Show verification failed
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
🤖 Fix all issues with AI agents
In @.env.example:
- Around line 27-30: Add the missing server-side Firebase Admin SDK environment
variables to .env.example so the backend can initialize the SDK in
src/lib/firebase_admin.ts; specifically add entries for FIREBASE_PROJECT_ID,
FIREBASE_CLIENT_EMAIL, and FIREBASE_PRIVATE_KEY with placeholder values and a
short comment that these are required for server-side initialization (used by
the firebase admin initialization code in src/lib/firebase_admin.ts and
endpoints like /api/submit-solution).
In `@src/components/contests/ContestProblems.tsx`:
- Around line 63-70: The anchor uses problem.problemLink which is typed string |
undefined, so guard against undefined by conditionally rendering the <a> or
using a safe fallback; update the render in ContestProblems.tsx to only output
the anchor with className styles.viewButton and target/rel attributes when
problem.problemLink is truthy (or alternatively render a non-clickable
element/button with a disabled style when undefined), referencing the
problem.problemLink property and preserving the existing view text and
attributes when present.
- Around line 13-24: The effect that calls generateProblemsForContest currently
sets problems directly and provides no loading or error feedback and depends on
the whole contest object; update the ContestProblems component to add loading
and error state (e.g., isLoading, error via useState), set isLoading=true before
calling generateProblemsForContest and false after, catch and save any exception
to error and avoid swallowing it, and render a loading indicator and an error
message in the UI when appropriate instead of showing "0 Problems"; also change
the effect dependency from contest to contest.contestId (or other stable
primitive) to avoid unnecessary re-runs. Use the existing functions/setters
(generateProblemsForContest, setProblems, setIsSubmitModalOpen) and the
useEffect block to implement these changes.
In `@src/components/contests/PastContestsList.tsx`:
- Around line 22-25: In PastContestsList, the fetch block uses res.json()
without checking HTTP status; update the code in the async fetch logic to check
res.ok first (after awaiting fetch("/api/past-contests")) and handle non-2xx
responses by throwing or logging an error (optionally parsing error body via
res.text() or res.json()), only call res.json() and setContests(data.contests ||
[]) when res.ok is true; ensure any thrown error is caught in the existing
try/catch so the component doesn't attempt to use invalid data.
- Around line 46-58: The getTimeAgo function contains a dead branch: after
returning "Yesterday" when days === 1, the subsequent if (days < 7) uses a
redundant days === 1 ternary that can never run; update the (days < 7) branch in
getTimeAgo to unconditionally return `${days} days ago` (or handle
singular/plural by checking days === 1 only if you remove the earlier
"Yesterday" return), removing the unreachable ternary so the function returns
"Today", "Yesterday", "X days ago" for 2–6 days, then weeks/months as before.
In `@src/components/contests/SolutionSubmitModal.tsx`:
- Line 151: The setTimeout in SolutionSubmitModal that calls onSuccess and
onClose after 2s can fire after the component unmounts; store the timer ID in a
ref (e.g. successTimerRef) when you call setTimeout in the submit success path
(where setSuccess is used) and in the component's useEffect cleanup or useEffect
with empty deps clearTimeout(successTimerRef.current) to prevent invoking
onSuccess/onClose or setState on an unmounted component; ensure you also clear
the timer before setting a new one if the success path can run multiple times.
- Around line 106-141: The read using getDocs(q) inside runTransaction is
outside Firestore's transactional snapshot and allows a race where two
concurrent submissions both see no prior document and both get coins; change the
logic to use a deterministic document ID (e.g., combine user.uid and
problem.problemId) and perform transaction.get(doc(db, "contest_submissions",
deterministicId)) to check existence and transaction.set on that same
deterministic doc (use the existing newSubmissionRef/doc identifiers to locate
the right spot), and only transaction.update the user's coins if the
transactional read shows no prior submission; additionally, the setTimeout
mentioned should be moved into a useEffect hook and store its timer id so you
can clearTimeout on cleanup to avoid it firing after unmount.
In `@src/pages/api/past-contests.ts`:
- Around line 95-97: The catch block in src/pages/api/past-contests.ts is
swallowing the error variable `err`—update the handler’s catch to log the actual
error (include `err` or `err.stack`) instead of just a static warning; locate
the catch that currently calls console.warn("⚠️ Clist failed for past contests,
returning fallback") and change it to include the error details (or use
console.error) so the `err` value is output with the message for debugging.
- Around line 65-67: The code assigns const contests: PastContest[] =
data.objects and then calls .filter() without guarding against data or
data.objects being null/undefined; add a null guard or default to an empty array
before using array methods. Locate the assignment to contests (and any
subsequent .filter() usage) and change it to use a safe value (e.g., const
rawObjects = data?.objects ?? []; const contests = rawObjects.filter(...)) or
explicitly check data && Array.isArray(data.objects) and handle the error path
(log and return an empty list or appropriate response) so the endpoint won't
crash if CLIST changes shape.
In `@src/pages/api/submit-solution.ts`:
- Around line 44-51: The handler currently skips the minimum length check when
language === "Verified", trusting an unvalidated request field; fix by
validating language against SUPPORTED_LANGUAGES (or an explicit Verified flag)
before applying length rules: ensure the request body’s language is one of
SUPPORTED_LANGUAGES, treat verified submissions via a separate verified
submission flow/flag (not by setting language to "Verified"), and only bypass
the code.length < 10 check when the submission is authenticated/marked as
verified by your verification logic; update the validation in the
submit-solution handler (references: language, code, SUPPORTED_LANGUAGES) to
enforce this separation and reject unknown language values.
- Around line 29-42: The handler currently trusts userId from the request body;
instead validate the caller by verifying an auth token (e.g., Firebase) and
derive the user ID from the verified token. In submit-solution handler read the
Authorization bearer token (or session cookie), call
admin.auth().verifyIdToken(token) (or your auth library's verify method), obtain
uid and replace use of body.userId with that uid, return 401 on missing/invalid
token, and remove/ignore userId from req.body so coins/credits are awarded only
to the authenticated uid.
- Around line 53-65: The duplicate-check query (existingSubmissions) runs
outside the transaction causing a race; move that query inside the Firestore
transaction so the check and subsequent writes are atomic. Inside the
transaction callback (the code using adminDb.runTransaction), call
transaction.get(...) on the same query built from
submissionsRef.where('userId','==',userId).where('problemId','==',problemId).limit(1),
determine isFirstSubmission from the returned snapshot, compute coinsEarned
accordingly, then perform the create of the submission doc and the user coin
update within the same transaction; remove the external existingSubmissions read
and any logic that relies on it outside the transaction.
In `@src/pages/api/verify-codeforces.ts`:
- Around line 14-62: The handler in verify-codeforces.ts lacks any
authentication or rate-limiting; before calling the Codeforces API you should
validate the caller (e.g., check a session via next-auth or validate a bearer
token/header) and return 401/403 for unauthenticated requests, then enforce
per-user or global rate limits (in-memory, Redis, or a library) to throttle
calls to the fetch logic (the apiUrl/fetch block) to prevent abuse; add these
checks at the top of the exported async function handler (before reading
req.body and before the fetch), and log and respond with appropriate error codes
when auth or rate limits fail.
- Line 29: The URL construction for Codeforces checks is unsafe and too narrow:
encode the user-supplied handle using encodeURIComponent before interpolating
into the apiUrl (the apiUrl variable in the verify-codeforces handler) to
prevent malformed URLs, and increase the count parameter (or make it
configurable via an environment variable or handler option) instead of
hardcoding count=50 so the verification scans more submissions; update the
apiUrl construction to use the encoded handle and replace the literal count with
a larger value or a configurable constant.
- Around line 46-54: The code calls data.result.some(...) without guarding
against data.result being null/undefined; update the isSolved computation
(referencing targetContestId, isSolved, contestId, problemIndex) to treat
data.result as an array (e.g., use Array.isArray(data.result) ? data.result : []
or default to []) before calling .some(), so the .some() call never runs on a
non-array and avoids a TypeError when the API returns no result field.
In `@src/types/contest-submission.ts`:
- Line 12: The field declaration submittedAt: Date | any collapses to any and
loses type safety; change it to a precise union that covers Firestore sentinels
and returned timestamps, e.g. submittedAt: Date | FirebaseFirestore.FieldValue |
FirebaseFirestore.Timestamp (or the equivalent FieldValue/Timestamp types from
your Firebase SDK), and add the necessary import/namespace references so the
compiler recognizes FirebaseFirestore.FieldValue and
FirebaseFirestore.Timestamp; update the submittedAt type in the interface in
src/types/contest-submission.ts and import the appropriate types from your
Firebase package.
In `@src/utils/generate-contest-problems.ts`:
- Around line 25-31: getProblemTitle currently uses parseInt(contestId) which
returns NaN for non-numeric IDs and breaks the seed math; replace parseInt usage
with a deterministic string-to-number hash of contestId (optionally include
problemIndex) to produce a numeric seed. Compute a simple stable hash (e.g.,
iterate chars, multiply/add and take Math.abs) then combine with
LETTERS.indexOf(problemIndex) as the existing code does, use that seed for the
difficulty calculation (seed % 3) and for selecting a title (Math.floor(seed /
3) % titles.length) so getProblemTitle, LETTERS and PROBLEM_TITLES continue to
work for any contestId string.
- Around line 134-158: The switch in buildProblemLink declares consts
(fixedBaseUrl, contestId) inside case clauses which can cause TDZ issues; fix by
wrapping each case body that declares const in its own block (e.g., case
'codeforces': { ... } and case 'atcoder': { ... }) so fixedBaseUrl and contestId
are scoped to their blocks, then return from inside those blocks. Ensure no
cross-case fallthrough remains.
In `@tsconfig.json`:
- Line 3: Update the tsconfig.json compilerOptions "jsx" setting from
"react-jsx" to "preserve" to match Next.js recommendations; locate the "jsx" key
under compilerOptions in tsconfig.json and replace its value so the project uses
Next/SWC for JSX transformation.
🧹 Nitpick comments (11)
.env.example (2)
27-30: Address static analysis hints and formatting consistency.The dotenv-linter tool has flagged a couple of minor issues:
- Missing blank line at the end of the file (line 30).
- Keys should be alphabetically ordered:
CLIST_API_KEYshould come beforeCLIST_USERNAME.- The comment on line 28 has inconsistent capitalization: "Clist_API_key" should be "CLIST_API_KEY" for consistency.
📝 Proposed formatting fixes
-# Clist Username and Clist_API_key for past contests -CLIST_USERNAME="Add your Clist Api username" +# CLIST_API_KEY and CLIST_USERNAME for past contests CLIST_API_KEY="Add your key" +CLIST_USERNAME="Add your Clist Api username" +
4-10: Consider using placeholder values for Firebase credentials in .env.example.The
.env.examplefile currently contains what appear to be actual production Firebase credentials rather than placeholder values. While Firebase public API keys are designed to be exposed in client-side code and are protected by security rules, it's a best practice to use placeholder values in.env.examplefiles to avoid accidentally exposing project-specific configuration.Consider replacing these with placeholder values like:
NEXT_PUBLIC_FIREBASE_API_KEY="your-firebase-api-key" NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-project.firebaseapp.com"Note: This is a pre-existing issue not introduced by this PR, so it can be addressed separately.
src/components/contests/ContestProblems.module.css (1)
200-220: Rename keyframes to kebab-case to satisfy the Stylelintkeyframes-name-patternrule.The linter flags
fadeInandslideUpas violations of the kebab-case naming convention for keyframes. Since this rule is configured project-wide, rename them for consistency.Proposed fix
-@keyframes fadeIn { +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } -@keyframes slideUp { +@keyframes slide-up { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }Also update the references on lines 15 and 28:
- animation: fadeIn 0.2s ease-out; + animation: fade-in 0.2s ease-out;- animation: slideUp 0.3s ease-out; + animation: slide-up 0.3s ease-out;src/components/contests/SolutionSubmitModal.module.css (2)
18-53: Rename keyframes to kebab-case per Stylelintkeyframes-name-patternrule.Same issue as in
ContestProblems.module.css—fadeIn→fade-in,slideUp→slide-up. Update theanimationreferences on lines 15 and 38 accordingly.
150-160: Replace deprecatedclipproperty withclip-path.The
clipproperty is deprecated per the Stylelintproperty-no-deprecatedrule. Replace with the modern equivalent for the visually-hidden pattern.Proposed fix
.fileInput { /* Hidden but accessible via label */ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; - clip: rect(0, 0, 0, 0); + clip-path: inset(50%); border: 0; }src/pages/contests-list.tsx (1)
180-181: Prefer a stable key over array index.Using
idxas the React key can cause subtle rendering issues if the list gets reordered or filtered. Consider using a combination of unique contest properties.Proposed fix
- filteredContests.map((contest, idx) => ( - <div key={idx} className={styles.card}> + filteredContests.map((contest) => ( + <div key={`${contest.platform}-${contest.contestName}-${contest.startTime}`} className={styles.card}>src/components/contests/PastContestsList.tsx (1)
119-120: Prefer a stable unique key over array index.Using
idxas the React key can cause incorrect reconciliation if the list is reordered or filtered. Each contest has acontestId— use that (or a composite key) instead.Proposed fix
- filteredContests.map((contest, idx) => ( - <div key={idx} className={styles.card}> + filteredContests.map((contest) => ( + <div key={contest.contestId} className={styles.card}>src/utils/generate-contest-problems.ts (1)
36-68: Client-side calls to Codeforces API on every modal open — no caching.
fetchCodeforcesProblemsis called fromContestProblems.tsx(client-sideuseEffect) every time a user clicks "View Problems." This hammers the Codeforces API unnecessarily, and the data is static. Consider adding a simple in-memory cache (e.g., aMap<string, Map<string, string>>keyed by contestId) or moving this fetch server-side togenerateProblemsForContestbehind an API route.src/pages/api/past-contests.ts (1)
33-99: No caching — every page load triggers a full CLIST API call.The PR description mentions "caching and fallback," but only the fallback (empty array) is implemented. Since contest data changes infrequently (daily at most), consider adding a simple in-memory cache with a TTL (e.g., 5–10 minutes) to avoid hammering the CLIST API and improve response times.
Example in-memory cache
// Add above the handler let cachedData: { contests: PastContest[]; timestamp: number } | null = null; const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes // Inside handler, before the fetch: if (cachedData && Date.now() - cachedData.timestamp < CACHE_TTL_MS) { return res.status(200).json({ contests: cachedData.contests, source: "clist" }); } // After building the contests array: cachedData = { contests, timestamp: Date.now() };src/components/contests/SolutionSubmitModal.tsx (2)
293-301: Prefer CSS modules over inline styles for consistency.Line 298 uses
style={{ backgroundColor: '#3b82f6', marginRight: 'auto', marginLeft: '10px' }}while the rest of the component usesstyles.*from CSS modules. Move these styles toSolutionSubmitModal.module.cssfor consistency and easier theming.
196-215:handleSubmitvalidates code length but not against trivial/malicious payloads.A 10-character minimum is easily gamed (e.g.,
"aaaaaaaaaa"). If coin rewards are at stake, consider server-side validation in the API route or at least a more meaningful client-side check. This is a minor concern since the actual security boundary should be server-side.
|
|
||
| # Clist Username and Clist_API_key for past contests | ||
| CLIST_USERNAME="Add your Clist Api username" | ||
| CLIST_API_KEY="Add your key" No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the complete .env.example file to see all variables
cat -n .env.exampleRepository: OpenLake/canonforces
Length of output: 1333
🏁 Script executed:
# Search for Firebase Admin SDK usage in the codebase
rg -n 'FIREBASE_PROJECT_ID|FIREBASE_CLIENT_EMAIL|FIREBASE_PRIVATE_KEY' --type-add 'env:*.env*' --type env -A 2 -B 2Repository: OpenLake/canonforces
Length of output: 462
🏁 Script executed:
# Search for Firebase Admin SDK initialization and usage in backend code
rg -n 'firebase-admin|admin\.initializeApp|admin\.credential' -t js -t ts -A 3 -B 1Repository: OpenLake/canonforces
Length of output: 1338
🏁 Script executed:
# Check if there's a PR description or changelog that mentions these variables
fd -e md | head -20 | xargs grep -l 'FIREBASE_PROJECT_ID\|FIREBASE_CLIENT_EMAIL\|FIREBASE_PRIVATE_KEY' 2>/dev/null || echo "No matches found in markdown files"Repository: OpenLake/canonforces
Length of output: 98
🏁 Script executed:
cat -n src/lib/firebase_admin.tsRepository: OpenLake/canonforces
Length of output: 855
🏁 Script executed:
# Check for any setup or configuration documentation that might mention these variables
fd -e md -e txt | xargs grep -l 'FIREBASE_PROJECT_ID\|FIREBASE_CLIENT_EMAIL\|FIREBASE_PRIVATE_KEY' 2>/dev/null || echo "No matches in documentation"Repository: OpenLake/canonforces
Length of output: 91
🏁 Script executed:
# Also check if there's a .env.local.example or deployment docs
find . -maxdepth 2 -type f \( -name "*.md" -o -name "DEPLOY*" -o -name "SETUP*" -o -name ".env*" \) | head -20Repository: OpenLake/canonforces
Length of output: 190
🏁 Script executed:
# Check README and CONTRIBUTING for setup instructions
grep -n -A 5 -B 5 'Firebase\|environment\|.env' README.md CONTRIBUTING.md 2>/dev/null | head -60Repository: OpenLake/canonforces
Length of output: 2597
Add required Firebase Admin SDK server-side environment variables to .env.example.
The PR adds Clist credentials but omits the server-side Firebase Admin SDK environment variables that are actively required by the codebase. The file src/lib/firebase_admin.ts explicitly initializes the Firebase Admin SDK with credentials from:
FIREBASE_PROJECT_IDFIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY
These variables are missing from .env.example and are not documented anywhere in the repository. Since the README directs developers to check .env.example for setup, this omission will cause deployment failures and misconfiguration. Backend routes like /api/submit-solution depend on these credentials.
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 30-30: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
[warning] 30-30: [UnorderedKey] The CLIST_API_KEY key should go before the CLIST_USERNAME key
(UnorderedKey)
🤖 Prompt for AI Agents
In @.env.example around lines 27 - 30, Add the missing server-side Firebase
Admin SDK environment variables to .env.example so the backend can initialize
the SDK in src/lib/firebase_admin.ts; specifically add entries for
FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, and FIREBASE_PRIVATE_KEY with
placeholder values and a short comment that these are required for server-side
initialization (used by the firebase admin initialization code in
src/lib/firebase_admin.ts and endpoints like /api/submit-solution).
| const [problems, setProblems] = useState<ContestProblem[]>([]); | ||
| const [selectedProblem, setSelectedProblem] = useState<ContestProblem | null>(null); | ||
| const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| // Generate problems for this contest | ||
| const loadProblems = async () => { | ||
| const generatedProblems = await generateProblemsForContest(contest); | ||
| setProblems(generatedProblems); | ||
| }; | ||
| loadProblems(); | ||
| }, [contest]); |
There was a problem hiding this comment.
Missing loading and error states for async problem fetching.
While generateProblemsForContest is fetching (which calls the Codeforces API for CF contests), users see "0 Problems" in the header and an empty grid with no indication that data is loading. Also, if the fetch fails, the error is silently swallowed.
Proposed fix — add loading and error states
const [problems, setProblems] = useState<ContestProblem[]>([]);
const [selectedProblem, setSelectedProblem] = useState<ContestProblem | null>(null);
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadProblems = async () => {
- const generatedProblems = await generateProblemsForContest(contest);
- setProblems(generatedProblems);
+ try {
+ setLoading(true);
+ setError(null);
+ const generatedProblems = await generateProblemsForContest(contest);
+ setProblems(generatedProblems);
+ } catch (err) {
+ console.error('Failed to load problems:', err);
+ setError('Failed to load problems. Please try again.');
+ } finally {
+ setLoading(false);
+ }
};
loadProblems();
- }, [contest]);
+ }, [contest.contestId]);Note: Using contest (an object) as the useEffect dependency instead of contest.contestId will re-trigger on every parent re-render if the object reference isn't stable.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [problems, setProblems] = useState<ContestProblem[]>([]); | |
| const [selectedProblem, setSelectedProblem] = useState<ContestProblem | null>(null); | |
| const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false); | |
| useEffect(() => { | |
| // Generate problems for this contest | |
| const loadProblems = async () => { | |
| const generatedProblems = await generateProblemsForContest(contest); | |
| setProblems(generatedProblems); | |
| }; | |
| loadProblems(); | |
| }, [contest]); | |
| const [problems, setProblems] = useState<ContestProblem[]>([]); | |
| const [selectedProblem, setSelectedProblem] = useState<ContestProblem | null>(null); | |
| const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| useEffect(() => { | |
| // Generate problems for this contest | |
| const loadProblems = async () => { | |
| try { | |
| setLoading(true); | |
| setError(null); | |
| const generatedProblems = await generateProblemsForContest(contest); | |
| setProblems(generatedProblems); | |
| } catch (err) { | |
| console.error('Failed to load problems:', err); | |
| setError('Failed to load problems. Please try again.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| loadProblems(); | |
| }, [contest.contestId]); |
🤖 Prompt for AI Agents
In `@src/components/contests/ContestProblems.tsx` around lines 13 - 24, The effect
that calls generateProblemsForContest currently sets problems directly and
provides no loading or error feedback and depends on the whole contest object;
update the ContestProblems component to add loading and error state (e.g.,
isLoading, error via useState), set isLoading=true before calling
generateProblemsForContest and false after, catch and save any exception to
error and avoid swallowing it, and render a loading indicator and an error
message in the UI when appropriate instead of showing "0 Problems"; also change
the effect dependency from contest to contest.contestId (or other stable
primitive) to avoid unnecessary re-runs. Use the existing functions/setters
(generateProblemsForContest, setProblems, setIsSubmitModalOpen) and the
useEffect block to implement these changes.
| <a | ||
| href={problem.problemLink} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={styles.viewButton} | ||
| > | ||
| View Problem ↗ | ||
| </a> |
There was a problem hiding this comment.
problemLink is optional — guard against undefined.
ContestProblem.problemLink is typed as string | undefined (see src/types/contest-submission.ts line 30). Rendering <a href={undefined}> produces a link that navigates to the current page. Either hide the link when unavailable or provide a fallback.
Proposed fix
- <a
- href={problem.problemLink}
- target="_blank"
- rel="noopener noreferrer"
- className={styles.viewButton}
- >
- View Problem ↗
- </a>
+ {problem.problemLink ? (
+ <a
+ href={problem.problemLink}
+ target="_blank"
+ rel="noopener noreferrer"
+ className={styles.viewButton}
+ >
+ View Problem ↗
+ </a>
+ ) : (
+ <span className={styles.viewButton} style={{ opacity: 0.5, cursor: 'default' }}>
+ No Link Available
+ </span>
+ )}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a | |
| href={problem.problemLink} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className={styles.viewButton} | |
| > | |
| View Problem ↗ | |
| </a> | |
| {problem.problemLink ? ( | |
| <a | |
| href={problem.problemLink} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className={styles.viewButton} | |
| > | |
| View Problem ↗ | |
| </a> | |
| ) : ( | |
| <span className={styles.viewButton} style={{ opacity: 0.5, cursor: 'default' }}> | |
| No Link Available | |
| </span> | |
| )} |
🤖 Prompt for AI Agents
In `@src/components/contests/ContestProblems.tsx` around lines 63 - 70, The anchor
uses problem.problemLink which is typed string | undefined, so guard against
undefined by conditionally rendering the <a> or using a safe fallback; update
the render in ContestProblems.tsx to only output the anchor with className
styles.viewButton and target/rel attributes when problem.problemLink is truthy
(or alternatively render a non-clickable element/button with a disabled style
when undefined), referencing the problem.problemLink property and preserving the
existing view text and attributes when present.
| try { | ||
| const res = await fetch("/api/past-contests"); | ||
| const data: ApiResponse = await res.json(); | ||
| setContests(data.contests || []); |
There was a problem hiding this comment.
Missing res.ok check before parsing JSON.
If the API returns a non-2xx status (e.g., 500), res.json() may still succeed but return an error payload—or it may throw on non-JSON bodies. Check res.ok before parsing to surface errors properly.
Proposed fix
const res = await fetch("/api/past-contests");
+ if (!res.ok) {
+ throw new Error(`API error: ${res.status}`);
+ }
const data: ApiResponse = await res.json();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const res = await fetch("/api/past-contests"); | |
| const data: ApiResponse = await res.json(); | |
| setContests(data.contests || []); | |
| try { | |
| const res = await fetch("/api/past-contests"); | |
| if (!res.ok) { | |
| throw new Error(`API error: ${res.status}`); | |
| } | |
| const data: ApiResponse = await res.json(); | |
| setContests(data.contests || []); |
🤖 Prompt for AI Agents
In `@src/components/contests/PastContestsList.tsx` around lines 22 - 25, In
PastContestsList, the fetch block uses res.json() without checking HTTP status;
update the code in the async fetch logic to check res.ok first (after awaiting
fetch("/api/past-contests")) and handle non-2xx responses by throwing or logging
an error (optionally parsing error body via res.text() or res.json()), only call
res.json() and setContests(data.contests || []) when res.ok is true; ensure any
thrown error is caught in the existing try/catch so the component doesn't
attempt to use invalid data.
| const getTimeAgo = (ts: number) => { | ||
| const now = Date.now(); | ||
| const diff = now - ts; | ||
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); | ||
|
|
||
| if (days === 0) return "Today"; | ||
| if (days === 1) return "Yesterday"; | ||
| if (days < 7) return days === 1 ? `1 day ago` : `${days} days ago`; | ||
| const weeks = Math.floor(days / 7); | ||
| if (days < 30) return weeks === 1 ? `1 week ago` : `${weeks} weeks ago`; | ||
| const months = Math.floor(days / 30); | ||
| return months === 1 ? `1 month ago` : `${months} months ago`; | ||
| }; |
There was a problem hiding this comment.
Dead code branch in getTimeAgo.
Line 53: the days === 1 ternary branch is unreachable because days === 1 is already caught and returned on line 52 (return "Yesterday"). When execution reaches line 53, days is guaranteed to be ≥ 2.
Proposed fix
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
- if (days < 7) return days === 1 ? `1 day ago` : `${days} days ago`;
+ if (days < 7) return `${days} days ago`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getTimeAgo = (ts: number) => { | |
| const now = Date.now(); | |
| const diff = now - ts; | |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); | |
| if (days === 0) return "Today"; | |
| if (days === 1) return "Yesterday"; | |
| if (days < 7) return days === 1 ? `1 day ago` : `${days} days ago`; | |
| const weeks = Math.floor(days / 7); | |
| if (days < 30) return weeks === 1 ? `1 week ago` : `${weeks} weeks ago`; | |
| const months = Math.floor(days / 30); | |
| return months === 1 ? `1 month ago` : `${months} months ago`; | |
| }; | |
| const getTimeAgo = (ts: number) => { | |
| const now = Date.now(); | |
| const diff = now - ts; | |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); | |
| if (days === 0) return "Today"; | |
| if (days === 1) return "Yesterday"; | |
| if (days < 7) return `${days} days ago`; | |
| const weeks = Math.floor(days / 7); | |
| if (days < 30) return weeks === 1 ? `1 week ago` : `${weeks} weeks ago`; | |
| const months = Math.floor(days / 30); | |
| return months === 1 ? `1 month ago` : `${months} months ago`; | |
| }; |
🤖 Prompt for AI Agents
In `@src/components/contests/PastContestsList.tsx` around lines 46 - 58, The
getTimeAgo function contains a dead branch: after returning "Yesterday" when
days === 1, the subsequent if (days < 7) uses a redundant days === 1 ternary
that can never run; update the (days < 7) branch in getTimeAgo to
unconditionally return `${days} days ago` (or handle singular/plural by checking
days === 1 only if you remove the earlier "Yesterday" return), removing the
unreachable ternary so the function returns "Today", "Yesterday", "X days ago"
for 2–6 days, then weeks/months as before.
| const targetContestId = parseInt(contestId); | ||
|
|
||
| const isSolved = data.result.some((submission: any) => { | ||
| return ( | ||
| submission.contestId === targetContestId && | ||
| submission.problem.index === problemIndex && | ||
| submission.verdict === 'OK' | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Missing null-safety on data.result.
If data.result is undefined or null (e.g., Codeforces API returns status: "OK" but with no result field for a user with zero submissions), calling .some() on it will throw a TypeError at runtime.
Proposed fix
const targetContestId = parseInt(contestId);
- const isSolved = data.result.some((submission: any) => {
+ const submissions = data.result ?? [];
+ const isSolved = submissions.some((submission: any) => {
return (
submission.contestId === targetContestId &&
submission.problem.index === problemIndex &&
submission.verdict === 'OK'
);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const targetContestId = parseInt(contestId); | |
| const isSolved = data.result.some((submission: any) => { | |
| return ( | |
| submission.contestId === targetContestId && | |
| submission.problem.index === problemIndex && | |
| submission.verdict === 'OK' | |
| ); | |
| }); | |
| const targetContestId = parseInt(contestId); | |
| const submissions = data.result ?? []; | |
| const isSolved = submissions.some((submission: any) => { | |
| return ( | |
| submission.contestId === targetContestId && | |
| submission.problem.index === problemIndex && | |
| submission.verdict === 'OK' | |
| ); | |
| }); |
🤖 Prompt for AI Agents
In `@src/pages/api/verify-codeforces.ts` around lines 46 - 54, The code calls
data.result.some(...) without guarding against data.result being null/undefined;
update the isSolved computation (referencing targetContestId, isSolved,
contestId, problemIndex) to treat data.result as an array (e.g., use
Array.isArray(data.result) ? data.result : [] or default to []) before calling
.some(), so the .some() call never runs on a non-array and avoids a TypeError
when the API returns no result field.
| platform: string; | ||
| language: string; | ||
| code: string; | ||
| submittedAt: Date | any; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Date | any collapses to any, defeating type safety.
This likely accommodates Firestore's serverTimestamp() sentinel (a FieldValue). Use a more precise union instead.
Proposed fix
+import { FieldValue } from "firebase/firestore";
+
export interface ContestSubmission {
// ...
- submittedAt: Date | any;
+ submittedAt: Date | FieldValue;
// ...
}🤖 Prompt for AI Agents
In `@src/types/contest-submission.ts` at line 12, The field declaration
submittedAt: Date | any collapses to any and loses type safety; change it to a
precise union that covers Firestore sentinels and returned timestamps, e.g.
submittedAt: Date | FirebaseFirestore.FieldValue | FirebaseFirestore.Timestamp
(or the equivalent FieldValue/Timestamp types from your Firebase SDK), and add
the necessary import/namespace references so the compiler recognizes
FirebaseFirestore.FieldValue and FirebaseFirestore.Timestamp; update the
submittedAt type in the interface in src/types/contest-submission.ts and import
the appropriate types from your Firebase package.
| function getProblemTitle(contestId: string, problemIndex: string): string { | ||
| const seed = parseInt(contestId) + LETTERS.indexOf(problemIndex); | ||
| const difficulty = (seed % 3 === 0) ? 'easy' : (seed % 3 === 1) ? 'medium' : 'hard'; | ||
| const titles = PROBLEM_TITLES[difficulty]; | ||
| const selectedTitle = titles[Math.floor(seed / 3) % titles.length]; | ||
| return selectedTitle; | ||
| } |
There was a problem hiding this comment.
parseInt(contestId) returns NaN for non-numeric IDs, causing undefined to be returned.
For platforms like LeetCode (e.g., "weekly-contest-423") or AtCoder (e.g., "abc123"), parseInt returns NaN. This propagates through the arithmetic: NaN % 3 → NaN, and titles[NaN] → undefined, so the function returns undefined instead of a string.
Use a hash-based seed that works for any string:
Proposed fix
function getProblemTitle(contestId: string, problemIndex: string): string {
- const seed = parseInt(contestId) + LETTERS.indexOf(problemIndex);
+ // Simple string hash to handle non-numeric contest IDs
+ let hash = 0;
+ for (let i = 0; i < contestId.length; i++) {
+ hash = (hash * 31 + contestId.charCodeAt(i)) | 0;
+ }
+ const seed = Math.abs(hash) + LETTERS.indexOf(problemIndex);
const difficulty = (seed % 3 === 0) ? 'easy' : (seed % 3 === 1) ? 'medium' : 'hard';
const titles = PROBLEM_TITLES[difficulty];
const selectedTitle = titles[Math.floor(seed / 3) % titles.length];
return selectedTitle;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getProblemTitle(contestId: string, problemIndex: string): string { | |
| const seed = parseInt(contestId) + LETTERS.indexOf(problemIndex); | |
| const difficulty = (seed % 3 === 0) ? 'easy' : (seed % 3 === 1) ? 'medium' : 'hard'; | |
| const titles = PROBLEM_TITLES[difficulty]; | |
| const selectedTitle = titles[Math.floor(seed / 3) % titles.length]; | |
| return selectedTitle; | |
| } | |
| function getProblemTitle(contestId: string, problemIndex: string): string { | |
| // Simple string hash to handle non-numeric contest IDs | |
| let hash = 0; | |
| for (let i = 0; i < contestId.length; i++) { | |
| hash = (hash * 31 + contestId.charCodeAt(i)) | 0; | |
| } | |
| const seed = Math.abs(hash) + LETTERS.indexOf(problemIndex); | |
| const difficulty = (seed % 3 === 0) ? 'easy' : (seed % 3 === 1) ? 'medium' : 'hard'; | |
| const titles = PROBLEM_TITLES[difficulty]; | |
| const selectedTitle = titles[Math.floor(seed / 3) % titles.length]; | |
| return selectedTitle; | |
| } |
🤖 Prompt for AI Agents
In `@src/utils/generate-contest-problems.ts` around lines 25 - 31, getProblemTitle
currently uses parseInt(contestId) which returns NaN for non-numeric IDs and
breaks the seed math; replace parseInt usage with a deterministic
string-to-number hash of contestId (optionally include problemIndex) to produce
a numeric seed. Compute a simple stable hash (e.g., iterate chars, multiply/add
and take Math.abs) then combine with LETTERS.indexOf(problemIndex) as the
existing code does, use that seed for the difficulty calculation (seed % 3) and
for selecting a title (Math.floor(seed / 3) % titles.length) so getProblemTitle,
LETTERS and PROBLEM_TITLES continue to work for any contestId string.
| function buildProblemLink(contest: PastContest, problemIndex: string): string { | ||
| const baseUrl = contest.contestLink; | ||
|
|
||
| switch (contest.platform.toLowerCase()) { | ||
| case 'codeforces': | ||
| // e.g., https://codeforces.com/contest/1234/problem/A | ||
| const fixedBaseUrl = baseUrl.replace('/contests/', '/contest/'); | ||
| return `${fixedBaseUrl}/problem/${problemIndex}`; | ||
|
|
||
| case 'leetcode': | ||
| // LeetCode uses slug-based URLs, harder to construct | ||
| return baseUrl; | ||
|
|
||
| case 'codechef': | ||
| // e.g., https://www.codechef.com/problems/PROBLEMCODE | ||
| return baseUrl; | ||
|
|
||
| case 'atcoder': | ||
| // e.g., https://atcoder.jp/contests/abc123/tasks/abc123_a | ||
| const contestId = contest.contestId; | ||
| return `https://atcoder.jp/contests/${contestId}/tasks/${contestId}_${problemIndex.toLowerCase()}`; | ||
|
|
||
| default: | ||
| return baseUrl; | ||
| } |
There was a problem hiding this comment.
Wrap const declarations in switch cases inside blocks.
Biome correctly flags that const fixedBaseUrl (line 140) and const contestId (line 153) are hoisted within the switch scope, making them accessible (as TDZ references) from other case clauses. Wrap each case body in a block.
Proposed fix
switch (contest.platform.toLowerCase()) {
- case 'codeforces':
- // e.g., https://codeforces.com/contest/1234/problem/A
- const fixedBaseUrl = baseUrl.replace('/contests/', '/contest/');
- return `${fixedBaseUrl}/problem/${problemIndex}`;
-
- case 'leetcode':
+ case 'codeforces': {
+ const fixedBaseUrl = baseUrl.replace('/contests/', '/contest/');
+ return `${fixedBaseUrl}/problem/${problemIndex}`;
+ }
+ case 'leetcode':
// LeetCode uses slug-based URLs, harder to construct
return baseUrl;
-
- case 'codechef':
+ case 'codechef':
// e.g., https://www.codechef.com/problems/PROBLEMCODE
return baseUrl;
-
- case 'atcoder':
- // e.g., https://atcoder.jp/contests/abc123/tasks/abc123_a
- const contestId = contest.contestId;
- return `https://atcoder.jp/contests/${contestId}/tasks/${contestId}_${problemIndex.toLowerCase()}`;
-
+ case 'atcoder': {
+ const contestId = contest.contestId;
+ return `https://atcoder.jp/contests/${contestId}/tasks/${contestId}_${problemIndex.toLowerCase()}`;
+ }
default:
return baseUrl;
}🧰 Tools
🪛 Biome (2.3.14)
[error] 140-140: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 153-153: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
🤖 Prompt for AI Agents
In `@src/utils/generate-contest-problems.ts` around lines 134 - 158, The switch in
buildProblemLink declares consts (fixedBaseUrl, contestId) inside case clauses
which can cause TDZ issues; fix by wrapping each case body that declares const
in its own block (e.g., case 'codeforces': { ... } and case 'atcoder': { ... })
so fixedBaseUrl and contestId are scoped to their blocks, then return from
inside those blocks. Ensure no cross-case fallthrough remains.
| { | ||
| "compilerOptions": { | ||
| "jsx": "preserve", | ||
| "jsx": "react-jsx", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if this is a Next.js project
ls -la | grep -E 'next.config|package.json' | head -10
cat package.json | jq '.dependencies, .devDependencies' 2>/dev/null | grep -i next
# Check the git history for the jsx setting change
git log --oneline -10 -- tsconfig.json
# Show the current tsconfig.json content
cat tsconfig.jsonRepository: OpenLake/canonforces
Length of output: 919
🏁 Script executed:
# Check if there are React imports in the codebase
fd -t f -e ts -e tsx . | head -30 | xargs rg -l 'import.*React' 2>/dev/null | head -10Repository: OpenLake/canonforces
Length of output: 360
🏁 Script executed:
# Check if the tsx files in the codebase are using automatic JSX runtime (no React import) or traditional style
rg -A 2 'import' src/common/components/Notifications/NotificationItem.tsx src/common/components/Loading/Loading.tsx src/common/components/CodeEditor/CodeEditor.tsx 2>/dev/null | head -40
# Check Next.js documentation / best practices for jsx setting
# Also verify the React version being used
cat package.json | jq '.dependencies.react, .dependencies."react-dom"' 2>/dev/nullRepository: OpenLake/canonforces
Length of output: 1376
🏁 Script executed:
# Check if there are any .jsx files (not just .tsx) that would be affected
fd -e jsx . src/ 2>/dev/null | head -10
# Verify if any component is explicitly NOT importing React (indicating automatic JSX runtime is expected)
rg 'jsx|React' src/common/components/CodeEditor/CodeEditor.tsx -A 5 -B 5 | head -40Repository: OpenLake/canonforces
Length of output: 1348
🏁 Script executed:
# Check if Next.js actually overwrites tsconfig jsx setting or if it respects it
# Look at next.config.js to see any jsx-related configuration
cat next.config.js
# Check for any eslint or other config that might relate to jsx
fd -t f 'eslint\|prettier' . --max-depth 2
cat .eslintrc* 2>/dev/null || cat .eslintignore 2>/dev/null || echo "No eslint config found"
# Verify the actual behavior: check if this is causing any issues by looking at compilation or test results
git log --oneline -20 | grep -i jsx || echo "No jsx-related commits found"Repository: OpenLake/canonforces
Length of output: 775
🌐 Web query:
Next.js 16 tsconfig jsx setting react-jsx vs preserve official documentation
💡 Result:
In Next.js (including v16 docs), the recommended tsconfig.json setting is:
{
"compilerOptions": {
"jsx": "preserve"
}
}Next.js’ official migration guides explicitly tell you to set "jsx": "preserve" for compatibility. [1] [2]
What “react-jsx” vs “preserve” means (TypeScript official docs)
"jsx": "react-jsx": TypeScript transforms JSX into calls that use the automatic JSX runtime (React 17+). [3]"jsx": "preserve": TypeScript does not transform JSX; it leaves JSX in the output for another toolchain step to handle. [3]
TypeScript also notes that JSX typing behavior differs based on jsx mode (e.g., classic vs automatic runtime typing). [4]
Bottom line: If you want the official Next.js config, use jsx: "preserve"; Next’s build pipeline handles the actual JSX transform. [1] [2]
Sources:
[1] Next.js “Migrating from Create React App” (TypeScript config includes jsx: "preserve") (nextjs.org)
[2] Next.js “Migrating from Vite” (Step: set jsx to preserve) (nextjs.org)
[3] TypeScript tsconfig option jsx (react-jsx vs preserve) (typescriptlang.org)
[4] TypeScript Handbook: JSX / how jsx affects typing/runtime expectations (typescriptlang.org)
Citations:
- 1: https://nextjs.org/docs/14/pages/building-your-application/upgrading/from-create-react-app?utm_source=openai
- 2: https://nextjs.org/docs/14/pages/building-your-application/upgrading/from-vite?utm_source=openai
- 3: https://www.typescriptlang.org/tsconfig/jsx.html?utm_source=openai
- 4: https://www.typescriptlang.org/docs/handbook/jsx?utm_source=openai
Change "jsx" to "preserve" to align with Next.js best practices.
Next.js officially recommends "jsx": "preserve" in tsconfig.json—the framework uses its own compiler (SWC) to handle JSX transformation. Although noEmit: true limits the impact on compilation, this setting deviates from Next.js conventions and can cause type-checking inconsistencies. Update it to match the Next.js standard configuration.
🤖 Prompt for AI Agents
In `@tsconfig.json` at line 3, Update the tsconfig.json compilerOptions "jsx"
setting from "react-jsx" to "preserve" to match Next.js recommendations; locate
the "jsx" key under compilerOptions in tsconfig.json and replace its value so
the project uses Next/SWC for JSX transformation.
📦 Pull Request
Submit changes for review
Title: feat: Add "Past Contests" Practice Mode & Secure Solution Submission
📌 Related Issue
✨ Changes Introduced
Frontend Features
Backend API & Security
/api/submit-solution- Handles solution submissions securely using Firebase Admin SDK (prevents client-side permission errors)./api/verify-codeforces- Verifies user submissions against the Codeforces API to prevent cheating./api/past-contests- Fetches historical contest data from Clist API with caching and fallback mechanisms.🤔 Why This Change?
🖼️ Screenshots / Loom Video
Screencast from 2026-02-14 11-23-40.webm
🧪 Testing
📝 Documentation Updates
✅ Checklist
feature/past-contests-submission)🚀 Deployment Notes
FIREBASE_PROJECT_ID,FIREBASE_CLIENT_EMAIL, andFIREBASE_PRIVATE_KEYin .env for the new secure submission API to work. #Summary by CodeRabbit