Skip to content

Commit 5d13b36

Browse files
Merge pull request #97 from DevLoversTeam/develop
Release v0.3.0
2 parents ac9f137 + 8221a79 commit 5d13b36

94 files changed

Lines changed: 75395 additions & 18175 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.

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
6666
- Hardened Q&A list rendering to prevent crashes on malformed list data
6767
- Fixed malformed list items in Git question #96
6868
- Allowed unauthenticated access to Quiz and Leaderboard pages (guest flow)
69+
70+
## [0.3.0] - 2026-01-01
71+
72+
### Added
73+
74+
- Social authentication via Google and GitHub (OAuth)
75+
- System theme–based favicon switching (light / dark via `prefers-color-scheme`)
76+
- Quiz cards redesign with categories, progress indicators, and status badges
77+
- Countdown timer for quizzes with auto-submit on expiration
78+
- Per-user quiz progress tracking (best score, attempts, completion %)
79+
- Category-based quiz browsing with responsive tabs
80+
- Multilingual content additions:
81+
- HTML questions base
82+
- React questions base
83+
- Localized About page (uk / en / pl)
84+
- Unified platform & shop header with variant-based behavior
85+
86+
### Changed
87+
88+
- Login and signup flows updated to support OAuth providers
89+
- Authentication UI enhanced with provider buttons and separators
90+
- Quiz navigation and layout improved for better UX on desktop and mobile
91+
- Blog and footer text fully localized using i18n strings
92+
- Header/navigation logic centralized to prevent route-specific inconsistencies
93+
- Shop pages aligned with unified header and navigation system
94+
95+
### Fixed
96+
97+
- Fixed GitHub OAuth redirect by correctly passing and validating state
98+
parameter
99+
- Improved OAuth security with stronger CSRF protection
100+
- Removed duplicated and legacy header components
101+
- Prevented import breakages caused by outdated shop/platform shells
102+
- Improved robustness of quiz duration calculation with reliable fallbacks
103+
- Cleaned up redundant files, comments, and unused utilities

frontend/.env.example

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,16 @@ NEXT_PUBLIC_SITE_URL=
2121
NEXT_PUBLIC_SITE_URL=
2222

2323
TELEGRAM_BOT_TOKEN=
24-
TELEGRAM_CHAT_ID=
24+
TELEGRAM_CHAT_ID=
25+
26+
GOOGLE_CLIENT_ID=
27+
GOOGLE_CLIENT_SECRET=
28+
GOOGLE_CLIENT_REDIRECT_URI_LOCAL=
29+
GOOGLE_CLIENT_REDIRECT_URI_DEVELOP=
30+
GOOGLE_CLIENT_REDIRECT_URI_PROD=
31+
32+
GITHUB_CLIENT_ID_DEVELOP=
33+
GITHUB_CLIENT_SECRET_DEVELOP=
34+
GITHUB_CLIENT_REDIRECT_URI_DEVELOP=
35+
36+
APP_ENV=

frontend/app/[locale]/layout.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import Footer from '@/components/shared/Footer';
99
import { ThemeProvider } from '@/components/theme/ThemeProvider';
1010
import { getCurrentUser } from '@/lib/auth';
1111

12-
import {
13-
HeaderSwitcher,
14-
MainSwitcher,
15-
} from '@/components/header/HeaderSwitcher';
12+
import { MainSwitcher } from '@/components/header/MainSwitcher';
13+
import { AppChrome } from '@/components/header/AppChrome';
1614

1715
export const dynamic = 'force-dynamic';
1816

