Skip to content

Commit ee56430

Browse files
Merge pull request #278 from DevLoversTeam/develop
Release v0.5.4
2 parents c36c403 + 9c0a867 commit ee56430

52 files changed

Lines changed: 1306 additions & 868 deletions

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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
306306
- Improved stability of text selection detection for AI helper
307307
- Fixed locale duplication and routing edge cases
308308
- Reduced visual overlap issues on small mobile screens
309+
310+
## [0.5.3] - 2026-02-04
311+
312+
### Added
313+
314+
- Quiz performance improvements:
315+
- Redis-based answer verification replacing AES encryption
316+
- Server-side quiz cache initialization to reduce verification latency
317+
- Debug endpoints for inspecting and clearing quiz caches (development only)
318+
- Caching & data layer:
319+
- Persistent Redis caching for static Quiz and Q&A data (TTL removed)
320+
- Cache-aside strategy for quiz answers and Q&A content
321+
- Internationalization & accessibility:
322+
- Translations for blog categories, CTA variants, and UI components (en / uk / pl)
323+
- Improved aria-label coverage for navigation, cart, theme toggle, and search
324+
- Developer experience:
325+
- Finalized ESLint Flat Config for frontend
326+
- Stable Prettier + Tailwind class sorting workflow
327+
- Consistent format-on-save behavior across the team
328+
329+
### Changed
330+
331+
- Quiz system refactor:
332+
- Simplified answer verification flow using Redis lookups
333+
- Improved guest session restoration after quiz completion
334+
- Language switch now preserves quiz results for guest users
335+
- Layout & UI refinements:
336+
- Removed duplicate padding on quiz routes
337+
- Improved mobile alignment for Quiz Rules and headers
338+
- Refined leaderboard component structure and lint stability
339+
- Shop module cleanup:
340+
- Normalized component naming (PascalCase)
341+
- Reorganized test structure under domain boundaries
342+
- Unified active-state and hover styling across shop routes
343+
- Blog UI improvements:
344+
- Fixed mobile paddings and spacing consistency
345+
- Improved responsive header and layout behavior
346+
347+
### Fixed
348+
349+
- Fixed mobile layout misalignment on quiz pages
350+
- Fixed guest language switch issues on quiz result screen
351+
- Improved WCAG color contrast compliance across quiz UI
352+
- Fixed ESLint, Prettier, and test configuration inconsistencies
353+
- Removed unused files, dead code, and outdated utilities
354+
- Improved reliability of quiz session restoration and state handling
355+
356+
## [0.5.4] - 2026-02-05
357+
358+
### Added
359+
360+
- Quiz SEO & performance improvements:
361+
- Dynamic metadata generation for quizzes list and quiz detail pages
362+
- i18n-aware meta titles and descriptions (en / uk / pl)
363+
- Browserslist configuration targeting modern browsers
364+
- Quiz content updates:
365+
- Expanded JavaScript Fundamentals quiz from 10 to 40 questions
366+
- Dashboard UI improvements:
367+
- New DynamicGridBackground for cleaner visual hierarchy
368+
- Refined ProfileCard and StatsCard layouts
369+
- Accessibility & i18n:
370+
- Improved aria-label coverage across navigation and UI controls
371+
- Refined English, Polish, and Ukrainian UI copy and punctuation
372+
373+
### Changed
374+
375+
- Quiz UX refinements:
376+
- Countdown timer animation stabilized on tab switch and session restore
377+
- Emoji replaced with icon-based indicators for consistent styling
378+
- Anti-cheat logic improved to distinguish touch vs mouse events
379+
- Q&A experience improvements:
380+
- Pagination scroll now targets section instead of page top
381+
- Mobile tap lock resolved by clearing text selection on interaction
382+
- Home & layout updates:
383+
- Improved code card sizing and responsive behavior
384+
- Online users counter repositioned for better mobile UX
385+
- Shop UX refinements:
386+
- Canonicalized legacy “View all” filters
387+
- Improved cart CTA behavior and badge layering
388+
- Blog & CMS:
389+
- Refactored blog image rendering and filtering logic
390+
- Improved pagination state handling
391+
- Styling & consistency:
392+
- Fixed Tailwind v4 canonical class warnings
393+
- Unified token-based styling across dashboard, 404 page, and controls
394+
395+
### Fixed
396+
397+
- Fixed mobile anti-cheat false positives on quiz pages
398+
- Removed render-blocking Font Awesome CSS
399+
- Fixed quiz timer progress bar desynchronization
400+
- Improved table text contrast in dark mode
401+
- Fixed cart badge overlay issues in header
402+
- Resolved multiple mobile spacing and padding inconsistencies

