Initial commit

This commit is contained in:
vitalijmulika
2025-12-11 19:22:31 +02:00
commit 485b904547
320 changed files with 69794 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
"use client";
import { memo } from "react";
import Image from "next/image";
import { cls } from "@/lib/utils";
interface LogoProps {
logoSrc?: string;
logoAlt?: string;
brandName?: string;
className?: string;
imageClassName?: string;
textClassName?: string;
}
const Logo = memo<LogoProps>(function Logo({
logoSrc,
logoAlt = "",
brandName = "Webild",
className = "",
imageClassName = "",
textClassName = ""
}) {
if (logoSrc) {
return (
<div className={cls("relative h-[var(--text-xl)] w-auto", className)}>
<Image
src={logoSrc}
alt={logoAlt}
width={100}
height={24}
className={cls("h-full w-auto object-contain", imageClassName)}
unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
/>
</div>
);
}
return (
<h2 className={cls("text-xl font-medium text-foreground", textClassName)}>
{brandName}
</h2>
);
});
Logo.displayName = "Logo";
export default Logo;

View File

@@ -0,0 +1,82 @@
"use client";
import { memo } from "react";
import Button from "../button/Button";
import ButtonTextUnderline from "../button/ButtonTextUnderline";
import Logo from "./Logo";
import { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface NavbarLayoutFloatingInlineProps {
navItems: NavItem[];
logoSrc?: string;
logoAlt?: string;
brandName?: string;
button: ButtonConfig;
className?: string;
navItemClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const NavbarLayoutFloatingInline = memo<NavbarLayoutFloatingInlineProps>(
function NavbarLayoutFloatingInline({
navItems,
logoSrc,
logoAlt = "",
brandName = "Webild",
button,
className = "",
navItemClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}) {
const theme = useTheme();
return (
<nav
role="navigation"
aria-label="Main navigation"
className={cls(
"fixed z-[100] top-6 w-full",
"transition-all duration-500 ease-in-out",
className
)}
>
<div className={cls(
"w-content-width mx-auto",
"flex items-center justify-between",
"card rounded-theme",
"p-3 pl-6 h-fit relative"
)}>
<Logo logoSrc={logoSrc} logoAlt={logoAlt} brandName={brandName} />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 hidden md:flex gap-6 items-center">
{navItems.map((item, index) => (
<ButtonTextUnderline
key={index}
text={item.name}
href={item.id}
className={cls("!text-base", navItemClassName)}
/>
))}
</div>
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
</div>
</nav>
);
}
);
export default NavbarLayoutFloatingInline;

View File

@@ -0,0 +1,92 @@
"use client";
import { memo } from "react";
import ExpandingMenu from "../expandingMenu/ExpandingMenu";
import Button from "../../button/Button";
import Logo from "../Logo";
import { useScrollDetection } from "./useScrollDetection";
import { useMenuAnimation } from "./useMenuAnimation";
import { useResponsive } from "./useResponsive";
import type { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface NavbarLayoutFloatingOverlayProps {
navItems: NavItem[];
logoSrc?: string;
logoAlt?: string;
brandName?: string;
button: ButtonConfig;
buttonClassName?: string;
buttonTextClassName?: string;
}
const NavbarLayoutFloatingOverlay = memo<NavbarLayoutFloatingOverlayProps>(
function NavbarLayoutFloatingOverlay({
navItems,
logoSrc,
logoAlt = "",
brandName = "Webild",
button,
buttonClassName = "",
buttonTextClassName = "",
}) {
const theme = useTheme();
const isScrolled = useScrollDetection(50);
const { menuOpen, buttonZIndex, handleMenuToggle } =
useMenuAnimation();
const isMobile = useResponsive(768);
return (
<nav
role="navigation"
aria-label="Main navigation"
className={cls(
"fixed z-[100] top-6 w-full",
"transition-all duration-500 ease-in-out"
)}
>
<div
className={cls(
"w-content-width mx-auto",
"flex items-center justify-between",
"card rounded-theme backdrop-blur-xs",
"px-6 md:pr-3"
)}
style={{
height: "calc(var(--vw-0_75) + var(--vw-0_75) + var(--height-9))",
}}
>
<Logo logoSrc={logoSrc} logoAlt={logoAlt} brandName={brandName} />
<div
className="flex items-center transition-all duration-500 ease-in-out"
style={{ paddingRight: "calc(var(--height-9) + var(--vw-0_75))" }}
>
{!isMobile && (
<div className="hidden md:flex">
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
cls(buttonZIndex, buttonClassName),
buttonTextClassName
)}
/>
</div>
)}
<ExpandingMenu
isOpen={menuOpen}
onToggle={handleMenuToggle}
navItems={navItems}
isScrolled={isScrolled}
/>
</div>
</div>
</nav>
);
}
);
export default NavbarLayoutFloatingOverlay;

View File

@@ -0,0 +1,40 @@
import { useState, useCallback, useEffect } from 'react';
export const useMenuAnimation = () => {
const [menuOpen, setMenuOpen] = useState(false);
const [buttonZIndex, setButtonZIndex] = useState('z-[100]');
const [animationTimeoutId, setAnimationTimeoutId] = useState<NodeJS.Timeout | null>(null);
const handleMenuToggle = useCallback(() => {
const isOpening = !menuOpen;
setMenuOpen(prev => !prev);
if (animationTimeoutId) {
clearTimeout(animationTimeoutId);
}
if (isOpening) {
setButtonZIndex('z-0');
} else {
const timeoutId = setTimeout(() => {
setButtonZIndex('z-[100]');
}, 800);
setAnimationTimeoutId(timeoutId);
}
}, [menuOpen, animationTimeoutId]);
useEffect(() => {
return () => {
if (animationTimeoutId) {
clearTimeout(animationTimeoutId);
}
};
}, [animationTimeoutId]);
return {
menuOpen,
buttonZIndex,
handleMenuToggle,
setMenuOpen
};
};

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
import { throttle } from '@/utils/throttle';
export const useResponsive = (breakpoint: number = 768) => {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = throttle(() => {
setIsMobile(window.innerWidth < breakpoint);
}, 150);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [breakpoint]);
return isMobile;
};

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
import { throttle } from '@/utils/throttle';
export const useScrollDetection = (threshold: number = 50) => {
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = throttle(() => {
setIsScrolled(window.scrollY > threshold);
}, 100);
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [threshold]);
return isScrolled;
};

