Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
634976a
feat(ui): add light/dark system favicons
ViktorSvertoka Dec 28, 2025
d74270f
Merge pull request #86 from DevLoversTeam/feature/favicon
ViktorSvertoka Dec 28, 2025
c8c11a7
add: html + react & 1/3CSS translations json
AM1007 Dec 28, 2025
26101cd
Merge pull request #87 from DevLoversTeam/multlang
AM1007 Dec 28, 2025
1a5ad11
feat (auth): implement Google and GitHub sign up and log in supportin…
kryvosheyin Dec 28, 2025
fd2661b
fix(build): move OAuth env config to lib/env/auth to avoid module con…
kryvosheyin Dec 28, 2025
54f6d8c
fix(build): align evn variables, remove unnecessary assertions
kryvosheyin Dec 29, 2025
ffc59b6
feat(quiz): redesign quiz cards with categories and countdown timer
LesiaUKR Dec 29, 2025
fa42ba4
fix(quiz): add auth validation and error handling for guest flow
LesiaUKR Dec 29, 2025
b65d32d
fix(quiz): prevent division by zero in QuizCard percentage
LesiaUKR Dec 29, 2025
bd658ac
Merge pull request #89 from DevLoversTeam/sl/feat/quiz
ViktorSvertoka Dec 30, 2025
f2ec0f9
fix(auth,db): address OAuth security issues and complete Drizzle rela…
kryvosheyin Dec 30, 2025
78a041b
fix(db) align property names in schema and relations files
kryvosheyin Dec 30, 2025
42bbd30
fix: rename search query params, add state to google request, address…
kryvosheyin Dec 30, 2025
dd30a0b
fix: update tables order in schema.ts to avoid forward refferencing
kryvosheyin Dec 30, 2025
d9e6688
fix(schema): update mode to date from string to all timestamps
kryvosheyin Dec 30, 2025
b4238a8
Merge pull request #88 from DevLoversTeam/feat/social-oauth
ViktorSvertoka Dec 30, 2025
6e429db
fix(auth): add missing state to github api callback, clean up dead mi…
kryvosheyin Dec 30, 2025
a08c581
Merge pull request #91 from DevLoversTeam/feat/social-oauth
ViktorSvertoka Dec 30, 2025
8980fdd
add multilingual support to About page
TiZorii Dec 31, 2025
764d3c9
localize Footer component
TiZorii Dec 31, 2025
d80791a
Update blog components with i18n support
TiZorii Dec 31, 2025
a6b2e5c
update PricingSection
TiZorii Dec 31, 2025
ecc820f
Merge pull request #95 from DevLoversTeam/feature/multilingual-about
TiZorii Dec 31, 2025
772b008
Unify headers and clean up removed shop components
liudmylasovetovs Jan 1, 2026
23964bc
fix(shop-nav): correct active link logic and disable placeholder search
liudmylasovetovs Jan 1, 2026
c9daa28
fix(shop-nav) chore(i18n): complete Link migration and remove locale-…
liudmylasovetovs Jan 1, 2026
55b9e9c
Merge pull request #96 from DevLoversTeam/lso/feat/shop
ViktorSvertoka Jan 1, 2026
76bfdf2
refactor(files): clean up code and remove redundant comments
ViktorSvertoka Jan 1, 2026
f76827b
chore(release): update changelog for v0.3.0
ViktorSvertoka Jan 1, 2026
40cb6c2
chore(release): v0.3.0
ViktorSvertoka Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Hardened Q&A list rendering to prevent crashes on malformed list data
- Fixed malformed list items in Git question #96
- Allowed unauthenticated access to Quiz and Leaderboard pages (guest flow)

## [0.3.0] - 2026-01-01

### Added

- Social authentication via Google and GitHub (OAuth)
- System theme–based favicon switching (light / dark via `prefers-color-scheme`)
- Quiz cards redesign with categories, progress indicators, and status badges
- Countdown timer for quizzes with auto-submit on expiration
- Per-user quiz progress tracking (best score, attempts, completion %)
- Category-based quiz browsing with responsive tabs
- Multilingual content additions:
- HTML questions base
- React questions base
- Localized About page (uk / en / pl)
- Unified platform & shop header with variant-based behavior

### Changed

