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 35d15137..a56f36df 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); @@ -234,8 +249,12 @@ /* about-community */ @keyframes scroll { - from { transform: translateX(0); } - to { transform: translateX(-100%); } + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } } .animate-scroll { @@ -248,8 +267,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 { @@ -263,9 +286,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..c132f1af 100644 --- a/frontend/components/auth/logoutButton.tsx +++ b/frontend/components/auth/logoutButton.tsx @@ -1,8 +1,8 @@ 'use client'; import { useLocale } from 'next-intl'; +import { useTranslations } from 'next-intl'; import { LogOut } from 'lucide-react'; - import { logout } from '@/lib/logout'; type LogoutButtonProps = { @@ -11,6 +11,7 @@ type LogoutButtonProps = { export function LogoutButton({ iconOnly = false }: LogoutButtonProps) { const locale = useLocale(); + const t = useTranslations('navigation'); const handleLogout = async () => { await logout(); @@ -22,9 +23,9 @@ export function LogoutButton({ iconOnly = false }: LogoutButtonProps) { @@ -35,10 +36,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() {