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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

- Stronger password validation and confirmation flow
- Improved server-side validation and error reporting

## [1.0.3] - 2026-02-23

### Fixed

- Blog (Sanity):
- Fixed images not rendering on Vercel production
- Improved image optimization across blog pages and components
- SSR hydration mismatch in AchievementBadge (client/server state sync)
- Header hardcoded locale issues
- Notification rendering consistency
Comment on lines +692 to +697
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

"Improved image optimization" is an enhancement, not a bug fix — move it to ### Changed.

Per Keep a Changelog conventions (which this file explicitly follows at line 5), Fixed is exclusively for bug fixes. "Improved image optimization across blog pages and components" describes a capability improvement and belongs under ### Changed.

📝 Proposed edit
 - Blog (Sanity):
   - Fixed images not rendering on Vercel production
-  - Improved image optimization across blog pages and components
 ### Changed
 
+- Blog (Sanity):
+  - Improved image optimization across blog pages and components
+
 - Q&A UI:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Blog (Sanity):
- Fixed images not rendering on Vercel production
- Improved image optimization across blog pages and components
- SSR hydration mismatch in AchievementBadge (client/server state sync)
- Header hardcoded locale issues
- Notification rendering consistency
### Fixed
- Blog (Sanity):
- Fixed images not rendering on Vercel production
- SSR hydration mismatch in AchievementBadge (client/server state sync)
- Header hardcoded locale issues
- Notification rendering consistency
### Changed
- Blog (Sanity):
- Improved image optimization across blog pages and components
- Q&A UI:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 692 - 697, The changelog entry lists "Improved
image optimization across blog pages and components" under the Bug Fixes
section; move that bullet from the "Fixed" list under "Blog (Sanity):" into the
"### Changed" section (or create a "Changed" subsection if one doesn't exist) so
it reads as an enhancement; locate the exact bullet "- Improved image
optimization across blog pages and components" and remove it from the "Fixed"
block and add it under "Changed" to follow Keep a Changelog conventions.


### Changed

- Q&A UI:
- Updated Git category tab color for stronger visual distinction from HTML

- Header & Localization:
- Translated all header UI strings (en / uk / pl)
- Notifications now fully localized with locale-aware relative time
- LanguageSwitcher and User menu labels localized

- Dashboard UX:
- Unified dashboard cards styling using shared `dashboard-card` class
- Clickable profile stats with smooth scroll navigation
- Improved avatar detection logic

### Performance & Stability

- Improved client-render guards using stable subscription pattern
- Reduced hydration inconsistencies in production
Binary file modified assets/01-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/02-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/03-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/04-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/05-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/08-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
9 changes: 9 additions & 0 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { client } from '@/client';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
import { Link } from '@/i18n/routing';
import { formatBlogDate } from '@/lib/blog/date';
import { shouldBypassImageOptimization } from '@/lib/blog/image';

export const revalidate = 0;

Expand Down Expand Up @@ -285,6 +286,7 @@ function renderPortableText(
alt={postTitle || 'Post image'}
width={1200}
height={800}
unoptimized={shouldBypassImageOptimization(block.url)}
sizes="100vw"
className="my-6 h-auto w-full rounded-xl border border-gray-200"
/>
Expand Down Expand Up @@ -577,6 +579,7 @@ export default async function PostDetails({
src={post.mainImage}
alt={post.title || 'Post image'}
fill
unoptimized={shouldBypassImageOptimization(post.mainImage)}
className="object-contain"
/>
</div>
Expand Down Expand Up @@ -618,6 +621,9 @@ export default async function PostDetails({
src={item.mainImage}
alt={item.title || 'Post image'}
fill
unoptimized={shouldBypassImageOptimization(
item.mainImage
)}
className="object-cover transition-transform duration-300 group-hover:scale-[1.03]"
/>
</div>
Expand All @@ -640,6 +646,9 @@ export default async function PostDetails({
src={item.author.image}
alt={item.author.name || 'Author'}
fill
unoptimized={shouldBypassImageOptimization(
item.author.image
)}
className="object-cover"
/>
</span>
Expand Down
7 changes: 7 additions & 0 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FeaturedPostCtaButton } from '@/components/blog/FeaturedPostCtaButton';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
import { Link } from '@/i18n/routing';
import { formatBlogDate } from '@/lib/blog/date';
import { shouldBypassImageOptimization } from '@/lib/blog/image';

export const revalidate = 0;

Expand Down Expand Up @@ -121,6 +122,9 @@ export default async function BlogCategoryPage({
alt={featuredPost.title}
width={1400}
height={800}
unoptimized={shouldBypassImageOptimization(
featuredPost.mainImage
)}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
priority={false}
/>
Expand All @@ -142,6 +146,9 @@ export default async function BlogCategoryPage({
alt={featuredPost.author.name || 'Author'}
width={28}
height={28}
unoptimized={shouldBypassImageOptimization(
featuredPost.author.image
)}
className="h-7 w-7 rounded-full object-cover"
/>
)}
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
Loading