- Login and signup flows updated to support OAuth providers
- Authentication UI enhanced with provider buttons and separators
- Quiz navigation and layout improved for better UX on desktop and mobile
- Blog and footer text fully localized using i18n strings
- Header/navigation logic centralized to prevent route-specific inconsistencies
- Shop pages aligned with unified header and navigation system

### Fixed

- Fixed GitHub OAuth redirect by correctly passing and validating state
parameter
- Improved OAuth security with stronger CSRF protection
- Removed duplicated and legacy header components
- Prevented import breakages caused by outdated shop/platform shells
- Improved robustness of quiz duration calculation with reliable fallbacks
- Cleaned up redundant files, comments, and unused utilities
14 changes: 13 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,16 @@ NEXT_PUBLIC_SITE_URL=
NEXT_PUBLIC_SITE_URL=

TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
TELEGRAM_CHAT_ID=

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_REDIRECT_URI_LOCAL=
GOOGLE_CLIENT_REDIRECT_URI_DEVELOP=
GOOGLE_CLIENT_REDIRECT_URI_PROD=

GITHUB_CLIENT_ID_DEVELOP=
GITHUB_CLIENT_SECRET_DEVELOP=
GITHUB_CLIENT_REDIRECT_URI_DEVELOP=

APP_ENV=
14 changes: 8 additions & 6 deletions frontend/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import Footer from '@/components/shared/Footer';
import { ThemeProvider } from '@/components/theme/ThemeProvider';
import { getCurrentUser } from '@/lib/auth';

import {
HeaderSwitcher,
MainSwitcher,
} from '@/components/header/HeaderSwitcher';
import { MainSwitcher } from '@/components/header/MainSwitcher';
import { AppChrome } from '@/components/header/AppChrome';

export const dynamic = 'force-dynamic';

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

const userExists = Boolean(user);
const showAdminNavLink = process.env.NEXT_PUBLIC_ENABLE_ADMIN === 'true';