@@ -30,6 +28,9 @@ export default async function LocaleLayout({
3028
const messages = await getMessages({ locale });
3129
const user = await getCurrentUser();
3230

31+
const userExists = Boolean(user);
32+
const showAdminNavLink = process.env.NEXT_PUBLIC_ENABLE_ADMIN === 'true';
33+
3334
return (
3435
<NextIntlClientProvider messages={messages}>
3536
<ThemeProvider
@@ -38,8 +39,9 @@ export default async function LocaleLayout({
3839
enableSystem
3940
disableTransitionOnChange
4041
>
41-
<HeaderSwitcher userExists={Boolean(user)} />
42-
<MainSwitcher>{children}</MainSwitcher>
42+
<AppChrome userExists={userExists} showAdminLink={showAdminNavLink}>
43+
<MainSwitcher>{children}</MainSwitcher>
44+
</AppChrome>
4345

4446
<Footer />
4547
<Toaster position="top-right" richColors expand />

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useState } from "react";
66
import { useSearchParams } from "next/navigation";
77
import { getPendingQuizResult, clearPendingQuizResult } from "@/lib/guest-quiz";
88
import { Button } from "@/components/ui/button";
9+
import { OAuthButtons } from '@/components/auth/OAuthButtons';
910

1011
export default function LoginPage() {
1112
const searchParams = useSearchParams();
@@ -82,6 +83,14 @@ export default function LoginPage() {
8283
<div className="mx-auto max-w-sm py-12">
8384
<h1 className="mb-6 text-2xl font-semibold">Log in</h1>
8485

86+
<OAuthButtons />
87+
88+
<div className="my-4 flex items-center gap-3">
89+
<div className="h-px flex-1 bg-gray-200" />
90+
<span className="text-xs text-gray-500">or</span>
91+
<div className="h-px flex-1 bg-gray-200" />
92+
</div>
93+
8594
<form onSubmit={onSubmit} className="space-y-4">
8695
<input
8796
name="email"

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default async function QuizPage({ params }: QuizPageProps) {
2121
}
2222

2323
const questions = await getQuizQuestionsRandomized(quiz.id, locale);
24-
24+
2525
if (!questions.length) {
2626
return (
2727
<div className="min-h-screen flex items-center justify-center">
@@ -44,9 +44,9 @@ export default async function QuizPage({ params }: QuizPageProps) {
4444
)}
4545
<div className="mt-4 flex gap-4 text-sm text-gray-500">
4646
<span>Питань: {quiz.questionsCount}</span>
47-
{quiz.timeLimitSeconds && (
48-
<span>Час: {Math.floor(quiz.timeLimitSeconds / 60)} хв</span>
49-
)}
47+
<span>
48+
Час: {Math.floor((quiz.timeLimitSeconds ?? questions.length * 30) / 60)} хв
49+
</span>
5050
</div>
5151
</div>
5252

@@ -55,6 +55,7 @@ export default async function QuizPage({ params }: QuizPageProps) {
5555
quizId={quiz.id}
5656
questions={questions}
5757
userId={user?.id ?? null}
58+
timeLimitSeconds={quiz.timeLimitSeconds ?? questions.length * 30}
5859
/>
5960
{user && <PendingResultHandler userId={user.id} />}
6061
</div>
Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import { Link } from '@/i18n/routing';
2-
import { getActiveQuizzes } from '@/db/queries/quiz';
3-
4-
type PageProps = { params: Promise<{ locale: string }>; };
1+
import { getActiveQuizzes, getUserQuizzesProgress } from '@/db/queries/quiz';
2+
import { getCurrentUser } from '@/lib/auth';
3+
import QuizzesSection from '@/components/quiz/QuizzesSection';
54

5+
type PageProps = { params: Promise<{ locale: string }> };
66

77
export const dynamic = 'force-dynamic';
88

99
export default async function QuizzesPage({ params }: PageProps) {
1010
const { locale } = await params;
11+
const session = await getCurrentUser();
12+
1113
const quizzes = await getActiveQuizzes(locale);
1214

15+
let userProgressMap: Record<string, any> = {};
16+
17+
if (session?.id) {
18+
const progressMapData = await getUserQuizzesProgress(session.id);
19+
userProgressMap = Object.fromEntries(progressMapData);
20+
}
21+
1322
if (!quizzes.length) {
1423
return (
15-
<div className="mx-auto max-w-4xl py-12">
24+
<div className="mx-auto max-w-5xl py-12">
1625
<h1 className="text-3xl font-bold mb-4">Quizzes</h1>
1726
<p className="text-gray-600 dark:text-gray-400">
1827
No quizzes available yet. Please check back soon.
@@ -22,54 +31,18 @@ export default async function QuizzesPage({ params }: PageProps) {
2231
}
2332

2433
return (
25-
<div className="mx-auto max-w-4xl py-12">
26-
<div className="flex items-center justify-between mb-6">
27-
<div>
28-
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
29-
Practice
30-
</p>
31-
<h1 className="text-3xl font-bold">Quizzes</h1>
32-
<p className="text-gray-600 dark:text-gray-400">
33-
Choose a quiz to test your knowledge.
34-
</p>
35-
</div>
34+
<div className="mx-auto max-w-5xl py-12">
35+
<div className="mb-8">
36+
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
37+
Practice
38+
</p>
39+
<h1 className="text-3xl font-bold">Quizzes</h1>
40+
<p className="text-gray-600 dark:text-gray-400">
41+
Choose a quiz to test your knowledge.
42+
</p>
3643
</div>
3744

38-
<div className="grid gap-4">
39-
{quizzes.map((quiz) => (
40-
<div
41-
key={quiz.id}
42-
className="rounded-xl border border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-5 shadow-sm"
43-
>
44-
<div className="flex items-center justify-between gap-3">
45-
<div className="space-y-1">
46-
<h2 className="text-xl font-semibold">
47-
{quiz.title ?? quiz.slug}
48-
</h2>
49-
{quiz.description && (
50-
<p className="text-gray-600 dark:text-gray-400 text-sm">
51-
{quiz.description}
52-
</p>
53-
)}
54-
<div className="flex gap-3 text-xs text-gray-500">
55-
<span>{quiz.questionsCount} questions</span>
56-
{quiz.timeLimitSeconds && (
57-
<span>
58-
{Math.floor(quiz.timeLimitSeconds / 60)} min limit
59-
</span>
60-
)}
61-
</div>
62-
</div>
63-
<Link
64-
href={`/quiz/${quiz.slug}`}
65-
className="inline-flex items-center rounded-lg bg-blue-600 text-white px-3 py-2 text-sm font-medium hover:bg-blue-500 transition"
66-
>
67-
Start quiz
68-
</Link>
69-
</div>
70-
</div>
71-
))}
72-
</div>
45+
<QuizzesSection quizzes={quizzes} userProgressMap={userProgressMap} />
7346
</div>
7447
);
7548
}

frontend/app/[locale]/shop/admin/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type React from 'react';
2-
import Link from 'next/link';
2+
import { Link } from '@/i18n/routing';
3+
34
import { notFound, redirect } from 'next/navigation';
45

56
import {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import Link from "next/link";
1+
import { Link } from '@/i18n/routing';
2+
23
import { notFound } from "next/navigation";
34

45
import { getAdminOrderDetail } from "@/db/queries/shop/admin-orders";
@@ -42,8 +43,7 @@ export default async function AdminOrderDetailPage({
4243
</div>
4344

4445
<div className="flex gap-2">
45-
<Link
46-
href={`/${locale}/shop/admin/orders`}
46+
<Link href="/shop/admin/orders"
4747
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
4848
>
4949
Back

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Link from "next/link";
1+
import { Link } from '@/i18n/routing';
22

33
import { getAdminOrdersPage } from "@/db/queries/shop/admin-orders";
44
import { formatMoney, resolveCurrencyFromLocale, type CurrencyCode } from "@/lib/shop/currency";
@@ -84,7 +84,7 @@ export default async function AdminOrdersPage({
8484

8585
<td className="px-3 py-2">
8686
<Link
87-
href={`/${locale}/shop/admin/orders/${order.id}`}
87+
href={`/shop/admin/orders/${order.id}`}
8888
className="rounded-md border border-border px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
8989
>
9090
View

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import Link from "next/link"
1+
import { Link } from '@/i18n/routing';
2+
23

34
export default function ShopAdminHomePage() {
45
return (

0 commit comments

Comments
 (0)