View File

@@ -0,0 +1,86 @@
"use client";
import { useState, memo, useCallback } from "react";
import MobileMenu from "../mobileMenu/MobileMenu";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import Logo from "../Logo";
import { Plus } from "lucide-react";
import { NavbarProps } from "@/types/navigation";
import { useScrollState } from "./useScrollState";
import { cls } from "@/lib/utils";
const SCROLL_THRESHOLD = 50;
const NavbarStyleApple = ({
navItems,
logoSrc,
logoAlt = "",
brandName = "Webild",
}: NavbarProps) => {
const isScrolled = useScrollState(SCROLL_THRESHOLD);
const [menuOpen, setMenuOpen] = useState(false);
const handleMenuToggle = useCallback(() => {
setMenuOpen((prev) => !prev);
}, []);
const handleMobileNavClick = useCallback(() => {
setMenuOpen(false);
}, []);
return (
<nav
className={cls(
"fixed z-[1000] top-0 left-0 w-full transition-all duration-500 ease-in-out",
isScrolled
? "bg-background/80 backdrop-blur-sm h-15"
: "bg-background/0 backdrop-blur-0 h-20"
)}
>
<div className="flex items-center justify-between h-full w-content-width mx-auto">
<div className="flex items-center transition-all duration-500 ease-in-out">
<Logo logoSrc={logoSrc} logoAlt={logoAlt} brandName={brandName} />
</div>
<div
className="hidden md:flex items-center gap-6 transition-all duration-500 ease-in-out"
role="navigation"
>
{navItems.map((item, index) => (
<ButtonTextUnderline
key={index}
text={item.name}
href={item.id}
className="!text-base"
/>
))}
</div>
<button
className="flex md:hidden shrink-0 h-8 aspect-square rounded-theme bg-foreground items-center justify-center cursor-pointer"
onClick={handleMenuToggle}
aria-label="Toggle menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
<Plus
className={cls(
"w-1/2 h-1/2 text-background transition-transform duration-300",
menuOpen ? "rotate-45" : "rotate-0"
)}
strokeWidth={1.5}
aria-hidden="true"
/>
</button>
</div>
<MobileMenu
menuOpen={menuOpen}
onMenuToggle={handleMenuToggle}
navItems={navItems}
onNavClick={handleMobileNavClick}
/>
</nav>
);
};
export default memo(NavbarStyleApple);

