feat: major UI fixes and layout refinements - #165
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!
|
WalkthroughThis PR restructures UI/layout across multiple components and pages: the Header navigation is reorganized, the Profile page receives an extensive layout and styling overhaul, the POTD and Leaderboard pages are redesigned with new component layouts, Quiz pages gain sidebar content, and tsconfig.json updates JSX compilation settings. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/quiz/battle/[roomId].tsx (1)
183-197:⚠️ Potential issue | 🟠 MajorUnsubscribe
match_foundon effect cleanup.This effect reruns on room changes and reconnects, but the new handler is registered anonymously and never removed. After the first rematch/reconnect, you'll accumulate listeners and can fire duplicate toasts plus multiple
router.push()calls.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/quiz/battle/`[roomId].tsx around lines 183 - 197, The 'match_found' listener added with socket.on in the effect is anonymous and never removed, causing duplicate toasts and router.push calls when the effect reruns; fix it by extracting the handler into a named function (e.g., const onMatchFound = (data) => { ... }) and register it with socket.on('match_found', onMatchFound), then remove it in the effect cleanup via socket.off('match_found', onMatchFound) (or socket.removeListener) so setToasts and router.push are only triggered once per event.
🧹 Nitpick comments (7)
src/common/components/Layout/Layout.module.css (1)
9-28: Consolidate duplicate.mainselectors.There are two
.mainrule blocks (lines 9-16 and 19-28) with overlapping properties (background,padding,min-width). The second block overrides the first for shared properties, making the code confusing and harder to maintain. Consider merging them into a single rule.♻️ Proposed consolidation
.main { - /* margin-left:15rem ; */ + margin-left: 15rem; /* Same as sidebar width */ flex: 1; min-width: 0; background: `#f7f9fb`; padding: 2rem; - /* Add more styles as needed */ -} - - -.main { - margin-left: 15rem; - /* Same as sidebar width */ height: 100vh; overflow-y: auto; - background: `#f7f9fb`; - padding: 2rem; - min-width: 0; box-sizing: border-box; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/common/components/Layout/Layout.module.css` around lines 9 - 28, Merge the two duplicated .main rule blocks into a single .main selector: combine all unique properties (keep margin-left: 15rem, height: 100vh, overflow-y: auto, box-sizing: border-box) and the shared properties (background: `#f7f9fb`, padding: 2rem, min-width: 0, flex: 1 if needed) so no styles are lost, remove the redundant block and any commented-out duplicate rules, and ensure the final .main contains the consolidated, non-conflicting declarations.src/pages/leaderboard.tsx (3)
131-131: Consider moving inline styles to CSS module.The inline
style={{ flex: 1, minWidth: 0 }}could be extracted to the CSS module for consistency with the rest of the layout styling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/leaderboard.tsx` at line 131, Move the inline style on the div (currently written as style={{ flex: 1, minWidth: 0 }}) into the component's CSS module: add a descriptive class (e.g., .flexGrowContainer) to the leaderboard module, define the rules flex: 1 and min-width: 0 there, and replace the inline style with the new className on the div in src/pages/leaderboard.tsx; ensure the CSS module is imported (or reuse the existing module) and update the JSX to use that class to keep styling consistent with other layout styles.
313-318: Usenext/imagefor the mascot image.Replace
<img>with Next.jsImagecomponent for better performance and automatic optimization.♻️ Proposed fix
<div className={styles.sideMascot}> - <img - src="/images/leaderboard2.png" - alt="Leaderboard Mascot" - /> + <Image + src="/images/leaderboard2.png" + alt="Leaderboard Mascot" + width={300} + height={400} + /> </div>Adjust
widthandheightto match your design requirements.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/leaderboard.tsx` around lines 313 - 318, Replace the plain <img> with Next.js' Image component: import Image from 'next/image', then inside the styles.sideMascot div use <Image> with src set to "/images/leaderboard2.png", alt "Leaderboard Mascot", and explicit width and height (or layout/fill with corresponding parent styling) to enable optimization; keep the styles.sideMascot className on the wrapper (or pass className to Image if desired) and ensure the import and Image usage are updated in the leaderboard.tsx component.
197-206: Usenext/imagefor avatar images.The
<img>element should be replaced with Next.jsImagecomponent for automatic image optimization, lazy loading, and better LCP performance.♻️ Proposed fix
Import at top of file (if not already):
import Image from 'next/image';Then replace the img element:
- <img - src={user.photoURL} - alt="avatar" - style={{ - width: "100%", - height: "100%", - borderRadius: "50%", - objectFit: "cover" - }} - /> + <Image + src={user.photoURL} + alt="avatar" + fill + className="rounded-full object-cover" + />Note: When using
fill, the parent container needsposition: relative.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/leaderboard.tsx` around lines 197 - 206, Replace the plain <img> used in the leaderboard render with Next.js Image: import Image from 'next/image' at the top, swap the <img src={user.photoURL} ... /> inside the leaderboard component to an <Image> using either explicit width/height or layout="fill" (if using fill, ensure the avatar container element in the leaderboard JSX has position: relative and a fixed size), preserve alt text and objectFit behavior (cover) via the Image props or container styles, and keep using user.photoURL as the src so images are optimized and lazy-loaded.src/common/components/Header/Header.tsx (1)
1-6: Remove unused imports.
Head(line 1) andNotificationBell(line 5) are imported but not used in the component.🧹 Remove unused imports
-import Head from "next/head"; import Link from "next/link"; import Image from "next/image"; import * as ROUTES from "../../../constants/routes"; -import NotificationBell from "../NotificationBell/NotificationBell"; import styles from "./Header.module.css";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/common/components/Header/Header.tsx` around lines 1 - 6, The Header component currently imports Head and NotificationBell but never uses them; remove the unused imports by deleting the Head import (symbol: Head) and the NotificationBell import (symbol: NotificationBell) from the top of Header.tsx so only used modules (e.g., Link, Image, ROUTES, styles) remain; ensure no other references to those symbols exist in the file and run the build/lint to confirm no unused-import warnings.src/pages/quiz.tsx (1)
173-173: Avoid excessiveas anytype assertions.Multiple occurrences of
(profileUser as any)bypass TypeScript's type safety (lines 173, 177, 182, 189, 190, 198, 211-212, etc.). Consider defining proper types for the user profile data or using type guards.♻️ Suggested approach
Define an interface for the profile user stats:
interface ProfileUserStats { quizzesPlayed?: number; correctAnswers?: number; totalAnswers?: number; streak?: number; }Then use it with proper null checking:
const stats = profileUser as ProfileUserStats | null; const quizzesPlayed = stats?.quizzesPlayed || 0; const correctAnswers = stats?.correctAnswers || 0; // etc.This provides type safety while handling potentially missing fields.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/quiz.tsx` at line 173, The code repeatedly uses the unsafe cast (profileUser as any) which bypasses TypeScript checks; define a proper interface (e.g., ProfileUserStats) for the expected profile shape and replace the casts by typing profileUser accordingly or by creating a local typed variable (e.g., const stats = profileUser as ProfileUserStats | null) and then use null-safe access (stats?.quizzesPlayed || 0, stats?.correctAnswers || 0, etc.) in the JSX where quizzesPlayed, correctAnswers, totalAnswers, streak are read (references: profileUser in quiz.tsx and the span rendering the quizzesPlayed value). Ensure you import/declare the interface near the component and remove the remaining as any usages and add basic null checks or default values.src/components/quiz/StartScreen.tsx (1)
157-167: Missing dependencies inuseEffectmay cause stale closures.The
useEffectat line 157-167 usesisMatchmaking,socket,authUser,activeUser,selectedTopic, andselectedDifficultybut only includes[isConnected]in the dependency array. While the comment indicates this is intentional ("Only trigger on connection state changes"), this can cause the effect to emit stale values whenisConnectedchanges.Consider either adding the missing dependencies or using refs to access current values without triggering re-runs:
♻️ Option 1: Add missing dependencies with guard
useEffect(() => { - if (isMatchmaking && isConnected && socket && authUser) { + if (!isMatchmaking || !isConnected || !socket || !authUser) return; console.log("Socket reconnected/changed while matchmaking. Re-emitting join_queue."); socket.emit('join_queue', { userId: authUser.uid, username: activeUser?.username || 'Guest', topic: selectedTopic, difficulty: selectedDifficulty }); - } - }, [isConnected]); // Only trigger on connection state changes + }, [isConnected, isMatchmaking, socket, authUser, activeUser?.username, selectedTopic, selectedDifficulty]);♻️ Option 2: Use refs for stable access
Store
selectedTopic,selectedDifficulty, etc. in refs if you truly only want to react toisConnectedchanges while using current values.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/quiz/StartScreen.tsx` around lines 157 - 167, The effect in useEffect (which re-emits 'join_queue') reads isMatchmaking, socket, authUser, activeUser, selectedTopic, and selectedDifficulty but only lists isConnected in the dependency array, causing stale closures; fix by either adding the missing dependencies to the dependency array (include isMatchmaking, socket, authUser, activeUser?.username, selectedTopic, selectedDifficulty alongside isConnected) and keep the guard that checks isMatchmaking && isConnected && socket && authUser before emitting, or if you intentionally only want to run on isConnected changes, move the current values into refs (for selectedTopic, selectedDifficulty, activeUser, authUser, socket, isMatchmaking) and read from those refs inside the effect so the effect dependency stays [isConnected] while using up-to-date data for the 'join_queue' emit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/common/components/Header/Header.tsx`:
- Around line 11-23: The logo wrapper div currently uses absolute positioning
("absolute -left-12") which causes clipping on narrow viewports; replace this by
removing the absolute positioning and instead place the Link/Image inline within
the Header's flex container (or use flex utilities and negative margin classes)
so the logo participates in normal flow and scales with parent width;
adjust/remove the hardcoded nav padding ("pl-16") and use gap/margin classes or
responsive utilities (e.g., sm/md prefixes) on the Link/Image or nav to control
spacing across breakpoints; update the Header component (the div containing
Link/Image and the nav) to use flex alignment and responsive spacing rather than
the absolute -left-12 offset.
In `@src/common/components/Profile/Profile.module.css`:
- Around line 29-34: The mobile breakpoint is changing styles on topSection (a
flex column) instead of the grid owner headerRow, so the two-column layout
remains 1fr 340px and can overflow; update the breakpoint rules to target
headerRow (not topSection) and set headerRow's grid-template-columns to a single
column (e.g., 1fr) at the small-screen breakpoint so the two items stack; apply
the same change to the other breakpoint block referenced (lines 599-621) where
the same incorrect selector is used.
- Around line 350-356: The activity panel overflows narrow viewports because
.activityPanel uses width: fit-content and .submissionsList is hard-coded to
480px; change .activityPanel to not use fit-content (use width:100% with a
sensible max-width) and update .submissionsList to use max-width:480px and
width:100% (or similar responsive rules) so the panel shrinks on small screens;
adjust related rules referenced around .activityPanel and .submissionsList (also
mentioned at 405-413) to use max-width/width:100% and box-sizing as needed to
prevent overflow.
In `@src/common/components/Profile/Profile.tsx`:
- Around line 131-138: The previewUrl, imageFile, and uploadError state are not
being cleared when editing ends or when user changes; update the edit lifecycle
to reset them whenever the edit session closes or user prop changes: in
handleEditToggle (when toggling from true to false) call setPreviewUrl(''),
setImageFile(null), and setUploadError(''); also add the same resets inside the
user-effect that runs on [user] (the existing useEffect that calls
setEditForm/setProfilePhotoUrl) and at the end of the successful save handler
(e.g., after onSave/handleSave completes) to ensure no stale preview or pending
upload remains. Ensure you reference and use the existing state setters
previewUrl, imageFile, and uploadError (and keep current behavior for
setProfilePhotoUrl/setEditForm).
- Around line 155-166: The uploadImageToCloudinary function currently calls
.json() on signResponse and uploadResponse without checking response.ok,
allowing error responses to be parsed and uploadData.secure_url to be undefined
and later persisted in handleSave; update uploadImageToCloudinary to check
signResponse.ok and uploadResponse.ok before parsing (throw or return a clear
error if not ok), and after parsing validate that uploadData.secure_url exists
(if missing, throw or return null) so handleSave can skip persisting an
undefined photoURL; reference variables signResponse, uploadResponse, signData,
uploadData and the function uploadImageToCloudinary (and handleSave caller) when
making the changes.
- Around line 145-166: Validate files before accepting or uploading: in
handleImageChange, check e.target.files[0] for MIME type (file.type
startsWith("image/")) and enforce a max size (e.g., <= 5MB) and only then call
setImageFile, revoke previous previewUrl, and setPreviewUrl; in
uploadImageToCloudinary, re-validate the File argument's type and size before
creating FormData and calling the sign/upload endpoints; also add corresponding
server-side validation in the sign-cloudinary-upload handler (re-check
content-type/size and reject with a 4xx and descriptive message if invalid) so
invalid or oversized files are blocked both client- and server-side.
In `@src/pages/index.tsx`:
- Around line 45-55: The two CTA buttons (the "Explore" button using
styles.button_blue and the "Dashboard" button that includes BsArrowRightCircle)
must perform navigation instead of being inert buttons: replace each <button>
with a navigational element (e.g., Next.js Link or an anchor with href) or
attach an onClick that calls router.push to the appropriate route (e.g.,
/explore and /dashboard), preserving the existing className and children for
styling and keeping the icon inside the interactive element; apply accessible
attributes (aria-label) as needed. Also make the same change for the similar
CTAs further down (the other pair around the Dashboard/Explore block) so both
hero and footer CTAs navigate.
- Around line 52-55: The button's icon animation never triggers because the icon
uses group-hover but the button lacks the group class; update the button element
(the JSX button that renders "Dashboard" and the BsArrowRightCircle component)
to include the "group" class in its className so the group-hover:translate-x-1
on BsArrowRightCircle will activate on button hover.
In `@src/pages/potd.tsx`:
- Around line 289-291: The formatDescription output is not sanitized before
being passed to dangerouslySetInnerHTML; update src/utils/formatDescription.tsx
(the formatDescription function) to sanitize user-provided HTML—either integrate
a vetted library like DOMPurify to clean the generated HTML string or perform
proper HTML-entity escaping before applying your regex transforms—so that any
tags/attributes (e.g., <img onerror=...>) are neutralized and the string
returned by formatDescription is safe to inject.
- Around line 136-159: The userSolved flag can be set true by fallback entries
because the current some() check treats fallback rows as real solves; update the
logic that sets userSolved (where setUserSolved(...) is called) to only consider
solver entries that are not fallbacks (i.e., s.uid === auth.currentUser?.uid &&
!s.isFallback), ensuring real solver objects from solversArray (created earlier)
remain valid even if they don't have isFallback set.
In `@src/pages/quiz.tsx`:
- Around line 125-127: Remove the dead React effect: delete the empty
useEffect([...]) block in src/pages/quiz.tsx (the useEffect call that has an
empty function body and dependency [user]) since it does nothing; ensure no
other logic depends on that hook and run tests/linting after removal.
In `@src/styles/Quiz.module.css`:
- Around line 857-869: The later shared rule is overriding .start-button-large's
intended flex: 1.5 by resetting it to flex: 1; update the later/shared selector
so it no longer forces flex: 1 (either remove the flex declaration from the
shared rule, scope that shared rule to exclude .start-button-large, or make the
.start-button-large rule more specific so it preserves flex: 1.5). Target the
.start-button-large declaration and the shared selector that sets flex: 1 (the
duplicate rule seen after .start-button-large) and adjust one of them so the
primary action keeps flex: 1.5.
---
Outside diff comments:
In `@src/pages/quiz/battle/`[roomId].tsx:
- Around line 183-197: The 'match_found' listener added with socket.on in the
effect is anonymous and never removed, causing duplicate toasts and router.push
calls when the effect reruns; fix it by extracting the handler into a named
function (e.g., const onMatchFound = (data) => { ... }) and register it with
socket.on('match_found', onMatchFound), then remove it in the effect cleanup via
socket.off('match_found', onMatchFound) (or socket.removeListener) so setToasts
and router.push are only triggered once per event.
---
Nitpick comments:
In `@src/common/components/Header/Header.tsx`:
- Around line 1-6: The Header component currently imports Head and
NotificationBell but never uses them; remove the unused imports by deleting the
Head import (symbol: Head) and the NotificationBell import (symbol:
NotificationBell) from the top of Header.tsx so only used modules (e.g., Link,
Image, ROUTES, styles) remain; ensure no other references to those symbols exist
in the file and run the build/lint to confirm no unused-import warnings.
In `@src/common/components/Layout/Layout.module.css`:
- Around line 9-28: Merge the two duplicated .main rule blocks into a single
.main selector: combine all unique properties (keep margin-left: 15rem, height:
100vh, overflow-y: auto, box-sizing: border-box) and the shared properties
(background: `#f7f9fb`, padding: 2rem, min-width: 0, flex: 1 if needed) so no
styles are lost, remove the redundant block and any commented-out duplicate
rules, and ensure the final .main contains the consolidated, non-conflicting
declarations.
In `@src/components/quiz/StartScreen.tsx`:
- Around line 157-167: The effect in useEffect (which re-emits 'join_queue')
reads isMatchmaking, socket, authUser, activeUser, selectedTopic, and
selectedDifficulty but only lists isConnected in the dependency array, causing
stale closures; fix by either adding the missing dependencies to the dependency
array (include isMatchmaking, socket, authUser, activeUser?.username,
selectedTopic, selectedDifficulty alongside isConnected) and keep the guard that
checks isMatchmaking && isConnected && socket && authUser before emitting, or if
you intentionally only want to run on isConnected changes, move the current
values into refs (for selectedTopic, selectedDifficulty, activeUser, authUser,
socket, isMatchmaking) and read from those refs inside the effect so the effect
dependency stays [isConnected] while using up-to-date data for the 'join_queue'
emit.
In `@src/pages/leaderboard.tsx`:
- Line 131: Move the inline style on the div (currently written as style={{
flex: 1, minWidth: 0 }}) into the component's CSS module: add a descriptive
class (e.g., .flexGrowContainer) to the leaderboard module, define the rules
flex: 1 and min-width: 0 there, and replace the inline style with the new
className on the div in src/pages/leaderboard.tsx; ensure the CSS module is
imported (or reuse the existing module) and update the JSX to use that class to
keep styling consistent with other layout styles.
- Around line 313-318: Replace the plain <img> with Next.js' Image component:
import Image from 'next/image', then inside the styles.sideMascot div use
<Image> with src set to "/images/leaderboard2.png", alt "Leaderboard Mascot",
and explicit width and height (or layout/fill with corresponding parent styling)
to enable optimization; keep the styles.sideMascot className on the wrapper (or
pass className to Image if desired) and ensure the import and Image usage are
updated in the leaderboard.tsx component.
- Around line 197-206: Replace the plain <img> used in the leaderboard render
with Next.js Image: import Image from 'next/image' at the top, swap the <img
src={user.photoURL} ... /> inside the leaderboard component to an <Image> using
either explicit width/height or layout="fill" (if using fill, ensure the avatar
container element in the leaderboard JSX has position: relative and a fixed
size), preserve alt text and objectFit behavior (cover) via the Image props or
container styles, and keep using user.photoURL as the src so images are
optimized and lazy-loaded.
In `@src/pages/quiz.tsx`:
- Line 173: The code repeatedly uses the unsafe cast (profileUser as any) which
bypasses TypeScript checks; define a proper interface (e.g., ProfileUserStats)
for the expected profile shape and replace the casts by typing profileUser
accordingly or by creating a local typed variable (e.g., const stats =
profileUser as ProfileUserStats | null) and then use null-safe access
(stats?.quizzesPlayed || 0, stats?.correctAnswers || 0, etc.) in the JSX where
quizzesPlayed, correctAnswers, totalAnswers, streak are read (references:
profileUser in quiz.tsx and the span rendering the quizzesPlayed value). Ensure
you import/declare the interface near the component and remove the remaining as
any usages and add basic null checks or default values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dc85b81c-d09d-464c-95d4-cb199952b482
⛔ Files ignored due to path filters (3)
public/images/foldHands.pngis excluded by!**/*.pngpublic/images/leaderboard.pngis excluded by!**/*.pngpublic/images/leaderboard2.pngis excluded by!**/*.png
📒 Files selected for processing (16)
src/common/components/Header/Header.tsxsrc/common/components/Layout/Layout.module.csssrc/common/components/NavigationMenu/NavigationMenu.module.csssrc/common/components/Profile/Profile.module.csssrc/common/components/Profile/Profile.tsxsrc/components/quiz/StartScreen.tsxsrc/pages/index.tsxsrc/pages/leaderboard.tsxsrc/pages/potd.tsxsrc/pages/quiz.tsxsrc/pages/quiz/battle/[roomId].tsxsrc/services/quizClient.tssrc/styles/Leaderboard.module.csssrc/styles/POTDpage.module.csssrc/styles/Quiz.module.csstsconfig.json
| <div className={`relative flex w-10/12 p-5 fc-black font-light max-w-screen-2xl mx-auto items-center`}> | ||
| <div className="absolute -left-12"> | ||
| <Link href={ROUTES.HOME} className="flex items-center gap-3"> | ||
| <Image | ||
| src="/images/logo.png" | ||
| alt="CanonForces Logo" | ||
| width={65} | ||
| height={65} | ||
| className="object-contain transition-transform duration-300 hover:scale-110" | ||
| priority | ||
| /> | ||
| </Link> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there are responsive styles or media queries for the Header
fd -t f 'Header.module.css' --exec cat {}
# Check for any Tailwind responsive utilities used in the header
rg -n 'className.*Header' src/common/components/Header/Repository: OpenLake/canonforces
Length of output: 99
🏁 Script executed:
cat -n src/common/components/Header/Header.tsxRepository: OpenLake/canonforces
Length of output: 1925
🏁 Script executed:
rg -n 'absolute|left-|responsive|md:|sm:|lg:' src/common/components/Header/ -A 2 -B 2Repository: OpenLake/canonforces
Length of output: 530
🏁 Script executed:
fd -t f '\.(css|module\.css)' src/common/components/Header/ --exec cat -n {}Repository: OpenLake/canonforces
Length of output: 120
🏁 Script executed:
fd -t f 'tailwind.config' --exec cat {}Repository: OpenLake/canonforces
Length of output: 728
🏁 Script executed:
fd -t f 'global.css' -o 'globals.css' -o 'styles.css' --exec cat -n {}Repository: OpenLake/canonforces
Length of output: 205
🏁 Script executed:
fd -t f '\.css$' src/ --exec rg -l 'media\s*query|@media' {}Repository: OpenLake/canonforces
Length of output: 786
🏁 Script executed:
rg -n '@media|media.*query' src/ --type css --type tsxRepository: OpenLake/canonforces
Length of output: 91
🏁 Script executed:
rg -n '@media' src/common/components/Header/ --type cssRepository: OpenLake/canonforces
Length of output: 46
🏁 Script executed:
cat -n src/common/components/Header/Header.module.cssRepository: OpenLake/canonforces
Length of output: 120
🏁 Script executed:
rg -n 'responsive|sm:|md:|lg:|xl:' src/common/components/Header/Header.tsxRepository: OpenLake/canonforces
Length of output: 46
Consider removing absolute positioning or adding responsive breakpoints.
The logo uses absolute -left-12 without responsive utilities. On narrower viewports, this fixed -48px offset combined with w-10/12 parent width could cause the logo to be clipped. The hardcoded pl-16 on the nav element is a workaround that breaks if spacing changes.
Use flexbox-based positioning (e.g., negative margins or gap adjustments) or add responsive breakpoints to adjust the offset for mobile viewports.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/common/components/Header/Header.tsx` around lines 11 - 23, The logo
wrapper div currently uses absolute positioning ("absolute -left-12") which
causes clipping on narrow viewports; replace this by removing the absolute
positioning and instead place the Link/Image inline within the Header's flex
container (or use flex utilities and negative margin classes) so the logo
participates in normal flow and scales with parent width; adjust/remove the
hardcoded nav padding ("pl-16") and use gap/margin classes or responsive
utilities (e.g., sm/md prefixes) on the Link/Image or nav to control spacing
across breakpoints; update the Header component (the div containing Link/Image
and the nav) to use flex alignment and responsive spacing rather than the
absolute -left-12 offset.
| .headerRow { | ||
| display: grid; | ||
| grid-template-columns: 1fr 340px; | ||
| gap: 1.5rem; | ||
| align-items: start; | ||
| } |
There was a problem hiding this comment.
The mobile breakpoint is targeting the wrong container.
headerRow owns the two-column grid, but the breakpoint rewrites topSection, which is a flex column. On small screens the header stays 1fr 340px and can overflow instead of stacking.
Suggested fix
`@media` (max-width: 900px) {
- .topSection {
- grid-template-columns: 1fr;
- }
+ .headerRow {
+ grid-template-columns: 1fr;
+ }
.profileCard,
.miniCfModule {
grid-column: span 1;
}Also applies to: 599-621
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/common/components/Profile/Profile.module.css` around lines 29 - 34, The
mobile breakpoint is changing styles on topSection (a flex column) instead of
the grid owner headerRow, so the two-column layout remains 1fr 340px and can
overflow; update the breakpoint rules to target headerRow (not topSection) and
set headerRow's grid-template-columns to a single column (e.g., 1fr) at the
small-screen breakpoint so the two items stack; apply the same change to the
other breakpoint block referenced (lines 599-621) where the same incorrect
selector is used.
| .activityPanel { | ||
| background: white; | ||
| border-radius: 28px; | ||
| padding: 1.25rem 1.5rem; | ||
| border: 1px solid #f1f5f9; | ||
| width: fit-content; | ||
| } |
There was a problem hiding this comment.
The activity panel still overflows narrow viewports.
.submissionsList is hard-coded to 480px, and .activityPanel sizes to its content. After the layout stacks to one column, this panel can still be wider than the phone.
Suggested fix
.activityPanel {
background: white;
border-radius: 28px;
padding: 1.25rem 1.5rem;
border: 1px solid `#f1f5f9`;
- width: fit-content;
+ width: 100%;
+ min-width: 0;
}
@@
.submissionsList {
display: flex;
flex-direction: column;
gap: 0.6rem;
- width: 480px;
+ width: min(480px, 100%);
+ max-width: 100%;
max-height: 300px;
overflow-y: auto;
padding-right: 0.5rem;
}Also applies to: 405-413
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/common/components/Profile/Profile.module.css` around lines 350 - 356, The
activity panel overflows narrow viewports because .activityPanel uses width:
fit-content and .submissionsList is hard-coded to 480px; change .activityPanel
to not use fit-content (use width:100% with a sensible max-width) and update
.submissionsList to use max-width:480px and width:100% (or similar responsive
rules) so the panel shrinks on small screens; adjust related rules referenced
around .activityPanel and .submissionsList (also mentioned at 405-413) to use
max-width/width:100% and box-sizing as needed to prevent overflow.
| useEffect(() => { | ||
| if (user) { | ||
| setEditForm({ | ||
| fullname: user.fullname || '', | ||
| email: user.email || '', | ||
| }); | ||
| setPreviewUrl(null); | ||
| setImageFile(null); | ||
| setUploadError(null); | ||
| setEditForm({ fullname: user.fullname || '', email: user.email || '' }); | ||
| setProfilePhotoUrl(user.photoURL || ""); | ||
| } | ||
| }, [user]); | ||
|
|
||
| const handleEditToggle = () => { | ||
| setIsEditing(!isEditing); | ||
| if (!isEditing && user) { | ||
| setEditForm({ | ||
| fullname: user.fullname || '', | ||
| email: user.email || '', | ||
| }); | ||
| setImageFile(null); | ||
| setPreviewUrl(null); | ||
| setUploadError(null); | ||
| } | ||
| }; | ||
| const handleEditToggle = () => setIsEditing(!isEditing); |
There was a problem hiding this comment.
Clear preview/upload state when the edit session ends.
previewUrl, imageFile, and uploadError now survive modal close, user changes, and successful saves. That leaves stale previews on the wrong profile and can silently re-upload an old file on the next save.
Also applies to: 145-150, 169-178
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/common/components/Profile/Profile.tsx` around lines 131 - 138, The
previewUrl, imageFile, and uploadError state are not being cleared when editing
ends or when user changes; update the edit lifecycle to reset them whenever the
edit session closes or user prop changes: in handleEditToggle (when toggling
from true to false) call setPreviewUrl(''), setImageFile(null), and
setUploadError(''); also add the same resets inside the user-effect that runs on
[user] (the existing useEffect that calls setEditForm/setProfilePhotoUrl) and at
the end of the successful save handler (e.g., after onSave/handleSave completes)
to ensure no stale preview or pending upload remains. Ensure you reference and
use the existing state setters previewUrl, imageFile, and uploadError (and keep
current behavior for setProfilePhotoUrl/setEditForm).
| const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = e.target.files?.[0]; | ||
| if (file) { | ||
| if (file.size > 5 * 1024 * 1024) { | ||
| setUploadError('File size must be less than 5MB'); | ||
| return; | ||
| } | ||
|
|
||
| const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; | ||
| if (!validTypes.includes(file.type)) { | ||
| setUploadError('Please select a valid image file (JPEG, PNG, or WebP)'); | ||
| return; | ||
| } | ||
|
|
||
| setImageFile(file); | ||
| setUploadError(null); | ||
|
|
||
| if (previewUrl) { | ||
| URL.revokeObjectURL(previewUrl); | ||
| } | ||
| if (previewUrl) URL.revokeObjectURL(previewUrl); | ||
| setPreviewUrl(URL.createObjectURL(file)); | ||
| } | ||
| }; | ||
|
|
||
| const uploadImageToCloudinary = async (file: File): Promise<string> => { | ||
| try { | ||
| const signResponse = await fetch('/api/sign-cloudinary-upload', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
|
|
||
| if (!signResponse.ok) { | ||
| throw new Error('Failed to get upload signature'); | ||
| } | ||
|
|
||
| const signData = await signResponse.json(); | ||
|
|
||
| const formData = new FormData(); | ||
| formData.append("file", file); | ||
| formData.append("upload_preset", signData.upload_preset); | ||
|
|
||
| const uploadUrl = `https://api.cloudinary.com/v1_1/${signData.cloud_name}/image/upload`; | ||
| const uploadResponse = await fetch(uploadUrl, { | ||
| method: "POST", | ||
| body: formData, | ||
| }); | ||
|
|
||
| const raw = await uploadResponse.text(); | ||
| console.log("Cloudinary raw response:", raw); | ||
|
|
||
| if (!uploadResponse.ok) { | ||
| throw new Error("Failed to upload image: " + raw); | ||
| } | ||
|
|
||
| const uploadData = JSON.parse(raw); | ||
| return uploadData.secure_url; | ||
|
|
||
| } catch (error) { | ||
| console.error("Error uploading to Cloudinary:", error); | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| const getProgressToNextRank = (rating: number | undefined, rank: string | undefined) => { | ||
| if (!rating || !rank) return 0; | ||
|
|
||
| const normalizedRank = rank.toLowerCase(); | ||
| const nextThreshold = rankThresholds[normalizedRank]; | ||
| if (!nextThreshold) return 0; | ||
|
|
||
| // Get previous threshold | ||
| const ranks = Object.keys(rankThresholds); | ||
| const idx = ranks.indexOf(normalizedRank); | ||
| const prevThreshold = idx > 0 ? rankThresholds[ranks[idx - 1]] : 0; | ||
|
|
||
| const progress = ((rating - prevThreshold) / (nextThreshold - prevThreshold)) * 100; | ||
| return Math.min(Math.max(Math.round(progress), 0), 100); | ||
| const signResponse = await fetch('/api/sign-cloudinary-upload', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| const signData = await signResponse.json(); | ||
| const formData = new FormData(); | ||
| formData.append("file", file); | ||
| formData.append("upload_preset", signData.upload_preset); | ||
| const uploadUrl = `https://api.cloudinary.com/v1_1/${signData.cloud_name}/image/upload`; | ||
| const uploadResponse = await fetch(uploadUrl, { method: "POST", body: formData }); | ||
| const uploadData = await uploadResponse.json(); | ||
| return uploadData.secure_url; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "Profile.tsx" -type f | head -20Repository: OpenLake/canonforces
Length of output: 107
🏁 Script executed:
find . -name "sign-cloudinary-upload.ts" -type f | head -20Repository: OpenLake/canonforces
Length of output: 105
🏁 Script executed:
wc -l ./src/common/components/Profile/Profile.tsxRepository: OpenLake/canonforces
Length of output: 111
🏁 Script executed:
wc -l ./src/pages/api/sign-cloudinary-upload.tsRepository: OpenLake/canonforces
Length of output: 108
🏁 Script executed:
sed -n '145,166p' ./src/common/components/Profile/Profile.tsxRepository: OpenLake/canonforces
Length of output: 1010
🏁 Script executed:
cat -n ./src/pages/api/sign-cloudinary-upload.tsRepository: OpenLake/canonforces
Length of output: 1060
🏁 Script executed:
# Search for file input element in Profile.tsx
grep -n "input" ./src/common/components/Profile/Profile.tsx | head -20Repository: OpenLake/canonforces
Length of output: 372
🏁 Script executed:
# Look for accept attribute or validation patterns
grep -n "accept\|validate\|size\|File\|image" ./src/common/components/Profile/Profile.tsx | head -30Repository: OpenLake/canonforces
Length of output: 731
🏁 Script executed:
sed -n '170,180p' ./src/common/components/Profile/Profile.tsxRepository: OpenLake/canonforces
Length of output: 474
🏁 Script executed:
# Check broader context around the upload
sed -n '140,185p' ./src/common/components/Profile/Profile.tsxRepository: OpenLake/canonforces
Length of output: 1713
Reinstate file validation before starting the Cloudinary upload.
handleImageChange() accepts any file without validation, and src/pages/api/sign-cloudinary-upload.ts has no server-side validation either. The accept="image/*" attribute is a client-side picker hint only and can be bypassed, leaving no guard against oversized or non-image file uploads.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/common/components/Profile/Profile.tsx` around lines 145 - 166, Validate
files before accepting or uploading: in handleImageChange, check
e.target.files[0] for MIME type (file.type startsWith("image/")) and enforce a
max size (e.g., <= 5MB) and only then call setImageFile, revoke previous
previewUrl, and setPreviewUrl; in uploadImageToCloudinary, re-validate the File
argument's type and size before creating FormData and calling the sign/upload
endpoints; also add corresponding server-side validation in the
sign-cloudinary-upload handler (re-check content-type/size and reject with a 4xx
and descriptive message if invalid) so invalid or oversized files are blocked
both client- and server-side.
| <button className="bg-white hover:bg-gray-50 text-gray-900 border-gray-100 font-bold px-8 py-3 rounded-xl shadow-sm transition-all duration-200 hover:scale-105 flex items-center justify-center gap-3 w-1/2"> | ||
| Dashboard | ||
| <BsArrowRightCircle size={"1.3em"} className="transition-transform duration-200 hover:translate-x-1" /> | ||
| <BsArrowRightCircle size={"1.3em"} className="transition-transform duration-200 group-hover:translate-x-1" /> | ||
| </button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the file and see the context around lines 52-55
cat -n src/pages/index.tsx | head -70 | tail -40Repository: OpenLake/canonforces
Length of output: 2527
🏁 Script executed:
# Get broader context around lines 52-55
cat -n src/pages/index.tsx | head -80 | tail -50Repository: OpenLake/canonforces
Length of output: 3163
Add group class to the button to activate the icon hover animation.
The group-hover:translate-x-1 class on the arrow icon only activates when a group-classed ancestor is hovered. The button is currently missing this class, so the animation never triggers.
Suggested fix
- <button className="bg-white hover:bg-gray-50 text-gray-900 border-gray-100 font-bold px-8 py-3 rounded-xl shadow-sm transition-all duration-200 hover:scale-105 flex items-center justify-center gap-3 w-1/2">
+ <button className="group bg-white hover:bg-gray-50 text-gray-900 border-gray-100 font-bold px-8 py-3 rounded-xl shadow-sm transition-all duration-200 hover:scale-105 flex items-center justify-center gap-3 w-1/2">📝 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.
| <button className="bg-white hover:bg-gray-50 text-gray-900 border-gray-100 font-bold px-8 py-3 rounded-xl shadow-sm transition-all duration-200 hover:scale-105 flex items-center justify-center gap-3 w-1/2"> | |
| Dashboard | |
| <BsArrowRightCircle size={"1.3em"} className="transition-transform duration-200 hover:translate-x-1" /> | |
| <BsArrowRightCircle size={"1.3em"} className="transition-transform duration-200 group-hover:translate-x-1" /> | |
| </button> | |
| <button className="group bg-white hover:bg-gray-50 text-gray-900 border-gray-100 font-bold px-8 py-3 rounded-xl shadow-sm transition-all duration-200 hover:scale-105 flex items-center justify-center gap-3 w-1/2"> | |
| Dashboard | |
| <BsArrowRightCircle size={"1.3em"} className="transition-transform duration-200 group-hover:translate-x-1" /> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/index.tsx` around lines 52 - 55, The button's icon animation never
triggers because the icon uses group-hover but the button lacks the group class;
update the button element (the JSX button that renders "Dashboard" and the
BsArrowRightCircle component) to include the "group" class in its className so
the group-hover:translate-x-1 on BsArrowRightCircle will activate on button
hover.
| if (solversArray.length < 3) { | ||
| // Fallback: fetch additional users to fill until at least 3 | ||
| const fallbackQ = query(collection(db, "users"), limit(10)); | ||
| const fallbackSnap = await getDocs(fallbackQ); | ||
| const fallbackUsers = fallbackSnap.docs | ||
| .map((d: any) => ({ uid: d.id, username: d.data().username || "User" })) | ||
| .filter((u: any) => !solversArray.some((s: Solver) => s.uid === u.uid)); | ||
|
|
||
| while (solversArray.length < 3 && fallbackUsers.length > 0) { | ||
| const nextUser = fallbackUsers.shift(); | ||
| if (nextUser) { | ||
| solversArray.push({ | ||
| uid: nextUser.uid, | ||
| username: nextUser.username, | ||
| solvedAt: new Date().toISOString(), // Placeholder time for fallback | ||
| isFallback: true | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| solversArray.sort((a, b) => new Date(a.solvedAt).getTime() - new Date(b.solvedAt).getTime()); | ||
| setDailySolvers(solversArray); | ||
| setUserSolved(auth.currentUser ? solversArray.some((s) => s.uid === auth.currentUser?.uid) : false); |
There was a problem hiding this comment.
Ignore fallback rows when deriving userSolved.
The fallback branch can append the logged-in user with isFallback: true, and the current some() check still treats that as a real solve. That will disable verification and block rewards for an unsolved POTD.
Suggested fix
- const fallbackUsers = fallbackSnap.docs
- .map((d: any) => ({ uid: d.id, username: d.data().username || "User" }))
- .filter((u: any) => !solversArray.some((s: Solver) => s.uid === u.uid));
+ const fallbackUsers = fallbackSnap.docs
+ .map((d: any) => ({ uid: d.id, username: d.data().username || "User" }))
+ .filter((u: any) =>
+ u.uid !== auth.currentUser?.uid &&
+ !solversArray.some((s: Solver) => s.uid === u.uid)
+ );
@@
- setUserSolved(auth.currentUser ? solversArray.some((s) => s.uid === auth.currentUser?.uid) : false);
+ setUserSolved(
+ auth.currentUser
+ ? solversArray.some((s) => !s.isFallback && s.uid === auth.currentUser.uid)
+ : false
+ );📝 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.
| if (solversArray.length < 3) { | |
| // Fallback: fetch additional users to fill until at least 3 | |
| const fallbackQ = query(collection(db, "users"), limit(10)); | |
| const fallbackSnap = await getDocs(fallbackQ); | |
| const fallbackUsers = fallbackSnap.docs | |
| .map((d: any) => ({ uid: d.id, username: d.data().username || "User" })) | |
| .filter((u: any) => !solversArray.some((s: Solver) => s.uid === u.uid)); | |
| while (solversArray.length < 3 && fallbackUsers.length > 0) { | |
| const nextUser = fallbackUsers.shift(); | |
| if (nextUser) { | |
| solversArray.push({ | |
| uid: nextUser.uid, | |
| username: nextUser.username, | |
| solvedAt: new Date().toISOString(), // Placeholder time for fallback | |
| isFallback: true | |
| }); | |
| } | |
| } | |
| } | |
| solversArray.sort((a, b) => new Date(a.solvedAt).getTime() - new Date(b.solvedAt).getTime()); | |
| setDailySolvers(solversArray); | |
| setUserSolved(auth.currentUser ? solversArray.some((s) => s.uid === auth.currentUser?.uid) : false); | |
| if (solversArray.length < 3) { | |
| // Fallback: fetch additional users to fill until at least 3 | |
| const fallbackQ = query(collection(db, "users"), limit(10)); | |
| const fallbackSnap = await getDocs(fallbackQ); | |
| const fallbackUsers = fallbackSnap.docs | |
| .map((d: any) => ({ uid: d.id, username: d.data().username || "User" })) | |
| .filter((u: any) => | |
| u.uid !== auth.currentUser?.uid && | |
| !solversArray.some((s: Solver) => s.uid === u.uid) | |
| ); | |
| while (solversArray.length < 3 && fallbackUsers.length > 0) { | |
| const nextUser = fallbackUsers.shift(); | |
| if (nextUser) { | |
| solversArray.push({ | |
| uid: nextUser.uid, | |
| username: nextUser.username, | |
| solvedAt: new Date().toISOString(), // Placeholder time for fallback | |
| isFallback: true | |
| }); | |
| } | |
| } | |
| } | |
| solversArray.sort((a, b) => new Date(a.solvedAt).getTime() - new Date(b.solvedAt).getTime()); | |
| setDailySolvers(solversArray); | |
| setUserSolved( | |
| auth.currentUser | |
| ? solversArray.some((s) => !s.isFallback && s.uid === auth.currentUser.uid) | |
| : false | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/potd.tsx` around lines 136 - 159, The userSolved flag can be set
true by fallback entries because the current some() check treats fallback rows
as real solves; update the logic that sets userSolved (where setUserSolved(...)
is called) to only consider solver entries that are not fallbacks (i.e., s.uid
=== auth.currentUser?.uid && !s.isFallback), ensuring real solver objects from
solversArray (created earlier) remain valid even if they don't have isFallback
set.
| <div | ||
| className={styles.problemDescription} | ||
| dangerouslySetInnerHTML={{ __html: formatDescription(fullDesc) }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd 'formatDescription' src -x sh -c 'echo "=== {} ==="; sed -n "1,220p" "{}"'
rg -n "DOMPurify|sanitize|sanitiz|dangerouslySetInnerHTML" srcRepository: OpenLake/canonforces
Length of output: 2257
Ensure formatDescription() sanitizes HTML before rendering with dangerouslySetInnerHTML.
The function in src/utils/formatDescription.tsx does not sanitize HTML. It only applies regex-based markdown transformations (bold, code, newlines, bullets) without escaping user input. Arbitrary HTML and attributes not matched by the regex patterns pass through unchanged to dangerouslySetInnerHTML. A crafted Firestore description containing tags like <img src=x onerror="alert('XSS')"> would execute in viewers' browsers.
Use a library like DOMPurify or implement proper HTML entity escaping to sanitize user-controlled content before rendering.
🧰 Tools
🪛 ast-grep (0.42.1)
[warning] 290-290: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/potd.tsx` around lines 289 - 291, The formatDescription output is
not sanitized before being passed to dangerouslySetInnerHTML; update
src/utils/formatDescription.tsx (the formatDescription function) to sanitize
user-provided HTML—either integrate a vetted library like DOMPurify to clean the
generated HTML string or perform proper HTML-entity escaping before applying
your regex transforms—so that any tags/attributes (e.g., <img onerror=...>) are
neutralized and the string returned by formatDescription is safe to inject.
| useEffect(() => { | ||
| }, [user]); | ||
|
|
There was a problem hiding this comment.
Remove empty useEffect.
This useEffect has an empty body and serves no purpose. It appears to be leftover from development or debugging.
🧹 Remove dead code
- useEffect(() => {
- }, [user]);
-📝 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.
| useEffect(() => { | |
| }, [user]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/quiz.tsx` around lines 125 - 127, Remove the dead React effect:
delete the empty useEffect([...]) block in src/pages/quiz.tsx (the useEffect
call that has an empty function body and dependency [user]) since it does
nothing; ensure no other logic depends on that hook and run tests/linting after
removal.
| .start-button-large { | ||
| width: 100%; | ||
| padding: 1rem; | ||
| background-image: linear-gradient(to right, #007bff, #0056b3); | ||
| flex: 1.5; | ||
| padding: 1rem 2rem; | ||
| background-color: #007bff; | ||
| color: white; | ||
| border: none; | ||
| border-radius: 12px; | ||
| font-size: 1.2rem; | ||
| font-weight: 700; | ||
| cursor: pointer; | ||
| transition: all 0.3s ease; | ||
| box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3); | ||
| transition: all 0.2s ease-in-out; | ||
| box-shadow: 0 4px 15px rgba(0, 123, 255, 0.25); | ||
| } |
There was a problem hiding this comment.
This shared rule wipes out the larger flex ratio above.
.start-button-large is set to flex: 1.5 earlier, but this later selector resets it to flex: 1. The primary action ends up the same width as the secondary buttons.
Suggested fix
-.start-button-large, .start-button-secondary {
- flex: 1;
- min-width: 200px;
-}
+.start-button-large {
+ min-width: 200px;
+}
+
+.start-button-secondary {
+ flex: 1;
+ min-width: 200px;
+}Also applies to: 1029-1032
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/styles/Quiz.module.css` around lines 857 - 869, The later shared rule is
overriding .start-button-large's intended flex: 1.5 by resetting it to flex: 1;
update the later/shared selector so it no longer forces flex: 1 (either remove
the flex declaration from the shared rule, scope that shared rule to exclude
.start-button-large, or make the .start-button-large rule more specific so it
preserves flex: 1.5). Target the .start-button-large declaration and the shared
selector that sets flex: 1 (the duplicate rule seen after .start-button-large)
and adjust one of them so the primary action keeps flex: 1.5.
aviralsaxena16
left a comment
There was a problem hiding this comment.
@hansikareddy29 The UI updates looks great , I am merging the changes
Major UI changes to
Summary by CodeRabbit
Release Notes
New Features
Improvements