Skip to content

Commit 0a3e034

Browse files
merge: resolve conflicts and sync develop changes
2 parents c49cf46 + cc6f39c commit 0a3e034

154 files changed

Lines changed: 11501 additions & 4956 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/actions/quiz.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ function calculateIntegrityScore(violations: ViolationEvent[]): number {
5252
return Math.max(0, 100 - penalty);
5353
}
5454

55-
5655
async function getQuizQuestionIds(quizId: string): Promise<string[]> {
5756
const rows = await db
5857
.select({ id: quizQuestions.id })
@@ -270,7 +269,8 @@ export async function initializeQuizCache(
270269
const { getOrCreateQuizAnswersCache, clearVerifiedQuestions } =
271270
await import('@/lib/quiz/quiz-answers-redis');
272271

273-
const { resolveRequestIdentifier } = await import('@/lib/quiz/resolve-identifier');
272+
const { resolveRequestIdentifier } =
273+
await import('@/lib/quiz/resolve-identifier');
274274
const { headers } = await import('next/headers');
275275
const headersList = await headers();
276276
const identifier = resolveRequestIdentifier(headersList);

frontend/app/[locale]/achievements-demo/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function AchievementsDemoPage() {
1313
uniqueQuizzes: 4,
1414
totalPoints: 80,
1515
topLeaderboard: false,
16-
hasStarredRepo: true, // demo: show star_gazer as earned
16+
hasStarredRepo: true, // demo: show star_gazer as earned
1717
sponsorCount: 0,
1818
hasNightOwl: false,
1919
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Metadata } from 'next';
21
import { FileQuestion, MessageSquare, ShoppingBag } from 'lucide-react';
2+
import { Metadata } from 'next';
33

44
import { Link } from '@/i18n/routing';
55

frontend/app/[locale]/admin/quiz/[id]/page.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { Metadata } from 'next';
22
import { notFound } from 'next/navigation';
33

44
import { QuizEditorList } from '@/components/admin/quiz/QuizEditorList';
5+
import { QuizMetadataEditor } from '@/components/admin/quiz/QuizMetadataEditor';
6+
import { QuizStatusControls } from '@/components/admin/quiz/QuizStatusControls';
7+
import { UploadMoreQuestions } from '@/components/admin/quiz/UploadMoreQuestions';
58
import { getAdminQuizFull } from '@/db/queries/quizzes/admin-quiz';
69
import { Link } from '@/i18n/routing';
710
import { issueCsrfToken } from '@/lib/security/csrf';
@@ -23,7 +26,16 @@ export default async function AdminQuizEditPage({
2326
const title =
2427
quiz.translations.en?.title ?? quiz.translations.uk?.title ?? quiz.slug;
2528

29+
const isDraft = quiz.status === 'draft';
30+
2631
const csrfToken = issueCsrfToken('admin:quiz:question:update');
32+
const csrfTokenDelete = isDraft
33+
? issueCsrfToken('admin:quiz:question:delete')
34+
: undefined;
35+
const csrfTokenAddQuestions = isDraft
36+
? issueCsrfToken('admin:quiz:questions:add')
37+
: undefined;
38+
const csrfTokenUpdate = issueCsrfToken('admin:quiz:update');
2739

2840
return (
2941
<div className="mx-auto max-w-5xl px-6 py-8">
@@ -38,15 +50,62 @@ export default async function AdminQuizEditPage({
3850

3951
<div className="mb-6">
4052
<h1 className="text-foreground text-2xl font-bold">{title}</h1>
41-
<p className="text-muted-foreground mt-1 text-sm">
42-
{quiz.questions.length} questions &middot; slug: {quiz.slug}
43-
</p>
53+
<div className="mt-1 flex items-center gap-3">
54+
<span className="text-muted-foreground text-sm">
55+
{quiz.questions.length} questions &middot; slug: {quiz.slug}
56+
</span>
57+
<span
58+
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${
59+
isDraft
60+
? 'bg-amber-500/10 text-amber-500'
61+
: 'bg-emerald-500/10 text-emerald-500'
62+
}`}
63+
>
64+
{isDraft ? 'Draft' : 'Ready'}
65+
</span>
66+
<span
67+
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${
68+
quiz.isActive
69+
? 'bg-emerald-500/10 text-emerald-500'
70+
: 'bg-muted text-muted-foreground'
71+
}`}
72+
>
73+
{quiz.isActive ? 'Active' : 'Inactive'}
74+
</span>
75+
</div>
76+
</div>
77+
<div className="mb-6">
78+
<QuizStatusControls
79+
quizId={quiz.id}
80+
status={quiz.status}
81+
isActive={quiz.isActive}
82+
csrfToken={csrfTokenUpdate}
83+
/>
4484
</div>
85+
<div className="mb-6">
86+
<QuizMetadataEditor
87+
quizId={quiz.id}
88+
translations={quiz.translations}
89+
timeLimitSeconds={quiz.timeLimitSeconds}
90+
csrfToken={csrfTokenUpdate}
91+
/>
92+
</div>
93+
94+
{isDraft && csrfTokenAddQuestions && (
95+
<div className="mb-6">
96+
<UploadMoreQuestions
97+
quizId={quiz.id}
98+
csrfToken={csrfTokenAddQuestions}
99+
/>
100+
</div>
101+
)}
45102

46103
<QuizEditorList
47104
questions={quiz.questions}
48105
quizId={quiz.id}
49106
csrfToken={csrfToken}
107+
csrfTokenDelete={csrfTokenDelete}
108+
isDraft={isDraft}
50109
/>
51110
</div>
52111
);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Metadata } from 'next';
2+
3+
import { CreateQuizForm } from '@/components/admin/quiz/CreateQuizForm';
4+
import { getAdminCategoryList } from '@/db/queries/categories/admin-categories';
5+
import { Link } from '@/i18n/routing';
6+
import { issueCsrfToken } from '@/lib/security/csrf';
7+
8+
export const metadata: Metadata = {
9+
title: 'New Quiz | DevLovers',
10+
};
11+
12+
export default async function AdminQuizNewPage() {
13+
const categories = await getAdminCategoryList();
14+
const csrfTokenQuiz = issueCsrfToken('admin:quiz:create');
15+
const csrfTokenCategory = issueCsrfToken('admin:category:create');
16+
17+
return (
18+
<div className="mx-auto max-w-5xl px-6 py-8">
19+
<div className="mb-6">
20+
<Link
21+
href="/admin/quiz"
22+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
23+
>
24+
&larr; Back to quizzes
25+
</Link>
26+
</div>
27+
28+
<h1 className="text-foreground mb-6 text-2xl font-bold">New Quiz</h1>
29+
30+
<CreateQuizForm
31+
categories={categories}
32+
csrfTokenQuiz={csrfTokenQuiz}
33+
csrfTokenCategory={csrfTokenCategory}
34+
/>
35+
</div>
36+
);
37+
}

frontend/app/[locale]/admin/quiz/page.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,36 @@ import { Metadata } from 'next';
22

33
import { QuizListTable } from '@/components/admin/quiz/QuizListTable';
44
import { getAdminQuizList } from '@/db/queries/quizzes/admin-quiz';
5+
import { Link } from '@/i18n/routing';
6+
import { issueCsrfToken } from '@/lib/security/csrf';
57

68
export const metadata: Metadata = {
79
title: 'Quiz Admin | DevLovers',
810
};
911

1012
export default async function AdminQuizPage() {
1113
const quizzes = await getAdminQuizList();
14+
const csrfTokenDelete = issueCsrfToken('admin:quiz:delete');
1215

1316
return (
1417
<div className="mx-auto max-w-5xl px-6 py-8">
15-
<h1 className="text-foreground text-2xl font-bold">Quizzes</h1>
16-
<p className="text-muted-foreground mt-1 text-sm">
17-
Manage quiz content, questions, and answers
18-
</p>
18+
<div className="flex items-center justify-between">
19+
<div>
20+
<h1 className="text-foreground text-2xl font-bold">Quizzes</h1>
21+
<p className="text-muted-foreground mt-1 text-sm">
22+
Manage quiz content, questions, and answers
23+
</p>
24+
</div>
25+
<Link
26+
href="/admin/quiz/new"
27+
className="bg-foreground text-background hover:bg-foreground/90 inline-flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors"
28+
>
29+
+ New Quiz
30+
</Link>
31+
</div>
1932

2033
<div className="mt-6">
21-
<QuizListTable quizzes={quizzes} />
34+
<QuizListTable quizzes={quizzes} csrfTokenDelete={csrfTokenDelete} />
2235
</div>
2336
</div>
2437
);

0 commit comments

Comments
 (0)