frontend/.browserslistrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
defaults and fully supports es6-module

frontend/app/[locale]/blog/[slug]/PostDetails.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ function renderPortableTextSpans(
8484
const text = child?.text || '';
8585
if (!text) return null;
8686
const marks = child?.marks || [];
87-
const linkKey = marks.find(mark => linkMap.has(mark));
8887

8988
let node: React.ReactNode = marks.length === 0 ? linkifyText(text) : text;
9089

@@ -280,11 +279,14 @@ function renderPortableText(
280279

281280
if (block?._type === 'image' && block?.url) {
282281
nodes.push(
283-
<img
282+
<Image
284283
key={block._key || `image-${i}`}
285284
src={block.url}
286285
alt={postTitle || 'Post image'}
287-
className="my-6 rounded-xl border border-gray-200"
286+
width={1200}
287+
height={800}
288+
sizes="100vw"
289+
className="my-6 h-auto w-full rounded-xl border border-gray-200"
288290
/>
289291
);
290292
i += 1;

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

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PostAuthQuizSync } from '@/components/auth/PostAuthQuizSync';
44
import { ProfileCard } from '@/components/dashboard/ProfileCard';
55
import { QuizSavedBanner } from '@/components/dashboard/QuizSavedBanner';
66
import { StatsCard } from '@/components/dashboard/StatsCard';
7+
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
78
import { getUserQuizStats } from '@/db/queries/quiz';
89
import { getUserProfile } from '@/db/queries/users';
910
import { redirect } from '@/i18n/routing';
@@ -75,42 +76,35 @@ export default async function DashboardPage({
7576
};
7677

7778
const outlineBtnStyles =
78-
'inline-flex items-center justify-center rounded-full border border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm px-6 py-2 text-sm font-medium text-slate-600 dark:text-slate-300 transition-colors hover:bg-white hover:text-sky-600 dark:hover:bg-slate-800 dark:hover:text-sky-400';
79+
'inline-flex items-center justify-center rounded-full border border-gray-200 dark:border-white/10 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm px-6 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 transition-colors hover:bg-white hover:text-(--accent-primary) dark:hover:bg-neutral-800 dark:hover:text-(--accent-primary)';
7980

8081
return (
81-
<main className="relative min-h-[calc(100vh-80px)] overflow-hidden">
82+
<div className="min-h-screen">
8283
<PostAuthQuizSync />
83-
<div
84-
className="pointer-events-none absolute inset-0 -z-10"
85-
aria-hidden="true"
84+
<DynamicGridBackground
85+
showStaticGrid
86+
className="min-h-screen bg-gray-50 py-12 transition-colors duration-300 dark:bg-transparent"
8687
>
87-
<div className="absolute inset-0 bg-linear-to-b from-sky-50 via-white to-rose-50 dark:from-slate-950 dark:via-slate-950 dark:to-black" />
88-
<div className="absolute top-0 left-1/4 h-96 w-xl -translate-x-1/2 rounded-full bg-sky-300/20 blur-3xl dark:bg-sky-500/10" />
89-
<div className="absolute right-0 bottom-0 h-104 w-104 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-500/10" />
90-
<div className="absolute bottom-10 left-10 h-80 w-[20rem] rounded-full bg-pink-300/20 blur-3xl dark:bg-fuchsia-500/10" />
91-
</div>
92-
93-
<div className="relative z-10 mx-auto max-w-5xl px-6 py-12">
94-
<header className="mb-12 flex flex-col justify-between gap-6 md:flex-row md:items-center">
95-
<div>
96-
<h1 className="text-4xl font-black tracking-tight drop-shadow-sm md:text-5xl">
97-
<span className="bg-linear-to-r from-sky-400 via-violet-400 to-pink-400 bg-clip-text text-transparent dark:from-sky-400 dark:via-indigo-400 dark:to-fuchsia-500">
98-
{t('title')}
99-
</span>
100-
</h1>
101-
<p className="mt-2 text-lg text-slate-600 dark:text-slate-400">
102-
{t('subtitle')}
103-
</p>
88+
<main className="relative z-10 mx-auto max-w-5xl px-6">
89+
<header className="mb-12 flex flex-col justify-between gap-6 md:flex-row md:items-center">
90+
<div>
91+
<h1 className="text-4xl font-black tracking-tight md:text-5xl">
92+
<span className="text-(--accent-primary)">{t('title')}</span>
93+
</h1>
94+
<p className="mt-2 text-lg text-gray-600 dark:text-gray-400">
95+
{t('subtitle')}
96+
</p>
97+
</div>
98+
99+
<span className={outlineBtnStyles}>{t('supportLink')}</span>
100+
</header>
101+
<QuizSavedBanner />
102+
<div className="grid gap-8 md:grid-cols-2">
103+
<ProfileCard user={userForDisplay} locale={locale} />
104+
<StatsCard stats={stats} />
104105
</div>
105-
106-
<span className={outlineBtnStyles}>{t('supportLink')}</span>
107-
</header>
108-
<QuizSavedBanner />
109-
<div className="grid gap-8 md:grid-cols-2">
110-
<ProfileCard user={userForDisplay} locale={locale} />
111-
<StatsCard stats={stats} />
112-
</div>
113-
</div>
114-
</main>
106+
</main>
107+
</DynamicGridBackground>
108+
</div>
115109
);
116110
}

frontend/app/[locale]/layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { AppChrome } from '@/components/header/AppChrome';
1010
import { MainSwitcher } from '@/components/header/MainSwitcher';
1111
import { CookieBanner } from '@/components/shared/CookieBanner';
1212
import Footer from '@/components/shared/Footer';
13-
import { OnlineCounterPopup } from '@/components/shared/OnlineCounterPopup';
1413
import { ThemeProvider } from '@/components/theme/ThemeProvider';
1514
import { locales } from '@/i18n/config';
1615
import { getCurrentUser } from '@/lib/auth';
@@ -73,7 +72,6 @@ export default async function LocaleLayout({
7372
{children}
7473
</MainSwitcher>
7574
</AppChrome>
76-
<OnlineCounterPopup />
7775

7876
<Footer />
7977
<Toaster position="top-right" richColors expand />

frontend/app/[locale]/q&a/page.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,21 @@ export default async function QAPage({
2727
const t = await getTranslations({ locale, namespace: 'qa' });
2828

2929
return (
30-
<DynamicGridBackground className="bg-gray-50 py-10 transition-colors duration-300 dark:bg-transparent">
31-
<main className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
32-
<div className="mb-8">
33-
<p className="text-sm font-semibold text-(--accent-primary)">
34-
{t('pretitle')}
35-
</p>
36-
<h1 className="text-3xl font-bold">{t('title')}</h1>
37-
<p className="text-gray-600 dark:text-gray-400">{t('subtitle')}</p>
38-
</div>
39-
<Suspense fallback={<>...</>}>
40-
<QaSection />
41-
</Suspense>
42-
</main>
43-
</DynamicGridBackground>
30+
<div className="min-h-screen">
31+
<DynamicGridBackground className="bg-gray-50 py-10 transition-colors duration-300 dark:bg-transparent">
32+
<main className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
33+
<div className="mb-8">
34+
<p className="text-sm font-semibold text-(--accent-primary)">
35+
{t('pretitle')}
36+
</p>
37+
<h1 className="text-3xl font-bold">{t('title')}</h1>
38+
<p className="text-gray-600 dark:text-gray-400">{t('subtitle')}</p>
39+
</div>
40+
<Suspense fallback={<>...</>}>
41+
<QaSection />
42+
</Suspense>
43+
</main>
44+
</DynamicGridBackground>
45+
</div>
4446
);
4547
}

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from 'next';
12
import { notFound, redirect } from 'next/navigation';
23
import { getTranslations } from 'next-intl/server';
34

@@ -6,6 +7,27 @@ import { stripCorrectAnswers } from '@/db/queries/quiz';
67
import { getQuizBySlug, getQuizQuestionsRandomized } from '@/db/queries/quiz';
78
import { getCurrentUser } from '@/lib/auth';
89

10+
type MetadataProps = { params: Promise<{ locale: string; slug: string }> };
11+
12+
export async function generateMetadata({
13+
params,
14+
}: MetadataProps): Promise<Metadata> {
15+
const { locale, slug } = await params;
16+
const t = await getTranslations({ locale, namespace: 'quiz.page' });
17+
const quiz = await getQuizBySlug(slug, locale);
18+
19+
if (!quiz) {
20+
return { title: t('notFoundTitle') };
21+
}
22+
23+
return {
24+
title: `${quiz.title} | ${t('metaSuffix')}`,
25+
description:
26+
quiz.description ??
27+
t('metaDescriptionFallback', { title: quiz.title ?? '' }),
28+
};
29+
}
30+
931
interface QuizPageProps {
1032
params: Promise<{ locale: string; slug: string }>;
1133
searchParams: Promise<{ seed?: string }>;
@@ -34,10 +56,10 @@ export default async function QuizPage({
3456

3557
const seed = Number.parseInt(seedParam, 10);
3658
if (Number.isNaN(seed)) {
37-
// eslint-disable-next-line react-hooks/purity -- redirect throws, value never used in render
59+
// eslint-disable-next-line react-hooks/purity -- redirect throws, value never used in render
3860
redirect(`/${locale}/quiz/${slug}?seed=${Date.now()}`);
3961
}
40-
62+
4163
const questions = await getQuizQuestionsRandomized(quiz.id, locale, seed);
4264

4365
const clientQuestions = stripCorrectAnswers(questions);

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from 'next';
12
import { getTranslations } from 'next-intl/server';
23

34
import QuizzesSection from '@/components/quiz/QuizzesSection';
@@ -7,6 +8,18 @@ import { getCurrentUser } from '@/lib/auth';
78

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

11+
export async function generateMetadata({
12+
params,
13+
}: PageProps): Promise<Metadata> {
14+
const { locale } = await params;
15+
const t = await getTranslations({ locale, namespace: 'quiz.list' });
16+
17+
return {
18+
title: t('metaTitle'),
19+
description: t('metaDescription'),
20+
};
21+
}
22+
1023
export const dynamic = 'force-dynamic';
1124

1225
export default async function QuizzesPage({ params }: PageProps) {

frontend/app/[locale]/shop/cart/CartPageClient.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { Minus, Plus, ShoppingBag, Trash2 } from 'lucide-react';
3+
import { Loader2, Minus, Plus, ShoppingBag, Trash2 } from 'lucide-react';
44
import Image from 'next/image';
55
import { useParams } from 'next/navigation';
66
import { useTranslations } from 'next-intl';
@@ -411,10 +411,23 @@ export default function CartPage() {
411411
/>
412412
<span className={SHOP_CTA_INSET} aria-hidden="true" />
413413

414-
<span className="relative z-10">
415-
{isCheckingOut
416-
? t('checkout.placing')
417-
: t('checkout.placeOrder')}
414+
<span className="relative z-10 inline-flex min-w-0 items-center justify-center gap-2">
415+
{isCheckingOut ? (
416+
<Loader2
417+
className="h-4 w-4 animate-spin"
418+
aria-hidden="true"
419+
/>
420+
) : null}
421+
422+
{/* visible label stays stable to avoid wrapping/layout shift */}
423+
<span className="truncate whitespace-nowrap">
424+
{t('checkout.placeOrder')}
425+
</span>
426+
427+
{/* screen readers can still get the “placing” state */}
428+
{isCheckingOut ? (
429+
<span className="sr-only">{t('checkout.placing')}</span>
430+
) : null}
418431
</span>
419432
</button>
420433

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default async function HomePage({
5252
</h2>
5353

5454
<Link
55-
href="/shop/products?filter=new"
55+
href="/shop/products?filter=newest"
5656
className="group border-border text-muted-foreground hover:text-foreground focus-visible:ring-offset-background inline-flex items-center gap-2 rounded-md border bg-transparent px-4 py-2 text-xs font-semibold tracking-[0.25em] uppercase shadow-none transition-[transform,box-shadow,color,filter] duration-500 ease-out hover:-translate-y-0.5 hover:shadow-[var(--shop-card-shadow-hover)] hover:brightness-110 focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)] focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
5757
aria-label={t('viewAll')}
5858
>

0 commit comments

Comments
 (0)