Skip to content

Commit 0da8434

Browse files
authored
(SP: 7) [UI] Quiz UI polish: tabs styling, category accents, color scheme (#181, #193, #194) (#195)
1 parent 82efdb1 commit 0da8434

18 files changed

Lines changed: 143 additions & 72 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ next-env.d.ts
6464
# Documentation (development only)
6565
.claude/
6666
CLAUDE.md
67-
frontend/docs/
67+
frontend/_dev-notes/
6868
frontend/.env.bak
6969

7070

frontend/.gitignore

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,5 @@ next-env.d.ts
4545

4646
# Documentation (only for development)
4747
CLAUDE.md
48-
docs/
48+
_dev-notes/
4949
.claude
50-
51-
!docs/
52-
!docs/security/
53-
!docs/security/origin-posture.md

frontend/app/[locale]/quizzes/page.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getActiveQuizzes, getUserQuizzesProgress } from '@/db/queries/quiz';
22
import { getCurrentUser } from '@/lib/auth';
33
import { getTranslations } from 'next-intl/server';
44
import QuizzesSection from '@/components/quiz/QuizzesSection';
5+
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
56

67
type PageProps = { params: Promise<{ locale: string }> };
78

@@ -23,7 +24,7 @@ export default async function QuizzesPage({ params }: PageProps) {
2324

2425
if (!quizzes.length) {
2526
return (
26-
<div className="mx-auto max-w-5xl py-12">
27+
<div className="mx-auto max-w-7xl py-12 px-4 sm:px-6 lg:px-8">
2728
<h1 className="text-3xl font-bold mb-4">{t('title')}</h1>
2829
<p className="text-gray-600 dark:text-gray-400">
2930
{t('noQuizzes')}
@@ -33,9 +34,10 @@ export default async function QuizzesPage({ params }: PageProps) {
3334
}
3435

3536
return (
36-
<div className="mx-auto max-w-5xl py-12">
37+
<DynamicGridBackground className="min-h-screen bg-gray-50 transition-colors duration-300 dark:bg-transparent py-10">
38+
<main className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
3739
<div className="mb-8">
38-
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
40+
<p className="text-sm text-[var(--accent-primary)] font-semibold">
3941
{t('practice')}
4042
</p>
4143
<h1 className="text-3xl font-bold">{t('title')}</h1>
@@ -45,6 +47,7 @@ export default async function QuizzesPage({ params }: PageProps) {
4547
</div>
4648

4749
<QuizzesSection quizzes={quizzes} userProgressMap={userProgressMap} />
48-
</div>
50+
</main>
51+
</DynamicGridBackground>
4952
);
5053
}

frontend/components/q&a/AccordionList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
AccordionTrigger,
88
AccordionContent,
99
} from '@/components/ui/accordion';
10-
import { qaTabStyles } from '@/data/qaTabs';
10+
import { categoryTabStyles } from '@/data/categoryStyles';
1111

1212
import CodeBlock from '@/components/q&a/CodeBlock';
1313
import type {
@@ -241,7 +241,7 @@ export default function AccordionList({ items }: { items: QuestionEntry[] }) {
241241
{items.map((q, idx) => {
242242
const key = q.id ?? idx;
243243
const accent =
244-
qaTabStyles[q.category as keyof typeof qaTabStyles]?.accent;
244+
categoryTabStyles[q.category as keyof typeof categoryTabStyles]?.accent;
245245
return (
246246
<AccordionItem
247247
key={key}

frontend/components/q&a/QaSection.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { Pagination } from '@/components/q&a/Pagination';
66
import { Tabs, TabsList, TabsContent } from '@/components/ui/tabs';
77
import { categoryData } from '@/data/category';
88
import { useQaTabs } from '@/components/q&a/useQaTabs';
9-
import { QaTabButton } from '@/components/q&a/QaTabButton';
10-
import { qaTabStyles } from '@/data/qaTabs';
9+
import { CategoryTabButton } from '@/components/shared/CategoryTabButton';
10+
import { categoryTabStyles } from '@/data/categoryStyles';
1111
import { cn } from '@/lib/utils';
1212
import type { CategorySlug } from '@/components/q&a/types';
1313

@@ -29,18 +29,18 @@ export default function TabsSection() {
2929
<Tabs value={active} onValueChange={handleCategoryChange}>
3030
<TabsList className="!bg-transparent !p-0 !h-auto !w-full flex flex-wrap items-stretch justify-start gap-3 mb-6">
3131
{categoryData.map(category => {
32-
const slug = category.slug as keyof typeof qaTabStyles;
32+
const slug = category.slug as keyof typeof categoryTabStyles;
3333
const value = slug as CategorySlug;
3434
return (
35-
<QaTabButton
35+
<CategoryTabButton
3636
key={slug}
3737
value={value}
3838
label={
3939
category.translations[localeKey] ??
4040
category.translations.en ??
4141
value
4242
}
43-
style={qaTabStyles[slug]}
43+
style={categoryTabStyles[slug]}
4444
isActive={active === value}
4545
/>
4646
);
@@ -78,7 +78,7 @@ export default function TabsSection() {
7878
currentPage={currentPage}
7979
totalPages={totalPages}
8080
onPageChange={handlePageChange}
81-
accentColor={qaTabStyles[active as keyof typeof qaTabStyles].accent}
81+
accentColor={categoryTabStyles[active as keyof typeof categoryTabStyles].accent}
8282
/>
8383
)}
8484
</div>

frontend/components/quiz/CountdownTimer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ export function CountdownTimer({
6868
if (percentage <= 30) {
6969
return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950/30 border-yellow-200 dark:border-yellow-800';
7070
}
71-
return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800';
71+
return 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800';
7272
};
7373

7474
const getProgressBarColor = () => {
7575
if (percentage <= 10) return 'bg-red-600';
7676
if (percentage <= 30) return 'bg-yellow-600';
77-
return 'bg-blue-600';
77+
return 'bg-green-600';
7878
};
7979

8080
if (!isActive) return null;

frontend/components/quiz/QuizCard.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useTranslations } from 'next-intl';
44
import { Link } from '@/i18n/routing';
55
import { Badge } from '@/components/ui/badge';
66
import { FileText, Clock } from 'lucide-react';
7+
import { categoryTabStyles } from '@/data/categoryStyles';
78

89
interface QuizCardProps {
910
quiz: {
@@ -14,6 +15,7 @@ interface QuizCardProps {
1415
questionsCount: number;
1516
timeLimitSeconds: number | null;
1617
categoryName: string | null;
18+
categorySlug: string | null;
1719
};
1820
userProgress?: {
1921
bestScore: number;
@@ -24,16 +26,33 @@ interface QuizCardProps {
2426

2527
export function QuizCard({ quiz, userProgress }: QuizCardProps) {
2628
const t = useTranslations('quiz.card');
29+
const slug = quiz.categorySlug as keyof typeof categoryTabStyles | null;
30+
const style = slug && categoryTabStyles[slug] ? categoryTabStyles[slug] : null;
31+
const accentColor = style?.accent ?? '#3B82F6'; // fallback blue
32+
2733
const percentage =
2834
userProgress && userProgress.totalQuestions > 0
2935
? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
3036
: 0;
3137

3238
return (
33-
<div className="flex flex-col rounded-xl border border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-5 shadow-sm hover:shadow-md transition-shadow">
39+
<div
40+
className="group/card relative flex flex-col rounded-xl border border-black/10 dark:border-white/10 hover:!border-[var(--accent)] bg-white dark:bg-neutral-900 p-5 shadow-sm overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
41+
style={{ '--accent': `${accentColor}60` } as React.CSSProperties}
42+
>
43+
<span
44+
className="pointer-events-none absolute -top-6 -right-6 w-24 h-24 rounded-full blur-[40px] opacity-0 group-hover/card:opacity-20 transition-opacity duration-500"
45+
style={{ backgroundColor: accentColor }}
46+
/>
3447
<div className="flex-grow">
3548
<div className="flex gap-2 mb-3">
36-
<Badge variant="blue">
49+
<Badge
50+
variant="default"
51+
style={{
52+
backgroundColor: `${accentColor}20`,
53+
color: accentColor,
54+
}}
55+
>
3756
{quiz.categoryName ?? t('uncategorized')}
3857
</Badge>
3958
{userProgress && <Badge variant="success">{t('completed')}</Badge>}
@@ -48,11 +67,11 @@ export function QuizCard({ quiz, userProgress }: QuizCardProps) {
4867
)}
4968
<div className="flex gap-3 text-xs text-gray-500 mb-3">
5069
<span className="flex items-center gap-1">
51-
<FileText className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400" />
70+
<FileText className="w-3.5 h-3.5" style={{ color: accentColor }} />
5271
{quiz.questionsCount} {t('questions')}
5372
</span>
5473
<span className="flex items-center gap-1">
55-
<Clock className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400" />
74+
<Clock className="w-3.5 h-3.5" style={{ color: accentColor }} />
5675
{Math.floor(
5776
(quiz.timeLimitSeconds ?? quiz.questionsCount * 30) / 60
5877
)}{' '}
@@ -76,17 +95,26 @@ export function QuizCard({ quiz, userProgress }: QuizCardProps) {
7695
</div>
7796
<div className="h-1.5 bg-gray-200 dark:bg-neutral-800 rounded-full overflow-hidden">
7897
<div
79-
className="h-full bg-blue-600 rounded-full transition-all"
80-
style={{ width: `${percentage}%` }}
98+
className="h-full rounded-full transition-all"
99+
style={{ width: `${percentage}%`, backgroundColor: accentColor }}
81100
/>
82101
</div>
83102
</div>
84103
)}
85104
<Link
86105
href={`/quiz/${quiz.slug}`}
87-
className="btn block w-full text-center rounded-lg text-white px-4 py-2.5 text-sm font-medium transition-colors"
106+
className="group relative block w-full overflow-hidden text-center rounded-xl border px-4 py-2.5 text-sm font-semibold transition-all duration-300"
107+
style={{
108+
borderColor: `${accentColor}50`,
109+
backgroundColor: `${accentColor}15`,
110+
color: accentColor,
111+
}}
88112
>
89113
{userProgress ? t('retake') : t('start')}
114+
<span
115+
className="pointer-events-none absolute left-1/2 top-1/2 h-[150%] w-[80%] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[20px] opacity-0 transition-opacity duration-300 group-hover:opacity-30"
116+
style={{ backgroundColor: accentColor }}
117+
/>
90118
</Link>
91119
</div>
92120
);

frontend/components/quiz/QuizContainer.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { QuizQuestionClient } from '@/db/queries/quiz';
1818
import { ConfirmModal } from '@/components/ui/confirm-modal';
1919
import { Button } from '@/components/ui/button';
2020
import { FileText, Ban, AlertTriangle, Clock } from 'lucide-react';
21+
import { categoryTabStyles } from '@/data/categoryStyles';
2122

2223
interface Answer {
2324
questionId: string;
@@ -149,6 +150,8 @@ export function QuizContainer({
149150
const tRules = useTranslations('quiz.rules');
150151
const tExit = useTranslations('quiz.exitModal');
151152
const tQuestion = useTranslations('quiz.question');
153+
const categoryStyle = categorySlug ? categoryTabStyles[categorySlug as keyof typeof categoryTabStyles] : null;
154+
const accentColor = categoryStyle?.accent ?? '#3B82F6';
152155
const [isPending, startTransition] = useTransition();
153156
const [state, dispatch] = useReducer(quizReducer, {
154157
status: 'rules',
@@ -195,7 +198,6 @@ const { markQuitting } = useQuizGuards({
195198
resetViolations,
196199
});
197200

198-
199201
// Sync seed to URL for language switch persistence
200202
useEffect(() => {
201203
if (!searchParams.has('seed')) {
@@ -369,7 +371,7 @@ const confirmQuit = () => {
369371
if (onBackToTopics) {
370372
onBackToTopics();
371373
} else {
372-
window.location.href = `/${locale}/`;
374+
window.location.href = `/${locale}/q&a`;
373375
}
374376
};
375377

@@ -423,10 +425,21 @@ const confirmQuit = () => {
423425
</div>
424426
</div>
425427
</div>
426-
427-
<Button onClick={handleStart} className="w-full" size="md">
428-
{tRules('startButton')}
429-
</Button>
428+
<button
429+
onClick={handleStart}
430+
className="group relative w-full overflow-hidden text-center rounded-xl border px-6 py-3 text-base font-semibold transition-all duration-300"
431+
style={{
432+
borderColor: `${accentColor}50`,
433+
backgroundColor: `${accentColor}15`,
434+
color: accentColor,
435+
}}
436+
>
437+
{tRules('startButton')}
438+
<span
439+
className="pointer-events-none absolute left-1/2 top-1/2 h-[150%] w-[80%] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[20px] opacity-0 transition-opacity duration-300 group-hover:opacity-30"
440+
style={{ backgroundColor: accentColor }}
441+
/>
442+
</button>
430443
</div>
431444
);
432445
}
@@ -493,6 +506,7 @@ const confirmQuit = () => {
493506
onAnswer={handleAnswer}
494507
onNext={handleNext}
495508
isLoading={isPending}
509+
accentColor={accentColor}
496510
/>
497511
<ConfirmModal
498512
isOpen={showExitModal}

frontend/components/quiz/QuizProgress.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ export function QuizProgress({ current, total, answers }: QuizProgressProps) {
8282
<div
8383
key={index}
8484
className={cn(
85-
'relative flex items-center justify-center w-9 h-9 rounded-full transition-all border-2 text-sm font-medium',
86-
isCurrent && !isAnswered && 'border-blue-500 bg-blue-50 dark:bg-blue-950',
87-
isAnswered && isCorrect && 'border-green-500 bg-green-500',
88-
isAnswered && !isCorrect && 'border-red-500 bg-red-500',
89-
!isAnswered && !isCurrent && 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900'
85+
'relative flex items-center justify-center w-9 h-9 rounded-full transition-all border text-sm font-medium',
86+
isCurrent && !isAnswered && 'border-blue-500 bg-blue-500/20 dark:bg-blue-500/20',
87+
isAnswered && isCorrect && 'border-green-500 bg-green-500/20 dark:bg-green-500/20',
88+
isAnswered && !isCorrect && 'border-red-500 bg-red-500/20 dark:bg-red-500/20',
89+
!isAnswered && !isCurrent && 'border-gray-300 dark:border-gray-600 bg-white/90 dark:bg-neutral-900/80'
9090
)}
9191
>
9292
{isAnswered ? (
@@ -103,7 +103,14 @@ export function QuizProgress({ current, total, answers }: QuizProgressProps) {
103103
</span>
104104
)}
105105
{isCurrent && (
106-
<div className="absolute inset-0 rounded-full border-2 border-blue-500 animate-pulse" />
106+
<div
107+
className={cn(
108+
"absolute inset-0 rounded-full border-1 animate-pulse",
109+
!isAnswered && "border-blue-500",
110+
isAnswered && isCorrect && "border-green-500",
111+
isAnswered && !isCorrect && "border-red-500"
112+
)}
113+
/>
107114
)}
108115
</div>
109116
);

0 commit comments

Comments
 (0)