View File

@@ -0,0 +1,28 @@
import { useState, useEffect, useRef } from 'react';
export const useScrollState = (threshold: number = 50) => {
const [isScrolled, setIsScrolled] = useState(false);
const rafRef = useRef<number | undefined>(undefined);
useEffect(() => {
const handleScroll = () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
setIsScrolled(window.scrollY > threshold);
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [threshold]);
return isScrolled;
};

View File

@@ -0,0 +1,63 @@
"use client";
import { memo } from "react";
import Button from "../button/Button";
import Logo from "./Logo";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface NavbarStyleMinimalProps {
logoSrc?: string;
logoAlt?: string;
brandName?: string;
button: ButtonConfig;
className?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const NavbarStyleMinimal = memo<NavbarStyleMinimalProps>(
function NavbarStyleMinimal({
logoSrc,
logoAlt = "",
brandName = "Webild",
button,
className = "",
buttonClassName = "",
buttonTextClassName = "",
}) {
const theme = useTheme();
return (
<nav
role="navigation"
aria-label="Main navigation"
className={cls(
"fixed z-[100] top-6 w-full",
"transition-all duration-500 ease-in-out",
className
)}
>
<div className={cls(
"w-content-width mx-auto",
"flex items-center justify-between"
)}>
<Logo logoSrc={logoSrc} logoAlt={logoAlt} brandName={brandName} />
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
</div>
</nav>
);
}
);
export default NavbarStyleMinimal;

View File

@@ -0,0 +1,147 @@
"use client";
import { useCallback, memo } from 'react';
import { useResponsiveMenuWidth } from './useResponsiveMenuWidth';
import { useButtonClick } from '@/components/button/useButtonClick';
interface NavItem {
id: string;
name: string;
}
interface ExpandingMenuProps {
isOpen: boolean;
onToggle: () => void;
navItems: NavItem[];
isScrolled?: boolean;
}
const ExpandingMenu = memo<ExpandingMenuProps>(function ExpandingMenu({
isOpen,
onToggle,
navItems,
isScrolled = false
}) {
const { isMounted, menuWidth } = useResponsiveMenuWidth();
const handleNavClick = useCallback(() => {
onToggle();
}, [onToggle]);
return (
<div
className={`
rounded-theme-capped absolute top-3 right-3
transition-[top] duration-500 ease-in-out
${isScrolled ? '' : ''}
`}>
<div
aria-hidden="true"
className={`
primary-button
backdrop-blur-xs
transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-foreground rounded-theme-capped absolute top-0 right-0
${isOpen
? 'w-full h-full'
: 'h-9 w-[var(--height-9)]'
}
`}
/>
<div className={`
relative p-6 flex flex-col gap-6
transition-all duration-500 ease-[cubic-bezier(0.5,0.5,0,1)]
pointer-events-auto origin-[100%_0]
${isOpen
? 'scale-100 opacity-100 visible'
: 'scale-[0.15] opacity-0 invisible'
}
`}
style={{
transition: 'all 0.5s cubic-bezier(0.5, 0.5, 0, 1), transform 0.7s cubic-bezier(0.5, 0.5, 0, 1)',
width: isMounted ? menuWidth : 'var(--width-20)'
}}
>
<p className="text-xl text-background" aria-hidden="true">Menu</p>
<ul
role="menu"
className="relative list-none flex flex-col gap-3 m-0 p-0"
>
{navItems.map((item) => {
const MenuButton = () => {
const handleClick = useButtonClick(item.id, handleNavClick);
return (
<button
aria-label={`Navigate to ${item.name}`}
className={`
text-background flex justify-between items-center
no-underline bg-none border-none cursor-pointer w-full
font-inherit group
`}
onClick={handleClick}
>
<span className="text-base">
{item.name}
</span>
<div className="bg-current rounded-theme-capped shrink-0 h-2 aspect-square" />
</button>
);
};
return (
<li
key={item.id}
role="menuitem"
className="m-0 p-0 list-none"
>
<MenuButton />
</li>
);
})}
</ul>
</div>
<button
aria-label={isOpen ? 'Close menu' : 'Open menu'}
aria-expanded={isOpen}
aria-controls="navigation-menu"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
pointer-events-auto cursor-pointer rounded-theme-capped
flex justify-center items-center
h-9 w-[var(--height-9)] aspect-square absolute top-0 right-0
bg-transparent border-none
${isOpen
? '-translate-x-3 translate-y-3'
: 'translate-x-0 translate-y-0'
}
`}
onClick={onToggle}
>
<span
aria-hidden="true"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-background w-[40%] h-0.25 absolute
${isOpen
? 'translate-y-0 rotate-45'
: '-translate-y-1 hover:translate-y-1'
}
`} />
<span
aria-hidden="true"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-background w-[40%] h-0.25 absolute
${isOpen
? 'translate-y-0 -rotate-45'
: 'translate-y-1 hover:-translate-y-1'
}
`} />
</button>
</div>
);
});
export default ExpandingMenu;

View File

@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
export const useResponsiveMenuWidth = () => {
const [isMounted, setIsMounted] = useState(false);
const [menuWidth, setMenuWidth] = useState('var(--width-20)');
useEffect(() => {
setIsMounted(true);
const handleResize = () => {
setMenuWidth(
window.innerWidth >= 768
? 'var(--width-20)'
: 'calc(var(--width-80) - var(--vw-0_75) * 2)'
);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return { isMounted, menuWidth };
};

View File

@@ -0,0 +1,79 @@
"use client";
import { memo, Fragment } from 'react';
import { ArrowRight, Plus } from 'lucide-react';
import { NavItem } from '@/types/navigation';
import { useMenuAnimation } from './useMenuAnimation';
import { useButtonClick } from '@/components/button/useButtonClick';
interface MobileMenuProps {
menuOpen: boolean;
onMenuToggle: () => void;
navItems: NavItem[];
onNavClick: (id: string) => void;
children?: React.ReactNode;
}
const MobileMenu = ({
menuOpen,
onMenuToggle,
navItems,
onNavClick,
children
}: MobileMenuProps) => {
const menuRef = useMenuAnimation(menuOpen);
return (
<div
id="mobile-menu"
className="md:hidden z-10 fixed top-3 left-1/2 -translate-x-1/2 h-fit rounded-theme-capped card p-6 flex flex-col gap-6 opacity-0"
style={{ width: 'calc(100vw - var(--vw-0_75) * 2)' }}
ref={menuRef}
role="navigation"
aria-label="Mobile navigation menu"
>
<div className="w-full flex justify-between items-center">
<p className="text-xl text-foreground">Menu</p>
<button
className="shrink-0 h-8 aspect-square rounded-theme bg-foreground flex items-center justify-center cursor-pointer"
onClick={onMenuToggle}
aria-label="Close menu"
>
<Plus className="w-1/2 h-1/2 text-background rotate-45" strokeWidth={1.5} aria-hidden="true" />
</button>
</div>
<div className="flex flex-col gap-4">
{navItems.map((item, index) => {
const NavButton = () => {
const handleClick = useButtonClick(item.id, () => onNavClick(item.id));
return (
<button
className="w-full h-fit flex justify-between items-center cursor-pointer"
onClick={handleClick}
aria-label={`Navigate to ${item.name}`}
>
<p className="text-base font-medium">{item.name}</p>
<ArrowRight strokeWidth={1.5} className="h-[var(--text-base)] w-auto text-foreground" aria-hidden="true" />
</button>
);
};
return (
<Fragment key={index}>
<NavButton />
{index < navItems.length - 1 && (
<div className="w-full h-px bg-gradient-to-r from-transparent via-foreground/20 to-transparent" />
)}
</Fragment>
);
})}
</div>
{children && (
<div className="flex gap-3 items-center">{children}</div>
)}
</div>
);
};
export default memo(MobileMenu);

View File

@@ -0,0 +1,40 @@
import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
export const useMenuAnimation = (menuOpen: boolean) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (menuRef.current) {
if (menuOpen) {
gsap.to(menuRef.current, {
y: "0%",
opacity: 1,
pointerEvents: "auto",
duration: 0.8,
ease: "power3.out"
});
} else {
gsap.to(menuRef.current, {
y: "-135%",
opacity: 1,
pointerEvents: "none",
duration: 0.8,
ease: "power3.inOut"
});
}
}
}, [menuOpen]);
useEffect(() => {
if (menuRef.current) {
gsap.set(menuRef.current, {
y: "-135%",
opacity: 1,
pointerEvents: "none"
});
}
}, []);
return menuRef;
};