From 37852d522e8761f2fbca7ba8bfc1cb77e397d995 Mon Sep 17 00:00:00 2001 From: YNazymko12 Date: Sun, 25 Jan 2026 13:11:08 +0100 Subject: [PATCH 1/4] (SP: 3) [Frontend] Refactor Header UI and navigation states - Add icon to the language switcher - Add GitHub icon with stars indicator (frontend only) - Update logo styles - Improve touch interaction styles - Verify correct placement and alignment of all header components - Make mobile header modal full-screen - Disable background scroll when mobile menu is open - Highlight active navigation item - Update navigation styles: - Highlight Shop link when user is on Home pages - Highlight Home link when user is on Shop pages - Style changes only, no routing or logic changes --- frontend/app/api/stats/route.ts | 15 ++ frontend/app/globals.css | 37 +++- frontend/components/auth/logoutButton.tsx | 29 ++- .../components/blog/BlogCategoryLinks.tsx | 39 ++-- frontend/components/header/AppMobileMenu.tsx | 182 +++++++++++++----- frontend/components/header/DesktopActions.tsx | 64 ++++++ frontend/components/header/DesktopNav.tsx | 48 +++++ frontend/components/header/MainSwitcher.tsx | 15 +- frontend/components/header/MobileActions.tsx | 42 ++++ frontend/components/header/NavLink.tsx | 32 +++ frontend/components/header/UnifiedHeader.tsx | 124 +++--------- .../components/shared/AnimatedNavLink.tsx | 116 +++++++++++ frontend/components/shared/Footer.tsx | 6 +- .../components/shared/GitHubStarButton.tsx | 121 ++++++++++++ frontend/components/shared/HeaderButton.tsx | 141 ++++++++++++++ frontend/components/shared/HeroSection.tsx | 2 - .../components/shared/LanguageSwitcher.tsx | 22 ++- frontend/components/shared/Logo.tsx | 31 +++ frontend/components/shop/header/nav-links.tsx | 43 ++--- frontend/components/theme/ThemeToggle.tsx | 5 +- frontend/lib/navigation.ts | 2 +- 21 files changed, 880 insertions(+), 236 deletions(-) create mode 100644 frontend/app/api/stats/route.ts create mode 100644 frontend/components/header/DesktopActions.tsx create mode 100644 frontend/components/header/DesktopNav.tsx create mode 100644 frontend/components/header/MobileActions.tsx create mode 100644 frontend/components/header/NavLink.tsx create mode 100644 frontend/components/shared/AnimatedNavLink.tsx create mode 100644 frontend/components/shared/GitHubStarButton.tsx create mode 100644 frontend/components/shared/HeaderButton.tsx create mode 100644 frontend/components/shared/Logo.tsx diff --git a/frontend/app/api/stats/route.ts b/frontend/app/api/stats/route.ts new file mode 100644 index 00000000..15e7e636 --- /dev/null +++ b/frontend/app/api/stats/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getPlatformStats } from '@/lib/about/stats'; + +export async function GET() { + try { + const stats = await getPlatformStats(); + return NextResponse.json(stats); + } catch (error) { + console.error('Failed to fetch platform stats:', error); + return NextResponse.json( + { error: 'Failed to fetch stats' }, + { status: 500 } + ); + } +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 1aca32ae..024d27b0 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -51,6 +51,7 @@ --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); + --logo-foreground: oklch(0.25 0.08 250); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); @@ -88,6 +89,7 @@ --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); + --logo-foreground: oklch(0.95 0.05 350); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); @@ -135,6 +137,10 @@ } } +.container-main { + @apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8; +} + .qa-accordion-item:hover, .qa-accordion-item:focus-within, .qa-accordion-item[data-state='open'] { @@ -178,6 +184,15 @@ width: 0%; } } +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + @keyframes shrink { from { transform: scaleX(1); @@ -225,8 +240,12 @@ /* about-community */ @keyframes scroll { - from { transform: translateX(0); } - to { transform: translateX(-100%); } + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } } .animate-scroll { @@ -239,8 +258,12 @@ /* Vertical Marquee Animation */ @keyframes marquee-vertical { - 0% { transform: translateY(0); } - 100% { transform: translateY(-50%); } /* Рухаємось на 50%, бо контент дубльовано */ + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-50%); + } /* Рухаємось на 50%, бо контент дубльовано */ } .animate-marquee-vertical { @@ -254,9 +277,9 @@ /* Hide scrollbar for horizontal scroll containers */ .scrollbar-hide { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } .scrollbar-hide::-webkit-scrollbar { - display: none; /* Chrome, Safari and Opera */ + display: none; /* Chrome, Safari and Opera */ } diff --git a/frontend/components/auth/logoutButton.tsx b/frontend/components/auth/logoutButton.tsx index 7ac868e5..421a99c4 100644 --- a/frontend/components/auth/logoutButton.tsx +++ b/frontend/components/auth/logoutButton.tsx @@ -24,7 +24,7 @@ export function LogoutButton({ iconOnly = false }: LogoutButtonProps) { onClick={handleLogout} aria-label="Log out" title="Log out" - className="flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" + className="flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground active:bg-secondary active:text-foreground" > @@ -35,10 +35,31 @@ export function LogoutButton({ iconOnly = false }: LogoutButtonProps) { ); } diff --git a/frontend/components/blog/BlogCategoryLinks.tsx b/frontend/components/blog/BlogCategoryLinks.tsx index e3207821..4422b106 100644 --- a/frontend/components/blog/BlogCategoryLinks.tsx +++ b/frontend/components/blog/BlogCategoryLinks.tsx @@ -1,8 +1,11 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { Link, usePathname } from '@/i18n/routing'; +import { usePathname } from '@/i18n/routing'; +import { Home } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { AnimatedNavLink } from '@/components/shared/AnimatedNavLink'; +import { HeaderButton } from '@/components/shared/HeaderButton'; type Category = { _id: string; @@ -43,12 +46,6 @@ export function BlogCategoryLinks({ }; return categoryTranslations[key] || categoryName; }; - const baseLink = - linkClassName || - 'rounded-md px-3 py-2 text-sm font-medium transition-colors ' + - 'hover:bg-secondary hover:text-foreground ' + - 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ' + - 'focus-visible:ring-offset-2 focus-visible:ring-offset-background'; const items = categories .map(category => ({ @@ -60,38 +57,26 @@ export function BlogCategoryLinks({ return ( diff --git a/frontend/components/header/AppMobileMenu.tsx b/frontend/components/header/AppMobileMenu.tsx index 90629de5..1ceac0be 100644 --- a/frontend/components/header/AppMobileMenu.tsx +++ b/frontend/components/header/AppMobileMenu.tsx @@ -1,14 +1,14 @@ 'use client'; -import { Menu, X } from 'lucide-react'; +import { Menu, X, LogIn, ShoppingBag, Home } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; -import { Link } from '@/i18n/routing'; +import { Link, usePathname } from '@/i18n/routing'; import { SITE_LINKS } from '@/lib/navigation'; import { NAV_LINKS } from '@/components/shop/header/nav-links'; -import { BlogCategoryLinks } from '@/components/blog/BlogCategoryLinks'; import { LogoutButton } from '@/components/auth/logoutButton'; +import { HeaderButton } from '@/components/shared/HeaderButton'; export type AppMobileMenuVariant = 'platform' | 'shop' | 'blog'; @@ -26,10 +26,23 @@ export function AppMobileMenu({ blogCategories = [], }: Props) { const t = useTranslations('navigation'); + const pathname = usePathname(); const [open, setOpen] = useState(false); - - const close = () => setOpen(false); - const toggle = () => setOpen(prev => !prev); + const [isAnimating, setIsAnimating] = useState(false); + + const close = () => { + setIsAnimating(false); + setTimeout(() => setOpen(false), 200); + }; + + const toggle = () => { + if (open) { + close(); + } else { + setOpen(true); + setTimeout(() => setIsAnimating(true), 10); + } + }; useEffect(() => { if (!open) return; @@ -48,12 +61,49 @@ export function AppMobileMenu({ return []; }, [variant]); + useEffect(() => { + if (open) { + const scrollY = window.scrollY; + + document.body.style.position = 'fixed'; + document.body.style.top = `-${scrollY}px`; + document.body.style.width = '100%'; + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.width = ''; + document.body.style.overflow = ''; + + window.scrollTo(0, scrollY); + }; + } + }, [open]); + + const linkClass = (isActive: boolean) => + `rounded-md px-3 py-2 text-sm font-medium transition-colors ${ + isActive + ? 'text-[var(--accent-primary)]' + : 'text-muted-foreground active:text-[var(--accent-hover)]' + }`; + + const slugify = (value: string) => + value + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-'); + return ( <> + ); + } + + return ( + + {content} + + ); +} diff --git a/frontend/components/shared/HeroSection.tsx b/frontend/components/shared/HeroSection.tsx index 2b8cb44f..3601f4af 100644 --- a/frontend/components/shared/HeroSection.tsx +++ b/frontend/components/shared/HeroSection.tsx @@ -58,8 +58,6 @@ export default function HeroSection() {
); diff --git a/frontend/components/header/DesktopActions.tsx b/frontend/components/header/DesktopActions.tsx index 15412438..d0e627f2 100644 --- a/frontend/components/header/DesktopActions.tsx +++ b/frontend/components/header/DesktopActions.tsx @@ -48,7 +48,7 @@ export function DesktopActions({ {isBlog && } - + {isShop && } diff --git a/frontend/components/header/NavLink.tsx b/frontend/components/header/NavLink.tsx index ec162378..9d66f983 100644 --- a/frontend/components/header/NavLink.tsx +++ b/frontend/components/header/NavLink.tsx @@ -13,12 +13,12 @@ export function NavLink({ href, children, className = '' }: NavLinkProps) { const pathname = usePathname(); const isActive = (() => { - const cleanPathname = pathname.replace(/^\/(uk|en|pl)/, '') || '/'; + const cleanPathname = pathname.replace(/^\/(uk|en|pl)(?=\/|$)/, '') || '/'; if (href === '/' && cleanPathname === '/') return true; if (href !== '/') { - return cleanPathname.startsWith(href); + return cleanPathname === href || cleanPathname.startsWith(`${href}/`); } return false; diff --git a/frontend/components/shared/GitHubStarButton.tsx b/frontend/components/shared/GitHubStarButton.tsx index a6abda88..04bd9393 100644 --- a/frontend/components/shared/GitHubStarButton.tsx +++ b/frontend/components/shared/GitHubStarButton.tsx @@ -4,7 +4,6 @@ import { Star } from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; interface GitHubStarButtonProps { - org: string; className?: string; } @@ -20,17 +19,19 @@ export function GitHubStarButton({ className = '' }: GitHubStarButtonProps) { const response = await fetch('/api/stats'); if (response.ok) { const data = await response.json(); - const starsStr = data.githubStars; + const starsStr = + typeof data?.githubStars === 'string' + ? data.githubStars + : String(data?.githubStars ?? '0'); let starsNum = 0; - - if (starsStr.includes('k+')) { + const normalized = starsStr.replace(/,/g, '').toLowerCase(); + if (normalized.includes('k+')) { starsNum = Math.floor( - parseFloat(starsStr.replace('k+', '')) * 1000 + parseFloat(normalized.replace('k+', '')) * 1000 ); } else { - starsNum = parseInt(starsStr); + starsNum = parseInt(normalized, 10); } - setFinalCount(starsNum); } } catch (error) { diff --git a/frontend/components/shared/HeaderButton.tsx b/frontend/components/shared/HeaderButton.tsx index 05a25533..cd886b90 100644 --- a/frontend/components/shared/HeaderButton.tsx +++ b/frontend/components/shared/HeaderButton.tsx @@ -119,6 +119,7 @@ export function HeaderButton({ @@ -133,8 +139,8 @@ export function HeaderButton({ href={href} onClick={onClick} className={baseClasses} - aria-label={label} - title={label} + aria-label={resolvedLabel} + title={resolvedLabel} > {content}