From 0b8d0018f81d21128c0c264f9d07b462a4d9f0af Mon Sep 17 00:00:00 2001 From: warnigo Date: Fri, 30 May 2025 22:43:28 +0500 Subject: [PATCH 1/6] feat: some working button and header --- src/shared/config/index.ts | 1 + src/shared/config/routes.ts | 7 + src/shared/ui/button/Button.tsx | 266 +++++++++++++++--- .../ui/loading-spinner/LoadingSpinner.tsx | 25 ++ src/shared/ui/loading-spinner/index.ts | 1 + src/shared/ui/logo/Logo.tsx | 9 +- src/widgets/header/Header.tsx | 4 +- src/widgets/header/config.ts | 11 +- 8 files changed, 269 insertions(+), 55 deletions(-) create mode 100644 src/shared/config/index.ts create mode 100644 src/shared/config/routes.ts create mode 100644 src/shared/ui/loading-spinner/LoadingSpinner.tsx create mode 100644 src/shared/ui/loading-spinner/index.ts diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts new file mode 100644 index 0000000..49800c7 --- /dev/null +++ b/src/shared/config/index.ts @@ -0,0 +1 @@ +export * from './routes' diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts new file mode 100644 index 0000000..bad31c4 --- /dev/null +++ b/src/shared/config/routes.ts @@ -0,0 +1,7 @@ +export const ROUTES = { + HOME: '/', + ABOUT: '/about', + SHOWCASE: '/showcase', + BLOG: '/blog', + CONTACT: '/contact', +} diff --git a/src/shared/ui/button/Button.tsx b/src/shared/ui/button/Button.tsx index 7103fff..59379da 100644 --- a/src/shared/ui/button/Button.tsx +++ b/src/shared/ui/button/Button.tsx @@ -1,37 +1,60 @@ 'use client' -import { type ButtonHTMLAttributes, type ElementType, forwardRef, type ReactNode } from 'react' +import { + type ButtonHTMLAttributes, + type ElementType, + forwardRef, + type ReactNode, + useEffect, + useRef, +} from 'react' import { cva, type VariantProps } from 'class-variance-authority' +import { gsap } from 'gsap' +import type React from 'react' +import { LoadingSpinner } from '@shared/ui/loading-spinner' import { cn } from '@lib/utils' export const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-95', + 'relative inline-flex items-center justify-center font-medium transition-colors duration-200 overflow-hidden disabled:pointer-events-none disabled:opacity-40 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-2 focus-visible:ring-offset-background', { variants: { variant: { - default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', - outline: - 'border border-primary bg-background text-primary shadow-sm hover:bg-accent hover:text-primary', - secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + default: [ + 'bg-gradient-to-r from-primary to-primary/90 text-white shadow-lg shadow-primary/20', + 'before:absolute before:inset-0 before:bg-white/10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300', + ], + secondary: [ + 'bg-accent/50 text-foreground border border-accent shadow-sm', + 'hover:bg-accent/70 hover:shadow-md transition-all duration-200', + ], + outline: [ + 'border-2 border-primary/20 text-primary bg-transparent', + 'hover:bg-primary/5 hover:border-primary/40 transition-all duration-200', + ], + ghost: ['text-foreground bg-transparent', 'hover:bg-accent/30 transition-all duration-200'], + destructive: [ + 'bg-gradient-to-r from-destructive to-destructive/90 text-white shadow-lg shadow-destructive/20', + 'before:absolute before:inset-0 before:bg-white/10 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300', + ], + link: [ + 'text-primary bg-transparent underline-offset-4 p-0 h-auto', + 'hover:underline transition-all duration-200', + ], }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3 text-xs', - lg: 'h-11 rounded-md px-8 text-base', - xl: 'h-14 rounded-lg px-12 text-lg font-semibold', - icon: 'h-10 w-10', - 'icon-sm': 'h-8 w-8', - 'icon-lg': 'h-12 w-12', + sm: 'h-8 px-4 text-xs rounded-xl gap-2 font-medium', + default: 'h-10 px-5 text-sm rounded-xl gap-2.5 font-medium', + lg: 'h-12 px-7 text-base rounded-2xl gap-3 font-semibold', + xl: 'h-14 px-9 text-lg rounded-2xl gap-3.5 font-bold tracking-wide', + icon: 'h-10 w-10 rounded-xl', + 'icon-sm': 'h-8 w-8 rounded-xl', + 'icon-lg': 'h-12 w-12 rounded-2xl', }, loading: { - true: 'cursor-not-allowed opacity-70', - false: '', + true: 'cursor-not-allowed', + false: 'cursor-pointer', }, }, defaultVariants: { @@ -49,6 +72,7 @@ type ButtonProps = ButtonHTMLAttributes & loading?: boolean leftIcon?: ReactNode rightIcon?: ReactNode + hoverText?: string } export const Button = forwardRef( @@ -64,47 +88,197 @@ export const Button = forwardRef( leftIcon, rightIcon, disabled, + hoverText, ...props }, ref ) => { + const internalRef = useRef(null) + const childrenRef = useRef(null) + const hoverTextRef = useRef(null) + const tl = useRef(null) + + const buttonRef = (ref as React.RefObject) || internalRef const isDisabled = disabled || loading + useEffect(() => { + const button = buttonRef.current + const childrenEl = childrenRef.current + const hoverTextEl = hoverTextRef.current + + if (!button || !hoverText || !childrenEl || !hoverTextEl) return + + if (tl.current) { + tl.current.kill() + } + tl.current = gsap.timeline({ paused: true }) + + const handleMouseEnter = (e: MouseEvent): void => { + if (isDisabled) return + + const rect = button.getBoundingClientRect() + const mouseX = e.clientX - rect.left + const mouseY = e.clientY - rect.top + + const centerX = rect.width / 2 + const centerY = rect.height / 2 + + let fromDirection = { x: 0, y: 0 } + let toDirection = { x: 0, y: 0 } + + if (Math.abs(mouseX - centerX) > Math.abs(mouseY - centerY)) { + if (mouseX < centerX) { + fromDirection = { x: -100, y: 0 } + toDirection = { x: 100, y: 0 } + } else { + fromDirection = { x: 100, y: 0 } + toDirection = { x: -100, y: 0 } + } + } else { + if (mouseY < centerY) { + fromDirection = { x: 0, y: -100 } + toDirection = { x: 0, y: 100 } + } else { + fromDirection = { x: 0, y: 100 } + toDirection = { x: 0, y: -100 } + } + } + + if (tl.current) { + tl.current.clear() + } + + gsap.to(button, { + x: (mouseX - centerX) * 0.1, + y: (mouseY - centerY) * 0.1, + scale: 1.05, + duration: 0.3, + ease: 'power2.out', + }) + + if (hoverTextEl && childrenEl) { + gsap.set(hoverTextEl, { + x: fromDirection.x + '%', + y: fromDirection.y + '%', + opacity: 0, + }) + + gsap.to(hoverTextEl, { + x: '0%', + y: '0%', + opacity: 1, + duration: 0.4, + ease: 'back.out(1.7)', + }) + + gsap.to(childrenEl, { + x: toDirection.x + '%', + y: toDirection.y + '%', + opacity: 0, + duration: 0.3, + ease: 'power2.in', + }) + } + } + + const handleMouseLeave = (): void => { + if (isDisabled) return + + gsap.to(button, { + x: 0, + y: 0, + scale: 1, + duration: 0.4, + ease: 'elastic.out(1, 0.5)', + }) + + if (hoverTextEl && childrenEl) { + gsap.to(hoverTextEl, { + opacity: 0, + duration: 0.2, + ease: 'power2.in', + }) + + gsap.to(childrenEl, { + x: '0%', + y: '0%', + opacity: 1, + duration: 0.4, + ease: 'back.out(1.7)', + delay: 0.1, + }) + } + } + + const handleMouseMove = (e: MouseEvent): void => { + if (isDisabled) return + + const rect = button.getBoundingClientRect() + const mouseX = e.clientX - rect.left + const mouseY = e.clientY - rect.top + const centerX = rect.width / 2 + const centerY = rect.height / 2 + + gsap.to(button, { + x: (mouseX - centerX) * 0.1, + y: (mouseY - centerY) * 0.1, + duration: 0.2, + ease: 'power2.out', + }) + } + + button.addEventListener('mouseenter', handleMouseEnter) + button.addEventListener('mouseleave', handleMouseLeave) + button.addEventListener('mousemove', handleMouseMove) + + return () => { + button.removeEventListener('mouseenter', handleMouseEnter) + button.removeEventListener('mouseleave', handleMouseLeave) + button.removeEventListener('mousemove', handleMouseMove) + if (tl.current) { + tl.current.kill() + } + } + }, [isDisabled, hoverText, buttonRef]) + const content = ( <> - {loading ? ( - : null} + + {!loading && leftIcon ? ( + + {leftIcon} + + ) : null} + + {children ? ( + + {children} + + ) : null} + + {hoverText ? ( + - - - + {hoverText} + + ) : null} + + {!loading && rightIcon ? ( + + {rightIcon} + ) : null} - {!loading && leftIcon ? {leftIcon} : null} - {children} - {!loading && rightIcon ? {rightIcon} : null} ) if (asChild) { return ( @@ -115,8 +289,8 @@ export const Button = forwardRef( return ( diff --git a/src/widgets/header/config.ts b/src/widgets/header/config.ts index d78080b..c452aac 100644 --- a/src/widgets/header/config.ts +++ b/src/widgets/header/config.ts @@ -1,7 +1,8 @@ +import { ROUTES } from '@shared/config' + export const MAIN_HEADER_CONFIG = [ - { title: 'home', href: '/' }, - { title: 'about', href: '/about' }, - { title: 'showcase', href: '/showcase' }, - { title: 'blog', href: '/blog' }, - { title: 'contact', href: '/contact' }, + { title: 'about', href: ROUTES.ABOUT }, + { title: 'showcase', href: ROUTES.SHOWCASE }, + { title: 'blog', href: ROUTES.BLOG }, + { title: 'contact', href: ROUTES.CONTACT }, ] From f99e6f16f1c560292272a29813914294b8e6a059 Mon Sep 17 00:00:00 2001 From: warnigo Date: Sun, 1 Jun 2025 09:32:38 +0500 Subject: [PATCH 2/6] feat: some update header, and other changes --- src/shared/types/index.ts | 0 src/shared/types/locale.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/shared/types/index.ts create mode 100644 src/shared/types/locale.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/types/locale.ts b/src/shared/types/locale.ts new file mode 100644 index 0000000..e69de29 From 4b42ea7b100d8c8bb924022bd68261dfdcdb8518 Mon Sep 17 00:00:00 2001 From: warnigo Date: Sun, 1 Jun 2025 09:33:13 +0500 Subject: [PATCH 3/6] feat: some update header, and other changes --- messages/en.json | 7 ++++- messages/ru.json | 15 ++++++--- messages/uz.json | 15 ++++++--- package.json | 1 + pnpm-lock.yaml | 12 ++++++++ src/shared/config/routes.ts | 1 + src/shared/styles/globals.css | 5 +-- src/shared/types/index.ts | 1 + src/shared/types/locale.ts | 1 + src/shared/ui/logo/Logo.tsx | 9 ++++-- src/widgets/footer/Footer.tsx | 2 +- src/widgets/header/Header.tsx | 57 +++++++++++++++++++++++++++-------- src/widgets/header/config.ts | 7 +++-- 13 files changed, 102 insertions(+), 31 deletions(-) diff --git a/messages/en.json b/messages/en.json index da6739c..7d80d91 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5,6 +5,11 @@ "about": "About", "contact": "Contact", "showcase": "Showcase", - "blog": "Blog" + "blog": "Blog", + "usecases": "Use cases", + "offers": "Offers", + "careers": "Careers", + "jobs": "Jobs", + "news": "News" } } diff --git a/messages/ru.json b/messages/ru.json index da6739c..eae296d 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1,10 +1,15 @@ { "Brand": "FlakeForge", "Layout": { - "home": "Home", - "about": "About", - "contact": "Contact", - "showcase": "Showcase", - "blog": "Blog" + "home": "Главная", + "about": "О нас", + "contact": "Контакты", + "showcase": "Витрина", + "blog": "Блог", + "usecases": "Примеры использования", + "offers": "Предложения", + "careers": "Карьера", + "jobs": "Вакансии", + "news": "Новости" } } diff --git a/messages/uz.json b/messages/uz.json index da6739c..5d16e69 100644 --- a/messages/uz.json +++ b/messages/uz.json @@ -1,10 +1,15 @@ { "Brand": "FlakeForge", "Layout": { - "home": "Home", - "about": "About", - "contact": "Contact", - "showcase": "Showcase", - "blog": "Blog" + "home": "Bosh sahifa", + "about": "Haqida", + "contact": "Aloqa", + "showcase": "Ko‘rgazma", + "blog": "Blog", + "usecases": "Qo‘llanilish holatlari", + "offers": "Takliflar", + "careers": "Karyeralar", + "jobs": "Ish o‘rinlari", + "news": "Yangiliklar" } } diff --git a/package.json b/package.json index 622937c..f07c4b2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "gsap": "^3.13.0", + "lucide-react": "^0.511.0", "next": "15.3.2", "next-intl": "^4.1.0", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 217bd77..d67a11c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: gsap: specifier: ^3.13.0 version: 3.13.0 + lucide-react: + specifier: ^0.511.0 + version: 0.511.0(react@19.1.0) next: specifier: 15.3.2 version: 15.3.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -2279,6 +2282,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.511.0: + resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -5554,6 +5562,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.511.0(react@19.1.0): + dependencies: + react: 19.1.0 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts index bad31c4..15f3029 100644 --- a/src/shared/config/routes.ts +++ b/src/shared/config/routes.ts @@ -1,6 +1,7 @@ export const ROUTES = { HOME: '/', ABOUT: '/about', + NEWS: '/news', SHOWCASE: '/showcase', BLOG: '/blog', CONTACT: '/contact', diff --git a/src/shared/styles/globals.css b/src/shared/styles/globals.css index d61b433..396a9de 100644 --- a/src/shared/styles/globals.css +++ b/src/shared/styles/globals.css @@ -3,12 +3,13 @@ @theme { /* Light mode colors */ --color-primary: #FF4C24; + --color-foreground: #000000; --color-secondary: #34a853; --color-background: #ffffff; --color-text: #000000; --color-destructive: #ef4444; --color-accent: #f1f5f9; - --color-input: #d1d5db; + --color-input: #c4c4c4; --color-ring: #3b82f6; /* Dark mode colors */ @@ -60,7 +61,7 @@ body { background-color: var(--background); color: var(--foreground); - @apply antialiased font-sans; + @apply antialiased font-sans border-input; transition: background-color 0.3s ease, color 0.3s ease; } } diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index e69de29..1640559 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -0,0 +1 @@ +export type { LocalesType } from './locale' diff --git a/src/shared/types/locale.ts b/src/shared/types/locale.ts index e69de29..cdeb272 100644 --- a/src/shared/types/locale.ts +++ b/src/shared/types/locale.ts @@ -0,0 +1 @@ +export type LocalesType = 'en' | 'ru' | 'uz' diff --git a/src/shared/ui/logo/Logo.tsx b/src/shared/ui/logo/Logo.tsx index f675327..5ab68b0 100644 --- a/src/shared/ui/logo/Logo.tsx +++ b/src/shared/ui/logo/Logo.tsx @@ -2,9 +2,14 @@ import { type FC } from 'react' import { ROUTES } from '@shared/config' import { Link } from '@lib/i18n' +import { cn } from '@lib/utils' -export const Logo: FC = () => ( - +type Props = { + className?: string +} + +export const Logo: FC = ({ className }) => ( +
FlakeForge
) diff --git a/src/widgets/footer/Footer.tsx b/src/widgets/footer/Footer.tsx index fb2cd1e..3f59874 100644 --- a/src/widgets/footer/Footer.tsx +++ b/src/widgets/footer/Footer.tsx @@ -1,7 +1,7 @@ import { type FC } from 'react' export const Footer: FC = () => ( -
footer
+
footer
) Footer.displayName = 'Footer' diff --git a/src/widgets/header/Header.tsx b/src/widgets/header/Header.tsx index b342475..e7a5fae 100644 --- a/src/widgets/header/Header.tsx +++ b/src/widgets/header/Header.tsx @@ -1,29 +1,47 @@ 'use client' import { type FC } from 'react' -import { useTranslations } from 'next-intl' -import { useTheme } from 'next-themes' +import { useLocale, useTranslations } from 'next-intl' +import { type LocalesType } from '@shared/types' import { Logo } from '@shared/ui' import { Button } from '@shared/ui/button' import { MAIN_HEADER_CONFIG } from '@widgets/header/config' -import { Link } from '@lib/i18n' +import { Link, LOCALES, usePathname, useRouter } from '@lib/i18n' export const Header: FC = () => { - const { theme, setTheme } = useTheme() const t = useTranslations('Layout') + const router = useRouter() + const pathname = usePathname() + const currentLocale = useLocale() + + const handleChangeLocale = (): void => { + const currentIndex = LOCALES.indexOf(currentLocale as LocalesType) + const nextIndex = (currentIndex + 1) % LOCALES.length + const nextLocale = LOCALES[nextIndex] + + router.replace(pathname, { locale: nextLocale }) + } return ( -
-
- +
+
+
+ +
-
) diff --git a/src/widgets/header/config.ts b/src/widgets/header/config.ts index c452aac..b357754 100644 --- a/src/widgets/header/config.ts +++ b/src/widgets/header/config.ts @@ -1,8 +1,11 @@ import { ROUTES } from '@shared/config' export const MAIN_HEADER_CONFIG = [ + { title: 'home', href: ROUTES.HOME }, + { title: 'offers', href: '/offers' }, { title: 'about', href: ROUTES.ABOUT }, - { title: 'showcase', href: ROUTES.SHOWCASE }, - { title: 'blog', href: ROUTES.BLOG }, + { title: 'careers', href: '/careers' }, + { title: 'jobs', href: '/jobs' }, + { title: 'news', href: ROUTES.NEWS }, { title: 'contact', href: ROUTES.CONTACT }, ] From 371ad7f037e9d2e856d43e699e5b15dd7d2d1c73 Mon Sep 17 00:00:00 2001 From: warnigo Date: Sat, 14 Jun 2025 10:10:01 +0500 Subject: [PATCH 4/6] fix some added animte on header --- src/shared/styles/globals.css | 14 +- src/shared/ui/logo/Logo.tsx | 24 ++- src/widgets/header/Header.tsx | 305 ++++++++++++++++++++++++++++++++-- src/widgets/header/config.ts | 4 +- 4 files changed, 325 insertions(+), 22 deletions(-) diff --git a/src/shared/styles/globals.css b/src/shared/styles/globals.css index 396a9de..a0d1129 100644 --- a/src/shared/styles/globals.css +++ b/src/shared/styles/globals.css @@ -14,16 +14,17 @@ /* Dark mode colors */ --color-primary-dark: #FF4C24; + --color-foreground-dark: #e5e5e5; --color-secondary-dark: #4ade80; - --color-background-dark: #030303; - --color-text-dark: #ffffff; + --color-background-dark: #0a0a0a; + --color-text-dark: #e5e5e5; --color-destructive-dark: #dc2626; --color-accent-dark: #1e293b; --color-input-dark: #4b5563; --color-ring-dark: #60a5fa; - /* fonts */ - --font-sans: 'Markdisc', sans-serif; + /* Fonts */ + --font-sans: 'Orkney', sans-serif; } @layer base { @@ -32,7 +33,7 @@ --primary: var(--color-primary); --secondary: var(--color-secondary); --background: var(--color-background); - --foreground: var(--color-text); + --foreground: var(--color-foreground); --destructive: var(--color-destructive); --accent: var(--color-accent); --input: var(--color-input); @@ -47,7 +48,7 @@ --primary: var(--color-primary-dark); --secondary: var(--color-secondary-dark); --background: var(--color-background-dark); - --foreground: var(--color-text-dark); + --foreground: var(--color-foreground-dark); --destructive: var(--color-destructive-dark); --accent: var(--color-accent-dark); --input: var(--color-input-dark); @@ -66,7 +67,6 @@ } } -/* Add this at the top of your CSS file */ @font-face { font-family: 'Orkney'; src: url('public/fonts/Orkney-Light.woff2') format('woff2'); diff --git a/src/shared/ui/logo/Logo.tsx b/src/shared/ui/logo/Logo.tsx index 5ab68b0..d7e60db 100644 --- a/src/shared/ui/logo/Logo.tsx +++ b/src/shared/ui/logo/Logo.tsx @@ -1,5 +1,7 @@ import { type FC } from 'react' +import { gsap } from 'gsap' + import { ROUTES } from '@shared/config' import { Link } from '@lib/i18n' import { cn } from '@lib/utils' @@ -9,7 +11,27 @@ type Props = { } export const Logo: FC = ({ className }) => ( - + { + gsap.to(e.currentTarget, { + scale: 1.02, + duration: 0.3, + ease: 'power2.out', + }) + }} + onMouseLeave={e => { + gsap.to(e.currentTarget, { + scale: 1, + duration: 0.3, + ease: 'power2.out', + }) + }} + >
FlakeForge
) diff --git a/src/widgets/header/Header.tsx b/src/widgets/header/Header.tsx index e7a5fae..c80f349 100644 --- a/src/widgets/header/Header.tsx +++ b/src/widgets/header/Header.tsx @@ -1,7 +1,10 @@ 'use client' -import { type FC } from 'react' +import { type FC, useEffect, useRef, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' +import { useTheme } from 'next-themes' + +import { gsap } from 'gsap' import { type LocalesType } from '@shared/types' import { Logo } from '@shared/ui' @@ -14,6 +17,79 @@ export const Header: FC = () => { const router = useRouter() const pathname = usePathname() const currentLocale = useLocale() + const { theme, setTheme } = useTheme() + + const headerRef = useRef(null) + const logoRef = useRef(null) + const navRef = useRef(null) + const actionsRef = useRef(null) + const hoverLineRef = useRef(null) + + const [hoveredNav, setHoveredNav] = useState(null) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + useEffect(() => { + const ctx = gsap.context(() => { + gsap.fromTo( + headerRef.current, + { y: -40, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 1, + ease: 'power3.out', + delay: 0.1, + } + ) + + gsap.fromTo( + logoRef.current, + { scale: 0.9, opacity: 0, x: -20 }, + { + scale: 1, + opacity: 1, + x: 0, + duration: 0.8, + ease: 'back.out(1.2)', + delay: 0.3, + } + ) + + gsap.fromTo( + navRef.current?.children[1]?.children, + { y: 20, opacity: 0, scale: 0.95 }, + { + y: 0, + opacity: 1, + scale: 1, + duration: 0.6, + stagger: 0.1, + ease: 'back.out(1.1)', + delay: 0.5, + } + ) + + gsap.fromTo( + actionsRef.current?.children, + { x: 30, opacity: 0, scale: 0.9 }, + { + x: 0, + opacity: 1, + scale: 1, + duration: 0.7, + stagger: 0.1, + ease: 'back.out(1.1)', + delay: 0.7, + } + ) + }) + + return () => ctx.revert() + }, []) const handleChangeLocale = (): void => { const currentIndex = LOCALES.indexOf(currentLocale as LocalesType) @@ -23,26 +99,144 @@ export const Header: FC = () => { router.replace(pathname, { locale: nextLocale }) } + const handleThemeToggle = (): void => { + setTheme(theme === 'dark' ? 'light' : 'dark') + } + + const handleNavHover = (href: string, element: HTMLElement): void => { + setHoveredNav(href) + + gsap.to(element, { + y: -2, + scale: 1.05, + duration: 0.3, + ease: 'power2.out', + }) + + const rect = element.getBoundingClientRect() + const navRect = navRef.current?.getBoundingClientRect() + + if (hoverLineRef.current && navRect) { + gsap.to(hoverLineRef.current, { + width: rect.width + 8, + x: rect.left - navRect.left - 4, + opacity: 1, + duration: 0.4, + ease: 'power3.out', + }) + } + } + + const handleNavLeave = (element: HTMLElement): void => { + gsap.to(element, { + y: 0, + scale: 1, + duration: 0.3, + ease: 'power2.out', + }) + } + + const handleNavContainerLeave = (): void => { + setHoveredNav(null) + + if (hoverLineRef.current) { + gsap.to(hoverLineRef.current, { + opacity: 0, + duration: 0.3, + ease: 'power2.out', + }) + } + } + + if (!mounted) { + return null + } + return ( -
+
-
- +
+
-
-
-