Skip to content

Commit 6a9a01c

Browse files
Merge pull request #342 from DevLoversTeam/sl/feat/quiz
2 parents 38bebb2 + 650e960 commit 6a9a01c

26 files changed

Lines changed: 3460 additions & 2809 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { AdminSidebar } from '@/components/admin/AdminSidebar';
2+
import { guardAdminPage } from '@/lib/auth/guard-admin-page';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
export default async function AdminLayout({
7+
children,
8+
}: {
9+
children: React.ReactNode;
10+
}) {
11+
await guardAdminPage();
12+
13+
return (
14+
<div className="flex h-[calc(100vh-4rem)] overflow-hidden">
15+
<AdminSidebar />
16+
<main className="flex-1 overflow-y-auto">{children}</main>
17+
</div>
18+
);
19+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Metadata } from 'next';
2+
import { FileQuestion, MessageSquare, ShoppingBag } from 'lucide-react';
3+
4+
import { Link } from '@/i18n/routing';
5+
6+
export const metadata: Metadata = {
7+
title: 'Admin | DevLovers',
8+
description: 'Administrative dashboard',
9+
};
10+
11+
const SECTIONS = [
12+
{
13+
title: 'Shop',
14+
description: 'Manage products, orders, and inventory',
15+
href: '/admin/shop' as const,
16+
icon: ShoppingBag,
17+
},
18+
{
19+
title: 'Quiz',
20+
description: 'Edit quizzes, questions, and view statistics',
21+
href: '/admin/quiz' as const,
22+
icon: FileQuestion,
23+
},
24+
{
25+
title: 'Q&A',
26+
description: 'Manage interview questions and answers',
27+
href: '/admin/q&a' as const,
28+
icon: MessageSquare,
29+
},
30+
];
31+
32+
export default function AdminHomePage() {
33+
return (
34+
<div className="mx-auto max-w-5xl px-6 py-8">
35+
<h1 className="text-foreground text-2xl font-bold">Admin Dashboard</h1>
36+
<p className="text-muted-foreground mt-1 text-sm">
37+
Manage content across the platform
38+
</p>
39+
40+
<div className="mt-6 grid gap-4 sm:grid-cols-2">
41+
{SECTIONS.map(section => (
42+
<Link
43+
key={section.href}
44+
href={section.href}
45+
className="border-border hover:bg-muted/50 flex items-start gap-4 rounded-lg border p-5 transition-colors"
46+
>
47+
<section.icon className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
48+
<div>
49+
<div className="text-foreground text-base font-semibold">
50+
{section.title}
51+
</div>
52+
<div className="text-muted-foreground mt-1 text-sm">
53+
{section.description}
54+
</div>
55+
</div>
56+
</Link>
57+
))}
58+
</div>
59+
</div>
60+
);
61+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Q&A Admin | DevLovers',
5+
};
6+
7+
export default function AdminQaPage() {
8+
return (
9+
<div className="mx-auto max-w-5xl px-6 py-8">
10+
<h1 className="text-foreground text-2xl font-bold">Q&A</h1>
11+
<p className="text-muted-foreground mt-1 text-sm">
12+
Manage interview questions and answers
13+
</p>
14+
15+
<div className="border-border text-muted-foreground mt-6 rounded-lg border border-dashed p-8 text-center text-sm">
16+
Q&A management will be implemented here
17+
</div>
18+
</div>
19+
);
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Quiz Admin | DevLovers',
5+
};
6+
7+
export default function AdminQuizPage() {
8+
return (
9+
<div className="mx-auto max-w-5xl px-6 py-8">
10+
<h1 className="text-foreground text-2xl font-bold">Quizzes</h1>
11+
<p className="text-muted-foreground mt-1 text-sm">
12+
Manage quiz content, questions, and answers
13+
</p>
14+
15+
<div className="border-border text-muted-foreground mt-6 rounded-lg border border-dashed p-8 text-center text-sm">
16+
Quiz list and editor will be implemented here
17+
</div>
18+
</div>
19+
);
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Quiz Statistics | DevLovers',
5+
};
6+
7+
export default function AdminQuizStatisticsPage() {
8+
return (
9+
<div className="mx-auto max-w-5xl px-6 py-8">
10+
<h1 className="text-foreground text-2xl font-bold">Quiz Statistics</h1>
11+
<p className="text-muted-foreground mt-1 text-sm">
12+
Analytics, trends, and user metrics
13+
</p>
14+
15+
<div className="border-border text-muted-foreground mt-6 rounded-lg border border-dashed p-8 text-center text-sm">
16+
Statistics dashboard will be implemented here
17+
</div>
18+
</div>
19+
);
20+
}

frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx renamed to frontend/app/[locale]/admin/shop/orders/[id]/RefundButton.tsx

File renamed without changes.

frontend/app/[locale]/shop/admin/orders/[id]/page.tsx renamed to frontend/app/[locale]/admin/shop/orders/[id]/page.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Metadata } from 'next';
22
import { notFound } from 'next/navigation';
33

4-
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
54
import { getAdminOrderDetail } from '@/db/queries/shop/admin-orders';
65
import { Link } from '@/i18n/routing';
7-
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
86
import {
97
type CurrencyCode,
108
formatMoney,
@@ -19,7 +17,6 @@ export const metadata: Metadata = {
1917
description: 'Review and manage order, including refunds and status checks.',
2018
};
2119

22-
export const dynamic = 'force-dynamic';
2320

2421
function pickMinor(minor: unknown, legacyMajor: unknown): number | null {
2522
if (typeof minor === 'number') return minor;
@@ -48,7 +45,6 @@ export default async function AdminOrderDetailPage({
4845
}: {
4946
params: Promise<{ locale: string; id: string }>;
5047
}) {
51-
await guardShopAdminPage();
5248

5349
const { locale, id } = await params;
5450

@@ -67,8 +63,6 @@ export default async function AdminOrderDetailPage({
6763
totalMinor === null ? '-' : formatMoney(totalMinor, currency, locale);
6864

6965
return (
70-
<>
71-
<ShopAdminTopbar />
7266

7367
<main
7468
className="mx-auto max-w-6xl px-4 py-8"
@@ -86,7 +80,7 @@ export default async function AdminOrderDetailPage({
8680

8781
<div className="flex shrink-0 flex-wrap items-center gap-2">
8882
<Link
89-
href="/shop/admin/orders"
83+
href="/admin/shop/orders"
9084
className="border-border text-foreground hover:bg-secondary rounded-md border px-3 py-1.5 text-sm font-medium transition-colors"
9185
>
9286
Back
@@ -288,6 +282,5 @@ export default async function AdminOrderDetailPage({
288282
</div>
289283
</section>
290284
</main>
291-
</>
292285
);
293286
}

frontend/app/[locale]/shop/admin/orders/page.tsx renamed to frontend/app/[locale]/admin/shop/orders/page.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Metadata } from 'next';
22
import { getTranslations } from 'next-intl/server';
33

4-
import { AdminPagination } from '@/components/shop/admin/AdminPagination';
5-
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
4+
import { AdminPagination } from '@/components/admin/shop/AdminPagination';
65
import { getAdminOrdersPage } from '@/db/queries/shop/admin-orders';
76
import { Link } from '@/i18n/routing';
8-
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
97
import { parsePage } from '@/lib/pagination';
108
import { CSRF_FORM_FIELD, issueCsrfToken } from '@/lib/security/csrf';
119
import {
@@ -20,7 +18,6 @@ export const metadata: Metadata = {
2018
description: 'View and manage orders in the DevLovers shop catalog.',
2119
};
2220

23-
export const dynamic = 'force-dynamic';
2421

2522
const PAGE_SIZE = 25;
2623

@@ -50,7 +47,6 @@ export default async function AdminOrdersPage({
5047
params: Promise<{ locale: string }>;
5148
searchParams: Promise<{ page?: string }>;
5249
}) {
53-
await guardShopAdminPage();
5450

5551
const { locale } = await params;
5652
const sp = await searchParams;
@@ -80,14 +76,12 @@ export default async function AdminOrdersPage({
8076
totalMinor === null ? '-' : formatMoney(totalMinor, currency, locale),
8177
itemCount: order.itemCount,
8278
paymentProvider: order.paymentProvider ?? '-',
83-
viewHref: `/shop/admin/orders/${order.id}`,
79+
viewHref: `/admin/shop/orders/${order.id}`,
8480
viewAriaLabel: t('viewOrder', { id: order.id }),
8581
};
8682
});
8783

8884
return (
89-
<>
90-
<ShopAdminTopbar />
9185

9286
<main
9387
className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
@@ -101,7 +95,7 @@ export default async function AdminOrdersPage({
10195
{t('title')}
10296
</h1>
10397

104-
<form action="/api/shop/admin/orders/reconcile-stale" method="post">
98+
<form action="/api/shop/admin/orders/reconcile-stale" method='post'>
10599
<input type="hidden" name={CSRF_FORM_FIELD} value={csrfToken} />
106100
<button
107101
type="submit"
@@ -302,13 +296,12 @@ export default async function AdminOrdersPage({
302296

303297
<div className="mt-4">
304298
<AdminPagination
305-
basePath="/shop/admin/orders"
299+
basePath="/admin/shop/orders"
306300
page={page}
307301
hasNext={hasNext}
308302
/>
309303
</div>
310304
</section>
311305
</main>
312-
</>
313306
);
314307
}
Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import { Metadata } from 'next';
22
import { getTranslations } from 'next-intl/server';
33

4-
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
54
import { Link } from '@/i18n/routing';
6-
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
75

86
export const metadata: Metadata = {
97
title: 'Shop Admin | DevLovers',
108
description: 'Manage products, orders, and settings for your shop.',
119
};
1210

13-
export const dynamic = 'force-dynamic';
1411

1512
export default async function ShopAdminHomePage() {
16-
await guardShopAdminPage();
13+
1714
const t = await getTranslations('shop.admin.page');
1815

1916
return (
20-
<>
21-
<ShopAdminTopbar />
22-
2317
<main
2418
className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
2519
aria-labelledby="shop-admin-title"
@@ -40,7 +34,7 @@ export default async function ShopAdminHomePage() {
4034
<ul className="grid gap-4 sm:grid-cols-2">
4135
<li>
4236
<Link
43-
href="/shop/admin/products"
37+
href="/admin/shop/products"
4438
className="border-border hover:bg-muted/50 block rounded-lg border p-4 transition-colors"
4539
>
4640
<div className="text-foreground text-base font-semibold">
@@ -54,7 +48,7 @@ export default async function ShopAdminHomePage() {
5448

5549
<li>
5650
<Link
57-
href="/shop/admin/orders"
51+
href="/admin/shop/orders"
5852
className="border-border hover:bg-muted/50 block rounded-lg border p-4 transition-colors"
5953
>
6054
<div className="text-foreground text-base font-semibold">
@@ -68,6 +62,5 @@ export default async function ShopAdminHomePage() {
6862
</ul>
6963
</section>
7064
</main>
71-
</>
7265
);
7366
}

frontend/app/[locale]/shop/admin/products/[id]/edit/page.tsx renamed to frontend/app/[locale]/admin/shop/products/[id]/edit/page.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { Metadata } from 'next';
33
import { notFound } from 'next/navigation';
44
import { z } from 'zod';
55

6-
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
76
import { db } from '@/db';
87
import { productPrices, products } from '@/db/schema';
9-
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
108
import { issueCsrfToken } from '@/lib/security/csrf';
119
import type { CurrencyCode } from '@/lib/shop/currency';
1210
import { currencyValues } from '@/lib/shop/currency';
@@ -18,7 +16,6 @@ export const metadata: Metadata = {
1816
description: 'Edit an existing product in the DevLovers shop catalog.',
1917
};
2018

21-
export const dynamic = 'force-dynamic';
2219

2320
const paramsSchema = z.object({ id: z.string().uuid() });
2421

@@ -37,7 +34,7 @@ export default async function EditProductPage({
3734
}: {
3835
params: Promise<{ id: string }>;
3936
}) {
40-
await guardShopAdminPage();
37+
4138

4239
const rawParams = await params;
4340
const parsed = paramsSchema.safeParse(rawParams);
@@ -84,8 +81,6 @@ export default async function EditProductPage({
8481
const csrfToken = issueCsrfToken('admin:products:update');
8582

8683
return (
87-
<>
88-
<ShopAdminTopbar />
8984
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
9085
<ProductForm
9186
mode="edit"
@@ -109,6 +104,5 @@ export default async function EditProductPage({
109104
}}
110105
/>
111106
</main>
112-
</>
113107
);
114108
}

0 commit comments

Comments
 (0)