Skip to content

Commit 0cf755c

Browse files
committed
feat(Blog): merge develop
2 parents d18554b + aa5654b commit 0cf755c

88 files changed

Lines changed: 5621 additions & 3889 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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
101101
- Prevented import breakages caused by outdated shop/platform shells
102102
- Improved robustness of quiz duration calculation with reliable fallbacks
103103
- Cleaned up redundant files, comments, and unused utilities
104+
105+
## [0.4.0] - 2026-01-21
106+
107+
### Added
108+
109+
- Complete authentication lifecycle:
110+
- Google & GitHub OAuth
111+
- Email verification
112+
- Password reset and recovery flows
113+
- Full internationalization (uk / en / pl) across:
114+
- Authentication pages
115+
- Dashboard
116+
- Contacts
117+
- About page
118+
- Blog, Q&A, Quiz
119+
- Privacy Policy and Terms & Conditions
120+
- New quiz content:
121+
- Angular, Vue.js, Node.js quizzes
122+
- HTML and React question bases
123+
- Advanced quiz experience:
124+
- Countdown timer with persistence and auto-submit
125+
- Encrypted/hashed answers to prevent client-side cheating
126+
- Session persistence with quit confirmation
127+
- Guest quiz results synced after authentication
128+
- Real-time online users counter with animated UI
129+
- GDPR-compliant cookie consent banner with i18n support
130+
- Unified platform & shop header system
131+
- System theme–based favicon switching (light / dark)
132+
- Initial SVG icon set for UI usage
133+
134+
### Changed
135+
136+
- Quiz UI redesigned:
137+
- Category-based tabs
138+
- Consistent QuizCard layout
139+
- Progress indicators and status badges
140+
- Q&A UI refreshed:
141+
- Unified layout with shared background
142+
- Improved pagination and accordion readability
143+
- Authentication pages refactored into reusable components
144+
- Blog experience improved:
145+
- Redesigned blog page and cards
146+
- Category pages and header search
147+
- Recommended posts section
148+
- Shop UI and layout unified across platform and admin views
149+
- Tailwind theme tokens centralized for theme-aware styling
150+
- Database migration history reset to a clean, linear baseline
151+
152+
### Fixed
153+
154+
- Fixed GitHub OAuth redirect and CSRF state handling
155+
- Fixed quiz timer issues when switching languages
156+
- Fixed Q&A API caching to always return fresh data
157+
- Resolved multiple accessibility issues (WCAG, W3C, Lighthouse 100%)
158+
- Hardened authentication redirects to prevent open-redirect vulnerabilities
159+
- Stabilized shop checkout, refund, inventory, and webhook flows
160+
- Improved Neon performance and reduced CU-hours usage
161+
- Cleaned up redundant files, comments, and legacy code
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1+
import { getPlatformStats } from "@/lib/about/stats"
2+
import { getSponsors } from "@/lib/about/github-sponsors"
3+
14
import { HeroSection } from "@/components/about/HeroSection"
5+
import { TopicsSection } from "@/components/about/TopicsSection"
26
import { FeaturesSection } from "@/components/about/FeaturesSection"
37
import { PricingSection } from "@/components/about/PricingSection"
48
import { CommunitySection } from "@/components/about/CommunitySection"
59

