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/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..15f3029 --- /dev/null +++ b/src/shared/config/routes.ts @@ -0,0 +1,8 @@ +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..a0d1129 100644 --- a/src/shared/styles/globals.css +++ b/src/shared/styles/globals.css @@ -3,26 +3,28 @@ @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 */ --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 { @@ -31,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); @@ -46,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); @@ -60,12 +62,11 @@ 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; } } -/* 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/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..1640559 --- /dev/null +++ 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 new file mode 100644 index 0000000..cdeb272 --- /dev/null +++ b/src/shared/types/locale.ts @@ -0,0 +1 @@ +export type LocalesType = 'en' | 'ru' | 'uz' 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..51f705e 100644 --- a/src/widgets/header/config.ts +++ b/src/widgets/header/config.ts @@ -1,7 +1,9 @@ +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: 'home', href: ROUTES.HOME }, + { title: 'showcase', href: ROUTES.SHOWCASE }, + { title: 'about', href: ROUTES.ABOUT }, + { title: 'news', href: ROUTES.NEWS }, + { title: 'contact', href: ROUTES.CONTACT }, ]