Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions frontend/actions/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use server';

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

import { db } from '@/db';
import { notifications } from '@/db/schema/notifications';

import { getCurrentUser } from '@/lib/auth';

export async function getNotifications() {
Expand Down
7 changes: 4 additions & 3 deletions frontend/actions/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,15 @@ export async function submitQuizAttempt(
const earnedAfter = computeAchievements(statsAfter).filter(a => a.earned);
const newlyEarned = earnedAfter.filter(a => !earnedBefore.has(a.id));

// Trigger notifications for any newly earned achievements
// Trigger notifications for any newly earned achievements.
// title/message are stable English fallbacks; NotificationBell renders
// them dynamically in the viewer's locale using metadata.badgeId.
for (const achievement of newlyEarned) {
// Find full object to get the fancy translated string (if needed) or just generic name
await createNotification({
userId: session.id,
type: 'ACHIEVEMENT',
title: 'Achievement Unlocked!',
message: `You just earned the ${achievement.id} badge!`,
message: achievement.id,
metadata: { badgeId: achievement.id, icon: achievement.icon },
});
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[locale]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {
} from '@/db/queries/quizzes/quiz';
import { getUserGlobalRank, getUserProfile } from '@/db/queries/users';
import { redirect } from '@/i18n/routing';
import { getCurrentUser } from '@/lib/auth';
import { computeAchievements } from '@/lib/achievements';
import { getCurrentUser } from '@/lib/auth';
import { getUserStatsForAchievements } from '@/lib/user-stats';

export async function generateMetadata({
Expand Down Expand Up @@ -216,7 +216,7 @@ export default async function DashboardPage({
totalAttempts={totalAttempts}
globalRank={globalRank}
/>
<div className="grid gap-8 lg:grid-cols-2">
<div id="stats" className="grid gap-8 scroll-mt-8 lg:grid-cols-2">
<StatsCard stats={stats} attempts={lastAttempts} />
<ActivityHeatmapCard attempts={attempts} locale={locale} currentStreak={currentStreak} />
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AppChrome } from '@/components/header/AppChrome';
import { MainSwitcher } from '@/components/header/MainSwitcher';
import { CookieBanner } from '@/components/shared/CookieBanner';
import Footer from '@/components/shared/Footer';
import { ScrollWatcher } from '@/components/shared/ScrollWatcher';
import { ThemeProvider } from '@/components/theme/ThemeProvider';
import { locales } from '@/i18n/config';
import { getCurrentUser } from '@/lib/auth';
Expand Down Expand Up @@ -78,6 +79,7 @@ export default async function LocaleLayout({
<Footer />
<Toaster position="top-right" richColors expand />
<CookieBanner />
<ScrollWatcher />
</ThemeProvider>
</NextIntlClientProvider>
);
Expand Down
10 changes: 8 additions & 2 deletions frontend/app/[locale]/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ export default async function LeaderboardPage() {
// ── Inject star_gazer if user has starred the repo ─────────────────
// Match by GitHub login (username) or by avatar URL base
const avatarBase = user.avatar?.split('?')[0] ?? '';
const isGitHubAvatar = (() => {
try {
return new URL(avatarBase).hostname === 'avatars.githubusercontent.com';
} catch {
return false;
}
})();
const hasStarred =
stargazerLogins.has(nameLower) ||
(avatarBase.includes('avatars.githubusercontent.com') &&
stargazerAvatars.has(avatarBase));
(isGitHubAvatar && stargazerAvatars.has(avatarBase));

if (hasStarred && !achievements.some(a => a.id === 'star_gazer')) {
const def = ACHIEVEMENTS.find(a => a.id === 'star_gazer');
Expand Down
128 changes: 72 additions & 56 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,17 @@
--sponsor-hover: #bf3989;
}

@property --scroll-thumb-alpha {
syntax: '<number>';
inherits: true;
initial-value: 0;
}

html {
overflow-x: hidden;
--scroll-thumb-alpha: 0;

transition: --scroll-thumb-alpha 0.3s ease;
}

*::-webkit-scrollbar {
Expand All @@ -130,22 +139,37 @@ html {
}

*::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.25);
background: rgba(0, 0, 0, var(--scroll-thumb-alpha));
border-radius: 3px;
}

:is(.dark) *::-webkit-scrollbar-thumb,
.dark::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
html:is(.dark) *::-webkit-scrollbar-thumb,
html .dark *::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, var(--scroll-thumb-alpha));
}

html.is-scrolling {
--scroll-thumb-alpha: 0.25;
}

html.is-scrolling:is(.dark),
html.is-scrolling .dark {
--scroll-thumb-alpha: 0.2;
}
Comment on lines 141 to 158
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scrollbar thumb transition won't animate in WebKit/Blink browsers.

The transition: background 0.3s ease on *::-webkit-scrollbar-thumb (line 135) is declared but WebKit/Blink browsers do not support CSS transitions on scrollbar pseudo-elements. The scrollbar will appear/disappear instantly rather than fading. This is purely cosmetic — the functionality is correct — but the transition declaration is effectively dead code in those browsers.

The Firefox scrollbar-color transition (line 151) has better support, so the fade effect may work there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/globals.css` around lines 132 - 145, The WebKit/Blink scrollbar
pseudo-element transition is ineffective; remove the dead transition from
*::-webkit-scrollbar-thumb and implement the fade via a CSS custom property on
html (e.g., --scroll-thumb-alpha) that the thumb uses like background:
rgba(0,0,0,var(--scroll-thumb-alpha)); add a transition on the html selector (or
html.is-scrolling) to animate --scroll-thumb-alpha between 0 and the target
(0.25 / 0.2 for dark) when toggling html.is-scrolling, and update the existing
html.is-scrolling and html.is-scrolling:is(.dark) / html.is-scrolling .dark
rules to set the appropriate --scroll-thumb-alpha values so the fade works
cross-browser without relying on pseudo-element transitions.


@supports (-moz-appearance: none) {
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 0.3s ease;
}

html.is-scrolling * {
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
}

:is(.dark) * {
html.is-scrolling:is(.dark) *,
html.is-scrolling .dark * {
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
}
Expand Down Expand Up @@ -207,12 +231,10 @@ html {
.qa-accordion-item {
position: relative;
overflow: hidden;
background-image: linear-gradient(
90deg,
transparent 0%,
transparent 54%,
var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%
);
background-image: linear-gradient(90deg,
transparent 0%,
transparent 54%,
var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%);
}

.qa-accordion-item:hover,
Expand Down Expand Up @@ -276,33 +298,30 @@ html {
}

@keyframes wave-clip {

0%,
100% {
clip-path: polygon(
0% 50%,
15% 48%,
32% 52%,
54% 60%,
70% 62%,
84% 60%,
100% 55%,
100% 100%,
0% 100%
);
clip-path: polygon(0% 50%,
15% 48%,
32% 52%,
54% 60%,
70% 62%,
84% 60%,
100% 55%,
100% 100%,
0% 100%);
}

50% {
clip-path: polygon(
0% 65%,
16% 70%,
34% 72%,
51% 68%,
67% 58%,
84% 52%,
100% 48%,
100% 100%,
0% 100%
);
clip-path: polygon(0% 65%,
16% 70%,
34% 72%,
51% 68%,
67% 58%,
84% 52%,
100% 48%,
100% 100%,
0% 100%);
}
}

Expand All @@ -322,8 +341,7 @@ html {
}

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

100% {
Expand Down Expand Up @@ -390,6 +408,10 @@ html {
perspective: 1000px;
}

.perspective-midrange {
perspective: 800px;
}

.preserve-3d {
transform-style: preserve-3d;
}
Expand All @@ -400,6 +422,7 @@ html {
}

@keyframes float {

0%,
100% {
transform: translateY(0);
Expand Down Expand Up @@ -468,16 +491,12 @@ html {
0 0 0 2px rgba(0, 0, 0, 0.4), 0 0 0 7px rgba(0, 0, 0, 0.1),
0 22px 60px rgba(0, 0, 0, 0.28);

--shop-hero-btn-success-bg: color-mix(
in oklab,
var(--shop-hero-btn-bg) 88%,
white
);
--shop-hero-btn-success-bg-hover: color-mix(
in oklab,
var(--shop-hero-btn-bg) 80%,
white
);
--shop-hero-btn-success-bg: color-mix(in oklab,
var(--shop-hero-btn-bg) 88%,
white);
--shop-hero-btn-success-bg-hover: color-mix(in oklab,
var(--shop-hero-btn-bg) 80%,
white);
--shop-hero-btn-success-shadow: 0 22px 60px rgba(0, 0, 0, 0.25);
--shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(0, 0, 0, 0.32);
}
Expand Down Expand Up @@ -530,16 +549,12 @@ html {
0 0 0 2px rgba(255, 45, 85, 0.7), 0 0 0 7px rgba(255, 45, 85, 0.22),
0 22px 70px rgba(255, 45, 85, 0.38);

--shop-hero-btn-success-bg: color-mix(
in oklab,
var(--accent-primary) 82%,
black
);
--shop-hero-btn-success-bg-hover: color-mix(
in oklab,
var(--accent-primary) 72%,
black
);
--shop-hero-btn-success-bg: color-mix(in oklab,
var(--accent-primary) 82%,
black);
--shop-hero-btn-success-bg-hover: color-mix(in oklab,
var(--accent-primary) 72%,
black);
--shop-hero-btn-success-shadow: 0 22px 60px rgba(255, 45, 85, 0.45);
--shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(255, 45, 85, 0.6);
}
Expand Down Expand Up @@ -599,10 +614,11 @@ html {
}

@media (prefers-reduced-motion: reduce) {

.animate-float,
.animate-spin-slow,
.animate-spin-slower,
.animate-dash-flow {
animation: none !important;
}
}
}
Loading