Skip to content

Commit 8b53f83

Browse files
Merge pull request #360 from DevLoversTeam/feat/header-translation
feat: header translations, dashboard UX polish & hydration fix
2 parents d226b09 + 5969fd5 commit 8b53f83

29 files changed

Lines changed: 403 additions & 211 deletions

frontend/actions/notifications.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use server';
22

3-
import { desc, eq, and } from 'drizzle-orm';
3+
import { and,desc, eq } from 'drizzle-orm';
44
import { revalidatePath } from 'next/cache';
55

66
import { db } from '@/db';
77
import { notifications } from '@/db/schema/notifications';
8-
98
import { getCurrentUser } from '@/lib/auth';
109

1110
export async function getNotifications() {

frontend/actions/quiz.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,15 @@ export async function submitQuizAttempt(
231231
const earnedAfter = computeAchievements(statsAfter).filter(a => a.earned);
232232
const newlyEarned = earnedAfter.filter(a => !earnedBefore.has(a.id));
233233

234-
// Trigger notifications for any newly earned achievements
234+
// Trigger notifications for any newly earned achievements.
235+
// title/message are stable English fallbacks; NotificationBell renders
236+
// them dynamically in the viewer's locale using metadata.badgeId.
235237
for (const achievement of newlyEarned) {
236-
// Find full object to get the fancy translated string (if needed) or just generic name
237238
await createNotification({
238239
userId: session.id,
239240
type: 'ACHIEVEMENT',
240241
title: 'Achievement Unlocked!',
241-
message: `You just earned the ${achievement.id} badge!`,
242+
message: achievement.id,
242243
metadata: { badgeId: achievement.id, icon: achievement.icon },
243244
});
244245
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
} from '@/db/queries/quizzes/quiz';
1818
import { getUserGlobalRank, getUserProfile } from '@/db/queries/users';
1919
import { redirect } from '@/i18n/routing';
20-
import { getCurrentUser } from '@/lib/auth';
2120
import { computeAchievements } from '@/lib/achievements';
21+
import { getCurrentUser } from '@/lib/auth';
2222
import { getUserStatsForAchievements } from '@/lib/user-stats';
2323

2424
export async function generateMetadata({
@@ -216,7 +216,7 @@ export default async function DashboardPage({
216216
totalAttempts={totalAttempts}
217217
globalRank={globalRank}
218218
/>
219-
<div className="grid gap-8 lg:grid-cols-2">
219+
<div id="stats" className="grid gap-8 scroll-mt-8 lg:grid-cols-2">
220220
<StatsCard stats={stats} attempts={lastAttempts} />
221221
<ActivityHeatmapCard attempts={attempts} locale={locale} currentStreak={currentStreak} />
222222
</div>

frontend/app/[locale]/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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 { ScrollWatcher } from '@/components/shared/ScrollWatcher';
1314
import { ThemeProvider } from '@/components/theme/ThemeProvider';
1415
import { locales } from '@/i18n/config';
1516
import { getCurrentUser } from '@/lib/auth';
@@ -78,6 +79,7 @@ export default async function LocaleLayout({
7879
<Footer />
7980
<Toaster position="top-right" richColors expand />
8081
<CookieBanner />
82+
<ScrollWatcher />
8183
</ThemeProvider>
8284
</NextIntlClientProvider>
8385
);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,16 @@ export default async function LeaderboardPage() {
7070
// ── Inject star_gazer if user has starred the repo ─────────────────
7171
// Match by GitHub login (username) or by avatar URL base
7272
const avatarBase = user.avatar?.split('?')[0] ?? '';
73+
const isGitHubAvatar = (() => {
74+
try {
75+
return new URL(avatarBase).hostname === 'avatars.githubusercontent.com';
76+
} catch {
77+
return false;
78+
}
79+
})();
7380
const hasStarred =
7481
stargazerLogins.has(nameLower) ||
75-
(avatarBase.includes('avatars.githubusercontent.com') &&
76-
stargazerAvatars.has(avatarBase));
82+
(isGitHubAvatar && stargazerAvatars.has(avatarBase));
7783

7884
if (hasStarred && !achievements.some(a => a.id === 'star_gazer')) {
7985
const def = ACHIEVEMENTS.find(a => a.id === 'star_gazer');

frontend/app/globals.css

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,17 @@
116116
--sponsor-hover: #bf3989;
117117
}
118118

119+
@property --scroll-thumb-alpha {
120+
syntax: '<number>';
121+
inherits: true;
122+
initial-value: 0;
123+
}
124+
119125
html {
120126
overflow-x: hidden;
127+
--scroll-thumb-alpha: 0;
128+
129+
transition: --scroll-thumb-alpha 0.3s ease;
121130
}
122131

123132
*::-webkit-scrollbar {
@@ -130,22 +139,37 @@ html {
130139
}
131140

132141
*::-webkit-scrollbar-thumb {
133-
background: rgba(0, 0, 0, 0.25);
142+
background: rgba(0, 0, 0, var(--scroll-thumb-alpha));
134143
border-radius: 3px;
135144
}
136145

137-
:is(.dark) *::-webkit-scrollbar-thumb,
138-
.dark::-webkit-scrollbar-thumb {
139-
background: rgba(255, 255, 255, 0.2);
146+
html:is(.dark) *::-webkit-scrollbar-thumb,
147+
html .dark *::-webkit-scrollbar-thumb {
148+
background: rgba(255, 255, 255, var(--scroll-thumb-alpha));
149+
}
150+
151+
html.is-scrolling {
152+
--scroll-thumb-alpha: 0.25;
153+
}
154+
155+
html.is-scrolling:is(.dark),
156+
html.is-scrolling .dark {
157+
--scroll-thumb-alpha: 0.2;
140158
}
141159

142160
@supports (-moz-appearance: none) {
143161
* {
144162
scrollbar-width: thin;
163+
scrollbar-color: transparent transparent;
164+
transition: scrollbar-color 0.3s ease;
165+
}
166+
167+
html.is-scrolling * {
145168
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
146169
}
147170

148-
:is(.dark) * {
171+
html.is-scrolling:is(.dark) *,
172+
html.is-scrolling .dark * {
149173
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
150174
}
151175
}
@@ -207,12 +231,10 @@ html {
207231
.qa-accordion-item {
208232
position: relative;
209233
overflow: hidden;
210-
background-image: linear-gradient(
211-
90deg,
212-
transparent 0%,
213-
transparent 54%,
214-
var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%
215-
);
234+
background-image: linear-gradient(90deg,
235+
transparent 0%,
236+
transparent 54%,
237+
var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%);
216238
}
217239

218240
.qa-accordion-item:hover,
@@ -276,33 +298,30 @@ html {
276298
}
277299

278300
@keyframes wave-clip {
301+
279302
0%,
280303
100% {
281-
clip-path: polygon(
282-
0% 50%,
283-
15% 48%,
284-
32% 52%,
285-
54% 60%,
286-
70% 62%,
287-
84% 60%,
288-
100% 55%,
289-
100% 100%,
290-
0% 100%
291-
);
304+
clip-path: polygon(0% 50%,
305+
15% 48%,
306+
32% 52%,
307+
54% 60%,
308+
70% 62%,
309+
84% 60%,
310+
100% 55%,
311+
100% 100%,
312+
0% 100%);
292313
}
293314

294315
50% {
295-
clip-path: polygon(
296-
0% 65%,
297-
16% 70%,
298-
34% 72%,
299-
51% 68%,
300-
67% 58%,
301-
84% 52%,
302-
100% 48%,
303-
100% 100%,
304-
0% 100%
305-
);
316+
clip-path: polygon(0% 65%,
317+
16% 70%,
318+
34% 72%,
319+
51% 68%,
320+
67% 58%,
321+
84% 52%,
322+
100% 48%,
323+
100% 100%,
324+
0% 100%);
306325
}
307326
}
308327

@@ -322,8 +341,7 @@ html {
322341
}
323342

324343
50% {
325-
transform: translate(var(--card-x, 0), var(--card-y, 0)) scale(1.05)
326-
rotate(calc(var(--card-rotate, 0deg) + var(--card-rotate-offset, 0deg)));
344+
transform: translate(var(--card-x, 0), var(--card-y, 0)) scale(1.05) rotate(calc(var(--card-rotate, 0deg) + var(--card-rotate-offset, 0deg)));
327345
}
328346

329347
100% {
@@ -390,6 +408,10 @@ html {
390408
perspective: 1000px;
391409
}
392410

411+
.perspective-midrange {
412+
perspective: 800px;
413+
}
414+
393415
.preserve-3d {
394416
transform-style: preserve-3d;
395417
}
@@ -400,6 +422,7 @@ html {
400422
}
401423

402424
@keyframes float {
425+
403426
0%,
404427
100% {
405428
transform: translateY(0);
@@ -468,16 +491,12 @@ html {
468491
0 0 0 2px rgba(0, 0, 0, 0.4), 0 0 0 7px rgba(0, 0, 0, 0.1),
469492
0 22px 60px rgba(0, 0, 0, 0.28);
470493

471-
--shop-hero-btn-success-bg: color-mix(
472-
in oklab,
473-
var(--shop-hero-btn-bg) 88%,
474-
white
475-
);
476-
--shop-hero-btn-success-bg-hover: color-mix(
477-
in oklab,
478-
var(--shop-hero-btn-bg) 80%,
479-
white
480-
);
494+
--shop-hero-btn-success-bg: color-mix(in oklab,
495+
var(--shop-hero-btn-bg) 88%,
496+
white);
497+
--shop-hero-btn-success-bg-hover: color-mix(in oklab,
498+
var(--shop-hero-btn-bg) 80%,
499+
white);
481500
--shop-hero-btn-success-shadow: 0 22px 60px rgba(0, 0, 0, 0.25);
482501
--shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(0, 0, 0, 0.32);
483502
}
@@ -530,16 +549,12 @@ html {
530549
0 0 0 2px rgba(255, 45, 85, 0.7), 0 0 0 7px rgba(255, 45, 85, 0.22),
531550
0 22px 70px rgba(255, 45, 85, 0.38);
532551

533-
--shop-hero-btn-success-bg: color-mix(
534-
in oklab,
535-
var(--accent-primary) 82%,
536-
black
537-
);
538-
--shop-hero-btn-success-bg-hover: color-mix(
539-
in oklab,
540-
var(--accent-primary) 72%,
541-
black
542-
);
552+
--shop-hero-btn-success-bg: color-mix(in oklab,
553+
var(--accent-primary) 82%,
554+
black);
555+
--shop-hero-btn-success-bg-hover: color-mix(in oklab,
556+
var(--accent-primary) 72%,
557+
black);
543558
--shop-hero-btn-success-shadow: 0 22px 60px rgba(255, 45, 85, 0.45);
544559
--shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(255, 45, 85, 0.6);
545560
}
@@ -599,10 +614,11 @@ html {
599614
}
600615

601616
@media (prefers-reduced-motion: reduce) {
617+
602618
.animate-float,
603619
.animate-spin-slow,
604620
.animate-spin-slower,
605621
.animate-dash-flow {
606622
animation: none !important;
607623
}
608-
}
624+
}

0 commit comments

Comments
 (0)