return (
<NextIntlClientProvider messages={messages}>
<ThemeProvider
Expand All @@ -38,8 +39,9 @@ export default async function LocaleLayout({
enableSystem
disableTransitionOnChange
>
<HeaderSwitcher userExists={Boolean(user)} />
<MainSwitcher>{children}</MainSwitcher>
<AppChrome userExists={userExists} showAdminLink={showAdminNavLink}>
<MainSwitcher>{children}</MainSwitcher>
</AppChrome>

<Footer />
<Toaster position="top-right" richColors expand />
Expand Down
9 changes: 9 additions & 0 deletions frontend/app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { getPendingQuizResult, clearPendingQuizResult } from "@/lib/guest-quiz";
import { Button } from "@/components/ui/button";
import { OAuthButtons } from '@/components/auth/OAuthButtons';

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

<OAuthButtons />

<div className="my-4 flex items-center gap-3">
<div className="h-px flex-1 bg-gray-200" />
<span className="text-xs text-gray-500">or</span>
<div className="h-px flex-1 bg-gray-200" />
</div>

<form onSubmit={onSubmit} className="space-y-4">
<input
name="email"
Expand Down
9 changes: 5 additions & 4 deletions frontend/app/[locale]/quiz/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default async function QuizPage({ params }: QuizPageProps) {
}

const questions = await getQuizQuestionsRandomized(quiz.id, locale);

if (!questions.length) {
return (
<div className="min-h-screen flex items-center justify-center">
Expand All @@ -44,9 +44,9 @@ export default async function QuizPage({ params }: QuizPageProps) {
)}
<div className="mt-4 flex gap-4 text-sm text-gray-500">
<span>Питань: {quiz.questionsCount}</span>
{quiz.timeLimitSeconds && (
<span>Час: {Math.floor(quiz.timeLimitSeconds / 60)} хв</span>
)}
<span>
Час: {Math.floor((quiz.timeLimitSeconds ?? questions.length * 30) / 60)} хв
</span>
</div>
</div>

Expand All @@ -55,6 +55,7 @@ export default async function QuizPage({ params }: QuizPageProps) {
quizId={quiz.id}
questions={questions}
userId={user?.id ?? null}
timeLimitSeconds={quiz.timeLimitSeconds ?? questions.length * 30}
/>
{user && <PendingResultHandler userId={user.id} />}
</div>
Expand Down
75 changes: 24 additions & 51 deletions frontend/app/[locale]/quizzes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { Link } from '@/i18n/routing';
import { getActiveQuizzes } from '@/db/queries/quiz';

type PageProps = { params: Promise<{ locale: string }>; };
import { getActiveQuizzes, getUserQuizzesProgress } from '@/db/queries/quiz';
import { getCurrentUser } from '@/lib/auth';
import QuizzesSection from '@/components/quiz/QuizzesSection';

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

export const dynamic = 'force-dynamic';

export default async function QuizzesPage({ params }: PageProps) {
const { locale } = await params;
const session = await getCurrentUser();

const quizzes = await getActiveQuizzes(locale);

let userProgressMap: Record<string, any> = {};

if (session?.id) {
const progressMapData = await getUserQuizzesProgress(session.id);
userProgressMap = Object.fromEntries(progressMapData);
}

if (!quizzes.length) {
return (
<div className="mx-auto max-w-4xl py-12">
<div className="mx-auto max-w-5xl py-12">
<h1 className="text-3xl font-bold mb-4">Quizzes</h1>
<p className="text-gray-600 dark:text-gray-400">
No quizzes available yet. Please check back soon.
Expand All @@ -22,54 +31,18 @@ export default async function QuizzesPage({ params }: PageProps) {
}

return (
<div className="mx-auto max-w-4xl py-12">
<div className="flex items-center justify-between mb-6">
<div>
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
Practice
</p>
<h1 className="text-3xl font-bold">Quizzes</h1>
<p className="text-gray-600 dark:text-gray-400">
Choose a quiz to test your knowledge.
</p>
</div>
<div className="mx-auto max-w-5xl py-12">
<div className="mb-8">
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
Practice
</p>
<h1 className="text-3xl font-bold">Quizzes</h1>
<p className="text-gray-600 dark:text-gray-400">
Choose a quiz to test your knowledge.
</p>
</div>

<div className="grid gap-4">
{quizzes.map((quiz) => (
<div
key={quiz.id}
className="rounded-xl border border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-5 shadow-sm"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-1">
<h2 className="text-xl font-semibold">
{quiz.title ?? quiz.slug}
</h2>
{quiz.description && (
<p className="text-gray-600 dark:text-gray-400 text-sm">
{quiz.description}
</p>
)}
<div className="flex gap-3 text-xs text-gray-500">
<span>{quiz.questionsCount} questions</span>
{quiz.timeLimitSeconds && (
<span>
{Math.floor(quiz.timeLimitSeconds / 60)} min limit
</span>
)}
</div>
</div>
<Link
href={`/quiz/${quiz.slug}`}
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"
>
Start quiz
</Link>
</div>
</div>
))}
</div>
<QuizzesSection quizzes={quizzes} userProgressMap={userProgressMap} />
</div>
);
}
3 changes: 2 additions & 1 deletion frontend/app/[locale]/shop/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type React from 'react';
import Link from 'next/link';
import { Link } from '@/i18n/routing';

import { notFound, redirect } from 'next/navigation';

import {
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/[locale]/shop/admin/orders/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link";
import { Link } from '@/i18n/routing';

import { notFound } from "next/navigation";

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

<div className="flex gap-2">
<Link
href={`/${locale}/shop/admin/orders`}
<Link href="/shop/admin/orders"
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
>
Back
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[locale]/shop/admin/orders/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Link from "next/link";
import { Link } from '@/i18n/routing';

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

<td className="px-3 py-2">
<Link
href={`/${locale}/shop/admin/orders/${order.id}`}
href={`/shop/admin/orders/${order.id}`}
className="rounded-md border border-border px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
>
View
Expand Down
3 changes: 2 additions & 1 deletion frontend/app/[locale]/shop/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link"
import { Link } from '@/i18n/routing';


export default function ShopAdminHomePage() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { currencyValues } from '@/lib/shop/currency';
const paramsSchema = z.object({ id: z.string().uuid() });
function parseMajorToMinor(value: string | number): number {
const s = String(value).trim().replace(',', '.');
// допускаємо "10", "10.5", "10.50"
if (!/^\d+(\.\d{1,2})?$/.test(s)) {
throw new Error(`Invalid money value: "${value}"`);
}
Expand Down
Loading