6-
export default function AboutPage() {
10+
export default async function AboutPage() {
11+
const [stats, sponsors] = await Promise.all([
12+
getPlatformStats(),
13+
getSponsors()
14+
])
15+
716
return (
8-
<main className="min-h-screen bg-neutral-950 text-white">
9-
<HeroSection />
17+
<main className="min-h-screen bg-gray-50 dark:bg-black overflow-hidden text-gray-900 dark:text-white
18+
w-[100vw] relative left-[50%] right-[50%] -ml-[50vw] -mr-[50vw]"
19+
>
20+
21+
<HeroSection stats={stats} />
22+
<TopicsSection />
1023
<FeaturesSection />
11-
<PricingSection />
24+
<PricingSection sponsors={sponsors} />
1225
<CommunitySection />
26+
1327
</main>
1428
)
15-
}
29+
}

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
1-
export const metadata = {
2-
title: "Contacts | DevLovers",
3-
description:
4-
"Get in touch with the DevLovers team for questions, feedback, or collaboration.",
5-
};
1+
import { getTranslations } from "next-intl/server";
2+
3+
export async function generateMetadata({
4+
params,
5+
}: {
6+
params: Promise<{ locale: string }>;
7+
}) {
8+
const { locale } = await params;
9+
const t = await getTranslations({ locale, namespace: "contacts" });
10+
11+
return {
12+
title: t("metaTitle"),
13+
description: t("metaDescription"),
14+
};
15+
}
16+
17+
export default async function ContactsPage() {
18+
const t = await getTranslations("contacts");
619

7-
export default function ContactsPage() {
820
return (
921
<main className="max-w-2xl mx-auto py-12 px-4">
10-
<h1 className="text-3xl font-bold mb-6">Contacts</h1>
11-
<p className="mb-4">We’d love to hear from you! 💬</p>
22+
<h1 className="text-3xl font-bold mb-6">{t("title")}</h1>
23+
<p className="mb-4">{t("subtitle")} 💬</p>
1224
<ul className="space-y-2">
1325
<li>
14-
📧 Email:{" "}
26+
📧 {t("email")}{" "}
1527
<a
1628
href="mailto:victor.svertoka@gmail.com"
1729
className="text-blue-600 hover:underline"
@@ -20,7 +32,7 @@ export default function ContactsPage() {
2032
</a>
2133
</li>
2234
<li>
23-
💼 LinkedIn:{" "}
35+
💼 {t("linkedin")}{" "}
2436
<a
2537
href="https://www.linkedin.com/in/viktor-svertoka/"
2638
target="_blank"
@@ -31,7 +43,7 @@ export default function ContactsPage() {
3143
</a>
3244
</li>
3345
<li>
34-
🧑‍💻 GitHub:{" "}
46+
🧑‍💻 {t("github")}{" "}
3547
<a
3648
href="https://github.com/ViktorSvertoka"
3749
target="_blank"

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { redirect } from '@/i18n/routing';
22
import { Link } from '@/i18n/routing'
3+
import { getTranslations } from 'next-intl/server';
34
import { getCurrentUser } from '@/lib/auth';
45
import { getUserProfile } from '@/db/queries/users';
56
import { getUserQuizStats } from '@/db/queries/quiz';
@@ -9,10 +10,19 @@ import { StatsCard } from '@/components/dashboard/StatsCard';
910
import { QuizSavedBanner } from '@/components/dashboard/QuizSavedBanner';
1011
import { PostAuthQuizSync } from "@/components/auth/PostAuthQuizSync";
1112

12-
export const metadata = {
13-
title: 'Dashboard | DevLovers',
14-
description: 'Track your progress and quiz performance.',
15-
};
13+
export async function generateMetadata({
14+
params,
15+
}: {
16+
params: Promise<{ locale: string }>;
17+
}) {
18+
const { locale } = await params;
19+
const t = await getTranslations({ locale, namespace: 'dashboard' });
20+
21+
return {
22+
title: t('metaTitle'),
23+
description: t('metaDescription'),
24+
};
25+
}
1626

1727
export default async function DashboardPage({ params }: { params: Promise<{ locale: string }> }) {
1828
const session = await getCurrentUser();
@@ -22,6 +32,8 @@ export default async function DashboardPage({ params }: { params: Promise<{ loca
2232
const user = await getUserProfile(session.id);
2333
if (!user) { redirect({ href: '/login', locale }); return; }
2434

35+
const t = await getTranslations('dashboard');
36+
2537
const attempts = await getUserQuizStats(session.id);
2638

2739
const totalAttempts = attempts.length;
@@ -36,7 +48,7 @@ export default async function DashboardPage({ params }: { params: Promise<{ loca
3648

3749
const lastActiveDate =
3850
totalAttempts > 0
39-
? new Date(attempts[0].completedAt).toLocaleDateString('uk-UA')
51+
? new Date(attempts[0].completedAt).toLocaleDateString(locale)
4052
: null;
4153

4254
const userForDisplay = {
@@ -74,21 +86,21 @@ export default async function DashboardPage({ params }: { params: Promise<{ loca
7486
<div>
7587
<h1 className="text-4xl md:text-5xl font-black tracking-tight drop-shadow-sm">
7688
<span className="bg-gradient-to-r from-sky-400 via-violet-400 to-pink-400 dark:from-sky-400 dark:via-indigo-400 dark:to-fuchsia-500 bg-clip-text text-transparent">
77-
Dashboard
89+
{t('title')}
7890
</span>
7991
</h1>
8092
<p className="mt-2 text-slate-600 dark:text-slate-400 text-lg">
81-
Welcome back to your training ground.
93+
{t('subtitle')}
8294
</p>
8395
</div>
8496

8597
<Link href="/contacts" className={outlineBtnStyles}>
86-
Support & Feedback
98+
{t('supportLink')}
8799
</Link>
88100
</header>
89101
<QuizSavedBanner />
90102
<div className="grid gap-8 md:grid-cols-2">
91-
<ProfileCard user={userForDisplay} />
103+
<ProfileCard user={userForDisplay} locale={locale} />
92104
<StatsCard stats={stats} />
93105
</div>
94106
</div>
Lines changed: 3 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,7 @@
11
"use client";
2-
import { Link } from "@/i18n/routing";
3-
import { useState } from "react";
4-
import { useSearchParams } from "next/navigation";
5-
import { Button } from "@/components/ui/button";
62

7-
export default function ForgotPasswordPage() {
8-
const searchParams = useSearchParams();
9-
const returnTo = searchParams.get("returnTo");
10-
11-
const [loading, setLoading] = useState(false);
12-
const [email, setEmail] = useState("");
13-
const [submitted, setSubmitted] = useState(false);
14-
const [error, setError] = useState<string | null>(null);
15-
16-
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
17-
e.preventDefault();
18-
setLoading(true);
19-
setError(null);
20-
21-
try {
22-
const res = await fetch("/api/auth/password-reset", {
23-
method: "POST",
24-
headers: { "Content-Type": "application/json" },
25-
body: JSON.stringify({ email }),
26-
});
27-
28-
if (!res.ok) {
29-
setError("Something went wrong. Please try again.");
30-
return;
31-
}
32-
33-
setSubmitted(true);
34-
} catch (err) {
35-
console.error("Password reset request failed:", err);
36-
setError("Network error. Please check your connection and try again.");
37-
} finally {
38-
setLoading(false);
39-
}
40-
}
41-
42-
return (
43-
<div className="mx-auto max-w-sm py-12">
44-
<h1 className="mb-6 text-2xl font-semibold">
45-
Forgot password
46-
</h1>
3+
import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";
474

48-
{submitted ? (
49-
<div className="rounded-md border border-green-400 bg-green-50 p-4 text-sm text-green-800">
50-
<p>
51-
If an account for{" "}
52-
<strong>{email}</strong> exists, we’ve sent a
53-
password reset link.
54-
</p>
55-
56-
<p className="mt-2">
57-
Please check your inbox and follow the
58-
instructions to reset your password.
59-
</p>
60-
61-
<Link
62-
href={
63-
returnTo
64-
? `/login?returnTo=${encodeURIComponent(returnTo)}`
65-
: "/login"
66-
}
67-
className="mt-4 inline-block underline"
68-
>
69-
Back to login
70-
</Link>
71-
</div>
72-
) : (
73-
<form onSubmit={onSubmit} className="space-y-4">
74-
<p className="text-sm text-gray-600">
75-
Enter your email address and we’ll send
76-
you a link to reset your password.
77-
</p>
78-
79-
<input
80-
type="email"
81-
required
82-
placeholder="Email"
83-
value={email}
84-
onChange={e => setEmail(e.target.value)}
85-
className="w-full rounded border px-3 py-2"
86-
/>
87-
88-
{error && (
89-
<p className="text-sm text-red-600">
90-
{error}
91-
</p>
92-
)}
93-
94-
<Button
95-
type="submit"
96-
disabled={loading}
97-
className="w-full"
98-
>
99-
{loading
100-
? "Sending reset link..."
101-
: "Send reset link"}
102-
</Button>
103-
</form>
104-
)}
105-
106-
{!submitted && (
107-
<p className="mt-4 text-sm text-gray-600">
108-
Remembered your password?{" "}
109-
<Link
110-
href={
111-
returnTo
112-
? `/login?returnTo=${encodeURIComponent(returnTo)}`
113-
: "/login"
114-
}
115-
className="underline"
116-
>
117-
Log in
118-
</Link>
119-
</p>
120-
)}
121-
</div>
122-
);
5+
export default function ForgotPasswordPage() {
6+
return <ForgotPasswordForm />;
1237
}

frontend/app/[locale]/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MainSwitcher } from '@/components/header/MainSwitcher';
1515
import { AppChrome } from '@/components/header/AppChrome';
1616

1717
import { CookieBanner } from '@/components/shared/CookieBanner';
18+
import { OnlineCounterPopup } from '@/components/shared/OnlineCounterPopup';
1819

1920
export const dynamic = 'force-dynamic';
2021

@@ -74,6 +75,7 @@ export default async function LocaleLayout({
7475
{children}
7576
</MainSwitcher>
7677
</AppChrome>
78+
<OnlineCounterPopup />
7779

7880
<Footer />
7981
<Toaster position="top-right" richColors expand />

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { getLeaderboardData } from '@/db/queries/leaderboard';
22
import LeaderboardClient from '@/components/leaderboard/LeaderboardClient';
3+
import { Metadata } from 'next';
4+
5+
export const metadata: Metadata = {
6+
title: 'Leaderboard | DevLovers',
7+
description: 'Top performers of the community',
8+
};
39

410
export const revalidate = 3600;
511

0 commit comments

Comments
 (0)