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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Improved accessibility and visual consistency across components
- Resolved locale duplication in Stripe checkout redirects
- Cleaned up redundant UI states, placeholders, and legacy styles

## [0.5.1] - 2026-01-31

### Added

- Enhanced About page experience:
- Refreshed Features and Pricing sections with clearer messaging
- Interactive particle-based backgrounds with reduced-motion support
- New reusable UI components: `ParticleCanvas`, `GradientBadge`,
`SectionHeading`
- Improved mobile responsiveness and layout stability
- Blog improvements:
- Dynamic grid backgrounds across blog pages
- Featured post CTA in blog hero
- Author filtering via URL with adaptive header behavior
- Improved 404 error pages:
- Fully localized (uk / en / pl)
- Unified global rendering strategy
- Clear navigation actions back to Home
- AI Word Helper updates:
- Switched model to `llama3-70b-8192` for improved response quality
- Accessibility & UX:
- Better keyboard navigation for highlighted terms
- Improved touch and mobile interaction handling

### Changed

- Home page refinements:
- Hero section refactored into smaller reusable components
- Updated color palette, spacing, animations, and mobile behavior
- Footer styling updated to match refreshed brand visuals
- Blog layout aligned with updated design language and spacing rules
- Default locale updated from `uk` to `en` with safer type validation
- Internal codebase cleanup:
- Improved i18n defaults and validation
- Better cache initialization and error handling

### Fixed

- Fixed blog post image rendering for latest posts
- Resolved layout centering issues on Leaderboard
- Improved stability of text selection detection for AI helper
- Fixed social icon hover styles in dark mode
- Reduced visual overlap issues on small mobile screens
13 changes: 11 additions & 2 deletions frontend/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getTranslations } from "next-intl/server"
import { getPlatformStats } from "@/lib/about/stats"
import { getSponsors } from "@/lib/about/github-sponsors"

Expand All @@ -7,6 +8,14 @@ import { FeaturesSection } from "@/components/about/FeaturesSection"
import { PricingSection } from "@/components/about/PricingSection"
import { CommunitySection } from "@/components/about/CommunitySection"

export async function generateMetadata() {
const t = await getTranslations("about")
return {
title: t("metaTitle"),
description: t("metaDescription"),
}
}

