diff --git a/QUIZ_REDESIGN_PROMPT_GUIDE (1).md b/QUIZ_REDESIGN_PROMPT_GUIDE (1).md new file mode 100644 index 0000000..76578c3 --- /dev/null +++ b/QUIZ_REDESIGN_PROMPT_GUIDE (1).md @@ -0,0 +1,1489 @@ +# QUIZ SYSTEM — UI REDESIGN PROMPT GUIDE + +> **Version:** 2.0 | **Style:** Refined Minimal × Academic Prestige +> **Purpose:** Step-by-step UI redesign prompts for AI or human UI developers. +> **Scope:** Frontend only — zero changes to backend, API, database, or business logic. + +--- + +## HOW TO USE THIS GUIDE + +Follow each **Phase** in order. Do not skip phases. + +Each phase contains a **prompt block** you paste directly to your UI developer or AI tool. Every phase targets one isolated UI section to avoid breaking other parts. After each phase, test all existing functionality before moving to the next. + +This guide only modifies: CSS, fonts, colors, layout, component visuals, animations, and icon choices. + +This guide never modifies: API calls, data bindings, route logic, state management, backend endpoints. + +--- + +## DESIGN SYSTEM FOUNDATION + +> Read this section fully before starting Phase 0. All phases reference these tokens. + +### Style Identity + +**Name:** `Aurum` — Refined Minimal with Academic Prestige energy +**Vibe:** Professional, clean, prestigious. Think Quizlet meets a top-tier academic institution. +Confident but not loud. Elegant but not cold. Interactive but not chaotic. + +**Design Philosophy:** +- Whitespace is a design element, not empty space +- Every color carries intent: Maroon = authority, Gold = achievement, Black = precision, White = clarity +- Interactions feel satisfying and polished, not flashy +- Components breathe — generous padding, clean edges, clear hierarchy + +--- + +### Typography System + +**Font Stack:** + +``` +DISPLAY FONT: "DM Serif Display" (Google Fonts) → hero headings, result screens, big score numbers +HEADING FONT: "Outfit" (Google Fonts) → question text, section titles, nav links, card headings +BODY FONT: "DM Sans" (Google Fonts) → answer options, descriptions, instructions, labels +MONO FONT: "JetBrains Mono" (Google Fonts) → timer digits, score counters, room codes +``` + +**Google Fonts import (add to global CSS or ``):** + +```html + + + +``` + +**Font Usage Rules:** + +- `DM Serif Display` — Use at 48px+ only. Never use for body, labels, or interactive elements. +- `Outfit` — Primary heading font. 32px section titles, 22px card titles, 18px nav and buttons. Weight 600–700. +- `DM Sans` — Body copy and answer text. 16px default, 14px captions. Weight 400 regular, 500 medium, 700 bold. +- `JetBrains Mono` — Always for numeric displays: timers, scores, room codes. Use letter-spacing: 0.05em. + +--- + +### Color Palette + +```css +:root { + /* === PRIMARY PALETTE === */ + --color-maroon: #800020; /* Deep Maroon — primary brand, headers, key accents */ + --color-maroon-dark: #5C0016; /* Darker Maroon — hover states, pressed states */ + --color-maroon-light: #A8002A; /* Lighter Maroon — secondary maroon use */ + --color-gold: #C9A84C; /* Antique Gold — achievement, highlights, badges */ + --color-gold-light: #E8C97A; /* Light Gold — hover on gold elements, backgrounds */ + --color-gold-subtle: #F5EDD6; /* Very light gold tint — subtle card backgrounds */ + + /* === NEUTRALS === */ + --color-black: #111111; /* Near-black — text, borders */ + --color-charcoal: #2C2C2C; /* Charcoal — secondary text, icons */ + --color-gray-dark: #6B6B6B; /* Dark gray — muted text, disabled labels */ + --color-gray-mid: #B8B8B8; /* Mid gray — borders, dividers */ + --color-gray-light: #E8E8E8; /* Light gray — subtle borders, hover backgrounds */ + --color-off-white: #F9F7F4; /* Warm off-white — page backgrounds */ + --color-white: #FFFFFF; /* Pure white — card surfaces */ + + /* === SEMANTIC / GAME STATE === */ + --color-correct: #1A7A4A; /* Deep green — correct answers */ + --color-correct-bg: #EBF7F1; /* Correct answer background */ + --color-wrong: #C0392B; /* Deep red — wrong answers */ + --color-wrong-bg: #FDECEA; /* Wrong answer background */ + --color-pending: #C9A84C; /* Gold — unanswered / in-progress */ + + /* === LEADERBOARD RANKS === */ + --color-rank-1: #C9A84C; /* Gold — 1st place */ + --color-rank-2: #9E9E9E; /* Silver — 2nd place */ + --color-rank-3: #A0522D; /* Bronze — 3rd place */ + + /* === TIMER STATES === */ + --color-timer-ok: #1A7A4A; /* Plenty of time */ + --color-timer-warn: #D97706; /* Running low */ + --color-timer-danger: #C0392B; /* Critical */ + + /* === SHADOWS === */ + --shadow-sm: 0 1px 3px rgba(17, 17, 17, 0.08), 0 1px 2px rgba(17, 17, 17, 0.04); + --shadow-md: 0 4px 12px rgba(17, 17, 17, 0.10), 0 2px 4px rgba(17, 17, 17, 0.06); + --shadow-lg: 0 8px 24px rgba(17, 17, 17, 0.12), 0 4px 8px rgba(17, 17, 17, 0.06); + --shadow-gold: 0 4px 16px rgba(201, 168, 76, 0.20); + --shadow-maroon:0 4px 16px rgba(128, 0, 32, 0.20); + + /* === BORDERS === */ + --border-light: 1px solid var(--color-gray-light); + --border-mid: 1px solid var(--color-gray-mid); + --border-gold: 1px solid var(--color-gold); + --border-maroon: 1px solid var(--color-maroon); + + /* === BORDER RADIUS === */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 9999px; + + /* === SPACING SCALE === */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 48px; + --space-8: 64px; + + /* === TRANSITIONS === */ + --transition-fast: all 0.15s ease; + --transition-base: all 0.25s ease; + --transition-slow: all 0.4s ease; +} +``` + +--- + +### Core Component Rules (apply to ALL phases) + +``` +1. SURFACES: Cards use --color-white background. Page uses --color-off-white. +2. BORDERS: Use --border-light for structural separation. --border-gold for featured/active. +3. SHADOWS: --shadow-md for cards, --shadow-lg for modals, --shadow-sm for small elements. +4. BUTTONS: Filled primary = maroon background + white text. + Outlined secondary = transparent + maroon border + maroon text. + Ghost = transparent + no border, shows on hover only. +5. INTERACTIVE: All interactive elements get a subtle lift on hover (transform: translateY(-1px) + shadow increase). +6. RADIUS: Consistent rounded corners. Use --radius-md for most components. +7. ICONS: Use shadcn/ui icon components (lucide-react). Prefer: Trophy, Star, Clock, CheckCircle, + XCircle, Users, Crown, Zap, BookOpen, ChevronRight, Medal. + Always pair icons with text labels. Never use icons alone without accessible labels. +8. ANIMATIONS: Subtle, purposeful. 150–400ms durations. Ease curves only. + Entrance: fade-in + slight translateY(8px → 0). No bouncing or pixel-jumping. +9. TYPOGRAPHY: Left-align body text. Center only for hero headings and result screens. +10. SPACING: Generous padding inside cards. Consistent gap between elements. + NO tight, cramped layouts anywhere in the app. +``` + +--- + +### Shadcn/UI Icon Reference + +These are the recommended icons from `lucide-react`. Use these consistently throughout the app. + +``` +Navigation & Actions: + BookOpen → Quiz / study sessions + Play → Start quiz / begin + ChevronRight → Next, proceed + Home → Return to home + Settings → Configuration + LogOut → Exit / leave session + +Game & Scoring: + Trophy → Leaderboard, rankings + Crown → First place / winner + Medal → Achievement / rank badges + Star → Rating, favorites + Zap → Streak, speed bonus + Target → Score, accuracy + +Players & Social: + Users → Player count, lobby + User → Individual player + UserCheck → Player joined confirmation + +Feedback: + CheckCircle → Correct answer + XCircle → Wrong answer + AlertCircle → Warning / time low + Info → Hint, tooltip + +Timer & Time: + Clock → Timer display + Timer → Countdown + +Host Controls: + SkipForward → Next question + Pause → Pause game + Eye → Reveal answer + Ban → Kick player +``` + +Import example: +```jsx +import { Trophy, CheckCircle, Clock } from "lucide-react" + +// Usage + +``` + +--- + +## PHASES OVERVIEW + +| Phase | Section | Est. Effort | +|-------|---------|-------------| +| Phase 0 | Global CSS Variables & Fonts | 1–2 hrs | +| Phase 1 | Navigation / Header Bar | 1 hr | +| Phase 2 | Home / Landing Screen | 2–3 hrs | +| Phase 3 | Quiz Lobby / Room Screen | 1–2 hrs | +| Phase 4 | Question Card Component | 2 hrs | +| Phase 5 | Answer Option Buttons | 1–2 hrs | +| Phase 6 | Timer Component | 1 hr | +| Phase 7 | Score / Points Display | 1 hr | +| Phase 8 | Correct / Wrong Feedback States | 1–2 hrs | +| Phase 9 | Leaderboard Screen | 2 hrs | +| Phase 10 | Results / End Screen | 2 hrs | +| Phase 11 | Host Controls Panel | 2 hrs | +| Phase 12 | Micro-interactions & Final Polish | 2–3 hrs | + +--- + +--- + +# PHASE 0 — Global CSS Variables & Font Setup + +## Goal +Inject the new design tokens system-wide without touching any component logic. +This is the foundation all other phases build upon. + +## Safety Rules +- Only edit: `global.css` / `index.css` / `app.css` (whichever is your global stylesheet) +- Do NOT edit any component files yet +- Do NOT rename or remove any class names referenced in JS/JSX/TS files +- Add variables inside `:root {}` only — do not remove existing variables yet + +## Prompt + +``` +You are setting up the global CSS design system for a quiz app redesign. +Do NOT touch any component files, JS, TS, or backend files. +Only modify the global stylesheet. + +STEP 1 — Add this Google Fonts import at the very top of the global stylesheet: + +@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;600&display=swap&display=swap'); + +STEP 2 — Add these CSS custom properties inside :root {}: + + /* Fonts */ + --font-display: 'DM Serif Display', Georgia, serif; + --font-heading: 'Outfit', sans-serif; + --font-body: 'DM Sans', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + /* Color Palette */ + --color-maroon: #800020; + --color-maroon-dark: #5C0016; + --color-maroon-light: #A8002A; + --color-gold: #C9A84C; + --color-gold-light: #E8C97A; + --color-gold-subtle: #F5EDD6; + --color-black: #111111; + --color-charcoal: #2C2C2C; + --color-gray-dark: #6B6B6B; + --color-gray-mid: #B8B8B8; + --color-gray-light: #E8E8E8; + --color-off-white: #F9F7F4; + --color-white: #FFFFFF; + + /* Game States */ + --color-correct: #1A7A4A; + --color-correct-bg: #EBF7F1; + --color-wrong: #C0392B; + --color-wrong-bg: #FDECEA; + --color-pending: #C9A84C; + + /* Leaderboard */ + --color-rank-1: #C9A84C; + --color-rank-2: #9E9E9E; + --color-rank-3: #A0522D; + + /* Timer States */ + --color-timer-ok: #1A7A4A; + --color-timer-warn: #D97706; + --color-timer-danger: #C0392B; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(17,17,17,0.08), 0 1px 2px rgba(17,17,17,0.04); + --shadow-md: 0 4px 12px rgba(17,17,17,0.10), 0 2px 4px rgba(17,17,17,0.06); + --shadow-lg: 0 8px 24px rgba(17,17,17,0.12), 0 4px 8px rgba(17,17,17,0.06); + --shadow-gold: 0 4px 16px rgba(201,168,76,0.20); + --shadow-maroon: 0 4px 16px rgba(128,0,32,0.20); + + /* Borders */ + --border-light: 1px solid #E8E8E8; + --border-mid: 1px solid #B8B8B8; + --border-gold: 1px solid #C9A84C; + --border-maroon: 1px solid #800020; + + /* Border Radius */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 9999px; + + /* Spacing */ + --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; + --space-5: 24px; --space-6: 32px; --space-7: 48px; --space-8: 64px; + + /* Transitions */ + --transition-fast: all 0.15s ease; + --transition-base: all 0.25s ease; + --transition-slow: all 0.40s ease; + +STEP 3 — Set global base styles (add after :root): + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-body); + background-color: var(--color-off-white); + color: var(--color-black); + line-height: 1.6; +} + +STEP 4 — Add these global keyframe animations: + +@keyframes fade-in-up { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scale-in { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes slide-in-right { + from { opacity: 0; transform: translateX(16px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 0 0 rgba(201,168,76,0.3); } + 50% { box-shadow: 0 0 0 8px rgba(201,168,76,0); } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} + +@keyframes pop { + 0% { transform: scale(1); } + 50% { transform: scale(1.06); } + 100% { transform: scale(1); } +} + +@keyframes flash-border { + 0%, 100% { border-color: var(--color-gold); } + 50% { border-color: transparent; } +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +STEP 5 — Add utility classes: + +.text-maroon { color: var(--color-maroon); } +.text-gold { color: var(--color-gold); } +.text-muted { color: var(--color-gray-dark); } +.bg-maroon { background-color: var(--color-maroon); } +.bg-gold-subtle { background-color: var(--color-gold-subtle); } +.icon-gold { color: var(--color-gold); } +.icon-maroon { color: var(--color-maroon); } + +After making changes, verify the page still loads and all existing functionality works. +``` + +--- + +--- + +# PHASE 1 — Navigation / Header Bar + +## Goal +Restyle the top navigation bar to match the Aurum design system. Clean, professional, with clear brand identity. + +## Safety Rules +- Only change CSS/styles of the nav component +- Do NOT change navigation links, route paths, or onClick handlers +- Do NOT remove or rename any existing CSS class names used in JS +- Prefix any new class names with `au-` to avoid conflicts + +## Prompt + +``` +You are restyling ONLY the navigation/header bar of a quiz app. +Do NOT change any routing logic, links, or JavaScript. +Do NOT remove existing class names — only add new styling on top. +Reference style: Quizlet.com nav — clean white bar, clear hierarchy, minimal. + +TARGET LOOK: + +NAV CONTAINER: +- background: var(--color-white) +- border-bottom: 1px solid var(--color-gray-light) +- box-shadow: var(--shadow-sm) +- height: 64px +- padding: 0 var(--space-6) +- display: flex, align-items: center, justify-content: space-between +- position: sticky, top: 0, z-index: 100 + +LOGO / BRAND: +- Text or image logo sits on the left +- Logo text: font-family var(--font-display), font-size 22px, color var(--color-maroon), letter-spacing -0.01em +- Add a small Trophy or BookOpen icon (lucide-react) before the logo text, color var(--color-gold), size 20px +- Do not change any routing logic attached to the logo + +NAV LINKS (middle): +- font-family: var(--font-heading), font-size: 15px, font-weight: 500 +- color: var(--color-charcoal) +- padding: 6px 14px +- border-radius: var(--radius-md) +- text-decoration: none +- transition: var(--transition-fast) +- hover: background var(--color-gold-subtle), color var(--color-maroon) +- Active state: background var(--color-gold-subtle), color var(--color-maroon), font-weight 600 + +CTA BUTTON (e.g., "Host Quiz", "Join"): +- Primary button: background var(--color-maroon), color var(--color-white) +- font-family: var(--font-heading), font-size: 14px, font-weight: 600 +- padding: 8px 20px +- border-radius: var(--radius-full) +- border: none +- box-shadow: var(--shadow-maroon) +- transition: var(--transition-fast) +- hover: background var(--color-maroon-dark), transform translateY(-1px), box-shadow: 0 6px 20px rgba(128,0,32,0.25) +- active: transform translateY(0), box-shadow: var(--shadow-sm) + +USER AVATAR / PROFILE PILL (if present): +- width: 36px, height: 36px, border-radius: var(--radius-full) +- border: 2px solid var(--color-gold) +- box-shadow: var(--shadow-gold) +- cursor: pointer + +MOBILE (below 768px): +- Hamburger icon: color var(--color-maroon), size 24px, use Menu icon from lucide-react +- Mobile drawer background: var(--color-white), padding var(--space-5) +- Mobile nav links: full width, padding var(--space-3) var(--space-4), border-bottom var(--border-light) + +Do not change any routing logic, href values, or JavaScript handlers. +``` + +--- + +--- + +# PHASE 2 — Home / Landing Screen + +## Goal +Redesign the main landing/home page. Clean, professional, confidence-inspiring. + +## Safety Rules +- Only restyle the home page component's CSS +- Do NOT change any data fetching, routing links, or state logic +- Preserve all existing button onClick handlers — only restyle the buttons + +## Prompt + +``` +You are restyling ONLY the home/landing page of a quiz app. +No logic changes. Visual redesign only. +Reference: Quizlet.com home — clean whitespace, confident typography, clear CTAs. + +PAGE BACKGROUND: +- background: var(--color-off-white) +- Add a very subtle dot-grid pattern using CSS: + background-image: radial-gradient(circle, rgba(128,0,32,0.06) 1px, transparent 1px); + background-size: 28px 28px; + +HERO SECTION: +- max-width: 1200px, margin: 0 auto, padding: var(--space-8) var(--space-6) +- Layout: centered, text-align center on desktop +- Hero eyebrow label (e.g., "The smarter way to quiz"): + font-family: var(--font-heading), font-size: 13px, font-weight: 600 + color: var(--color-maroon), text-transform: uppercase, letter-spacing: 0.12em + background: var(--color-gold-subtle), padding: 4px 16px, border-radius: var(--radius-full) + border: var(--border-gold), display: inline-block, margin-bottom: var(--space-4) + +- Main heading: + font-family: var(--font-display) + font-size: clamp(36px, 6vw, 72px) + color: var(--color-black) + line-height: 1.1 + letter-spacing: -0.02em + margin-bottom: var(--space-4) + Add a color highlight on a key word using: color var(--color-maroon) + +- Sub-heading: + font-family: var(--font-body), font-size: clamp(16px, 2vw, 20px), font-weight: 400 + color: var(--color-gray-dark), max-width: 600px, margin: 0 auto var(--space-6) + +PRIMARY CTA BUTTON ("Create Quiz", "Start"): +- background: var(--color-maroon) +- color: var(--color-white) +- font-family: var(--font-heading), font-size: 16px, font-weight: 600 +- padding: 14px 36px +- border-radius: var(--radius-full) +- border: none +- box-shadow: var(--shadow-maroon) +- display: inline-flex, align-items: center, gap: var(--space-2) +- Include Play icon (lucide-react) before button text, size 18px +- transition: var(--transition-fast) +- hover: background var(--color-maroon-dark), transform translateY(-2px), box-shadow: 0 8px 24px rgba(128,0,32,0.30) +- active: transform translateY(0) + +SECONDARY CTA BUTTON ("Join Game", "Browse"): +- background: var(--color-white) +- color: var(--color-maroon) +- border: var(--border-maroon) +- Same sizing and radius as primary +- hover: background var(--color-gold-subtle) + +FEATURE / BENEFIT CARDS (if present on home): +- Layout: 3-column grid on desktop, 1-column on mobile, gap: var(--space-5) +- Each card: + background: var(--color-white) + border: var(--border-light) + border-radius: var(--radius-lg) + padding: var(--space-6) + box-shadow: var(--shadow-sm) + animation: fade-in-up 0.4s ease both + animation-delay: calc(var(--card-index) * 0.1s) + transition: var(--transition-base) + hover: box-shadow var(--shadow-md), transform translateY(-2px) + +- Card icon container: + width: 44px, height: 44px, border-radius: var(--radius-md) + background: var(--color-gold-subtle), border: var(--border-gold) + display: flex, align-items: center, justify-content: center + margin-bottom: var(--space-4) + Icon: lucide-react icon, color var(--color-maroon), size 20px + +- Card title: font-family var(--font-heading), font-size: 18px, font-weight: 600, color var(--color-black), margin-bottom var(--space-2) +- Card description: font-family var(--font-body), font-size: 15px, color var(--color-gray-dark), line-height 1.6 + +Do not change any onClick, routing, or API calls. +``` + +--- + +--- + +# PHASE 3 — Quiz Lobby / Room Screen + +## Goal +Restyle the waiting room where players gather before a quiz starts. Clear, welcoming, with live energy. + +## Safety Rules +- Only restyle visuals +- Preserve: room code display logic, player join list data binding, countdown timer logic, host start button handler + +## Prompt + +``` +You are restyling ONLY the quiz lobby/waiting room screen. +No logic, state, or data changes. Visual redesign only. +Vibe: Premium waiting room — calm but with anticipation. Clear hierarchy. + +PAGE BACKGROUND: +- background: var(--color-off-white) with the same dot-grid as the home page + +ROOM CODE DISPLAY: +- Centered card container: + background: var(--color-white) + border: var(--border-gold) + border-radius: var(--radius-xl) + box-shadow: var(--shadow-gold) + padding: var(--space-6) var(--space-7) + max-width: 480px, margin: 0 auto + +- Label above code ("Room Code" / "Game PIN"): + font-family: var(--font-heading), font-size: 11px, font-weight: 600 + color: var(--color-gray-dark), text-transform: uppercase, letter-spacing: 0.15em + margin-bottom: var(--space-2) + +- Room code text: + font-family: var(--font-mono), font-size: clamp(32px, 6vw, 56px), font-weight: 600 + color: var(--color-maroon), letter-spacing: 0.08em, text-align: center + +- Copy button (if present): small ghost button with Copy icon (lucide-react), color var(--color-gold) + +PLAYER COUNT BADGE: +- display: inline-flex, align-items: center, gap: var(--space-2) +- background: var(--color-gold-subtle), border: var(--border-gold), border-radius: var(--radius-full) +- padding: 6px 16px +- Users icon (lucide-react), size 16px, color var(--color-maroon) +- Count text: font-family var(--font-heading), font-size: 15px, font-weight: 600, color var(--color-maroon) + +PLAYER AVATAR GRID: +- display: grid, grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)), gap: var(--space-3) +- max-width: 900px, margin: 0 auto + +- Each player tile: + background: var(--color-white) + border: var(--border-light) + border-radius: var(--radius-md) + padding: var(--space-3) var(--space-4) + display: flex, align-items: center, gap: var(--space-3) + box-shadow: var(--shadow-sm) + animation: scale-in 0.3s ease both + +- Player avatar circle: + width: 36px, height: 36px, border-radius: var(--radius-full) + background: var(--color-gold-subtle), border: var(--border-gold) + display: flex, align-items: center, justify-content: center + font-family: var(--font-heading), font-size: 14px, font-weight: 700, color: var(--color-maroon) + +- Player name: font-family var(--font-body), font-size: 14px, font-weight: 500, color var(--color-black) + +WAITING STATUS TEXT: +- font-family: var(--font-body), font-size: 14px, color: var(--color-gray-dark) +- Include a subtle animated pulse dot before the text: width 8px, height 8px, border-radius 50%, + background var(--color-correct), animation: pulse-glow 1.5s infinite + +HOST START BUTTON: +- background: var(--color-maroon) +- color: var(--color-white) +- font-family: var(--font-heading), font-size: 16px, font-weight: 600 +- padding: 14px 40px, border-radius: var(--radius-full) +- border: none, box-shadow: var(--shadow-maroon) +- Include Play icon (lucide-react) before text, size 18px +- hover: background var(--color-maroon-dark), transform translateY(-2px), box-shadow: 0 8px 24px rgba(128,0,32,0.30) + +Do not change any WebSocket connections, room join logic, player state, or host controls logic. +``` + +--- + +--- + +# PHASE 4 — Question Card Component + +## Goal +Restyle the main question display card shown during an active quiz. Clear, readable, focused. + +## Safety Rules +- Only restyle the question card component CSS +- Preserve: question text data binding, question number logic, image display, media handling +- Do NOT change timer logic — only restyle the timer visuals (Phase 6 handles that) + +## Prompt + +``` +You are restyling ONLY the question card component in a quiz app. +No data or logic changes. +Target: Clean, focused reading experience. The question should be the hero of the screen. + +QUESTION CARD CONTAINER: +- background: var(--color-white) +- border: var(--border-light) +- border-radius: var(--radius-xl) +- box-shadow: var(--shadow-md) +- padding: var(--space-7) var(--space-6) +- max-width: 860px, margin: 0 auto +- position: relative +- animation: fade-in-up 0.3s ease + +QUESTION NUMBER BADGE (e.g., "Question 3 of 10"): +- position: absolute, top: -1px, left: var(--space-6) +- background: var(--color-maroon), color: var(--color-white) +- font-family: var(--font-heading), font-size: 11px, font-weight: 600 +- padding: 4px 14px +- border-radius: 0 0 var(--radius-md) var(--radius-md) +- text-transform: uppercase, letter-spacing: 0.1em + +QUESTION TEXT: +- font-family: var(--font-heading) +- font-size: clamp(18px, 2.5vw, 28px) +- font-weight: 600 +- color: var(--color-black) +- line-height: 1.4 +- text-align: center +- margin-top: var(--space-3) +- max-width: 720px, margin-left: auto, margin-right: auto + +CATEGORY / TAG BADGE (if present): +- position: absolute, top: var(--space-4), right: var(--space-4) +- background: var(--color-gold-subtle) +- color: var(--color-maroon) +- font-family: var(--font-body), font-size: 11px, font-weight: 600 +- padding: 3px 12px +- border-radius: var(--radius-full) +- border: var(--border-gold) + +QUESTION IMAGE (if exists): +- border-radius: var(--radius-md) +- border: var(--border-light) +- box-shadow: var(--shadow-sm) +- max-height: 280px, object-fit: cover +- margin: var(--space-4) auto +- display: block + +PAGE BACKGROUND DURING QUIZ: +- background: var(--color-off-white) with dot-grid overlay + +TOP BAR (question number + timer row): +- display: flex, justify-content: space-between, align-items: center +- max-width: 860px, margin: 0 auto var(--space-4), padding: 0 var(--space-1) + +Do not change any question data fetching, state, or timer logic. +``` + +--- + +--- + +# PHASE 5 — Answer Option Buttons + +## Goal +Restyle the answer option buttons. Clear, accessible, interactive. Four distinct options that are easy to scan. + +## Safety Rules +- Only restyle the answer buttons CSS +- Preserve ALL onClick handlers, submission logic, disabled states, and JS class toggling +- If JS adds classes like `.correct`, `.wrong`, `.selected` — keep those class names exactly as-is + +## Prompt + +``` +You are restyling ONLY the answer option buttons in a quiz app. +These are the 4 clickable answer choices shown during a question. +Do NOT change onClick handlers, state, submission logic, or disabled states. +Preserve class names: "correct", "wrong", "selected", "disabled" — add styles for them, don't rename. + +ANSWER GRID LAYOUT: +- Display as 2x2 grid on desktop: grid-template-columns: 1fr 1fr, gap: var(--space-3) +- On mobile (below 640px): single column +- max-width: 860px, margin: var(--space-5) auto 0 + +ANSWER BUTTON BASE STYLE: +- display: flex, align-items: center, gap: var(--space-3) +- width: 100% +- padding: var(--space-4) var(--space-5) +- font-family: var(--font-body), font-size: 16px, font-weight: 500 +- background: var(--color-white) +- color: var(--color-black) +- border: var(--border-mid) +- border-radius: var(--radius-md) +- box-shadow: var(--shadow-sm) +- cursor: pointer +- text-align: left +- transition: var(--transition-fast) +- min-height: 60px + +HOVER STATE (before answering): +- border-color: var(--color-maroon) +- box-shadow: var(--shadow-md) +- transform: translateY(-1px) +- background: var(--color-off-white) + +ACTIVE/CLICK STATE: +- transform: translateY(0) +- box-shadow: var(--shadow-sm) + +ANSWER LETTER BADGE (A, B, C, D — left side): +- width: 32px, height: 32px, flex-shrink: 0 +- background: var(--color-off-white) +- border: 1px solid var(--color-gray-light) +- border-radius: var(--radius-sm) +- font-family: var(--font-heading), font-size: 13px, font-weight: 700 +- color: var(--color-charcoal) +- display: flex, align-items: center, justify-content: center + +SELECTED STATE (.selected): +- border-color: var(--color-maroon) +- border-width: 2px +- background: rgba(128, 0, 32, 0.04) +- Letter badge: background var(--color-maroon), color white, border-color var(--color-maroon) + +CORRECT STATE (.correct — added by existing JS): +- background: var(--color-correct-bg) +- border-color: var(--color-correct) +- border-width: 2px +- color: var(--color-correct) +- animation: pop 0.3s ease +- Letter badge: background var(--color-correct), color white +- Add CheckCircle icon (lucide-react) on the right: position absolute, right var(--space-4), size 20px, color var(--color-correct) + +WRONG STATE (.wrong — added by existing JS): +- background: var(--color-wrong-bg) +- border-color: var(--color-wrong) +- border-width: 2px +- color: var(--color-wrong) +- animation: shake 0.4s ease +- Letter badge: background var(--color-wrong), color white +- Add XCircle icon (lucide-react) on the right: position absolute, right var(--space-4), size 20px, color var(--color-wrong) + +DISABLED STATE (.disabled): +- opacity: 0.5 +- cursor: not-allowed +- pointer-events: none +- box-shadow: none + +Do not change any JS logic, event handlers, or state management. +``` + +--- + +--- + +# PHASE 6 — Timer Component + +## Goal +Restyle the countdown timer. Minimal, readable, and clearly communicates urgency through color alone. + +## Safety Rules +- Only restyle CSS +- Preserve all timer logic, countdown state, and any JS that changes timer classes or values +- If JS changes classes on the timer based on time remaining, preserve those class names + +## Prompt + +``` +You are restyling ONLY the timer/countdown component in a quiz app. +Do NOT change any timer logic, countdown calculations, or class-toggling JavaScript. + +TIMER CONTAINER (circular): +- width: 64px, height: 64px +- border-radius: var(--radius-full) +- border: 3px solid var(--color-timer-ok) +- background: var(--color-white) +- display: flex, align-items: center, justify-content: center +- box-shadow: var(--shadow-sm) +- transition: border-color 0.3s ease, background 0.3s ease + +TIMER NUMBER: +- font-family: var(--font-mono), font-size: 20px, font-weight: 600 +- color: var(--color-timer-ok) +- line-height: 1 +- transition: color 0.3s ease + +WARNING STATE (keep existing class names your JS uses): +- border-color: var(--color-timer-warn) +- Timer number color: var(--color-timer-warn) +- No animation yet — just color change + +DANGER STATE (critical — keep existing class names): +- border-color: var(--color-timer-danger) +- background: rgba(192, 57, 43, 0.06) +- Timer number color: var(--color-timer-danger) +- animation: flash-border 0.6s infinite + +PROGRESS BAR TIMER VERSION (if a bar-style timer is used): +- height: 6px +- background: var(--color-gray-light) +- border-radius: var(--radius-full) +- overflow: hidden +- Fill element inside: + height: 100% + background: var(--color-timer-ok) + border-radius: var(--radius-full) + transition: width 1s linear, background 0.3s ease + Warning state fill: background var(--color-timer-warn) + Danger state fill: background var(--color-timer-danger) + +TIMER LABEL ("Time Left"): +- font-family: var(--font-body), font-size: 11px, font-weight: 500 +- color: var(--color-gray-dark), text-transform: uppercase, letter-spacing: 0.1em +- display: block, text-align: center, margin-top: var(--space-1) +- Add Clock icon (lucide-react) inline, size 11px, before the label text + +Do not change any timer countdown logic. +``` + +--- + +--- + +# PHASE 7 — Score / Points Display + +## Goal +Restyle the score counter displayed during the quiz. Prominent but not distracting. + +## Safety Rules +- Only restyle. Do NOT change score calculation logic or state management. + +## Prompt + +``` +You are restyling ONLY the score/points display component. +No logic changes. Preserve all data bindings and state. + +SCORE DISPLAY CONTAINER: +- background: var(--color-white) +- border: var(--border-gold) +- border-radius: var(--radius-md) +- box-shadow: var(--shadow-gold) +- padding: var(--space-2) var(--space-4) +- display: inline-flex, align-items: center, gap: var(--space-3) + +SCORE ICON: +- Trophy icon (lucide-react), size 18px, color var(--color-gold) + +SCORE LABEL ("Score", "Points", "PTS"): +- font-family: var(--font-body), font-size: 11px, font-weight: 500 +- color: var(--color-gray-dark), text-transform: uppercase, letter-spacing: 0.1em + +SCORE VALUE: +- font-family: var(--font-mono), font-size: 20px, font-weight: 600 +- color: var(--color-maroon) +- transition: color 0.2s ease + +SCORE INCREASE ANIMATION (when points are added — trigger by JS adding a class): +- .score-pop: animation: pop 0.35s ease + +STREAK BADGE (if streak counter exists): +- display: inline-flex, align-items: center, gap: var(--space-1) +- background: var(--color-gold-subtle) +- border: var(--border-gold) +- border-radius: var(--radius-full) +- padding: 2px 10px +- Zap icon (lucide-react), size 12px, color var(--color-gold) +- Streak count: font-family var(--font-heading), font-size: 13px, font-weight: 700, color var(--color-maroon) + +BONUS POINTS POPUP (floating "+200" text): +- position: absolute +- font-family: var(--font-mono), font-size: 14px, font-weight: 600 +- color: var(--color-correct) +- animation: fade-in-up 1s ease forwards +- pointer-events: none +- opacity fades out after 1s via animation + +Do not change score calculation, bonus logic, or state management. +``` + +--- + +--- + +# PHASE 8 — Correct / Wrong Answer Feedback States + +## Goal +Restyle the brief feedback overlay shown after a player answers. Clear, confident, not overwhelming. + +## Safety Rules +- Only restyle the feedback overlay/screen CSS +- Preserve: timing logic, transition to next question, score update calls + +## Prompt + +``` +You are restyling ONLY the answer feedback overlay/screen in a quiz app. +This appears briefly after a player answers (correct or wrong). +Do NOT change timing, transitions to next question, or score update logic. + +CORRECT ANSWER STATE: +- Background overlay or full-screen: background var(--color-correct-bg) +- Border (if card/modal): border 2px solid var(--color-correct), border-radius var(--radius-xl) +- box-shadow: 0 8px 32px rgba(26, 122, 74, 0.15) +- animation: scale-in 0.25s ease + +- Correct icon: + CheckCircle icon (lucide-react), size 56px, color var(--color-correct) + animation: pop 0.35s ease 0.1s both + +- "Correct!" heading: + font-family: var(--font-heading), font-size: clamp(24px, 4vw, 40px), font-weight: 700 + color: var(--color-correct) + margin-top: var(--space-3) + +- Points earned: + font-family: var(--font-mono), font-size: 18px, font-weight: 600 + color: var(--color-black) + background: var(--color-white), padding: var(--space-2) var(--space-4) + border-radius: var(--radius-full), border: var(--border-light) + display: inline-block, margin-top: var(--space-2) + Include Star icon (lucide-react), size 14px, color var(--color-gold), inline + +WRONG ANSWER STATE: +- Background: background var(--color-wrong-bg) +- Border: 2px solid var(--color-wrong) +- animation: shake 0.4s ease + +- Wrong icon: + XCircle icon (lucide-react), size 56px, color var(--color-wrong) + +- "Incorrect" heading: + font-family: var(--font-heading), font-size: clamp(24px, 4vw, 40px), font-weight: 700 + color: var(--color-wrong) + +- Correct answer revealed below: + font-family: var(--font-body), font-size: 16px, color: var(--color-charcoal) + background: var(--color-white), padding: var(--space-3) var(--space-4) + border-radius: var(--radius-md), border: var(--border-light) + margin-top: var(--space-3) + Label "The correct answer was:" in font-size 12px, color var(--color-gray-dark), display block above + +SPEED BONUS DISPLAY (if applicable): +- font-family: var(--font-mono), font-size: 13px, font-weight: 600 +- color: var(--color-gold), margin-top: var(--space-2) +- Include Zap icon (lucide-react), size 13px, inline + +Do not change any timing, auto-advance logic, or score update calls. +``` + +--- + +--- + +# PHASE 9 — Leaderboard Screen + +## Goal +Restyle the mid-game or post-game leaderboard. Prestigious, clear rankings, celebratory for top players. + +## Safety Rules +- Only restyle. Preserve player data binding, rank sorting logic, and any animations triggered by JS rank changes. + +## Prompt + +``` +You are restyling ONLY the leaderboard screen in a quiz app. +No logic changes. Preserve all data bindings and sorting logic. + +PAGE BACKGROUND: +- background: var(--color-off-white) with dot-grid overlay +- max-width: 720px, margin: 0 auto, padding: var(--space-6) + +LEADERBOARD HEADER: +- Text ("Leaderboard" / "Rankings"): + font-family: var(--font-display), font-size: clamp(28px, 4vw, 44px) + color: var(--color-black), text-align: center, margin-bottom: var(--space-5) +- Trophy icon (lucide-react) centered above heading, size 40px, color var(--color-gold) + +TOP 3 PODIUM SECTION (if applicable): +- Arrange as 2nd | 1st | 3rd (1st is tallest, center) +- Each podium block: + background: var(--color-white), border: var(--border-light) + border-radius: var(--radius-lg) var(--radius-lg) 0 0 + padding: var(--space-4), text-align: center +- 1st: border-color var(--color-gold), box-shadow: var(--shadow-gold) + Crown icon (lucide-react) above avatar, size 24px, color var(--color-gold) + +LEADERBOARD ROW (each player entry after top 3): +- background: var(--color-white) +- border: var(--border-light) +- border-radius: var(--radius-md) +- padding: var(--space-3) var(--space-4) +- display: flex, align-items: center, gap: var(--space-4) +- margin-bottom: var(--space-2) +- box-shadow: var(--shadow-sm) +- animation: slide-in-right 0.3s ease both +- animation-delay: calc(var(--row-index) * 0.06s) +- transition: var(--transition-fast) + +RANK NUMBER: +- width: 32px, text-align: center, flex-shrink: 0 +- font-family: var(--font-heading), font-size: 14px, font-weight: 700 +- #1: color var(--color-rank-1) +- #2: color var(--color-rank-2) +- #3: color var(--color-rank-3) +- #4+: color var(--color-gray-dark) + +PLAYER AVATAR CIRCLE: +- width: 40px, height: 40px, border-radius: var(--radius-full) +- background: var(--color-gold-subtle), border: var(--border-gold) +- display: flex, align-items: center, justify-content: center +- font-family: var(--font-heading), font-size: 15px, font-weight: 700, color var(--color-maroon) + +PLAYER NAME: +- font-family: var(--font-body), font-size: 15px, font-weight: 500 +- color: var(--color-black), flex: 1 + +PLAYER SCORE: +- font-family: var(--font-mono), font-size: 16px, font-weight: 600 +- color: var(--color-maroon), text-align: right + +TOP ROW (#1) SPECIAL: +- border-color: var(--color-gold), border-width: 2px +- box-shadow: var(--shadow-gold) +- background: var(--color-gold-subtle) + +Do not change player data fetching, rank sorting, or any real-time update logic. +``` + +--- + +--- + +# PHASE 10 — Results / End Screen + +## Goal +Restyle the final results screen. Celebratory, reflective, with clear next steps. + +## Safety Rules +- Only restyle. Preserve final score data, retry/replay logic, and share functionality. + +## Prompt + +``` +You are restyling ONLY the end/results screen of a quiz app. +No logic changes. Preserve all final score data, share buttons, and replay logic. +Vibe: Achievement unlocked — premium certificate energy. + +PAGE BACKGROUND: +- background: var(--color-off-white) with dot-grid overlay + +MAIN RESULT CARD: +- background: var(--color-white) +- border: var(--border-light) +- border-radius: var(--radius-xl) +- box-shadow: var(--shadow-lg) +- padding: var(--space-7) var(--space-6) +- max-width: 600px, margin: var(--space-7) auto +- text-align: center +- animation: scale-in 0.4s ease + +TOP ICON / BADGE: +- Trophy icon (lucide-react), size 64px, color var(--color-gold) +- animation: pop 0.5s ease 0.2s both + +RESULT HEADING ("Quiz Complete!" / "Well Done!"): +- font-family: var(--font-display) +- font-size: clamp(28px, 5vw, 52px) +- color: var(--color-black), margin-top: var(--space-4) + +FINAL SCORE DISPLAY: +- Centered block, margin: var(--space-5) 0 +- "Your Score" label: font-family var(--font-body), font-size 12px, color var(--color-gray-dark), + text-transform uppercase, letter-spacing 0.12em, display block, margin-bottom var(--space-2) +- Score number: font-family var(--font-mono), font-size: clamp(48px, 8vw, 80px), font-weight: 600 + color: var(--color-maroon) +- Thin gold divider line: height 2px, background var(--color-gold), width 80px, margin: var(--space-4) auto + +STATS ROW (accuracy, correct count, time): +- display: grid, grid-template-columns: repeat(3, 1fr), gap: var(--space-4), margin: var(--space-5) 0 +- Each stat cell: + background: var(--color-off-white), border: var(--border-light) + border-radius: var(--radius-md), padding: var(--space-4), text-align: center +- Stat icon: lucide-react icon, size 18px, color var(--color-gold), margin-bottom var(--space-2) + (Use Target for accuracy, CheckCircle for correct, Clock for time) +- Stat number: font-family var(--font-mono), font-size: 22px, font-weight: 600, color var(--color-maroon) +- Stat label: font-family var(--font-body), font-size: 12px, color var(--color-gray-dark), text-transform uppercase, letter-spacing 0.1em + +PLAY AGAIN BUTTON: +- background: var(--color-maroon), color: var(--color-white) +- font-family: var(--font-heading), font-size: 15px, font-weight: 600 +- padding: 13px 36px, border-radius: var(--radius-full), border: none +- box-shadow: var(--shadow-maroon) +- Include RotateCcw icon (lucide-react) before text, size 16px +- hover: background var(--color-maroon-dark), transform translateY(-1px) + +SHARE BUTTON (secondary): +- background: var(--color-white), color: var(--color-maroon) +- border: var(--border-maroon), border-radius: var(--radius-full) +- Same padding as Play Again +- Include Share2 icon (lucide-react), size 16px +- hover: background var(--color-gold-subtle) + +HOME BUTTON: +- Ghost style: background transparent, border: none +- color: var(--color-gray-dark), font-family var(--font-body), font-size 14px +- Include Home icon (lucide-react), size 14px, inline +- hover: color var(--color-maroon) + +Do not change any score calculation, share API, or routing logic. +``` + +--- + +--- + +# PHASE 11 — Host Controls Panel + +## Goal +Restyle the host dashboard. Clean, authoritative, with clear action hierarchy so the host always knows what to do next. + +## Safety Rules +- Only restyle. Preserve all host control logic: next question, pause, kick player, end quiz, show answers. +- Do NOT rename button IDs or classes used in event listeners. + +## Prompt + +``` +You are restyling ONLY the host controls panel/dashboard of a quiz app. +No logic changes. Preserve all control button handlers, player management logic, and real-time state. + +HOST PANEL LAYOUT: +- Sidebar or top panel: background var(--color-white), border-right var(--border-light) (if sidebar) +- box-shadow: var(--shadow-md) +- padding: var(--space-5) +- Section label headers: font-family var(--font-body), font-size 11px, font-weight 600, + color var(--color-gray-dark), text-transform uppercase, letter-spacing 0.12em, margin-bottom var(--space-3) + +NEXT QUESTION BUTTON (primary host action): +- background: var(--color-maroon), color: var(--color-white) +- font-family: var(--font-heading), font-size: 15px, font-weight: 600 +- padding: 12px 28px, border-radius: var(--radius-md), border: none +- box-shadow: var(--shadow-maroon) +- display: flex, align-items: center, gap: var(--space-2), width: 100% +- SkipForward icon (lucide-react), size 16px +- hover: background var(--color-maroon-dark), transform translateY(-1px) + +REVEAL ANSWER BUTTON: +- background: var(--color-gold-subtle), color: var(--color-maroon) +- border: var(--border-gold), border-radius: var(--radius-md) +- Same padding/sizing as Next Question +- Eye icon (lucide-react), size 16px + +PAUSE BUTTON: +- background: var(--color-off-white), color: var(--color-charcoal) +- border: var(--border-mid), border-radius: var(--radius-md) +- Pause icon (lucide-react), size 16px +- hover: border-color var(--color-maroon), color var(--color-maroon) + +END QUIZ BUTTON: +- background: transparent, color: var(--color-wrong) +- border: 1px solid var(--color-wrong), border-radius: var(--radius-md) +- font-size: 14px, padding: 10px 20px +- hover: background var(--color-wrong-bg) +- Requires confirm dialog before firing — do NOT remove the confirmation logic + +CONNECTED PLAYERS COUNT: +- display: flex, align-items: center, gap: var(--space-2) +- Users icon (lucide-react), size 16px, color var(--color-gold) +- Count: font-family var(--font-mono), font-size: 18px, font-weight: 600, color var(--color-maroon) +- Label: font-family var(--font-body), font-size: 12px, color var(--color-gray-dark) + +PLAYER LIST IN HOST VIEW: +- Each row: padding var(--space-2) var(--space-3), border-bottom var(--border-light) +- display: flex, align-items: center, justify-content: space-between +- Player name: font-family var(--font-body), font-size: 14px, color var(--color-black) +- Player score: font-family var(--font-mono), font-size: 14px, color var(--color-maroon), font-weight: 600 +- Kick button: font-size 12px, color var(--color-gray-dark), background transparent, border none + Ban icon (lucide-react), size 14px + hover: color var(--color-wrong) + +Do not touch any WebSocket, player kick logic, or game state management. +``` + +--- + +--- + +# PHASE 12 — Micro-interactions & Final Polish + +## Goal +Final layer of polish: focus states, loading states, empty states, scrollbars, consistency audit, and mobile check. + +## Safety Rules +- CSS only additions. No logic changes. +- Test on mobile, tablet, and desktop before finalizing. + +## Prompt + +``` +You are doing FINAL POLISH on the quiz app UI after all components have been restyled. +This phase is CSS-only micro-interactions and consistency fixes. +No logic changes whatsoever. + +1. BUTTON CONSISTENCY AUDIT: + Every button must have: + - font-family: var(--font-heading) or var(--font-body) + - Proper hover state: transform translateY(-1px) + shadow increase + - Active state: transform translateY(0) + shadow decrease + - transition: var(--transition-fast) + - cursor: pointer + - Minimum height: 40px + +2. LOADING STATES: + - Skeleton loaders: + background: linear-gradient(90deg, var(--color-gray-light) 25%, var(--color-off-white) 50%, var(--color-gray-light) 75%) + background-size: 200% 100% + animation: shimmer 1.5s infinite + border-radius: var(--radius-sm) + - Loading spinner (if used): border 3px solid var(--color-gray-light), + border-top-color var(--color-maroon), border-radius 50%, animation: spin 0.8s linear infinite + @keyframes spin { to { transform: rotate(360deg); } } + +3. EMPTY STATES: + - Container: text-align center, padding var(--space-8) var(--space-6) + - Icon: appropriate lucide-react icon, size 48px, color var(--color-gray-mid), margin-bottom var(--space-4) + - Message: font-family var(--font-heading), font-size 18px, color var(--color-gray-dark) + - Subtext: font-family var(--font-body), font-size 14px, color var(--color-gray-mid) + +4. SCROLLBAR STYLING (webkit): + ::-webkit-scrollbar { width: 6px; } + ::-webkit-scrollbar-track { background: transparent; } + ::-webkit-scrollbar-thumb { background: var(--color-gray-mid); border-radius: var(--radius-full); } + ::-webkit-scrollbar-thumb:hover { background: var(--color-maroon-light); } + +5. FOCUS STATES (accessibility — never remove): + *:focus-visible { + outline: 2px solid var(--color-gold); + outline-offset: 3px; + border-radius: var(--radius-sm); + } + +6. INPUT / FORM ELEMENTS (find and fix any unstyled inputs): + input, textarea, select { + font-family: var(--font-body); + font-size: 15px; + border: var(--border-mid); + border-radius: var(--radius-md); + padding: var(--space-3) var(--space-4); + background: var(--color-white); + color: var(--color-black); + transition: var(--transition-fast); + } + input:focus, textarea:focus, select:focus { + border-color: var(--color-maroon); + box-shadow: 0 0 0 3px rgba(128,0,32,0.08); + outline: none; + } + +7. MODAL / DIALOG (find and fix any unstyled dialogs): + background: var(--color-white) + border: var(--border-light) + border-radius: var(--radius-xl) + box-shadow: var(--shadow-lg) + padding: var(--space-6) + max-width: 480px + +8. TOOLTIP (if used): + background: var(--color-black), color: var(--color-white) + font-family: var(--font-body), font-size: 12px + padding: var(--space-2) var(--space-3) + border-radius: var(--radius-sm) + box-shadow: var(--shadow-md) + +9. MOBILE RESPONSIVE FINAL CHECK: + - All grid layouts: 1 column on screens below 640px + - Font sizes: use clamp() for all display/heading sizes + - Touch targets: minimum 44px height for all interactive elements + - Padding: reduce var(--space-7) to var(--space-5) on mobile + - Test that no content overflows horizontally + +10. PAGE TRANSITION: + On route change, the entering page gets: + animation: fade-in 0.2s ease + This applies to the top-level page wrapper only. + +Do not change any functionality. +``` + +--- + +--- + +## POST-REDESIGN CHECKLIST + +Run through this checklist after all phases are complete. + +``` +VISUAL +[ ] Color palette is consistent: Maroon / Gold / Off-white / White / Black only +[ ] DM Serif Display used only for hero headings and result screens +[ ] Outfit used for headings, card titles, buttons, nav links +[ ] DM Sans used for body text, answer options, labels, descriptions +[ ] JetBrains Mono used for all timers, scores, codes, numeric data +[ ] No default browser styles leaking (no blue link outlines, no default selects) +[ ] Dot-grid background texture consistent across all pages +[ ] All cards have consistent border-radius and shadow +[ ] Gold is used for achievements, highlights, accents — not structural elements +[ ] Maroon is used for primary actions, CTAs, active states +[ ] Icons from lucide-react used consistently throughout +[ ] Every icon is paired with a text label (accessible) +[ ] Shadows feel subtle and real — not decorative + +FUNCTIONALITY +[ ] Quiz can be created and started ✓ +[ ] Players can join via room code ✓ +[ ] Questions display correctly with data ✓ +[ ] Answering works and submits to backend ✓ +[ ] Score updates correctly ✓ +[ ] Leaderboard updates in real-time ✓ +[ ] Host controls all work ✓ +[ ] End screen shows correct final data ✓ +[ ] All API calls work (check Network tab — no 4xx/5xx errors) ✓ +[ ] No console errors ✓ + +RESPONSIVE +[ ] Mobile (375px): all screens usable, no horizontal scroll ✓ +[ ] Tablet (768px): layout correct ✓ +[ ] Desktop (1280px+): layout correct, max-widths respected ✓ +[ ] All touch targets minimum 44px height ✓ + +PERFORMANCE +[ ] Google Fonts loaded with display=swap (check Network tab) ✓ +[ ] No layout shift from font loading ✓ +[ ] Animations run at 60fps (check FPS in DevTools) ✓ +[ ] No unused CSS variables or redundant styles ✓ + +ACCESSIBILITY +[ ] All interactive elements have visible :focus-visible styles ✓ +[ ] Color contrast meets WCAG AA (maroon on white passes) ✓ +[ ] Icon-only elements have aria-label attributes ✓ +[ ] Error states use both color AND icon (not color alone) ✓ +``` + +--- + +## WHAT THIS GUIDE NEVER TOUCHES + +``` +Backend routes / controllers / services +Database schemas or queries +API endpoints or WebSocket logic +Authentication / session logic +Score calculation algorithms +Timer countdown logic (visuals only) +Player join / kick logic (visuals only) +Any .env or configuration files +Any test files +``` + +--- + +## ICON QUICK REFERENCE (lucide-react) + +| Use Case | Icon Name | Notes | +|----------|-----------|-------| +| App logo accent | `BookOpen` | Pair with brand name | +| Start quiz | `Play` | Inside CTA buttons | +| Leaderboard | `Trophy` | Gold color | +| First place | `Crown` | Gold color | +| Achievement | `Medal` | Gold / silver / bronze | +| Correct answer | `CheckCircle` | Green (#1A7A4A) | +| Wrong answer | `XCircle` | Red (#C0392B) | +| Timer | `Clock` | Changes color with state | +| Score | `Trophy` | Maroon color | +| Streak | `Zap` | Gold color | +| Players | `Users` | Maroon color | +| Next question | `SkipForward` | Host panel | +| Reveal answer | `Eye` | Host panel | +| Pause | `Pause` | Host panel | +| End quiz | `StopCircle` | Red/destructive | +| Kick player | `Ban` | Small, subtle | +| Home | `Home` | Navigation | +| Share | `Share2` | Results screen | +| Replay | `RotateCcw` | Results screen | +| Settings | `Settings` | Nav | +| Copy code | `Copy` | Lobby room code | + +--- + +*End of QUIZ_REDESIGN_PROMPT_GUIDE.md* +*Version 2.0 — Aurum Design System | Maroon × Gold × White × Black* +*This guide is designed to be read by an AI assistant or human UI developer as a sequential redesign instruction set.* diff --git a/QUIZ_REDESIGN_PROMPT_GUIDE.md b/QUIZ_REDESIGN_PROMPT_GUIDE.md index 9b952c3..12f0924 100644 --- a/QUIZ_REDESIGN_PROMPT_GUIDE.md +++ b/QUIZ_REDESIGN_PROMPT_GUIDE.md @@ -1,1098 +1,136 @@ -# 🎮 QUIZ SYSTEM — UI REDESIGN PROMPT GUIDE -> **Version:** 1.0 | **Style:** Neo-Brutalism × Pixel-Pop Hybrid -> **Purpose:** Step-by-step UI redesign prompts for AI or human UI developers. -> **Scope:** Frontend only — zero changes to backend, API, database, or business logic. +# IntelliQuiz UI Redesign Prompt Guide ---- +Version: 2.0 +Style: Clean Minimal (Quizlet-inspired) +Scope: Frontend only. Do not change backend, API, database, auth rules, or business logic. -## 📌 HOW TO USE THIS GUIDE +## Objectives -- Follow each **Phase** in order. Do NOT skip phases. -- Each phase has a **prompt block** you paste directly to your UI developer or AI tool. -- Every phase targets **one isolated UI section** to avoid breaking other parts. -- After each phase: **test all existing functionality** before moving to the next. -- This guide only changes: CSS, fonts, colors, layout, component visuals, animations. -- This guide never changes: API calls, data bindings, route logic, state management, backend endpoints. +- Keep interfaces clean, readable, and calm. +- Use only the approved palette: maroon, gold, black, white. +- Remove pixel-art and neo-brutalist patterns completely. +- Preserve all existing functionality and route behavior. ---- +## Hard Constraints +- Do not edit API calls, hooks, reducers, services, or route guards. +- Do not rename data fields or component props tied to runtime logic. +- Do not import style sheets across unrelated pages to borrow visuals. +- Do not use heavy borders, offset shadows, or pixel-like hover movement. -## 🎨 DESIGN SYSTEM FOUNDATION +## Visual Direction -> Read this section first. All phases below reference these tokens. +- Reference feel: quizlet.com/latest +- Tone: clean, modern, academic, organized. +- Layout: comfortable spacing, strong content hierarchy, minimal ornament. +- Iconography: simple, consistent line icons only. -### Style Identity -**Name:** `PixelBrute` — Neo-Brutalism structure with Pixel-Pop energy -**Vibe:** Bold, loud, fun, game-like. Think Kahoot meets a retro arcade cabinet. -**References:** -- Image 1: Pixel platformer game — chunky pixel fonts, bright sky palette, pixel sprites -- Image 2: Neon green NFT app — lime green background, white cards, bold black typography, playful doodle accents -- Image 3: Yellow/purple gaming page — electric yellow base, deep purple sections, rounded chunky elements, retro game controller icons +## Approved Palette ---- - -### 🔤 Typography System - -``` -DISPLAY FONT: "Press Start 2P" (Google Fonts) → use for scores, game over, question numbers, big headings -HEADING FONT: "Boogaloo" (Google Fonts) → use for question text, section titles, CTA buttons -BODY FONT: "Nunito" (Google Fonts) → use for answer options, descriptions, instructions -MONO/DATA FONT: "Share Tech Mono" (Google Fonts) → use for timers, countdowns, point values -``` - -**Google Fonts import (add to global CSS or index.html ``):** -```html - - -``` - -**Font Usage Rules:** -- `Press Start 2P` → max 3 sizes only: 48px (hero), 24px (score), 14px (labels). Never use for body text. -- `Boogaloo` → 32px headings, 22px subheadings, 18px buttons -- `Nunito` → 16px body, 14px captions. Weight 700 for answer options, 400 for descriptions -- `Share Tech Mono` → always uppercase, letter-spacing: 0.1em, for any number/timer display - ---- - -### 🎨 Color Palette +Use these values consistently: ```css :root { - /* === PRIMARY PALETTE === */ - --color-primary: #FFE500; /* Electric Yellow — main brand color, buttons, highlights */ - --color-secondary: #5B21FF; /* Deep Violet — headers, nav, large sections */ - --color-accent-pink: #FF2D78; /* Hot Pink — wrong answer states, alerts */ - --color-accent-green: #00E87A; /* Neon Green — correct answer states, success */ - --color-accent-orange: #FF6B00; /* Pixel Orange — streaks, bonuses, fire effects */ - - /* === NEUTRALS === */ - --color-black: #0D0D0D; /* Near-black — borders, text, shadows */ - --color-white: #FAFAF5; /* Warm white — card backgrounds */ - --color-cream: #FFF8DC; /* Cream — page backgrounds (light mode) */ - - /* === GAME STATE COLORS === */ - --color-correct: #00E87A; /* Correct answer */ - --color-wrong: #FF2D78; /* Wrong answer */ - --color-timer-ok: #FFE500; /* Timer — plenty of time */ - --color-timer-warn: #FF6B00; /* Timer — running low */ - --color-timer-danger: #FF2D78; /* Timer — critical */ - - /* === LEADERBOARD RANKS === */ - --color-rank-1: #FFD700; /* Gold */ - --color-rank-2: #C0C0C0; /* Silver */ - --color-rank-3: #CD7F32; /* Bronze */ - - /* === NEO-BRUTALISM SHADOW === */ - --shadow-brutal: 4px 4px 0px var(--color-black); - --shadow-brutal-lg: 6px 6px 0px var(--color-black); - --shadow-brutal-hover: 2px 2px 0px var(--color-black); - - /* === BORDER === */ - --border-brutal: 3px solid var(--color-black); - --border-radius-pixel: 4px; /* Slightly rounded for pixel feel */ - --border-radius-card: 8px; /* Cards */ - --border-radius-btn: 6px; /* Buttons */ - - /* === GRID BACKGROUND TEXTURE === */ - --bg-grid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Crect width='20' height='20' fill='none'/%3E%3Cpath d='M 20 0 L 0 0 0 20' fill='none' stroke='%230D0D0D' stroke-width='0.3' stroke-opacity='0.08'/%3E%3C/svg%3E"); + --color-maroon: #800020; + --color-maroon-dark: #5C0016; + --color-gold: #C9A84C; + --color-gold-light: #E8C97A; + --color-black: #111111; + --color-white: #FFFFFF; + --color-off-white: #F9F7F4; + --color-gray-mid: #B8B8B8; + --color-gray-dark: #6B6B6B; } ``` ---- - -### 🧱 Core Component Rules (apply to ALL phases) - -``` -1. BORDERS: All interactive elements use --border-brutal (3px solid black) -2. SHADOWS: All cards/buttons use --shadow-brutal. On hover: translate(-2px, -2px) + shadow grows -3. BUTTONS: Flat fill + border + shadow. NO gradients. Active state: translate(4px, 4px) shadow disappears -4. CARDS: White/cream background + thick border + offset shadow -5. BACKGROUNDS: Page bg = --color-cream with --bg-grid overlay (subtle grid like Image 3) -6. ANIMATIONS: CSS only. Pixel-jump keyframes for correct answers. Shake keyframe for wrong answers -7. NO GLASSMORPHISM, no blur effects, no soft shadows, no rounded corners above 8px -8. PIXEL ACCENTS: Use CSS box-shadow stacking to simulate pixel-art borders on key elements -``` - ---- - -## 🚦 PHASES OVERVIEW - -| Phase | Section | Est. Effort | -|-------|---------|-------------| -| Phase 0 | Global CSS Variables & Fonts | 1–2 hrs | -| Phase 1 | Navigation / Header Bar | 1–2 hrs | -| Phase 2 | Home / Landing Screen | 2–3 hrs | -| Phase 3 | Quiz Lobby / Room Screen | 1–2 hrs | -| Phase 4 | Question Card Component | 2–3 hrs | -| Phase 5 | Answer Option Buttons | 1–2 hrs | -| Phase 6 | Timer Component | 1 hr | -| Phase 7 | Score / Points Display | 1 hr | -| Phase 8 | Correct / Wrong Feedback States | 1–2 hrs | -| Phase 9 | Leaderboard Screen | 2 hrs | -| Phase 10 | Results / End Screen | 2 hrs | -| Phase 11 | Host Controls Panel | 2 hrs | -| Phase 12 | Micro-interactions & Final Polish | 2–3 hrs | - ---- - ---- - -# PHASE 0 — Global CSS Variables & Font Setup - -## 🎯 Goal -Inject the new design tokens system-wide WITHOUT touching any component logic. -This is the foundation. All other phases build on this. - -## ⚠️ Safety Rules -- Only edit: `global.css` / `index.css` / `app.css` (whichever is your global stylesheet) -- Do NOT edit any component files yet -- Do NOT change any class names that are referenced in JS/JSX/TS files -- Add variables ONLY inside `:root {}` — do not replace existing variables yet, add alongside them - -## 📋 Prompt to give your UI developer / AI - -``` -You are redesigning the global CSS foundation of a quiz app (similar to Kahoot/Quizlet). -Do NOT touch any component files, JS, TS, or backend files. -Only modify the global stylesheet. - -TASK: Add the following CSS custom properties to the :root {} block in the global stylesheet. -If a :root block already exists, ADD these variables inside it without removing existing ones. -Also add the Google Fonts import at the very top of the file. - -Add this import at the top: -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Boogaloo&family=Nunito:wght@400;700;900&family=Share+Tech+Mono&display=swap'); - -Add these variables inside :root: - --font-display: 'Press Start 2P', monospace; - --font-heading: 'Boogaloo', cursive; - --font-body: 'Nunito', sans-serif; - --font-mono: 'Share Tech Mono', monospace; - --color-primary: #FFE500; - --color-secondary: #5B21FF; - --color-accent-pink: #FF2D78; - --color-accent-green: #00E87A; - --color-accent-orange: #FF6B00; - --color-black: #0D0D0D; - --color-white: #FAFAF5; - --color-cream: #FFF8DC; - --color-correct: #00E87A; - --color-wrong: #FF2D78; - --color-timer-ok: #FFE500; - --color-timer-warn: #FF6B00; - --color-timer-danger: #FF2D78; - --color-rank-1: #FFD700; - --color-rank-2: #C0C0C0; - --color-rank-3: #CD7F32; - --shadow-brutal: 4px 4px 0px #0D0D0D; - --shadow-brutal-lg: 6px 6px 0px #0D0D0D; - --shadow-brutal-hover: 2px 2px 0px #0D0D0D; - --border-brutal: 3px solid #0D0D0D; - --border-radius-pixel: 4px; - --border-radius-card: 8px; - --border-radius-btn: 6px; - -Also add these global keyframe animations at the bottom of the global stylesheet: -@keyframes pixel-jump { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-12px); } -} -@keyframes pixel-shake { - 0%, 100% { transform: translateX(0); } - 20% { transform: translateX(-6px); } - 40% { transform: translateX(6px); } - 60% { transform: translateX(-4px); } - 80% { transform: translateX(4px); } -} -@keyframes pixel-pop { - 0% { transform: scale(1); } - 50% { transform: scale(1.15); } - 100% { transform: scale(1); } -} -@keyframes pixel-flash { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } -} -@keyframes float { - 0%, 100% { transform: translateY(0px); } - 50% { transform: translateY(-8px); } -} - -After making changes, verify the page still loads and all existing functionality works. -Report what file was changed and show the diff. -``` - ---- - ---- - -# PHASE 1 — Navigation / Header Bar - -## 🎯 Goal -Restyle the top navigation bar to match the PixelBrute design system. - -## ⚠️ Safety Rules -- Only change CSS/styles of the nav component -- Do NOT change navigation links, route paths, or onClick handlers -- Do NOT remove or rename any existing CSS class names used in JS -- Add new classes with prefix `pb-` (PixelBrute) to avoid conflicts - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the navigation/header bar of a quiz app. -Do NOT change any routing logic, links, or JavaScript. -Do NOT remove existing class names — only add new styling on top. -Reference style: Bold yellow top bar like Image 3 (gaming landing page), -thick black border on bottom, game-controller energy. - -TARGET LOOK: -- Background: var(--color-secondary) [deep violet #5B21FF] -- Bottom border: 4px solid var(--color-black) -- Box shadow: 0 4px 0 var(--color-black) -- Logo text: font-family var(--font-display), color var(--color-primary) [yellow], font-size 16px, text-shadow: 2px 2px 0 var(--color-black) -- Nav links: font-family var(--font-heading), color white, font-size 18px, uppercase -- Nav link hover: background var(--color-primary), color var(--color-black), padding 4px 10px, border: 2px solid var(--color-black), box-shadow: var(--shadow-brutal-hover), transition: all 0.1s -- Active nav link: background var(--color-primary), color var(--color-black) -- User avatar/profile pill: border: var(--border-brutal), box-shadow: var(--shadow-brutal), background white -- CTA button in nav (e.g., "Host Quiz", "Join"): background var(--color-primary), color var(--color-black), font-family var(--font-heading), border: var(--border-brutal), box-shadow: var(--shadow-brutal), border-radius: var(--border-radius-btn), padding: 8px 20px, font-size: 18px -- CTA button hover: transform translateX(-2px) translateY(-2px), box-shadow: var(--shadow-brutal-lg) -- CTA button active: transform translateX(4px) translateY(4px), box-shadow: none - -Keep the navbar fully responsive. On mobile: hamburger icon stays, just restyle it to use --color-primary color. -``` - ---- - ---- - -# PHASE 2 — Home / Landing Screen - -## 🎯 Goal -Redesign the main landing/home page hero section. - -## ⚠️ Safety Rules -- Only restyle the home page component's CSS -- Do NOT change any data fetching, routing links, or state logic -- Preserve all existing button onClick handlers — only restyle the buttons - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the home/landing page of a quiz app. -No logic changes. Only visual redesign. -Reference: Image 2 (neon green NFT app energy — big bold text, white cards on vivid background) -and Image 3 (yellow/purple gaming page — chunky layout, game icons, retro fun). - -TARGET LOOK FOR THE HOME PAGE: - -PAGE BACKGROUND: -- Background color: var(--color-cream) [#FFF8DC] -- Add subtle grid overlay using CSS background-image: - background-image: linear-gradient(rgba(13,13,13,0.06) 1px, transparent 1px), - linear-gradient(90deg, rgba(13,13,13,0.06) 1px, transparent 1px); - background-size: 24px 24px; - -HERO SECTION: -- Main heading (e.g., "Quiz Time!" or app name): - font-family: var(--font-display) - font-size: clamp(32px, 6vw, 72px) - color: var(--color-black) - text-shadow: 4px 4px 0 var(--color-secondary), 8px 8px 0 rgba(91,33,255,0.2) - line-height: 1.1 -- Sub-heading: font-family var(--font-heading), font-size 24px, color var(--color-secondary) -- Hero background section: var(--color-primary) [yellow], with the grid overlay on top -- Decorative pixel-art style border on the hero block: - border: 4px solid var(--color-black) - box-shadow: 8px 8px 0 var(--color-secondary) - -PRIMARY CTA BUTTON ("Create Quiz", "Join Game", "Start"): -- background: var(--color-secondary) -- color: var(--color-primary) -- font-family: var(--font-heading) -- font-size: 24px -- padding: 14px 36px -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-btn) -- hover: transform translate(-3px, -3px), box-shadow: 9px 9px 0 var(--color-black) -- active: transform translate(6px, 6px), box-shadow: none - -SECONDARY CTA BUTTON: -- background: var(--color-white) -- color: var(--color-black) -- Same border and shadow rules as primary -- hover: background var(--color-primary) - -FEATURE CARDS (if present on home): -- background: var(--color-white) -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-card) -- padding: 24px -- Card icon area: background var(--color-primary), padding 12px, border: 2px solid var(--color-black), display inline-block -- Card title: font-family var(--font-heading), font-size 22px -- Card body: font-family var(--font-body), font-size 15px -- Card hover: transform translate(-3px, -3px), box-shadow: 9px 9px 0 var(--color-black) - -Do not change any onClick, routing, or API calls. -``` - ---- - ---- - -# PHASE 3 — Quiz Lobby / Room Screen - -## 🎯 Goal -Restyle the "waiting room" or lobby where players join before a quiz starts. - -## ⚠️ Safety Rules -- Only restyle visuals -- Preserve: room code display logic, player join list data binding, countdown timer logic, host start button handler - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the quiz lobby/waiting room screen. -No logic, state, or data changes. Visual redesign only. -Vibe: Pixel arcade game lobby — like a retro game select screen. -Reference: Image 1 (pixel platformer) for the chunky bordered UI panels. - -ROOM CODE DISPLAY: -- Large display box: background var(--color-secondary), border: 4px solid var(--color-black) -- box-shadow: 8px 8px 0 var(--color-black) -- Room code text: font-family var(--font-display), font-size 36px, color var(--color-primary) -- Label above code ("Game PIN" or "Room Code"): font-family var(--font-heading), color white, font-size 18px, letter-spacing 0.1em, uppercase +Usage rules: -PLAYER LIST / AVATAR GRID: -- Each player tile: background var(--color-white), border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) -- border-radius: var(--border-radius-card) -- Player name: font-family var(--font-heading), font-size 16px, color var(--color-black) -- Avatar background: rotate through [var(--color-primary), var(--color-secondary), var(--color-accent-pink), var(--color-accent-green), var(--color-accent-orange)] based on player index % 5 -- New player joining animation: animation: pixel-pop 0.3s ease-out +- Maroon: primary actions, active states, key accents. +- Gold: highlights, chips, focus outlines, supportive accents. +- Black: primary text and icon strokes. +- White and off-white: cards and page backgrounds. -WAITING TEXT / STATUS: -- "Waiting for players..." : font-family var(--font-display), font-size 12px, color var(--color-secondary) -- Add blinking cursor effect: animation: pixel-flash 1s infinite +## Typography Rules -HOST START BUTTON: -- background: var(--color-accent-green) -- color: var(--color-black) -- font-family: var(--font-heading) -- font-size: 28px -- padding: 16px 48px -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-btn) -- hover: transform translate(-3px,-3px), box-shadow: 9px 9px 0 var(--color-black) +- Display and heading: clean geometric sans only. +- Body: highly readable sans at 14px to 16px base. +- Use strong hierarchy: + - Page title: 34px to 52px + - Section title: 20px to 28px + - Body text: 14px to 16px + - Metadata: 12px to 13px -PAGE BACKGROUND: var(--color-cream) with grid overlay (same as home page) +## Component Guidelines -Do not change any WebSocket connections, room join logic, player state, or host controls logic. -``` +Cards: ---- +- Border: 1px neutral border. +- Radius: 14px to 20px. +- Shadow: soft, single-direction modern shadow. +- No dashed comic borders except intentionally empty-state only. ---- +Buttons: -# PHASE 4 — Question Card Component +- Primary: maroon background, white text. +- Secondary: white background, neutral border. +- Radius: 10px to 12px. +- No jump-on-hover transforms. -## 🎯 Goal -Restyle the main question display card that shows during active quiz. +Status pills: -## ⚠️ Safety Rules -- Only restyle the question card component CSS -- Preserve: question text data binding, question number logic, image display (if any), media handling -- Do NOT change timer logic — only restyle the timer visuals (that comes in Phase 6) +- Keep compact and readable. +- Use soft tinted backgrounds with dark text. +- Maintain consistent sizing and spacing. -## 📋 Prompt to give your UI developer / AI +## Interaction Rules -``` -You are restyling ONLY the question card component in a quiz app. -No data or logic changes. -Reference: Image 3 (the purple game section card) + Image 1 (chunky pixel-bordered panels). +- Keep motion subtle and optional. +- Avoid dramatic hover animations. +- Focus visible state is required on all interactive elements. +- Prefer clarity over animation. -QUESTION CARD CONTAINER: -- background: var(--color-secondary) [deep violet] -- border: 4px solid var(--color-black) -- box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-card) -- padding: 32px 40px -- min-height: 180px -- position: relative -- max-width: 900px, centered +## What to Remove -QUESTION NUMBER BADGE (e.g., "Q 3"): -- position: absolute, top: -18px, left: 24px -- background: var(--color-primary) -- color: var(--color-black) -- font-family: var(--font-display) -- font-size: 12px -- padding: 6px 16px -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) +- Pixel fonts and retro/game-console type styles. +- Thick black borders and hard offset shadows. +- Brutalist push-button interactions. +- Mixed imported dashboard styles from other modules. +- Neon multi-color gradients outside the approved palette. -QUESTION TEXT: -- font-family: var(--font-heading) -- font-size: clamp(20px, 3vw, 34px) -- color: var(--color-white) -- line-height: 1.3 -- text-align: center +## Dashboard Implementation Checklist -CATEGORY / TAG BADGE (if present): -- background: var(--color-accent-pink) -- color: white -- font-family: var(--font-body) -- font-size: 12px -- font-weight: 700 -- padding: 3px 12px -- border: 2px solid var(--color-black) -- border-radius: 3px -- position: absolute, top: -18px, right: 24px +- Hero section with clear title, subtitle, and one primary action. +- Four compact KPI cards. +- Two-column content area: + - Quick actions panel + - Recent quizzes panel +- Empty state with concise copy and primary CTA. +- Mobile layout collapses to one column cleanly. -QUESTION IMAGE (if exists): -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) -- border-radius: var(--border-radius-pixel) -- max-height: 240px, object-fit: cover +## Prompt Template (For AI UI Tasks) -PAGE BACKGROUND DURING QUIZ: -- background: var(--color-cream) with grid overlay +Use this exact template when requesting redesign work: -Do not change any question data fetching, state, or timer logic. +```text +Redesign this page using a clean minimal Quizlet-inspired style. +Use only maroon, gold, black, and white palette tokens. +Remove all pixel-art and neo-brutalist styling patterns. +Keep all existing functionality exactly the same. +Do not modify backend logic, API hooks, or routing behavior. +Improve spacing, hierarchy, icon consistency, and readability. +Use subtle shadows, simple borders, and accessible focus-visible states. +Deliver updated component markup and CSS only. +After changes, run lint/build and report results. ``` ---- - ---- - -# PHASE 5 — Answer Option Buttons - -## 🎯 Goal -Restyle the 4 answer option buttons. This is one of the most impactful visual changes. - -## ⚠️ Safety Rules -- Only restyle the answer buttons CSS -- Preserve ALL onClick handlers, answer submission logic, disabled states logic, and correct/wrong class toggling -- If existing code adds classes like `.correct`, `.wrong`, `.selected` via JS — keep those class names, just add new styles for them - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the answer option buttons in a quiz app. -These are the 4 clickable answer choices shown during a quiz question. -Do NOT change onClick handlers, state, submission logic, or disabled states. -If existing JS adds classes like "correct", "wrong", "selected", "disabled" — preserve those class names exactly. - -ANSWER BUTTON BASE STYLE (all 4 options): -- display: flex, align-items: center, gap: 12px -- width: 100% -- padding: 16px 24px -- font-family: var(--font-heading) -- font-size: 20px -- font-weight: 700 (Nunito bold) -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) -- border-radius: var(--border-radius-btn) -- cursor: pointer -- transition: transform 0.08s, box-shadow 0.08s -- text-align: left -- position: relative -- overflow: hidden - -ANSWER OPTION COLOR VARIANTS (assign one per answer A/B/C/D): -- Option A: background #FF6B6B (coral red), color white -- Option B: background #4ECDC4 (teal), color var(--color-black) -- Option C: background #FFE500 (yellow), color var(--color-black) -- Option D: background #A855F7 (purple), color white - -ANSWER LETTER BADGE (A, B, C, D — shown on left side of button): -- width: 36px, height: 36px -- background: rgba(0,0,0,0.2) -- border: 2px solid rgba(0,0,0,0.3) -- border-radius: 4px -- font-family: var(--font-display) -- font-size: 11px -- display: flex, align-items: center, justify-content: center -- flex-shrink: 0 - -HOVER STATE (not yet answered): -- transform: translate(-2px, -2px) -- box-shadow: var(--shadow-brutal-lg) - -ACTIVE/CLICK STATE: -- transform: translate(4px, 4px) -- box-shadow: none - -SELECTED STATE (.selected class): -- border-width: 4px -- border-color: var(--color-black) -- brightness: 1.1 - -CORRECT STATE (.correct class — added by existing JS): -- background: var(--color-correct) [#00E87A] !important -- color: var(--color-black) !important -- border-color: var(--color-black) -- animation: pixel-jump 0.4s ease-out -- Add checkmark icon via ::after pseudo-element: content "✓", position absolute, right 16px, font-size 24px, font-weight 900 - -WRONG STATE (.wrong class — added by existing JS): -- background: var(--color-wrong) [#FF2D78] !important -- color: white !important -- animation: pixel-shake 0.4s ease-out -- Add X icon via ::after: content "✗", position absolute, right 16px, font-size 24px - -DISABLED STATE (.disabled class): -- opacity: 0.5 -- cursor: not-allowed -- pointer-events: none - -ANSWER GRID LAYOUT: -- Display as 2x2 grid on desktop: grid-template-columns: 1fr 1fr, gap: 16px -- On mobile: single column -- Max-width: 900px, centered - -Do not change any JS logic whatsoever. -``` - ---- - ---- - -# PHASE 6 — Timer Component - -## 🎯 Goal -Restyle the countdown timer shown during questions. - -## ⚠️ Safety Rules -- Only restyle CSS -- Preserve all timer logic, countdown state, and any JS that changes timer classes or values -- If JS changes a class on the timer based on time remaining, preserve those class names - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the timer/countdown component in a quiz app. -Do NOT change any timer logic, countdown calculations, or class-toggling JavaScript. - -TIMER CONTAINER: -- display: flex, align-items: center, justify-content: center -- width: 72px, height: 72px (circular) -- border: 4px solid var(--color-black) -- border-radius: 50% -- box-shadow: var(--shadow-brutal) -- background: var(--color-timer-ok) [yellow] — default state -- position: relative -- transition: background 0.3s - -TIMER NUMBER TEXT: -- font-family: var(--font-display) -- font-size: 20px -- color: var(--color-black) -- line-height: 1 - -WARNING STATE (when timer class changes to indicate low time — keep existing class names): -- background: var(--color-timer-warn) [#FF6B00] -- animation: pixel-flash 0.8s infinite - -DANGER STATE (critical time — keep existing class names): -- background: var(--color-timer-danger) [#FF2D78] -- color: white -- animation: pixel-flash 0.4s infinite -- transform: scale(1.1) - -PROGRESS BAR VERSION (if a bar-style timer exists): -- height: 16px -- background: var(--color-timer-ok) -- border: var(--border-brutal) -- border-radius: var(--border-radius-pixel) -- box-shadow: var(--shadow-brutal) -- The fill/progress element inside: transition: width 1s linear - -Do not change any timer countdown logic. -``` - ---- - ---- - -# PHASE 7 — Score / Points Display - -## 🎯 Goal -Restyle the score/points counter shown to players during the quiz. - -## ⚠️ Safety Rules -- Only restyle. Do NOT change score calculation logic or state management. - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the score/points display component. -No logic changes. Preserve all data bindings and state. - -SCORE DISPLAY CONTAINER: -- background: var(--color-black) -- border: 3px solid var(--color-primary) -- box-shadow: 4px 4px 0 var(--color-primary) -- padding: 8px 20px -- border-radius: var(--border-radius-card) -- display: inline-flex, align-items: center, gap: 10px - -SCORE LABEL ("SCORE", "PTS", "POINTS"): -- font-family: var(--font-display) -- font-size: 9px -- color: rgba(255,255,255,0.5) -- text-transform: uppercase -- letter-spacing: 0.15em - -SCORE NUMBER VALUE: -- font-family: var(--font-display) -- font-size: 22px -- color: var(--color-primary) -- transition: all 0.3s - -SCORE INCREASE ANIMATION (when points are added — trigger via JS adding a class): -- Create class .score-pop: animation: pixel-pop 0.3s ease-out - -STREAK BADGE (if streak counter exists): -- background: var(--color-accent-orange) -- color: white -- font-family: var(--font-heading) -- font-size: 14px -- padding: 2px 10px -- border: 2px solid var(--color-black) -- border-radius: 3px -- box-shadow: 2px 2px 0 var(--color-black) - -BONUS POINTS POPUP (floating +200, +BONUS text that appears briefly): -- position: absolute -- font-family: var(--font-display) -- font-size: 14px -- color: var(--color-accent-green) -- text-shadow: 2px 2px 0 var(--color-black) -- animation: float 1s ease-out forwards, then fade out -- pointer-events: none - -Do not change score calculation, bonus logic, or state management. -``` - ---- - ---- - -# PHASE 8 — Correct / Wrong Answer Feedback States - -## 🎯 Goal -Restyle the full-screen or overlay feedback shown after answering (correct/wrong flash screens). - -## ⚠️ Safety Rules -- Only restyle the feedback overlay/screen CSS -- Preserve: timing logic, transition to next question, score update calls - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the answer feedback overlay/screen in a quiz app. -This is the screen shown briefly after a player answers (correct or wrong). -Do NOT change timing, transitions to next question, or score update logic. -Reference: Image 1 (pixel game — chunky bordered feedback panels). - -CORRECT ANSWER OVERLAY / STATE: -- Background: var(--color-correct) [#00E87A] -- Add grid overlay on top (same grid as other pages) -- Big checkmark or "CORRECT!" heading: - font-family: var(--font-display) - font-size: clamp(28px, 5vw, 56px) - color: var(--color-black) - text-shadow: 4px 4px 0 rgba(0,0,0,0.2) -- Points earned display: font-family var(--font-display), font-size 24px, color var(--color-black) -- Animate entrance: scale from 0.5 to 1, animation: pixel-pop 0.3s ease-out - -WRONG ANSWER OVERLAY / STATE: -- Background: var(--color-wrong) [#FF2D78] -- "WRONG!" heading: same font rules, color white -- Correct answer shown below: font-family var(--font-heading), font-size 20px, background rgba(0,0,0,0.2), padding 12px 20px, border-radius 4px, color white -- Shake animation on load: animation: pixel-shake 0.4s ease-out - -SHARED FEEDBACK RULES: -- border: 4px solid var(--color-black) (if it's a card/modal, not full screen) -- box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-card) -- Transition in: animation: pixel-pop 0.2s ease-out - -TIME BONUS DISPLAY (if shown): -- font-family: var(--font-mono) -- font-size: 14px -- color: var(--color-black) -- opacity: 0.7 -- "SPEED BONUS +100" etc. - -Do not change any timing, auto-advance, or score update logic. -``` - ---- - ---- - -# PHASE 9 — Leaderboard Screen - -## 🎯 Goal -Restyle the mid-game or post-game leaderboard. - -## ⚠️ Safety Rules -- Only restyle. Preserve player data binding, rank sorting logic, and any animations triggered by JS rank changes. - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the leaderboard screen in a quiz app. -No logic changes. Preserve all data bindings and sorting logic. -Reference: Image 3 (gaming leaderboard energy) + Image 2 (bold stats display from NFT app). - -PAGE / SECTION BACKGROUND: -- Background: var(--color-secondary) [deep violet] -- Grid overlay (same as other pages but lighter: rgba(255,255,255,0.05)) - -LEADERBOARD TITLE ("LEADERBOARD", "TOP PLAYERS"): -- font-family: var(--font-display) -- font-size: clamp(20px, 4vw, 40px) -- color: var(--color-primary) -- text-shadow: 3px 3px 0 var(--color-black) -- text-align: center -- letter-spacing: 0.05em - -LEADERBOARD ROW (each player entry): -- background: var(--color-white) -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) -- border-radius: var(--border-radius-card) -- padding: 12px 20px -- display: flex, align-items: center, gap: 16px -- margin-bottom: 10px - -RANK NUMBER (#1, #2, #3): -- font-family: var(--font-display) -- font-size: 16px -- width: 40px, text-align: center -- #1: color var(--color-rank-1) [gold] -- #2: color var(--color-rank-2) [silver] -- #3: color var(--color-rank-3) [bronze] -- #4+: color var(--color-black), opacity 0.5 - -PLAYER AVATAR CIRCLE: -- width: 44px, height: 44px, border-radius: 50% -- border: 3px solid var(--color-black) -- Colors rotate: index % 5 maps to [primary, secondary, accent-pink, accent-green, accent-orange] -- Player initials: font-family var(--font-heading), font-size 18px, color white - -PLAYER NAME: -- font-family: var(--font-heading) -- font-size: 20px -- color: var(--color-black) -- flex: 1 - -PLAYER SCORE: -- font-family: var(--font-display) -- font-size: 14px -- color: var(--color-secondary) -- text-align: right - -#1 ROW SPECIAL STYLING: -- background: var(--color-primary) -- border-width: 4px -- box-shadow: var(--shadow-brutal-lg) -- Scale: transform scale(1.02) -- Rank number gets a crown emoji via ::before or a pixel crown SVG - -ROW ENTRANCE ANIMATION (stagger): -- Each row: animation: pixel-pop 0.2s ease-out -- Stagger with animation-delay: calc(var(--row-index) * 0.08s) -- Set --row-index via inline style on each row element - -Do not change player data fetching, rank sorting, or any real-time update logic. -``` - ---- - ---- - -# PHASE 10 — Results / End Screen - -## 🎯 Goal -Restyle the final results screen shown at the end of a quiz. - -## ⚠️ Safety Rules -- Only restyle. Preserve final score data, retry/replay logic, share functionality. - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the end/results screen of a quiz app. -No logic changes. Preserve all final score data, share buttons, and replay logic. -Vibe: Pixel game "GAME OVER / YOU WIN" screen. Reference: Image 1 directly. - -PAGE BACKGROUND: -- Full page: background var(--color-primary) [yellow] -- Grid overlay -- Consider a pixel-art style landscape silhouette at the bottom (CSS only, using box-shadows) - -MAIN RESULT HEADING ("QUIZ COMPLETE!", "GAME OVER", "YOU WIN!"): -- font-family: var(--font-display) -- font-size: clamp(28px, 6vw, 72px) -- color: var(--color-secondary) -- text-shadow: 6px 6px 0 var(--color-black), -2px -2px 0 var(--color-black) -- text-align: center -- animation: pixel-pop 0.5s ease-out - -FINAL SCORE DISPLAY: -- Large centered card: background var(--color-black), border: 4px solid var(--color-primary) -- box-shadow: 8px 8px 0 var(--color-secondary) -- border-radius: var(--border-radius-card) -- padding: 32px 48px -- Score number: font-family var(--font-display), font-size clamp(36px, 6vw, 80px), color var(--color-primary) -- "FINAL SCORE" label: font-family var(--font-display), font-size 10px, color white, opacity 0.5, letter-spacing 0.2em - -STATS ROW (accuracy, correct count, time, etc.): -- display grid, grid-template-columns repeat(3, 1fr), gap 16px -- Each stat cell: background var(--color-white), border var(--border-brutal), box-shadow var(--shadow-brutal), padding 16px, border-radius var(--border-radius-card), text-align center -- Stat number: font-family var(--font-display), font-size 22px, color var(--color-secondary) -- Stat label: font-family var(--font-body), font-size 12px, color var(--color-black), opacity 0.6, uppercase, letter-spacing 0.1em - -PLAY AGAIN BUTTON: -- background: var(--color-secondary) -- color: var(--color-primary) -- font-family: var(--font-heading), font-size: 26px -- padding: 14px 40px -- border: var(--border-brutal), box-shadow: var(--shadow-brutal-lg) -- border-radius: var(--border-radius-btn) -- hover: transform translate(-3px,-3px), box-shadow 9px 9px 0 var(--color-black) - -SHARE BUTTON (secondary): -- background: var(--color-white) -- color: var(--color-black) -- Same border/shadow rules - -HOME BUTTON (tertiary): -- background: transparent -- color: var(--color-black) -- border: var(--border-brutal) -- box-shadow: var(--shadow-brutal) - -RANK/MEDAL BADGE (if player rank is shown): -- font-family: var(--font-display) -- #1 badge: background gold, black border, black text, font-size 12px, padding 8px 20px, box-shadow var(--shadow-brutal) - -Do not change any score calculation, share API, or routing logic. -``` - ---- - ---- - -# PHASE 11 — Host Controls Panel - -## 🎯 Goal -Restyle the host dashboard/controls panel (the screen only the quiz host/teacher sees). - -## ⚠️ Safety Rules -- Only restyle. Preserve all host control logic: next question, pause, kick player, end quiz, show answers. -- Do NOT rename button IDs or classes used in event listeners. - -## 📋 Prompt to give your UI developer / AI - -``` -You are restyling ONLY the host controls panel / dashboard of a quiz app. -No logic changes. Preserve all control button handlers, player management logic, and real-time state. - -HOST PANEL LAYOUT: -- Sidebar or top panel: background var(--color-black), border-right or border-bottom 4px solid var(--color-primary) -- Control section label: font-family var(--font-display), font-size 9px, color var(--color-primary), letter-spacing 0.2em, uppercase - -NEXT QUESTION BUTTON (primary host action): -- background: var(--color-accent-green) -- color: var(--color-black) -- font-family: var(--font-heading), font-size: 22px -- padding: 12px 32px -- border: var(--border-brutal), box-shadow: var(--shadow-brutal) -- border-radius: var(--border-radius-btn) -- hover/active: same brutal press effect as other buttons - -SHOW ANSWER / REVEAL BUTTON: -- background: var(--color-primary) -- color: var(--color-black) -- Same styling rules - -PAUSE BUTTON: -- background: var(--color-accent-orange) -- color: white -- Same styling rules - -END QUIZ BUTTON: -- background: var(--color-accent-pink) -- color: white -- Same styling rules -- Requires confirm dialog before firing — do NOT remove the confirmation logic - -CONNECTED PLAYERS COUNT: -- font-family: var(--font-display), font-size: 14px, color: var(--color-primary) -- Label: "PLAYERS ONLINE", font-family var(--font-display), font-size 8px, color rgba(255,255,255,0.4) - -PLAYER LIST IN HOST VIEW: -- Each player row: background rgba(255,255,255,0.05), border-bottom 1px solid rgba(255,255,255,0.08) -- Player name: font-family var(--font-body), color white -- Player score: font-family var(--font-mono), color var(--color-primary), text-align right -- Kick button: small, background transparent, color var(--color-accent-pink), border 1px solid var(--color-accent-pink), font-size 11px - -Do not touch any WebSocket, player kick logic, or game state management. -``` - ---- - ---- - -# PHASE 12 — Micro-interactions & Final Polish - -## 🎯 Goal -Add the final layer of polish: hover effects, transitions, loading states, empty states, and overall cohesion check. - -## ⚠️ Safety Rules -- CSS only additions. No logic changes. -- Test on mobile, tablet, and desktop before finalizing. - -## 📋 Prompt to give your UI developer / AI - -``` -You are doing FINAL POLISH on the quiz app UI after all components have been restyled. -This phase is CSS-only micro-interactions and consistency fixes. -No logic changes whatsoever. - -TASKS: - -1. BUTTON CONSISTENCY AUDIT: - - Every button in the app must have: border var(--border-brutal), box-shadow var(--shadow-brutal) - - Every button must have: hover → translate(-2px,-2px) + larger shadow - - Every button must have: active → translate(4px,4px) + box-shadow none - - Check all buttons have font-family var(--font-heading) or var(--font-display) - -2. LOADING STATES: - - Any loading spinner: replace with a blinking pixel-art style using CSS box-shadows - - Loading text: font-family var(--font-display), font-size 12px, animation: pixel-flash 1s infinite - - Skeleton loaders: background repeating-linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%), background-size 200% 100%, animation: shimmer 1.5s infinite - -3. EMPTY STATES: - - No results / empty leaderboard: font-family var(--font-heading), color var(--color-secondary), font-size 20px - - Add a simple pixel-art dashed border box around empty state messages - -4. SCROLLBAR STYLING (webkit): - ::-webkit-scrollbar { width: 8px; } - ::-webkit-scrollbar-track { background: var(--color-cream); border-left: 1px solid var(--color-black); } - ::-webkit-scrollbar-thumb { background: var(--color-secondary); border: 2px solid var(--color-black); } - -5. FOCUS STATES (accessibility): - *:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; } - -6. PAGE TRANSITIONS: - - On route change: add a class that does a quick fade (opacity 0 to 1, 150ms) - -7. PIXEL DECORATIVE ACCENTS: - - Add 4–6 small decorative pixel stars/sparkles using CSS ::before/::after on section headings - - Use box-shadow stacking on small elements to create pixel-art style accents - -8. MOBILE RESPONSIVE FINAL CHECK: - - All grid-template-columns on mobile: 1 column - - Font sizes: use clamp() for all display fonts - - Touch targets: minimum 44px height for all interactive elements - - Test that brutal shadows don't clip on small screens (reduce shadow from 6px to 3px on mobile) - -9. COHESION CHECK — find and fix any elements that still look "default/unstyled": - - Input fields: border var(--border-brutal), box-shadow var(--shadow-brutal), font-family var(--font-body) - - Select dropdowns: same border/shadow rules - - Modals/Dialogs: background var(--color-white), border: 4px solid var(--color-black), box-shadow: 8px 8px 0 var(--color-secondary) - - Tooltips: background var(--color-black), color white, font-family var(--font-body), font-size 12px, padding 4px 10px, border-radius 3px - -Do not change any functionality. -``` - ---- - ---- - -## ✅ POST-REDESIGN CHECKLIST - -After all phases are complete, run through this checklist: - -``` -VISUAL -[ ] All buttons have brutal border + shadow + press effect -[ ] Color palette is consistent across all screens -[ ] Press Start 2P font used only for scores, numbers, big headings -[ ] Boogaloo used for questions, body headings, buttons -[ ] Nunito used for body text and answer options -[ ] Share Tech Mono used for timers and data values -[ ] No default browser styles leaking through (no blue outline links, no default selects) -[ ] Grid background texture visible on all page backgrounds - -FUNCTIONALITY -[ ] Quiz can be created and started ✓ -[ ] Players can join via room code ✓ -[ ] Questions display correctly with data ✓ -[ ] Answering works and submits to backend ✓ -[ ] Score updates correctly ✓ -[ ] Leaderboard updates in real-time ✓ -[ ] Host controls all work ✓ -[ ] End screen shows correct final data ✓ -[ ] All API calls work (check Network tab — no 4xx/5xx errors) ✓ -[ ] No console errors ✓ - -RESPONSIVE -[ ] Mobile (375px): all screens usable ✓ -[ ] Tablet (768px): layout correct ✓ -[ ] Desktop (1280px+): layout correct ✓ - -PERFORMANCE -[ ] Google Fonts loaded (check Network tab) ✓ -[ ] No layout shift from font loading (add font-display: swap to import) ✓ -[ ] Animations don't cause jank (check FPS in DevTools) ✓ -``` - ---- - -## 🚫 WHAT THIS GUIDE NEVER TOUCHES - -``` -❌ Backend routes / controllers / services -❌ Database schemas or queries -❌ API endpoints -❌ WebSocket / real-time connection logic -❌ Authentication / session logic -❌ Score calculation algorithms -❌ Timer countdown logic (only visuals) -❌ Player join / kick logic (only visuals) -❌ Any .env or configuration files -❌ Any test files -``` - ---- - -## 📎 REFERENCE IMAGES SUMMARY - -| Image | Key Elements to Extract | -|-------|------------------------| -| Image 1 (Pixel Platformer) | Chunky pixel-art border panels, orange/yellow text with dark outline, heart-based health bars, pixel button style, green landscape background | -| Image 2 (NFT App) | Lime green background energy, clean white cards, bold black sans-serif headings, colorful pastel illustration accents, stat display layout | -| Image 3 (Gaming Page) | Electric yellow page background, deep purple content sections, white card containers, retro game controller icons, bold chunky typography mix | +## QA Acceptance Criteria -**Combined direction = PixelBrute:** -Yellow + Violet + Pink + Green + Orange palette. -Thick black borders everywhere. -Offset box shadows (brutalist). -Pixel-art display font for scores and headings. -Rounded but bold body font for readability. -Grid background texture throughout. -Game-state feedback (correct/wrong) with full-color flash states. +- Page looks clean and modern, not game-like. +- No pixel or neo-brutalist visuals remain. +- Color usage follows maroon/gold/black/white system. +- Buttons, cards, and pills are visually consistent. +- No broken routes, actions, or data rendering. +- Build passes successfully. ---- +## Change Log -*End of QUIZ_REDESIGN_PROMPT_GUIDE.md* -*This file is designed to be read by an AI assistant or human UI developer as a sequential redesign instruction set.* +- v2.0: Replaced old pixel and neo-brutalist system with clean minimalist system and strict palette guidance. diff --git a/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/controllers/AccessController.java b/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/controllers/AccessController.java index 1b9efe8..88da10f 100644 --- a/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/controllers/AccessController.java +++ b/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/controllers/AccessController.java @@ -4,6 +4,7 @@ import com.intelliquiz.api.auth.internal.application.services.AccessResolutionService; import com.intelliquiz.api.auth.internal.presentation.dto.request.AccessCodeRequest; import com.intelliquiz.api.auth.internal.presentation.dto.request.PublicJoinRequest; +import com.intelliquiz.api.auth.internal.presentation.dto.request.UpdateTeamNameRequest; import com.intelliquiz.api.auth.internal.presentation.dto.response.AccessResolutionResponse; import com.intelliquiz.api.quiz.QuizFacade; import com.intelliquiz.api.quiz.dto.QuizInfoDto; @@ -235,4 +236,26 @@ public ResponseEntity joinPublicQuiz( return ResponseEntity.ok(response); } + + /** + * Updates a team's name (used for setting avatars). + * Requires the team's access code for verification. + */ + @PutMapping("/teams/{teamId}/name") + @Operation( + summary = "Update team name", + description = "Updates a team's name. Requires the team's access code for verification. Useful for setting avatars in names." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Name updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid request or access code"), + @ApiResponse(responseCode = "404", description = "Team not found") + }) + public ResponseEntity updateTeamName( + @PathVariable Long teamId, + @Valid @RequestBody UpdateTeamNameRequest request) { + + teamFacade.updateTeamNameWithAccessCode(teamId, request.name(), request.accessCode()); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/dto/request/UpdateTeamNameRequest.java b/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/dto/request/UpdateTeamNameRequest.java new file mode 100644 index 0000000..8a3eb78 --- /dev/null +++ b/backend/src/main/java/com/intelliquiz/api/auth/internal/presentation/dto/request/UpdateTeamNameRequest.java @@ -0,0 +1,21 @@ +package com.intelliquiz.api.auth.internal.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * Request DTO for updating a team's name with access code verification. + */ +@Schema(description = "Request body for updating team name") +public record UpdateTeamNameRequest( + @Schema(description = "New name for the team (can include avatar encoding)", example = "Team Alpha|avatar-1.png", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "Name is required") + @Size(max = 100, message = "Name must not exceed 100 characters") + String name, + + @Schema(description = "Access code of the team for verification", example = "ABC123", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "Access code is required") + String accessCode +) { +} diff --git a/backend/src/main/java/com/intelliquiz/api/realtime/internal/presentation/controllers/QuizSSEController.java b/backend/src/main/java/com/intelliquiz/api/realtime/internal/presentation/controllers/QuizSSEController.java index 010710c..8e403ef 100644 --- a/backend/src/main/java/com/intelliquiz/api/realtime/internal/presentation/controllers/QuizSSEController.java +++ b/backend/src/main/java/com/intelliquiz/api/realtime/internal/presentation/controllers/QuizSSEController.java @@ -287,6 +287,24 @@ private Object buildGameStatePayload(Long quizId, GameState gameState, Integer q ? QuestionPayload.fromDto(orderedQuestions.get(safeQuestionIndex)) : null; + // Calculate current rankings for the initial snapshot + List leaderboard = teamFacade.getTeamsByQuiz(quizId).stream() + .sorted(Comparator.comparingInt(com.intelliquiz.api.team.dto.TeamInfoDto::totalScore).reversed()) + .toList(); + + List rankings = new ArrayList<>(); + int rank = 1; + for (com.intelliquiz.api.team.dto.TeamInfoDto team : leaderboard) { + final int itemRank = rank++; + rankings.add(new Object() { + public final Long teamId = team.id(); + public final String teamName = team.name(); + public final Integer totalScore = team.totalScore(); + public final Integer score = team.totalScore(); + public final Integer rank = itemRank; + }); + } + return new Object() { public final String state = serializedState; public final String gameState = serializedState; @@ -296,6 +314,8 @@ private Object buildGameStatePayload(Long quizId, GameState gameState, Integer q public final QuestionPayload currentQuestion = currentQuestionPayload; public final Boolean participantNavigationEnabled = quizSessionManager.isParticipantNavigationEnabled(quizId); public final Boolean timerActive = timerService.isTimerActive(quizId); + public final java.util.List teamResults = rankings; + public final java.util.List rankingsData = rankings; public final java.time.LocalDateTime timestamp = java.time.LocalDateTime.now(); }; } diff --git a/backend/src/main/java/com/intelliquiz/api/team/TeamFacade.java b/backend/src/main/java/com/intelliquiz/api/team/TeamFacade.java index 39a29c4..7c5bda9 100644 --- a/backend/src/main/java/com/intelliquiz/api/team/TeamFacade.java +++ b/backend/src/main/java/com/intelliquiz/api/team/TeamFacade.java @@ -100,6 +100,13 @@ public void bindDeviceId(Long teamId, String deviceId) { }); } + /** + * Updates a team's name if the provided access code matches. + */ + public void updateTeamNameWithAccessCode(Long teamId, String newName, String accessCode) { + teamRegistrationService.updateTeamNameWithAccessCode(teamId, newName, accessCode); + } + private TeamInfoDto toDto(Team team) { return new TeamInfoDto( team.getId(), diff --git a/backend/src/main/java/com/intelliquiz/api/team/internal/application/services/TeamRegistrationService.java b/backend/src/main/java/com/intelliquiz/api/team/internal/application/services/TeamRegistrationService.java index a175982..a43ca8e 100644 --- a/backend/src/main/java/com/intelliquiz/api/team/internal/application/services/TeamRegistrationService.java +++ b/backend/src/main/java/com/intelliquiz/api/team/internal/application/services/TeamRegistrationService.java @@ -126,6 +126,24 @@ public Team updateTeamName(Long teamId, String newName) { return teamRepository.save(team); } + /** + * Updates a team's name if the provided access code matches. + * Used by participants to update their profile (e.g. adding an avatar). + */ + public Team updateTeamNameWithAccessCode(Long teamId, String newName, String accessCode) { + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new EntityNotFoundException("Team", teamId)); + + if (!team.getAccessCode().equals(accessCode)) { + throw new IllegalArgumentException("Invalid access code"); + } + + assertQuizNotArchived(team.getQuizId()); + + team.setName(newName); + return teamRepository.save(team); + } + private void assertQuizNotArchived(Long quizId) { var quiz = quizFacade.findQuizInfo(quizId) .orElseThrow(() -> new EntityNotFoundException("Quiz", quizId)); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index c7b6e45..6d21fce 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -26,7 +26,7 @@ spring.jpa.open-in-view=false spring.jpa.hibernate.ddl-auto=${SPRING_JPA_HIBERNATE_DDL_AUTO:update} spring.jpa.show-sql=false -server.port=${SERVER_PORT:8082} +server.port=${SERVER_PORT:8090} # SSL Configuration (disabled - HTTPS handled by frontend/reverse proxy) server.ssl.enabled=${SSL_ENABLED:false} diff --git a/frontend/intelliquiz-frontend/build.out b/frontend/intelliquiz-frontend/build.out new file mode 100644 index 0000000..60500b1 --- /dev/null +++ b/frontend/intelliquiz-frontend/build.out @@ -0,0 +1,18 @@ + +> intelliquiz-frontend@0.0.0 build +> tsc -b && vite build + +vite v7.3.1 building client environment for production... +transforming... +✓ 1821 modules transformed. +rendering chunks... +computing gzip size... +dist/index.html  0.48 kB │ gzip: 0.31 kB +dist/assets/index-b6b0w2t4.css 230.57 kB │ gzip: 39.17 kB +dist/assets/index-CWcYmhFj.js 596.26 kB │ gzip: 167.28 kB + +(!) Some chunks are larger than 500 kB after minification. Consider: +- Using dynamic import() to code-split the application +- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks +- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit. +✓ built in 6.12s diff --git a/frontend/intelliquiz-frontend/package.json b/frontend/intelliquiz-frontend/package.json index 617a15c..d74f952 100644 --- a/frontend/intelliquiz-frontend/package.json +++ b/frontend/intelliquiz-frontend/package.json @@ -14,11 +14,15 @@ "dependencies": { "@tanstack/react-query": "^5.90.16", "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-icons": "^5.5.0", - "react-router-dom": "^7.1.0" + "react-router-dom": "^7.1.0", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/frontend/intelliquiz-frontend/public/avatars/10th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/10th-avatar.png new file mode 100644 index 0000000..3357697 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/10th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/1st-avatar.png b/frontend/intelliquiz-frontend/public/avatars/1st-avatar.png new file mode 100644 index 0000000..89c2af2 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/1st-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/2nd-avatar.png b/frontend/intelliquiz-frontend/public/avatars/2nd-avatar.png new file mode 100644 index 0000000..682d5a7 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/2nd-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/3rd-avatar.png b/frontend/intelliquiz-frontend/public/avatars/3rd-avatar.png new file mode 100644 index 0000000..08debab Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/3rd-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/4th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/4th-avatar.png new file mode 100644 index 0000000..b7b22f6 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/4th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/5th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/5th-avatar.png new file mode 100644 index 0000000..9911d7a Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/5th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/6th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/6th-avatar.png new file mode 100644 index 0000000..6ba81d2 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/6th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/7th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/7th-avatar.png new file mode 100644 index 0000000..6fc98c1 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/7th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/8th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/8th-avatar.png new file mode 100644 index 0000000..2f92f52 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/8th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/avatars/9th-avatar.png b/frontend/intelliquiz-frontend/public/avatars/9th-avatar.png new file mode 100644 index 0000000..0a14b13 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/avatars/9th-avatar.png differ diff --git a/frontend/intelliquiz-frontend/public/effects/streak-fire.png b/frontend/intelliquiz-frontend/public/effects/streak-fire.png new file mode 100644 index 0000000..b7bec27 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/effects/streak-fire.png differ diff --git a/frontend/intelliquiz-frontend/public/host-lobby.mp3 b/frontend/intelliquiz-frontend/public/host-lobby.mp3 new file mode 100644 index 0000000..975fb33 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/host-lobby.mp3 differ diff --git a/frontend/intelliquiz-frontend/public/landing-page sounds.mp3 b/frontend/intelliquiz-frontend/public/landing-page sounds.mp3 new file mode 100644 index 0000000..aa23091 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/landing-page sounds.mp3 differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/README.txt b/frontend/intelliquiz-frontend/public/lobby-backgrounds/README.txt new file mode 100644 index 0000000..0778950 --- /dev/null +++ b/frontend/intelliquiz-frontend/public/lobby-backgrounds/README.txt @@ -0,0 +1,7 @@ +Place your background images here. +Supported filenames: +- bg-1.jpg +- bg-2.jpg +- bg-3.jpg +- bg-4.jpg +- bg-5.jpg diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-1.png b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-1.png new file mode 100644 index 0000000..2a35126 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-1.png differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-2.jpg b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-2.jpg new file mode 100644 index 0000000..01313f7 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-2.jpg differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-3.jpg b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-3.jpg new file mode 100644 index 0000000..aaca496 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-3.jpg differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-4.png b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-4.png new file mode 100644 index 0000000..d738610 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-4.png differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-5.png b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-5.png new file mode 100644 index 0000000..5d7b7cd Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-5.png differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-6.png b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-6.png new file mode 100644 index 0000000..825bf07 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-6.png differ diff --git a/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-7.jpg b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-7.jpg new file mode 100644 index 0000000..00e9b39 Binary files /dev/null and b/frontend/intelliquiz-frontend/public/lobby-backgrounds/bg-7.jpg differ diff --git a/frontend/intelliquiz-frontend/public/sounds/.gitkeep b/frontend/intelliquiz-frontend/public/sounds/.gitkeep new file mode 100644 index 0000000..efca90f --- /dev/null +++ b/frontend/intelliquiz-frontend/public/sounds/.gitkeep @@ -0,0 +1 @@ +# Sound files will be placed here diff --git a/frontend/intelliquiz-frontend/src/components/admin/AdminLayout.tsx b/frontend/intelliquiz-frontend/src/components/admin/AdminLayout.tsx index f6968a5..6c7ebe6 100644 --- a/frontend/intelliquiz-frontend/src/components/admin/AdminLayout.tsx +++ b/frontend/intelliquiz-frontend/src/components/admin/AdminLayout.tsx @@ -1,11 +1,11 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import { - BiHomeAlt, - BiBookOpen, - BiLogOut, - BiChevronDown, -} from 'react-icons/bi'; + Home, + BookOpen, + LogOut, + ChevronDown, +} from 'lucide-react'; import '../../styles/admin.css'; import { authApi, currentUserApi } from '../../services/api'; import { useAuth } from '../../contexts/AuthContext'; @@ -28,8 +28,8 @@ export default function AdminLayout() { // Build nav items based on user's permissions const navItems: NavItem[] = [ - { path: '/admin', label: 'Dashboard', icon: }, - { path: '/admin/quizzes', label: 'My Quizzes', icon: }, + { path: '/admin', label: 'Dashboard', icon: }, + { path: '/admin/quizzes', label: 'My Quizzes', icon: }, ]; useEffect(() => { @@ -111,12 +111,8 @@ export default function AdminLayout() { }; return ( -
- - - {/* Top Navigation - Same as SuperAdmin */} +
+ {/* Top Navigation */}
{/* Logo */} @@ -125,7 +121,7 @@ export default function AdminLayout() { className="pb-nav-logo-wrap" >
- +
IntelliQuiz
@@ -159,7 +155,7 @@ export default function AdminLayout() {
{username || 'Admin'}
Admin
- + {profileDropdownOpen && ( @@ -168,7 +164,7 @@ export default function AdminLayout() { onClick={handleLogout} className="pb-nav-dropdown-item" > - + Logout
@@ -179,7 +175,7 @@ export default function AdminLayout() { {/* Main Content */} -
+
diff --git a/frontend/intelliquiz-frontend/src/components/game/QuestionDisplay.tsx b/frontend/intelliquiz-frontend/src/components/game/QuestionDisplay.tsx index 5223fea..bfe2966 100644 --- a/frontend/intelliquiz-frontend/src/components/game/QuestionDisplay.tsx +++ b/frontend/intelliquiz-frontend/src/components/game/QuestionDisplay.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { CheckCircle, XCircle, Check } from 'lucide-react'; import type { QuestionData } from '../../services/api'; interface QuestionDisplayProps { @@ -30,9 +31,12 @@ const QuestionDisplay: React.FC = ({ ? (question.options.length >= 2 ? question.options.slice(0, 2) : ['True', 'False']) : question.options; - const getOptionClass = (option: string) => { - const isSelected = selectedOption === option; - const isCorrect = correctAnswer === option; + const normalize = (val: string | null | undefined) => (val || '').trim().toLowerCase(); + + const getOptionClass = (option: string, letter: string) => { + const normSelected = normalize(selectedOption); + const isSelected = normSelected === normalize(option) || normSelected === letter.toLowerCase(); + const isCorrect = showCorrectAnswer && (normalize(correctAnswer) === normalize(option) || normalize(correctAnswer) === letter.toLowerCase()); const isWrong = showCorrectAnswer && isSelected && !isCorrect; let classes = `${prefix}-answer-btn`; @@ -60,39 +64,45 @@ const QuestionDisplay: React.FC = ({
{/* Question Header */}
-

- Question {questionNumber} of {totalQuestions} -

- {question.points} pts +
+ Question + {questionNumber} + / {totalQuestions} +
+
+ {question.points} + PTS +
- {/* Question Text */} -
-

- {question.text} -

+ {/* Question Card */} +
+
+

+ {question.text} +

+
+
{/* Answer UI */} {questionType === 'IDENTIFICATION' ? ( -
- !disabled && onSelectOption?.(e.target.value)} - placeholder="Type your answer" - disabled={disabled} - className={`${prefix}-answer-btn`} - style={{ - width: '100%', - cursor: disabled ? 'not-allowed' : 'text', - textAlign: 'left', - padding: '14px 16px', - }} - /> +
+
+ !disabled && onSelectOption?.(e.target.value)} + placeholder="Type your answer here..." + disabled={disabled} + className={`${prefix}-answer-input`} + /> +
+
{showCorrectAnswer && correctAnswer && ( -
-

Accepted Answer: {correctAnswer}

+
+
Accepted Answer
+
{correctAnswer}
)}
@@ -100,15 +110,18 @@ const QuestionDisplay: React.FC = ({
{optionList.map((option, index) => { const letter = String.fromCharCode(65 + index); - const isSelected = selectedOption === option; - const isCorrect = showCorrectAnswer && correctAnswer === option; + const normSelected = normalize(selectedOption); + const isSelected = normSelected === normalize(option) || normSelected === letter.toLowerCase(); + + const normCorrect = normalize(correctAnswer); + const isCorrect = showCorrectAnswer && (normCorrect === normalize(option) || normCorrect === letter.toLowerCase()); return ( ); })} diff --git a/frontend/intelliquiz-frontend/src/components/game/ResultEffects.tsx b/frontend/intelliquiz-frontend/src/components/game/ResultEffects.tsx new file mode 100644 index 0000000..97e51c5 --- /dev/null +++ b/frontend/intelliquiz-frontend/src/components/game/ResultEffects.tsx @@ -0,0 +1,241 @@ +import React, { useEffect, useRef } from 'react'; + +// ======== CONFETTI CANVAS (for correct answers) ======== +export const ConfettiCanvas: React.FC<{ duration?: number }> = ({ duration = 3000 }) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const W = window.innerWidth; + const H = window.innerHeight; + canvas.width = W * dpr; + canvas.height = H * dpr; + ctx.scale(dpr, dpr); + + const COLORS = [ + '#d4af37', '#fbbf24', '#10b981', '#ef4444', '#3b82f6', + '#f472b6', '#a78bfa', '#fde68a', '#7a1733', '#22d3ee', + ]; + + type Particle = { + x: number; y: number; w: number; h: number; + vx: number; vy: number; gravity: number; + rotation: number; rotSpeed: number; + color: string; alpha: number; + shape: 'rect' | 'circle' | 'star'; + }; + + const particles: Particle[] = []; + const shapes: Particle['shape'][] = ['rect', 'circle', 'star']; + + for (let i = 0; i < 150; i++) { + particles.push({ + x: Math.random() * W, + y: -Math.random() * H * 0.5, + w: Math.random() * 10 + 4, + h: Math.random() * 6 + 4, + vx: (Math.random() - 0.5) * 8, + vy: Math.random() * 4 + 2, + gravity: 0.08 + Math.random() * 0.05, + rotation: Math.random() * Math.PI * 2, + rotSpeed: (Math.random() - 0.5) * 0.15, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + alpha: 1, + shape: shapes[Math.floor(Math.random() * shapes.length)], + }); + } + + const drawStar = (cx: number, cy: number, r: number) => { + ctx.beginPath(); + for (let i = 0; i < 10; i++) { + const rad = i % 2 === 0 ? r : r * 0.4; + const angle = (Math.PI / 5) * i - Math.PI / 2; + ctx.lineTo(cx + rad * Math.cos(angle), cy + rad * Math.sin(angle)); + } + ctx.closePath(); + ctx.fill(); + }; + + let animId = 0; + const startTime = Date.now(); + + const loop = () => { + const elapsed = Date.now() - startTime; + const fadeRatio = elapsed > duration * 0.6 ? 1 - (elapsed - duration * 0.6) / (duration * 0.4) : 1; + if (elapsed > duration) { + ctx.clearRect(0, 0, W, H); + return; + } + + ctx.clearRect(0, 0, W, H); + + particles.forEach((p) => { + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(p.rotation); + ctx.globalAlpha = Math.max(0, p.alpha * fadeRatio); + ctx.fillStyle = p.color; + + if (p.shape === 'rect') { + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + } else if (p.shape === 'circle') { + ctx.beginPath(); + ctx.arc(0, 0, p.w / 2, 0, Math.PI * 2); + ctx.fill(); + } else { + drawStar(0, 0, p.w / 2); + } + + ctx.restore(); + + p.x += p.vx; + p.vy += p.gravity; + p.y += p.vy; + p.rotation += p.rotSpeed; + p.vx *= 0.99; + }); + + animId = requestAnimationFrame(loop); + }; + loop(); + + return () => cancelAnimationFrame(animId); + }, [duration]); + + return ( + + ); +}; + +// ======== ERROR PARTICLES (for wrong answers) ======== +export const ErrorParticles: React.FC<{ duration?: number }> = ({ duration = 2000 }) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const W = window.innerWidth; + const H = window.innerHeight; + canvas.width = W * dpr; + canvas.height = H * dpr; + ctx.scale(dpr, dpr); + + type Shard = { + x: number; y: number; size: number; + vx: number; vy: number; + rotation: number; rotSpeed: number; + color: string; alpha: number; + }; + + const shards: Shard[] = []; + const colors = ['#ef4444', '#dc2626', '#b91c1c', '#991b1b', '#7f1d1d', '#fca5a5', '#f87171']; + + const cx = W / 2; + const cy = H / 2; + + for (let i = 0; i < 60; i++) { + const angle = Math.random() * Math.PI * 2; + const speed = Math.random() * 6 + 3; + shards.push({ + x: cx, + y: cy, + size: Math.random() * 8 + 3, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + rotation: Math.random() * Math.PI * 2, + rotSpeed: (Math.random() - 0.5) * 0.2, + color: colors[Math.floor(Math.random() * colors.length)], + alpha: 1, + }); + } + + let animId = 0; + const startTime = Date.now(); + + const loop = () => { + const elapsed = Date.now() - startTime; + if (elapsed > duration) { + ctx.clearRect(0, 0, W, H); + return; + } + + ctx.clearRect(0, 0, W, H); + + // Shockwave ring + const ringProgress = Math.min(elapsed / 600, 1); + if (ringProgress < 1) { + const ringRadius = ringProgress * Math.min(W, H) * 0.3; + ctx.beginPath(); + ctx.arc(cx, cy, ringRadius, 0, Math.PI * 2); + ctx.strokeStyle = `rgba(239, 68, 68, ${0.4 * (1 - ringProgress)})`; + ctx.lineWidth = 4 * (1 - ringProgress); + ctx.stroke(); + } + + // Shards + shards.forEach((s) => { + const fadeRatio = 1 - elapsed / duration; + ctx.save(); + ctx.translate(s.x, s.y); + ctx.rotate(s.rotation); + ctx.globalAlpha = Math.max(0, s.alpha * fadeRatio); + ctx.fillStyle = s.color; + + // Draw angular shards + ctx.beginPath(); + ctx.moveTo(0, -s.size); + ctx.lineTo(s.size * 0.5, 0); + ctx.lineTo(0, s.size * 0.6); + ctx.lineTo(-s.size * 0.5, 0); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); + + s.x += s.vx; + s.y += s.vy; + s.vy += 0.12; + s.vx *= 0.98; + s.rotation += s.rotSpeed; + }); + + animId = requestAnimationFrame(loop); + }; + loop(); + + return () => cancelAnimationFrame(animId); + }, [duration]); + + return ( + + ); +}; diff --git a/frontend/intelliquiz-frontend/src/components/game/ScoreboardDisplay.tsx b/frontend/intelliquiz-frontend/src/components/game/ScoreboardDisplay.tsx index 2bb859e..77e8474 100644 --- a/frontend/intelliquiz-frontend/src/components/game/ScoreboardDisplay.tsx +++ b/frontend/intelliquiz-frontend/src/components/game/ScoreboardDisplay.tsx @@ -1,154 +1,543 @@ -import React from 'react'; -import { BiMedal, BiTrophy } from 'react-icons/bi'; +import React, { useEffect, useRef, useState } from 'react'; +import { Target, Crown, Sparkles, Trophy, Medal, Award, Star } from 'lucide-react'; import type { RankingEntry } from '../../services/api'; +import { parseSmartName } from '../../utils/nameUtils'; interface ScoreboardDisplayProps { rankings: RankingEntry[]; highlightTeamId?: number; isFinal?: boolean; - variant?: 'proctor' | 'participant'; title?: string; } +// ======== CANVAS PARTICLE BACKGROUND ======== +const ParticleCanvas: React.FC<{ width: number; height: number }> = ({ width, height }) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + ctx.scale(dpr, dpr); + + type Particle = { + x: number; y: number; r: number; dx: number; dy: number; + color: string; alpha: number; decay: number; shape: 'circle' | 'star' | 'diamond'; + }; + const particles: Particle[] = []; + const colors = ['#d4af37', '#f59e0b', '#fbbf24', '#fde68a', '#7a1733', '#e11d48', '#f472b6']; + + const drawStar = (cx: number, cy: number, r: number) => { + ctx.beginPath(); + for (let i = 0; i < 10; i++) { + const rad = i % 2 === 0 ? r : r * 0.4; + const angle = (Math.PI / 5) * i - Math.PI / 2; + ctx.lineTo(cx + rad * Math.cos(angle), cy + rad * Math.sin(angle)); + } + ctx.closePath(); + ctx.fill(); + }; + + const drawDiamond = (cx: number, cy: number, r: number) => { + ctx.beginPath(); + ctx.moveTo(cx, cy - r); + ctx.lineTo(cx + r * 0.6, cy); + ctx.lineTo(cx, cy + r); + ctx.lineTo(cx - r * 0.6, cy); + ctx.closePath(); + ctx.fill(); + }; + + for (let i = 0; i < 50; i++) { + const shapes: Particle['shape'][] = ['circle', 'star', 'diamond']; + particles.push({ + x: Math.random() * width, + y: Math.random() * height, + r: Math.random() * 3.5 + 1, + dx: (Math.random() - 0.5) * 0.5, + dy: -(Math.random() * 0.3 + 0.1), + color: colors[Math.floor(Math.random() * colors.length)], + alpha: Math.random() * 0.45 + 0.1, + decay: 0.0015 + Math.random() * 0.003, + shape: shapes[Math.floor(Math.random() * shapes.length)], + }); + } + + let id = 0; + const loop = () => { + ctx.clearRect(0, 0, width, height); + particles.forEach((p) => { + ctx.globalAlpha = p.alpha; + ctx.fillStyle = p.color; + if (p.shape === 'star') drawStar(p.x, p.y, p.r); + else if (p.shape === 'diamond') drawDiamond(p.x, p.y, p.r); + else { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } + + p.x += p.dx; p.y += p.dy; p.alpha -= p.decay; + if (p.alpha <= 0) { p.x = Math.random() * width; p.y = height + 5; p.alpha = Math.random() * 0.45 + 0.1; } + if (p.x < 0 || p.x > width) p.dx *= -1; + }); + ctx.globalAlpha = 1; + id = requestAnimationFrame(loop); + }; + loop(); + return () => cancelAnimationFrame(id); + }, [width, height]); + + return ; +}; + +// ======== SVG STAR ======== +const SvgStar: React.FC<{ size?: number; color?: string }> = ({ size = 16, color = '#d4af37' }) => ( + + + +); + +// ======== RANK ICONS (pure Lucide, no emoji) ======== +const getRankIcon = (rank: number) => { + if (rank === 1) return ; + if (rank === 2) return ; + if (rank === 3) return ; + if (rank <= 5) return ; + return ; +}; + +// ======== RANK NUMBER + ACCENT ICON ======== +const RANK_META: Record = { + 1: { + bg: 'linear-gradient(135deg, #fbbf24, #d4af37, #b8860b)', + rowBg: 'linear-gradient(135deg, #fef9c3 0%, #fde68a 40%, #fbbf24 100%)', + border: '#d4af37', text: '#78350f', glow: '0 4px 15px rgba(212,175,55,0.2)', + barColor: '#d4af37', iconColor: '#78350f', accent: '#fef3c7', + }, + 2: { + bg: 'linear-gradient(135deg, #d1d5db, #9ca3af, #6b7280)', + rowBg: 'linear-gradient(135deg, #f9fafb 0%, #f3f4f6 40%, #e5e7eb 100%)', + border: '#9ca3af', text: '#1f2937', glow: '0 4px 15px rgba(107,114,128,0.15)', + barColor: '#6b7280', iconColor: '#374151', accent: '#f3f4f6', + }, + 3: { + bg: 'linear-gradient(135deg, #f59e0b, #d97706, #b45309)', + rowBg: 'linear-gradient(135deg, #fffbeb 0%, #fde68a 40%, #fdba74 100%)', + border: '#d97706', text: '#78350f', glow: '0 4px 15px rgba(217,119,6,0.2)', + barColor: '#d97706', iconColor: '#78350f', accent: '#fef3c7', + }, +}; + +const DEFAULT_META = { + bg: 'linear-gradient(135deg, #9ca3af, #6b7280)', + rowBg: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)', + border: '#e2e8f0', text: '#374151', glow: '0 2px 8px rgba(0,0,0,0.05)', + barColor: '#7a1733', iconColor: '#6b7280', accent: '#f8fafc', +}; + +const AVATAR_COLORS = [ + 'linear-gradient(135deg, #7a1733, #e11d48)', 'linear-gradient(135deg, #1e40af, #3b82f6)', + 'linear-gradient(135deg, #065f46, #10b981)', 'linear-gradient(135deg, #6d28d9, #a78bfa)', + 'linear-gradient(135deg, #9a3412, #f97316)', 'linear-gradient(135deg, #be185d, #f472b6)', + 'linear-gradient(135deg, #0e7490, #22d3ee)', +]; + +// ======== MAIN COMPONENT ======== const ScoreboardDisplay: React.FC = ({ - rankings, - highlightTeamId, - isFinal = false, - variant = 'proctor', - title, + rankings, highlightTeamId, isFinal = false, title, }) => { - const prefix = variant === 'participant' ? 'participant' : 'proctor'; - - const getRankIcon = (rank: number) => { - if (rank === 1) return ; - if (rank === 2) return ; - if (rank === 3) return ; - return `#${rank}`; - }; - - const getInitials = (teamName: string) => { - const words = teamName.trim().split(/\s+/).filter(Boolean); - if (words.length === 0) return '?'; - if (words.length === 1) return words[0].slice(0, 2).toUpperCase(); - return (words[0][0] + words[1][0]).toUpperCase(); - }; - - // Calculate ranks with ties - const rankedTeams = rankings.map((team, index) => { - // Find the actual rank (accounting for ties) - let actualRank = 1; - for (let i = 0; i < index; i++) { - if (rankings[i].score > team.score) { - actualRank = i + 2; - } else if (rankings[i].score === team.score) { - actualRank = rankings[i].rank || i + 1; - } + const wrapRef = useRef(null); + const [wrapSize, setWrapSize] = useState({ w: 900, h: 600 }); + const [scores, setScores] = useState>({}); + const prevRef = useRef>({}); + + useEffect(() => { + const el = wrapRef.current; + if (!el) return; + const obs = new ResizeObserver(([e]) => setWrapSize({ w: e.contentRect.width, h: e.contentRect.height })); + obs.observe(el); + return () => obs.disconnect(); + }, []); + + // Animated score count-up with ease-out + useEffect(() => { + const steps = 28; let step = 0; + const timer = setInterval(() => { + step++; + const t = 1 - Math.pow(1 - step / steps, 3); + const cur: Record = {}; + rankings.forEach(r => { cur[r.teamId] = Math.round((prevRef.current[r.teamId] ?? 0) + (r.score - (prevRef.current[r.teamId] ?? 0)) * t); }); + setScores(cur); + if (step >= steps) { clearInterval(timer); const f: Record = {}; rankings.forEach(r => { f[r.teamId] = r.score; }); prevRef.current = f; } + }, 22); + return () => clearInterval(timer); + }, [rankings]); + + const initials = (n: string) => { const w = n.trim().split(/\s+/).filter(Boolean); return !w.length ? '?' : w.length === 1 ? w[0].slice(0, 2).toUpperCase() : (w[0][0] + w[1][0]).toUpperCase(); }; + + const ranked = rankings.map((t, i) => { + let r = 1; + for (let j = 0; j < i; j++) { + if (rankings[j].score > t.score) r = j + 2; + else if (rankings[j].score === t.score) r = rankings[j].rank || j + 1; } - return { ...team, displayRank: team.rank || actualRank }; + return { ...t, displayRank: t.rank || r }; }); - if (rankings.length === 0) { + const maxScore = Math.max(...rankings.map(r => r.score), 1); + + if (!rankings.length) { return ( -
-

No scores yet

+
+
+ +
+

Synchronizing Leaderboard

+

Waiting for live data from the host...

+ + + +
+
+
); } return ( -
- {/* Header */} -
-

- {title ?? (isFinal ? 'Final Results' : 'Scoreboard')} -

- {isFinal && ( -

Congratulations to all participants!

- )} -
+
+ {isFinal && } - {/* Podium for Final Results */} - {isFinal && rankings.length >= 3 && ( -
- {/* 2nd Place */} -
-
-
-

{rankedTeams[1]?.teamName}

-

{rankedTeams[1]?.score} pts

-
-
+ {/* ===== BANNER ===== */} +
+ {/* Left arrow - Refined Ribbon End */} + + + + + - {/* 1st Place */} -
-
-
-

{rankedTeams[0]?.teamName}

-

{rankedTeams[0]?.score} pts

-
-
+
+
+

{title ?? (isFinal ? 'Final Results' : 'Ranking')}

+
- {/* 3rd Place */} -
-
-
-

{rankedTeams[2]?.teamName}

-

{rankedTeams[2]?.score} pts

-
+ {/* Right arrow - Refined Ribbon End */} + + + + + +
+ + {!isFinal && ( +
+
+ LIVE
)} - {/* Rankings List */} -
- {rankedTeams.map((entry, index) => { - const isHighlighted = entry.teamId === highlightTeamId; - const isTopThree = entry.displayRank <= 3; + {/* ===== ROWS ===== */} +
+ {ranked.map((entry, idx) => { + const { name, avatarId } = parseSmartName(entry.teamName); + const isTop = entry.displayRank <= 3; + const isHi = entry.teamId === highlightTeamId; + const m = isTop ? RANK_META[entry.displayRank] : DEFAULT_META; + const ds = scores[entry.teamId] ?? entry.score; + const pct = maxScore > 0 ? (ds / maxScore) * 100 : 0; return (
- {/* Rank */} -
- {getRankIcon(entry.displayRank)} + {/* === RANK BADGE === */} +
+
{getRankIcon(entry.displayRank)}
+ {entry.displayRank}
- {/* Team Avatar */} -
- {getInitials(entry.teamName)} + {/* === AVATAR === */} +
+ {avatarId ? ( + + ) : ( + initials(name) + )}
- {/* Team Name */} -
-

- {entry.teamName} -

- {isHighlighted && ( -

Your Team

- )} + {/* === TEAM INFO === */} +
+
+ {name} + {isHi && ( +
+ + YOU +
+ )} +
+ {/* Progress bar */} +
+
+
+
- {/* Score */} -
-

- {entry.score} -

-

points

+ {/* === SCORE === */} +
+ + {ds} + pts
); })}
+ + {/* Footer */} + {isFinal && ( +
+ + Congratulations to all participants! + +
+ )}
); }; +// ==================== STYLES ==================== +const S: Record = { + wrap: { + fontFamily: "'Montserrat', sans-serif", + width: '100%', + maxWidth: '960px', + margin: '0 auto', + padding: '0 0 24px', + position: 'relative', + overflow: 'hidden', + }, + + // Banner + bannerWrap: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0, marginBottom: '6px', position: 'relative', zIndex: 1 }, + banner: { + display: 'flex', alignItems: 'center', gap: '16px', + padding: '16px 52px', + background: 'linear-gradient(135deg, #4a0c1e 0%, #7a1733 40%, #8b111f 70%, #561026 100%)', + boxShadow: '0 12px 40px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,255,255,0.2), inset 0 -2px 4px rgba(0,0,0,0.3)', + border: '3px solid #d4af37', + position: 'relative', + overflow: 'hidden', + }, + bannerShine: { + position: 'absolute', + top: 0, left: '-100%', width: '50%', height: '100%', + background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent)', + transform: 'skewX(-25deg)', + animation: 'shimmer 4s infinite', + }, + bannerText: { + margin: 0, fontSize: '26px', fontWeight: 900, color: '#fde68a', + letterSpacing: '0.12em', textTransform: 'uppercase' as const, + textShadow: '0 4px 12px rgba(0,0,0,0.6), 0 0 20px rgba(212,175,55,0.4)', + filter: 'drop-shadow(0 2px 2px rgba(0,0,0,0.5))', + }, + + // Live + liveWrap: { display: 'flex', justifyContent: 'center', marginBottom: '20px', position: 'relative', zIndex: 1 }, + liveBadge: { + display: 'flex', alignItems: 'center', gap: '4px', + padding: '5px 14px', background: 'linear-gradient(135deg, #ef4444, #dc2626)', + borderRadius: '9999px', color: '#fff', fontSize: '10px', fontWeight: 900, + letterSpacing: '0.12em', boxShadow: '0 4px 16px rgba(239,68,68,0.5)', + animation: 'scoreLivePulse 2s ease-in-out infinite', + }, + + // List + list: { display: 'flex', flexDirection: 'column', gap: '12px', position: 'relative', zIndex: 1 }, + + // Row + row: { + display: 'flex', alignItems: 'center', gap: '16px', + padding: '16px 24px', borderRadius: '18px', + transition: 'all 0.3s cubic-bezier(0.4,0,0.2,1)', + animation: 'scoreSlideIn 0.45s cubic-bezier(0.4,0,0.2,1) both', + cursor: 'default', position: 'relative', overflow: 'hidden', + }, + + // Empty State + emptyCard: { + display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + padding: '60px 40px', background: 'rgba(255, 255, 255, 0.7)', + backdropFilter: 'blur(16px)', borderRadius: '32px', + border: '1px solid rgba(255, 255, 255, 0.4)', + boxShadow: '0 20px 50px rgba(0,0,0,0.1)', + margin: '40px auto', maxWidth: '600px', textAlign: 'center', + }, + emptyIconPulse: { + marginBottom: '24px', animation: 'trophyFloat 3s infinite ease-in-out', + }, + emptyTitle: { + fontFamily: "'Montserrat', sans-serif", fontWeight: 900, fontSize: '24px', + color: '#4a0c1e', margin: '0 0 8px', letterSpacing: '-0.01em', + }, + emptySub: { + fontFamily: "'Montserrat', sans-serif", fontWeight: 500, fontSize: '15px', + color: '#6b7280', margin: 0, + }, + emptyLoadingBar: { + width: '120px', height: '4px', background: 'rgba(0,0,0,0.05)', + borderRadius: '2px', marginTop: '32px', overflow: 'hidden', + }, + emptyLoadingProgress: { + width: '40%', height: '100%', background: '#d4af37', + borderRadius: '2px', animation: 'syncProgress 2s infinite ease-in-out', + }, + + // Rank + rankOuter: { + width: '56px', height: '56px', borderRadius: '16px', flexShrink: 0, + display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + gap: '1px', position: 'relative', overflow: 'hidden', + border: '2px solid rgba(255,255,255,0.25)', + }, + rankIcon: { display: 'flex', color: 'rgba(255,255,255,0.9)', lineHeight: 0 }, + rankNum: { + fontSize: '14px', fontWeight: 900, color: '#fff', lineHeight: 1, + textShadow: '0 1px 4px rgba(0,0,0,0.3)', + }, + + // Avatar + avatar: { + width: '50px', height: '50px', borderRadius: '50%', flexShrink: 0, + display: 'flex', alignItems: 'center', justifyContent: 'center', + fontWeight: 900, fontSize: '17px', color: '#fff', + border: '3px solid rgba(255,255,255,0.6)', + boxShadow: '0 4px 14px rgba(0,0,0,0.15)', + }, + + // Info + info: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: '8px' }, + nameRow: { display: 'flex', alignItems: 'center', gap: '10px' }, + name: { fontWeight: 800, fontSize: '17px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontFamily: "'Montserrat', sans-serif" }, + youBadge: { + display: 'inline-flex', alignItems: 'center', gap: '4px', + padding: '4px 12px', background: 'linear-gradient(135deg, #880015 0%, #b11226 100%)', + color: '#fff', borderRadius: '50px', fontSize: '10px', fontWeight: 900, + letterSpacing: '0.08em', flexShrink: 0, + boxShadow: '0 4px 12px rgba(136,0,21,0.3)', + animation: 'youPulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1)', + }, + + // Progress + track: { + height: '10px', background: 'rgba(255,255,255,0.55)', borderRadius: '9999px', + overflow: 'hidden', position: 'relative', border: '1px solid rgba(0,0,0,0.04)', + }, + fill: { + height: '100%', borderRadius: '9999px', + transition: 'width 0.85s cubic-bezier(0.34,1.56,0.64,1)', + position: 'relative', zIndex: 1, + }, + shimmer: { + position: 'absolute', inset: 0, + background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.5) 50%, transparent 100%)', + animation: 'scoreShimmer 2s linear infinite', zIndex: 2, + }, + + // Score chip + chip: { + display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0, + padding: '10px 18px', borderRadius: '14px', + border: '2px solid', backdropFilter: 'blur(6px)', + boxShadow: '0 2px 10px rgba(0,0,0,0.05)', + }, + chipVal: { fontWeight: 900, fontSize: '24px', lineHeight: 1, fontVariantNumeric: 'tabular-nums' }, + chipUnit: { fontSize: '10px', fontWeight: 800, color: '#9ca3af', textTransform: 'uppercase' as const, letterSpacing: '0.08em' }, + + // Empty + empty: { + textAlign: 'center', padding: '60px 24px', + background: 'linear-gradient(135deg, #fffbf0, #fff)', borderRadius: '24px', + border: '2px solid rgba(212,175,55,0.2)', boxShadow: '0 8px 32px rgba(0,0,0,0.05)', + fontFamily: "'Montserrat', sans-serif", + }, + + // Footer + footer: { + display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px', + marginTop: '24px', padding: '14px 24px', + background: 'linear-gradient(135deg, #4a0c1e, #7a1733)', borderRadius: '14px', + border: '2px solid rgba(212,175,55,0.25)', boxShadow: '0 4px 20px rgba(74,12,30,0.2)', + position: 'relative', zIndex: 1, + }, + footerTxt: { fontWeight: 700, fontSize: '14px', color: '#fde68a', letterSpacing: '0.02em' }, +}; + +// ==================== KEYFRAMES ==================== +const kfId = 'scoreboard-v3-kf'; +if (typeof document !== 'undefined' && !document.getElementById(kfId)) { + const el = document.createElement('style'); + el.id = kfId; + el.textContent = ` + @keyframes scoreSlideIn { + 0% { opacity: 0; transform: translateX(-20px) scale(0.97); } + 100% { opacity: 1; transform: translateX(0) scale(1); } + } + @keyframes scoreLivePulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.06); } + } + @keyframes scoreShimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } + } + @keyframes scoreFlicker { + 0% { transform: scale(1) rotate(-4deg); opacity: 0.8; } + 100% { transform: scale(1.15) rotate(4deg); opacity: 1; } + } + `; + document.head.appendChild(el); +} + export default ScoreboardDisplay; diff --git a/frontend/intelliquiz-frontend/src/components/game/TeamGrid.tsx b/frontend/intelliquiz-frontend/src/components/game/TeamGrid.tsx index f0c8626..cae4b91 100644 --- a/frontend/intelliquiz-frontend/src/components/game/TeamGrid.tsx +++ b/frontend/intelliquiz-frontend/src/components/game/TeamGrid.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { ConnectedTeam } from '../../hooks/useSSE'; +import { parseSmartName } from '../../utils/nameUtils'; interface TeamGridProps { teams: ConnectedTeam[]; @@ -24,24 +25,31 @@ const TeamGrid: React.FC = ({ teams, highlightTeamId, variant = ' return (
- {teams.map((team, index) => ( -
- -
- - Connected -
-

- {team.name} -

-
- ))} + {teams.map((team, index) => { + const { name, avatarId } = parseSmartName(team.name); + return ( +
+ +
+ + Connected +
+

+ {name} +

+
+ ); + })}
); }; diff --git a/frontend/intelliquiz-frontend/src/components/game/Timer.tsx b/frontend/intelliquiz-frontend/src/components/game/Timer.tsx index d9f06af..6454f2c 100644 --- a/frontend/intelliquiz-frontend/src/components/game/Timer.tsx +++ b/frontend/intelliquiz-frontend/src/components/game/Timer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; interface TimerProps { timeRemaining: number; @@ -20,9 +20,21 @@ const Timer: React.FC = ({ const percentage = totalTime > 0 ? (timeRemaining / totalTime) * 100 : 0; const isLow = timeRemaining <= 5; const isCritical = timeRemaining <= 3; + const prevTimeRef = useRef(timeRemaining); + const [tickAnimation, setTickAnimation] = React.useState(false); const prefix = variant === 'participant' ? 'participant' : 'proctor'; + // Trigger tick animation when time changes + useEffect(() => { + if (prevTimeRef.current !== timeRemaining && timeRemaining > 0) { + setTickAnimation(true); + const timer = setTimeout(() => setTickAnimation(false), 500); + prevTimeRef.current = timeRemaining; + return () => clearTimeout(timer); + } + }, [timeRemaining]); + const getTimerClass = () => { let classes = `${prefix}-timer`; if (large) classes += ` ${prefix}-timer-large`; @@ -38,6 +50,12 @@ const Timer: React.FC = ({ return classes; }; + const getValueClass = () => { + let classes = `${prefix}-timer-value`; + if (tickAnimation) classes += ' tick-animation'; + return classes; + }; + const minutes = Math.floor(Math.max(0, timeRemaining) / 60); const seconds = Math.max(0, timeRemaining) % 60; const secondsPadded = String(seconds).padStart(2, '0'); @@ -45,7 +63,7 @@ const Timer: React.FC = ({ return (
{/* Time Display */} -
+
{displayMode === 'clock' ? ( <> {minutes}m:{secondsPadded} diff --git a/frontend/intelliquiz-frontend/src/components/superadmin/SuperAdminLayout.tsx b/frontend/intelliquiz-frontend/src/components/superadmin/SuperAdminLayout.tsx index f5d7380..7109fd2 100644 --- a/frontend/intelliquiz-frontend/src/components/superadmin/SuperAdminLayout.tsx +++ b/frontend/intelliquiz-frontend/src/components/superadmin/SuperAdminLayout.tsx @@ -1,14 +1,14 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import { - BiHomeAlt, - BiUser, - BiShield, - BiLogOut, - BiChevronDown, - BiData, - BiBookOpen, -} from 'react-icons/bi'; + Home, + User, + Shield, + LogOut, + ChevronDown, + Database, + BookOpen, +} from 'lucide-react'; import '../../styles/superadmin.css'; import { authApi, currentUserApi } from '../../services/api'; import { useAuth } from '../../contexts/AuthContext'; @@ -20,10 +20,10 @@ interface NavItem { } const navItems: NavItem[] = [ - { path: '/superadmin', label: 'Dashboard', icon: }, - { path: '/superadmin/users', label: 'Users', icon: }, - { path: '/superadmin/permissions', label: 'Permissions', icon: }, - { path: '/superadmin/backups', label: 'Backups', icon: }, + { path: '/superadmin', label: 'Dashboard', icon: }, + { path: '/superadmin/users', label: 'Users', icon: }, + { path: '/superadmin/permissions', label: 'Permissions', icon: }, + { path: '/superadmin/backups', label: 'Backups', icon: }, ]; export default function SuperAdminLayout() { @@ -114,11 +114,7 @@ export default function SuperAdminLayout() { } return ( -
- - +
{/* Top Navigation */}
@@ -128,7 +124,7 @@ export default function SuperAdminLayout() { className="pb-nav-logo-wrap" >
- +
IntelliQuiz
@@ -162,7 +158,7 @@ export default function SuperAdminLayout() {
{username || 'Super Admin'}
Super Admin
- + {profileDropdownOpen && ( @@ -171,7 +167,7 @@ export default function SuperAdminLayout() { onClick={handleLogout} className="pb-nav-dropdown-item" > - + Logout
diff --git a/frontend/intelliquiz-frontend/src/hooks/useSSE.ts b/frontend/intelliquiz-frontend/src/hooks/useSSE.ts index 7f5379a..b5d68dd 100644 --- a/frontend/intelliquiz-frontend/src/hooks/useSSE.ts +++ b/frontend/intelliquiz-frontend/src/hooks/useSSE.ts @@ -98,6 +98,7 @@ export function useSSE( isCorrect: typeof entry.isCorrect === 'boolean' ? entry.isCorrect : undefined, pointsEarned: Number(entry.pointsEarned ?? 0), submittedAnswer: typeof entry.submittedAnswer === 'string' ? entry.submittedAnswer : undefined, + streak: Number(entry.streak ?? 0), } }) } @@ -244,8 +245,9 @@ export function useSSE( }) } - if (Array.isArray(data.teamResults)) { - setRankings(normalizeRankings(data.teamResults)) + const rankingsData = data.teamResults || data.rankings; + if (Array.isArray(rankingsData)) { + setRankings(normalizeRankings(rankingsData)); } } catch { setError('Failed to parse game state event') @@ -269,8 +271,9 @@ export function useSSE( const data = JSON.parse((evt as MessageEvent).data) setGameState('ANSWER_REVEAL') - if (Array.isArray(data.teamResults)) { - setRankings(normalizeRankings(data.teamResults)) + const rankingsData = data.teamResults || data.rankings; + if (Array.isArray(rankingsData)) { + setRankings(normalizeRankings(rankingsData)); } if (data.correctAnswer) { diff --git a/frontend/intelliquiz-frontend/src/index.css b/frontend/intelliquiz-frontend/src/index.css index 92d1ca9..245fc07 100644 --- a/frontend/intelliquiz-frontend/src/index.css +++ b/frontend/intelliquiz-frontend/src/index.css @@ -1,16 +1,27 @@ -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Boogaloo&family=Nunito:wght@400;700;900&family=Share+Tech+Mono&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;600&display=swap'); @import './styles/variables.css'; -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; -html, body { +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-body); + background-color: var(--color-off-white); + color: var(--color-black); + line-height: 1.6; margin: 0; padding: 0; - font-family: var(--font-body, 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); - background: var(--color-cream, #FFF8DC); - color: var(--color-black, #0D0D0D); } #root { @@ -18,40 +29,40 @@ html, body { } h1, h2, h3, h4, h5, h6 { - font-family: var(--font-heading, 'Boogaloo', cursive); + font-family: var(--font-heading); } code, kbd, samp { - font-family: var(--font-mono, 'Share Tech Mono', monospace); + font-family: var(--font-mono); } /* Scrollbar Styles */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 10px; + height: 10px; } ::-webkit-scrollbar-track { - background: #f1f1f1; + background: var(--color-gray-light); } ::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 4px; + background: var(--color-gray-mid); + border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { - background: #a1a1a1; + background: var(--color-gray-dark); } /* Selection */ ::selection { - background: rgba(248, 193, 7, 0.3); - color: var(--color-primary); + background: var(--color-gold-subtle); + color: var(--color-maroon); } /* Focus styles */ *:focus-visible { - outline: 2px solid var(--color-accent); + outline: 2px solid var(--color-maroon); outline-offset: 2px; } \ No newline at end of file diff --git a/frontend/intelliquiz-frontend/src/pages/admin/AdminRedesign.css b/frontend/intelliquiz-frontend/src/pages/admin/AdminRedesign.css index a90fa1b..6ab1bcb 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/AdminRedesign.css +++ b/frontend/intelliquiz-frontend/src/pages/admin/AdminRedesign.css @@ -4,24 +4,22 @@ .quiz-workspace-shell, .admin-question-shell, .question-bank-container { - background: #fff8dc; - background-image: radial-gradient(rgba(13, 13, 13, 0.05) 1px, transparent 1px); - background-size: 24px 24px; + background: linear-gradient(180deg, #fff8ee 0%, #fffdf8 100%); } .quiz-list-hero, .quiz-workspace-hero, .admin-page-header { - border: var(--border-brutal, 3px solid #0d0d0d); - border-radius: var(--border-radius-card, 8px); - box-shadow: var(--shadow-brutal-lg, 6px 6px 0 #0d0d0d); + border: 1px solid #e7dfe2; + border-radius: 22px; + box-shadow: 0 14px 28px rgba(17, 17, 17, 0.05); } .quiz-list-title, .quiz-workspace-title, .admin-page-title { - font-family: var(--font-display, 'Press Start 2P', monospace); - text-shadow: 2px 2px 0 #0d0d0d; + font-family: var(--font-heading, 'Boogaloo', cursive); + text-shadow: none; } .quiz-list-stat-chip, @@ -30,9 +28,9 @@ .admin-card, .admin-quiz-card, .admin-table-container { - border: var(--border-brutal, 3px solid #0d0d0d); - border-radius: var(--border-radius-card, 8px); - box-shadow: var(--shadow-brutal, 4px 4px 0 #0d0d0d); + border: 1px solid #e7dfe2; + border-radius: 20px; + box-shadow: 0 1px 2px rgba(17, 17, 17, 0.04); } .quiz-list-stat-chip:hover, @@ -40,8 +38,8 @@ .workspace-summary-card:hover, .admin-card:hover, .admin-quiz-card:hover { - transform: translate(-2px, -2px); - box-shadow: var(--shadow-brutal-lg, 6px 6px 0 #0d0d0d); + transform: none; + box-shadow: 0 1px 2px rgba(17, 17, 17, 0.04); } .admin-btn, @@ -49,11 +47,10 @@ .quiz-list-create-btn, .empty-cta, .quiz-workspace-back-btn { - border: var(--border-brutal, 3px solid #0d0d0d); - border-radius: var(--border-radius-btn, 6px); - box-shadow: var(--shadow-brutal, 4px 4px 0 #0d0d0d); - font-family: var(--font-heading, 'Boogaloo', cursive); - font-size: 20px; + border: 1px solid #ddd2d6; + border-radius: 12px; + box-shadow: none; + font-family: var(--font-body, 'Nunito', sans-serif); } .admin-btn:hover, @@ -61,55 +58,470 @@ .quiz-list-create-btn:hover, .empty-cta:hover, .quiz-workspace-back-btn:hover { - transform: translate(-2px, -2px); - box-shadow: var(--shadow-brutal-lg, 6px 6px 0 #0d0d0d); + transform: none; + box-shadow: none; } .admin-btn:active, .admin-btn-icon:active, .quiz-list-create-btn:active, .empty-cta:active, -.quiz-workspace-back-btn:active { - transform: translate(4px, 4px); +.questions-list-container .admin-empty-state .admin-btn { + min-height: 44px; + padding: 0 18px; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.questions-header-readonly { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.22); + color: #ffffff; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.question-actions .admin-btn:hover, +.question-actions .admin-btn:active, +.question-actions .admin-btn-icon:hover, +.question-actions .admin-btn-icon:active { + transform: none; box-shadow: none; } -.admin-form-input, -.admin-form-select, -.admin-form-textarea { - border: var(--border-brutal, 3px solid #0d0d0d); - border-radius: var(--border-radius-btn, 6px); - font-family: var(--font-body, 'Nunito', sans-serif); +.identification-answer { + background: #ffffff; + border-radius: 12px; + padding: 12px 14px; + line-height: 1.5; } -.admin-form-input:focus, -.admin-form-select:focus, -.admin-form-textarea:focus { - border-color: #5b21ff; - box-shadow: 0 0 0 4px rgba(91, 33, 255, 0.2); +.admin-modal, +.workspace-modal-large { + border: 1px solid #e5dcdf; + box-shadow: 0 18px 42px rgba(17, 17, 17, 0.08); + border-radius: 20px; } -.admin-badge-status, -.admin-badge, -.quiz-workspace-status-pill { - border: 2px solid #0d0d0d; - font-family: var(--font-heading, 'Boogaloo', cursive); - letter-spacing: 0.08em; +@media (max-width: 1024px) { + .quiz-list-title, + .quiz-workspace-title, + .admin-page-title { + font-size: 24px; + line-height: 1.2; + } + + .questions-header-actions { + justify-content: flex-start; + width: 100%; + } + + .question-card-header { + grid-template-columns: 1fr; + } + + .questions-page-header-content { + align-items: flex-start; + } } -.quiz-list-filter-card, -.workspace-info-strip, -.workspace-config-card, -.workspace-code-card, -.workspace-registration-card { - border: var(--border-brutal, 3px solid #0d0d0d); - box-shadow: var(--shadow-brutal, 4px 4px 0 #0d0d0d); +@media (max-width: 768px) { + .admin-btn, + .admin-btn-icon, + .quiz-list-create-btn, + .empty-cta, + .quiz-workspace-back-btn { + font-size: 16px; + } + + .quiz-list-stat-chip, + .workspace-insight-card, + .workspace-summary-card, + .admin-card, + .admin-quiz-card, + .admin-table-container, + .quiz-list-filter-card, + .workspace-info-strip, + .workspace-config-card, + .workspace-code-card, + .workspace-registration-card { + box-shadow: 0 1px 2px rgba(17, 17, 17, 0.04); + } + + .questions-header-actions { + width: 100%; + } + + .questions-header-actions .admin-btn { + width: 100%; + justify-content: center; + } + + .questions-page-header { + padding: 18px; + } + + .questions-page-header-content, + .questions-page-header-left { + width: 100%; + } + + .questions-page-header-left { + align-items: flex-start; + } + + .questions-page-header .admin-page-title { + font-size: 26px; + } + + .questions-page-header .admin-page-subtitle { + font-size: 13px; + } + + .questions-header-readonly { + width: 100%; + justify-content: center; + } + + .questions-list-container .admin-empty-state > div:last-child { + width: 100%; + } + + .questions-list-container .admin-empty-state .admin-btn { + width: 100%; + justify-content: center; + } + + .questions-pagination-shell { + flex-direction: column; + align-items: stretch; + } + + .questions-pagination-controls { + width: 100%; + justify-content: space-between; + } + + .questions-pagination-pages { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + flex: 1; + } + + .questions-pagination-nav, + .questions-pagination-page { + flex: 1 1 auto; + justify-content: center; + } + + .question-card { + padding: 18px; + } + + .question-options-wrapper { + padding: 14px; + } + + .question-option { + grid-template-columns: auto 1fr; + align-items: start; + } + + .question-option svg { + grid-column: 2 / -1; + justify-self: end; + } +} + +/* Questions header + empty-state hotfix (restore compact old layout) */ +.questions-header-actions { + display: inline-flex !important; + align-items: center !important; + justify-content: flex-end !important; + flex-wrap: wrap !important; + width: auto !important; + gap: 10px !important; +} + +.questions-header-actions .admin-btn { + width: auto !important; + min-height: 42px !important; + padding: 0 16px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; + white-space: nowrap; +} + +.questions-header-actions .admin-btn-primary { + min-width: 170px; +} + +.questions-header-actions .admin-btn svg { + flex-shrink: 0; +} + +.questions-empty-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +.questions-empty-actions .admin-btn { + min-height: 42px; + padding: 0 16px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +@media (max-width: 768px) { + .questions-header-actions { + display: flex !important; + width: 100% !important; + justify-content: flex-start !important; + } + + .questions-header-actions .admin-btn { + width: auto !important; + flex: 0 0 auto; + } + + .questions-empty-actions { + width: 100%; + justify-content: center; + } + + .questions-empty-actions .admin-btn { + width: auto; + } +} + +.question-options-wrapper { + background: #ffffff; + border-radius: 20px; + padding: 16px; + margin: 18px 0 0; +} + +.question-options-grid { + display: grid; + gap: 10px; +} + +.question-option { + background: #ffffff; + border-radius: 14px; + box-shadow: none; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 12px 14px; + min-height: 50px; + word-break: break-word; +} + +.question-option.is-correct { + background: #f4faef; + border-color: #d8e8d0; +} + +.question-option-key { + border-radius: 12px; + border: 1px solid #e7dfe2; + background: #fffaf0; + width: 34px; + height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; +} + +.question-actions { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #e7dfe2; +} + +.question-actions .admin-btn { + min-height: 42px; + padding: 0 16px; + border-radius: 12px; +} + +.questions-header-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.questions-header-actions .admin-btn { + min-height: 44px; + padding: 0 18px; + border-radius: 12px; + gap: 8px; + white-space: nowrap; + justify-content: flex-start; + width: 100%; + align-items: center; +} + +.questions-header-actions .admin-btn-primary { + padding: 0 20px; + + .questions-page-header-content { + align-items: flex-start; + } +} + +.questions-header-actions .admin-btn svg { + flex-shrink: 0; +} + +.questions-pagination-shell { + margin-top: 18px; + padding: 14px 16px; + border: 1px solid #e7dfe2; + border-radius: 18px; + background: #ffffff; + box-shadow: 0 1px 2px rgba(17, 17, 17, 0.04); + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.questions-pagination-summary { + margin: 0; + color: #665c60; + font-size: 13px; + font-weight: 600; +} + +.questions-pagination-controls { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.questions-pagination-nav, +.questions-pagination-page { + min-height: 38px; + border: 1px solid #d8cfd2; + border-radius: 10px; + background: #ffffff; + color: #4d3a3f; + font-weight: 700; + padding: 0 12px; + box-shadow: none; + cursor: pointer; +} + +.questions-pagination-nav { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.questions-pagination-page.is-active { + background: #7a1733; + color: #ffffff; + border-color: #6b1028; +} + +.questions-pagination-nav:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.questions-pagination-page:hover:not(.is-active), +.questions-pagination-nav:hover:not(:disabled) { + background: #faf7f8; + border-color: #cdbec3; + color: #7a1733; +} + +.questions-list-container .admin-empty-state { + padding: 40px 20px; + display: grid; + gap: 14px; + justify-items: center; + text-align: center; +} + +.questions-list-container .admin-empty-state > div:last-child { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; +} + +.questions-list-container .admin-empty-state .admin-btn { + min-height: 44px; + padding: 0 18px; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.questions-header-readonly { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.22); + color: #ffffff; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.question-actions .admin-btn:hover, +.question-actions .admin-btn:active, +.question-actions .admin-btn-icon:hover, +.question-actions .admin-btn-icon:active { + transform: none; + box-shadow: none; +} + +.identification-answer { + background: #ffffff; + border-radius: 12px; + padding: 12px 14px; + line-height: 1.5; } .admin-modal, .workspace-modal-large { - border: var(--border-brutal, 3px solid #0d0d0d); - box-shadow: var(--shadow-brutal-lg, 6px 6px 0 #0d0d0d); + border: 1px solid #e5dcdf; + box-shadow: 0 18px 42px rgba(17, 17, 17, 0.08); + border-radius: 20px; } @media (max-width: 1024px) { @@ -119,6 +531,19 @@ font-size: 24px; line-height: 1.2; } + + .questions-header-actions { + justify-content: flex-start; + width: 100%; + } + + .question-card-header { + grid-template-columns: 1fr; + } + + .questions-page-header-content { + align-items: flex-start; + } } @media (max-width: 768px) { @@ -127,7 +552,7 @@ .quiz-list-create-btn, .empty-cta, .quiz-workspace-back-btn { - font-size: 18px; + font-size: 16px; } .quiz-list-stat-chip, @@ -141,6 +566,92 @@ .workspace-config-card, .workspace-code-card, .workspace-registration-card { - box-shadow: 3px 3px 0 #0d0d0d; + box-shadow: 0 1px 2px rgba(17, 17, 17, 0.04); + } + + .questions-header-actions { + width: 100%; + } + + .questions-header-actions .admin-btn { + width: 100%; + justify-content: center; + } + + .questions-page-header { + padding: 18px; + } + + .questions-page-header-content, + .questions-page-header-left { + width: 100%; + } + + .questions-page-header-left { + align-items: flex-start; + } + + .questions-page-header .admin-page-title { + font-size: 26px; + } + + .questions-page-header .admin-page-subtitle { + font-size: 13px; + } + + .questions-header-readonly { + width: 100%; + justify-content: center; + } + + .questions-list-container .admin-empty-state > div:last-child { + width: 100%; + } + + .questions-list-container .admin-empty-state .admin-btn { + width: 100%; + justify-content: center; + } + + .questions-pagination-shell { + flex-direction: column; + align-items: stretch; + } + + .questions-pagination-controls { + width: 100%; + justify-content: space-between; + } + + .questions-pagination-pages { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + flex: 1; + } + + .questions-pagination-nav, + .questions-pagination-page { + flex: 1 1 auto; + justify-content: center; + } + + .question-card { + padding: 18px; + } + + .question-options-wrapper { + padding: 14px; + } + + .question-option { + grid-template-columns: auto 1fr; + align-items: start; + } + + .question-option svg { + grid-column: 2 / -1; + justify-self: end; } } diff --git a/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.css b/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.css index c3116b0..35a498b 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.css +++ b/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.css @@ -1,42 +1,596 @@ -/* Admin dashboard specific overrides layered on top of superadmin DashboardPage.css */ +/* ============================================================ + IntelliQuiz Admin Dashboard — Aurum v3 + New stat card design: Left-accent floating icon, no top bars + ============================================================ */ -.admin-stats-row-4 { +.admin-clean-dashboard { + max-width: 1280px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; +} + +/* ── HERO ─────────────────────────────────────────────────── */ +.admin-clean-hero { + border-radius: 22px; + background: linear-gradient(135deg, #6b0f23 0%, #880015 55%, #a8002c 100%); + box-shadow: 0 16px 40px rgba(136, 0, 21, 0.28); + padding: 30px 32px; + position: relative; + overflow: hidden; +} + +.admin-clean-hero::before { + content: ''; + position: absolute; + width: 360px; + height: 360px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.04); + right: -80px; + top: -120px; + pointer-events: none; +} + +.admin-clean-hero-content { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 24px; + position: relative; + z-index: 1; +} + +.admin-clean-hero-left { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + min-width: 0; +} + +.admin-clean-chip { + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.12); + color: #fce6eb; + padding: 5px 14px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.admin-clean-title { + margin: 0; + color: #ffffff; + font-family: var(--font-heading); + font-size: clamp(28px, 3.5vw, 44px); + line-height: 1.1; + letter-spacing: -0.02em; +} + +.admin-clean-subtitle { + margin: 0; + color: rgba(255, 255, 255, 0.7); + font-size: 15px; + line-height: 1.6; +} + +.admin-clean-hero-right { + flex-shrink: 0; +} + +/* ── PRIMARY & SECONDARY BUTTONS ─────────────────────────── */ +.admin-clean-btn-primary, +.admin-clean-btn-secondary { + border-radius: 10px; + font-weight: 700; + font-size: 14px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; +} + +.admin-clean-btn-primary { + padding: 11px 22px; + min-height: 46px; + background: #ffd166; + border: 1.5px solid #f0c040; + color: #5a1a00; + font-weight: 800; + box-shadow: 0 4px 14px rgba(255, 209, 102, 0.4); +} + +.admin-clean-btn-primary:hover { + background: #ffe08a; + box-shadow: 0 6px 20px rgba(255, 209, 102, 0.5); + transform: translateY(-1px); +} + +.admin-clean-btn-primary:active { + transform: translateY(1px); + box-shadow: none; +} + +.admin-clean-btn-secondary { + padding: 7px 14px; + min-height: 36px; + background: #ffffff; + border: 1.5px solid #d1d5db; + color: #374151; + font-size: 13px; +} + +.admin-clean-btn-secondary:hover { + border-color: #880015; + color: #880015; + background: #fff5f7; +} + +.admin-clean-btn-primary:focus-visible, +.admin-clean-btn-secondary:focus-visible, +.admin-clean-action:focus-visible, +.admin-clean-quiz-row:focus-visible { + outline: 2px solid #ffd166; + outline-offset: 2px; +} + +/* ── STATS ROW — Left-accent icon design ─────────────────── */ +.admin-clean-stats { + display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; +} + +.admin-clean-stat-card { + border-radius: 16px; + background: #ffffff; + border: 1.5px solid #e5e7eb; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + padding: 20px 18px; + display: flex; + align-items: flex-start; /* icon + text side by side at top */ + gap: 16px; + transition: transform 0.2s ease, box-shadow 0.2s ease; + position: relative; + overflow: hidden; +} + +/* Left colored indent strip */ +.admin-clean-stat-card::after { + content: ''; + position: absolute; + left: 0; + top: 14px; + bottom: 14px; + width: 4px; + border-radius: 0 4px 4px 0; +} + +/* Teal — Total */ +.admin-clean-stat-card:nth-child(1)::after { background: #0d9488; } +.admin-clean-stat-card:nth-child(1) .admin-clean-stat-icon { + background: #f0fdfa; + color: #0d9488; +} + +/* Amber — Live */ +.admin-clean-stat-card:nth-child(2)::after { background: #d97706; } +.admin-clean-stat-card:nth-child(2) .admin-clean-stat-icon { + background: #fffbeb; + color: #d97706; +} + +/* Emerald — Ready */ +.admin-clean-stat-card:nth-child(3)::after { background: #059669; } +.admin-clean-stat-card:nth-child(3) .admin-clean-stat-icon { + background: #ecfdf5; + color: #059669; +} + +/* Slate — Draft */ +.admin-clean-stat-card:nth-child(4)::after { background: #64748b; } +.admin-clean-stat-card:nth-child(4) .admin-clean-stat-icon { + background: #f8fafc; + color: #64748b; +} + +.admin-clean-stat-card:hover { + transform: translateY(-3px); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.09); +} + +.admin-clean-stat-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.admin-clean-stat-icon svg { + width: 22px; + height: 22px; +} + +.admin-clean-stat-body { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.admin-clean-stat-value { + margin: 0; + color: #111827; + font-size: 34px; + font-weight: 800; + line-height: 1; + letter-spacing: -0.02em; +} + +.admin-clean-stat-label { + margin: 0; + color: #6b7280; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +/* ── MAIN GRID ────────────────────────────────────────────── */ +.admin-clean-main-grid { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); + gap: 14px; + align-items: stretch; +} + +.admin-clean-panel { + border: 1.5px solid #e5e7eb; + border-radius: 18px; + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + padding: 20px; + display: flex; + flex-direction: column; +} + +.admin-clean-panel-actions { + display: flex; + flex-direction: column; + gap: 14px; + justify-content: space-between; +} + +.admin-clean-panel-head { + margin-bottom: 14px; +} + +.admin-clean-panel-head h2 { + margin: 0; + display: inline-flex; + align-items: center; + gap: 8px; + color: #111827; + font-family: var(--font-heading); + font-size: 20px; + font-weight: 800; + line-height: 1.2; } -.actions-grid-admin { +.admin-clean-panel-head h2 svg { + color: #880015; +} + +.admin-clean-panel-head-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +/* ── QUICK ACTION BUTTONS ─────────────────────────────────── */ +.admin-clean-actions-grid { + display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; } -.quiz-status.status-draft { - background: rgba(111, 78, 87, 0.16); - color: #6f4e57; +.admin-clean-action { + border: 1.5px solid #e5e7eb; + border-radius: 14px; + background: #ffffff; + padding: 14px; + display: flex; + align-items: center; + gap: 12px; + text-align: left; + cursor: pointer; + transition: all 0.2s ease; } -.quiz-status.status-ready { - background: rgba(212, 160, 23, 0.18); - color: #7c4b00; +.admin-clean-action:hover { + border-color: #880015; + background: #fff5f7; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(136, 0, 21, 0.1); } -.quiz-status.status-active { - background: rgba(122, 23, 51, 0.18); - color: #7a1733; +.admin-clean-action-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: #fff0f3; + border: 1.5px solid #ffcdd5; + color: #880015; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.2s ease; } -.quiz-status.status-archived { - background: rgba(107, 79, 88, 0.16); - color: #6b4f58; +.admin-clean-action:hover .admin-clean-action-icon { + background: #880015; + border-color: #880015; + color: #ffffff; } +.admin-clean-action strong { + display: block; + color: #111827; + font-size: 14px; + font-weight: 700; + line-height: 1.2; +} + +.admin-clean-action small { + display: block; + margin-top: 2px; + color: #6b7280; + font-size: 12px; +} + +/* ── RECOMMENDED FLOW NOTE — Full width, maximized text ───── */ +.admin-clean-actions-note { + border: 1.5px solid #bae6fd; + border-radius: 14px; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + padding: 16px 18px; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 16px; +} + +.admin-clean-actions-note-text { + min-width: 0; +} + +.admin-clean-actions-note h3 { + margin: 0 0 4px; + color: #0c4a6e; + font-size: 13px; + font-weight: 800; + letter-spacing: 0.01em; +} + +.admin-clean-actions-note p { + margin: 0; + color: #075985; + font-size: 12px; + line-height: 1.55; +} + +.admin-clean-actions-steps { + display: flex; + flex-direction: column; + gap: 5px; + flex-shrink: 0; +} + +.admin-clean-actions-steps span { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.admin-clean-actions-steps span:nth-child(1) { + background: #f1f5f9; + border: 1.5px solid #cbd5e1; + color: #475569; +} + +.admin-clean-actions-steps span:nth-child(2) { + background: #ecfdf5; + border: 1.5px solid #6ee7b7; + color: #065f46; +} + +.admin-clean-actions-steps span:nth-child(3) { + background: #fff0f3; + border: 1.5px solid #ffcdd5; + color: #880015; +} + +/* ── RECENT QUIZZES LIST ──────────────────────────────────── */ +.admin-clean-recent-list { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + justify-content: space-between; +} + +.admin-clean-quiz-row { + width: 100%; + border: 1.5px solid #e5e7eb; + border-radius: 12px; + background: #ffffff; + padding: 11px 14px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + text-align: left; + cursor: pointer; + transition: all 0.18s ease; +} + +.admin-clean-quiz-row:hover { + border-color: #880015; + background: #fff5f7; + box-shadow: 0 4px 12px rgba(136, 0, 21, 0.08); + transform: translateX(3px); +} + +.admin-clean-quiz-icon { + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #880015, #a8002c); + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.admin-clean-quiz-meta-wrap { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.admin-clean-quiz-title { + color: #111827; + font-size: 15px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.admin-clean-quiz-meta { + margin-top: 2px; + display: inline-flex; + align-items: center; + gap: 4px; + color: #9ca3af; + font-size: 12px; +} + +/* ── STATUS BADGES ────────────────────────────────────────── */ +.admin-clean-status { + border-radius: 999px; + padding: 4px 12px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + flex-shrink: 0; + border: 1.5px solid transparent; +} + +.admin-clean-status.status-draft { + background: #f8fafc; + border-color: #cbd5e1; + color: #475569; +} + +.admin-clean-status.status-ready { + background: #ecfdf5; + border-color: #6ee7b7; + color: #065f46; +} + +.admin-clean-status.status-active { + background: #fff0f3; + border-color: #ffcdd5; + color: #880015; +} + +.admin-clean-status.status-archived { + background: #f3f4f6; + border-color: #d1d5db; + color: #6b7280; +} + +/* ── EMPTY STATE ──────────────────────────────────────────── */ +.admin-clean-empty { + border: 1.5px dashed #d1d5db; + border-radius: 14px; + background: #f9fafb; + text-align: center; + padding: 28px 20px; +} + +.admin-clean-empty-icon { + width: 52px; + height: 52px; + margin: 0 auto 12px; + border-radius: 14px; + background: #fff0f3; + border: 1.5px solid #ffcdd5; + color: #880015; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.admin-clean-empty h3 { + margin: 0 0 6px; + color: #111827; + font-family: var(--font-heading); + font-size: 18px; +} + +.admin-clean-empty p { + margin: 0 0 16px; + color: #6b7280; + font-size: 14px; +} + +/* ── RESPONSIVE ───────────────────────────────────────────── */ @media (max-width: 1100px) { - .admin-stats-row-4 { + .admin-clean-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .admin-clean-main-grid { + grid-template-columns: 1fr; + } } -@media (max-width: 768px) { - .actions-grid-admin, - .admin-stats-row-4 { +@media (max-width: 1050px) { + .admin-clean-hero-content { grid-template-columns: 1fr; + gap: 14px; } + .admin-clean-hero-right { + align-items: flex-start; + } +} + +@media (max-width: 768px) { + .admin-clean-dashboard { gap: 14px; } + .admin-clean-hero { padding: 22px 20px; border-radius: 16px; } + .admin-clean-stats, .admin-clean-actions-grid { grid-template-columns: 1fr; } + .admin-clean-panel { border-radius: 14px; } + .admin-clean-actions-note { grid-template-columns: 1fr; } + .admin-clean-actions-steps { flex-direction: row; flex-wrap: wrap; } } diff --git a/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.tsx b/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.tsx index 330e529..b9f5df9 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.tsx +++ b/frontend/intelliquiz-frontend/src/pages/admin/DashboardPage.tsx @@ -1,26 +1,24 @@ import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { - BiBoltCircle, + BiBarChartAlt2, BiBookOpen, BiFile, - BiPlay, + BiFolderOpen, + BiLayer, + BiPlusCircle, BiRightArrowAlt, - BiStar, + BiRocket, BiTime, - BiTrendingUp, - BiTargetLock, } from 'react-icons/bi'; import { useQuizzes } from '../../hooks'; import { useAuth } from '../../contexts/AuthContext'; import '../../styles/admin.css'; -import '../superadmin/DashboardPage.css'; import './DashboardPage.css'; export default function AdminDashboardPage() { const navigate = useNavigate(); const { canEditQuiz } = useAuth(); - const username = localStorage.getItem('username') || 'Admin'; // React Query hook const { data: allQuizzes = [], isLoading } = useQuizzes(); @@ -35,7 +33,7 @@ export default function AdminDashboardPage() { draftQuizzes: quizzes.filter(q => q.status === 'DRAFT').length, }), [quizzes]); - const recentQuizzes = useMemo(() => quizzes.slice(0, 5), [quizzes]); + const recentQuizzes = useMemo(() => quizzes.slice(0, 3), [quizzes]); const getStatusClass = (status: string) => { const map: Record = { @@ -53,177 +51,129 @@ export default function AdminDashboardPage() { ); } - return ( -
-
-
-
-
-
-
-
+ const statItems = [ + { key: 'total', label: 'Total Quizzes', value: stats.totalQuizzes, Icon: BiLayer }, + { key: 'active', label: 'Live Now', value: stats.activeQuizzes, Icon: BiRocket }, + { key: 'ready', label: 'Ready', value: stats.readyQuizzes, Icon: BiBarChartAlt2 }, + { key: 'draft', label: 'Draft', value: stats.draftQuizzes, Icon: BiFile }, + ] as const; -
-
-
- - Welcome back! -
-

{username || 'AdminIT'}

-

Launch, track, and level up your quiz sessions from one command center.

+ return ( +
+
+
+
+ Admin Workspace +

Quiz Dashboard

+

+ Create, organize, and monitor quizzes from one workspace. +

-
-
-
+ -
-
-
-
- +
+ {statItems.map(({ key, label, value, Icon }) => ( +
+
+
-
- {stats.totalQuizzes} - Total Quizzes +
+

{value}

+

{label}

-
-
+ ))} +
-
-
-
- -
-
- {stats.activeQuizzes} - Live Now -
- {stats.activeQuizzes > 0 &&
} +
+
+
+

Quick Actions

-
-
-
-
-
- -
-
- {stats.readyQuizzes} - Ready -
-
-
-
- -
-
-
- -
-
- {stats.draftQuizzes} - Draft -
-
-
-
-
- -
-
-
-
- -

Quick Actions

-
-
- -
- -
-
-
-
- Tip -

Start with draft quizzes, then mark them ready when questions are polished.

+
+
+

Recommended flow

+

Draft quizzes first, then mark them ready before launching live sessions for your teams.

+
+
+ Draft + Ready + Live
-
-
-
- -

Recent Quizzes

-
-
-
+
{recentQuizzes.length > 0 ? ( - recentQuizzes.map((quiz, index) => { - const colors = ['#7a1733', '#9f2346', '#d4a017', '#6f4e57']; - return ( -
canEditQuiz(quiz.id, quiz.createdByUserId) ? navigate(`/admin/quizzes/${quiz.id}/questions`) : navigate('/admin/quizzes')} + recentQuizzes.map((quiz) => ( +
- );}) + + + {quiz.title} + + {quiz.questionCount || 0} questions + + + + {quiz.status} + + + )) ) : ( -
-
- -
+
+

No quizzes yet

-

Click create quiz to build your first game-ready set.

-
)}
-
+
); } diff --git a/frontend/intelliquiz-frontend/src/pages/admin/HostPage.tsx b/frontend/intelliquiz-frontend/src/pages/admin/HostPage.tsx index 723d98e..b26fb80 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/HostPage.tsx +++ b/frontend/intelliquiz-frontend/src/pages/admin/HostPage.tsx @@ -14,6 +14,7 @@ import { } from 'react-icons/bi'; import { quizzesApi, teamsApi, scoreboardApi, type Quiz, type Team, type ScoreboardEntry } from '../../services/api'; import { useAuth } from '../../contexts/AuthContext'; +import { parseSmartName } from '../../utils/nameUtils'; import '../../styles/admin.css'; export default function AdminHostPage() { @@ -188,25 +189,37 @@ export default function AdminHostPage() {
- {teams.length > 0 ? teams.map((team) => ( -
-
-
- + {teams.length > 0 ? teams.map((team) => { + const { name, avatarId } = parseSmartName(team.name); + return ( +
+
+
+ {avatarId ? ( + + ) : ( + + )} +
+ {name} +
+
+ {team.accessCode} +
- {team.name} -
-
- {team.accessCode} -
-
- )) : ( + ); + }) : (

No teams registered yet

@@ -229,25 +242,33 @@ export default function AdminHostPage() {
- {scoreboard.length > 0 ? scoreboard.slice(0, 5).map((entry) => ( -
-
-
- {entry.rank} + {scoreboard.length > 0 ? scoreboard.slice(0, 5).map((entry) => { + const { name, avatarId } = parseSmartName(entry.teamName); + return ( +
+
+
+ {avatarId ? ( + + ) : ( + entry.rank + )} +
+ {name}
- {entry.teamName} + {entry.score}
- {entry.score} -
- )) : ( + ); + }) : (

No scores yet

diff --git a/frontend/intelliquiz-frontend/src/pages/admin/QuestionBankPage.tsx b/frontend/intelliquiz-frontend/src/pages/admin/QuestionBankPage.tsx index 95ca4b3..83bddc8 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/QuestionBankPage.tsx +++ b/frontend/intelliquiz-frontend/src/pages/admin/QuestionBankPage.tsx @@ -427,87 +427,150 @@ const QuestionBankPage: React.FC = () => { display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }}>
-

- Import Questions to Quiz -

-

- Select a target quiz and choose questions to import. -

+ {/* Header */} +
+ +

+ Import Questions from Bank +

+
- + {/* Content */} +
+

+ Select a target quiz and choose questions to import. +

- {/* Bank items to select */} -
- {bankItems.map((item) => ( - - ))} + + + {/* Questions List */} +
+ {bankItems.length > 0 ? ( + bankItems.map((item) => ( + + )) + ) : ( +

+ No questions available in the bank. +

+ )} +
-
- + {/* Footer */} +
+ {selectedBankItems.size} selected -
+
+ {/* Cancel Button */} + + {/* Import Button */}
diff --git a/frontend/intelliquiz-frontend/src/pages/admin/QuestionManagementPage.tsx b/frontend/intelliquiz-frontend/src/pages/admin/QuestionManagementPage.tsx index 01da507..9b20f9f 100644 --- a/frontend/intelliquiz-frontend/src/pages/admin/QuestionManagementPage.tsx +++ b/frontend/intelliquiz-frontend/src/pages/admin/QuestionManagementPage.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { Eye, Edit2, Check, X } from 'lucide-react'; +import { useMemo, useState, useEffect } from 'react'; +import { Eye, Edit2, Check, X, ChevronLeft, ChevronRight, BookOpen, Layers3, Lock } from 'lucide-react'; import { Button } from '../../components/common/Button'; import { Modal } from '../../components/common/Modal'; import { Loader } from '../../components/common/Loader'; @@ -39,10 +39,12 @@ export default function QuestionManagementPage({ quizId: number; userPermissions: UserPermissions; }) { + const questionsPerPage = 4; const [questions, setQuestions] = useState([]); const [quiz, setQuiz] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [selectedQuestion, setSelectedQuestion] = useState(null); @@ -60,6 +62,7 @@ export default function QuestionManagementPage({ useEffect(() => { loadData(); + setCurrentPage(1); }, [quizId]); const loadData = async () => { @@ -84,6 +87,7 @@ export default function QuestionManagementPage({ setQuestions(questionsData); setQuiz(quizData); + setCurrentPage(1); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load data'); } finally { @@ -168,6 +172,38 @@ export default function QuestionManagementPage({ setShowPreview(true); }; + const sortedQuestions = useMemo( + () => [...questions].sort((left, right) => (left.position ?? left.id) - (right.position ?? right.id)), + [questions], + ); + + const totalPages = Math.max(1, Math.ceil(sortedQuestions.length / questionsPerPage)); + + const paginatedQuestions = useMemo(() => { + const startIndex = (currentPage - 1) * questionsPerPage; + return sortedQuestions.slice(startIndex, startIndex + questionsPerPage); + }, [currentPage, sortedQuestions]); + + const startQuestionIndex = (currentPage - 1) * questionsPerPage + 1; + const endQuestionIndex = Math.min(currentPage * questionsPerPage, sortedQuestions.length); + + const paginationPages = useMemo(() => { + if (totalPages <= 5) { + return Array.from({ length: totalPages }, (_, index) => index + 1); + } + + const pages = new Set([1, totalPages, currentPage]); + if (currentPage - 1 > 1) pages.add(currentPage - 1); + if (currentPage + 1 < totalPages) pages.add(currentPage + 1); + + return Array.from(pages).sort((left, right) => left - right); + }, [currentPage, totalPages]); + + const goToPage = (page: number) => { + const nextPage = Math.min(Math.max(page, 1), totalPages); + setCurrentPage(nextPage); + }; + const getDifficultyColor = (difficulty: string) => { switch (difficulty) { case 'EASY': @@ -187,11 +223,55 @@ export default function QuestionManagementPage({ return (
-
-

{quiz?.title} - Questions

-

- {userPermissions.canEditContent ? 'View and edit' : 'View'} quiz questions -

+
+
+
+
+
+ +
+
+

Quiz Questions

+

+ {quiz?.title || 'Quiz'} +

+
+
+ +

+ Review questions in a cleaner, easier-to-scan layout with page navigation for quicker browsing. +

+ +
+ + + {sortedQuestions.length} questions + + + + {userPermissions.canEditContent ? 'Editable access' : 'View only'} + + + Page {currentPage} of {totalPages} + +
+
+ +
+
+

Visible

+

{sortedQuestions.length === 0 ? 0 : `${startQuestionIndex}-${endQuestionIndex}`}

+
+
+

Per page

+

{questionsPerPage}

+
+
+

Mode

+

{userPermissions.canEditContent ? 'Manage' : 'Inspect'}

+
+
+
{error && setError(null)} />} @@ -203,40 +283,47 @@ export default function QuestionManagementPage({ )}
- {questions.length === 0 ? ( + {sortedQuestions.length === 0 ? (
No questions in this quiz yet.
) : ( - questions.map((question, index) => ( -
+ paginatedQuestions.map((question, index) => ( +
-
-
-

Question {index + 1}

-

{question.text}

+
+
+

+ Question {startQuestionIndex + index} +

+

+ {question.text} +

- - {question.difficulty} + + {question.difficulty.replace('_', ' ')}
-
-

Answer Options:

+
+

Answer Options

{question.answers.map(answer => (
+ + {String.fromCharCode(65 + question.answers.findIndex((item) => item.id === answer.id))} + {answer.isCorrect && ( - + )} - + {answer.text}
@@ -244,30 +331,33 @@ export default function QuestionManagementPage({
-
+
{userPermissions.canEditContent && ( <> )} @@ -278,6 +368,53 @@ export default function QuestionManagementPage({ )}
+ {sortedQuestions.length > questionsPerPage && ( +
+

+ Showing {startQuestionIndex} to {endQuestionIndex} of {sortedQuestions.length} questions +

+ +
+ + +
+ {paginationPages.map((page) => ( + + ))} +
+ + +
+
+ )} + {/* Preview Modal */}