export default async function AboutPage() {
const [stats, sponsors] = await Promise.all([
getPlatformStats(),
Expand All @@ -17,13 +26,13 @@ export default async function AboutPage() {
<main className="min-h-screen bg-gray-50 dark:bg-black overflow-hidden text-gray-900 dark:text-white
w-[100vw] relative left-[50%] right-[50%] -ml-[50vw] -mr-[50vw]"
>

<HeroSection stats={stats} />
<TopicsSection />
<FeaturesSection />
<PricingSection sponsors={sponsors} />
<CommunitySection />

</main>
)
}
7 changes: 5 additions & 2 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import { Link } from '@/i18n/routing';
import { formatBlogDate } from '@/lib/blog/date';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';

export const revalidate = 0;

Expand Down Expand Up @@ -289,7 +290,8 @@ export default async function PostDetails({
: null;

return (
<main className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<DynamicGridBackground className="bg-gray-50 transition-colors duration-300 dark:bg-transparent py-10">
<main className="relative z-10 mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
{breadcrumbsJsonLd && (
<script
type="application/ld+json"
Expand Down Expand Up @@ -501,7 +503,8 @@ export default async function PostDetails({
{post.resourceLink && null}

{(authorBio || authorName || authorMeta) && null}
</main>
</main>
</DynamicGridBackground>
);
}

Expand Down
162 changes: 82 additions & 80 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { client } from '@/client';
import { Link } from '@/i18n/routing';
import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid';
import { formatBlogDate } from '@/lib/blog/date';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
import { FeaturedPostCtaButton } from '@/components/blog/FeaturedPostCtaButton';

export const revalidate = 0;

Expand Down Expand Up @@ -85,89 +87,89 @@ export default async function BlogCategoryPage({
const featuredDate = formatBlogDate(featuredPost?.publishedAt);

return (
<main className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<nav className="mb-6" aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<li className="flex items-center gap-2">
<Link
href="/blog"
className="transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
>
{tNav('blog')}
</Link>
<span>&gt;</span>
</li>
<li className="flex items-center gap-2">
<span className="text-[var(--accent-primary)]" aria-current="page">
{categoryDisplay}
</span>
</li>
</ol>
</nav>
<h1 className="text-4xl font-bold mb-4 text-center">
{categoryDisplay}
</h1>
{featuredPost?.mainImage && (
<section className="mt-10">
<article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black">
<div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]">
<Image
src={featuredPost.mainImage}
alt={featuredPost.title}
width={1400}
height={800}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
priority={false}
/>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" />
<div className="absolute inset-x-0 bottom-0 p-6 sm:p-8">
{featuredPost.categories?.[0] && (
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{featuredPost.categories[0]}
</div>
)}
<h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl">
{featuredPost.title}
</h2>
<div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200">
{featuredPost.author?.image && (
<Image
src={featuredPost.author.image}
alt={featuredPost.author.name || 'Author'}
width={28}
height={28}
className="h-7 w-7 rounded-full object-cover"
/>
)}
{featuredPost.author?.name && (
<span>{featuredPost.author.name}</span>
)}
{featuredPost.author?.name && featuredDate && <span>·</span>}
{featuredDate && featuredPost.publishedAt && (
<time dateTime={featuredPost.publishedAt}>
{featuredDate}
</time>
)}
</div>
<DynamicGridBackground className="bg-gray-50 transition-colors duration-300 dark:bg-transparent py-10">
<main className="relative z-10 mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<nav className="mb-6" aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<li className="flex items-center gap-2">
<Link
href={`/blog/${featuredPost.slug.current}`}
className="absolute bottom-6 right-6 inline-flex h-11 w-11 items-center justify-center rounded-full bg-[var(--accent-primary)] text-white opacity-0 transition group-hover:opacity-100 hover:brightness-110"
aria-label={featuredPost.title}
href="/blog"
className="transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
>
<span aria-hidden="true">↗</span>
{tNav('blog')}
</Link>
</div>
</article>
</section>
)}
<div className="mt-12">
<BlogCategoryGrid posts={restPosts} />
</div>
{!posts.length && (
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>
)}
</main>
<span>&gt;</span>
</li>
<li className="flex items-center gap-2">
<span className="text-[var(--accent-primary)]" aria-current="page">
{categoryDisplay}
</span>
</li>
</ol>
</nav>
<h1 className="text-4xl font-bold mb-4 text-left">
{categoryDisplay}
</h1>
{featuredPost?.mainImage && (
<section className="mt-10">
<article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black">
<div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]">
<Image
src={featuredPost.mainImage}
alt={featuredPost.title}
width={1400}
height={800}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
priority={false}
/>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" />
<div className="absolute inset-x-0 bottom-0 p-6 sm:p-8">
{featuredPost.categories?.[0] && (
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{featuredPost.categories[0]}
</div>
)}
<h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl">
{featuredPost.title}
</h2>
<div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200">
{featuredPost.author?.image && (
<Image
src={featuredPost.author.image}
alt={featuredPost.author.name || 'Author'}
width={28}
height={28}
className="h-7 w-7 rounded-full object-cover"
/>
)}
{featuredPost.author?.name && (
<span>{featuredPost.author.name}</span>
)}
{featuredPost.author?.name && featuredDate && <span>·</span>}
{featuredDate && featuredPost.publishedAt && (
<time dateTime={featuredPost.publishedAt}>
{featuredDate}
</time>
)}
</div>
</div>
<FeaturedPostCtaButton
href={`/blog/${featuredPost.slug.current}`}
label={featuredPost.title || 'Read more'}
className="!absolute !bottom-6 !right-6 z-10 h-11 w-11 rounded-full bg-[var(--accent-primary)] text-white opacity-0 shadow-sm transition group-hover:opacity-100 focus-visible:opacity-100 group-focus-within:opacity-100"
/>
</article>
</section>
)}
<div className="mt-12">
<BlogCategoryGrid posts={restPosts} />
</div>
{!posts.length && (
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>
)}
</main>
</DynamicGridBackground>
);
}

Expand Down
31 changes: 20 additions & 11 deletions frontend/app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { unstable_noStore as noStore } from 'next/cache';
import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import BlogFilters from '@/components/blog/BlogFilters';
import { BlogPageHeader } from '@/components/blog/BlogPageHeader';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';

export const revalidate = 0;

Expand All @@ -22,12 +24,18 @@ export async function generateMetadata({

export default async function BlogPage({
params,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
noStore();
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'blog' });
const sp = searchParams ? await searchParams : undefined;
const authorParam =
typeof sp?.author === 'string' ? sp.author.trim() : '';
const hasAuthorFilter = authorParam.length > 0;

const posts = await client.withConfig({ useCdn: false }).fetch(
groq`
Expand Down Expand Up @@ -77,16 +85,17 @@ export default async function BlogPage({
const featuredPost = posts?.[0];

return (
<main className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<h1 className="text-4xl font-bold mb-4 text-center">{t('title')}</h1>
<p className="mx-auto max-w-2xl text-center text-base text-gray-500 dark:text-gray-400">
{t('subtitle')}
</p>
<BlogFilters
posts={posts}
categories={categories}
featuredPost={featuredPost}
/>
</main>
<DynamicGridBackground className="bg-gray-50 transition-colors duration-300 dark:bg-transparent py-10">
<main className="relative z-10 mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
{!hasAuthorFilter && (
<BlogPageHeader title={t('title')} subtitle={t('subtitle')} />
)}
<BlogFilters
posts={posts}
categories={categories}
featuredPost={featuredPost}
/>
</main>
</DynamicGridBackground>
);
}
8 changes: 0 additions & 8 deletions frontend/app/[locale]/not-found.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getTranslations } from 'next-intl/server';
import HeroSection from '@/components/shared/HeroSection';
import HeroSection from '@/components/home/HeroSection';

export async function generateMetadata({
params,
Expand Down
10 changes: 4 additions & 6 deletions frontend/app/[locale]/shop/admin/orders/[id]/RefundButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useRouter } from 'next/navigation';
import { useId, useState, useTransition } from 'react';
import { useTranslations } from 'next-intl';

type Props = {
orderId: string;
Expand All @@ -10,6 +11,7 @@ type Props = {

export function RefundButton({ orderId, disabled }: Props) {
const router = useRouter();
const t = useTranslations('shop.admin.refund');
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const errorId = useId();
Expand Down Expand Up @@ -59,13 +61,9 @@ export function RefundButton({ orderId, disabled }: Props) {
aria-busy={isPending}
aria-describedby={error ? errorId : undefined}
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-50"
title={
disabled
? 'Refund is only available for paid Stripe orders'
: undefined
}
title={disabled ? t('onlyForPaid') : undefined}
>
{isPending ? 'Refunding…' : 'Refund'}
{isPending ? t('refunding') : t('refund')}
</button>

{error ? (
Expand Down
Loading