diff --git a/app/[locale]/(site)/layout.tsx b/app/[locale]/(site)/layout.tsx index bed2e1d..6f964a3 100644 --- a/app/[locale]/(site)/layout.tsx +++ b/app/[locale]/(site)/layout.tsx @@ -4,8 +4,8 @@ import { getLocale } from "next-intl/server"; import { redirect } from "next/navigation"; import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop"; +import { SiteDock } from "@/components/layout/site-dock"; import { SiteFooter } from "@/components/layout/site-footer"; -import { SiteHeader } from "@/components/layout/site-header"; import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider"; import { isSuperAdmin } from "@/lib/admin-auth"; import { getMaintenanceMode, getSiteSettings, getSiteSettingsMediaBindings } from "@/lib/app-config"; @@ -44,7 +44,7 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps) <> - ; + disabled?: boolean; +}; + +const navItems: NavItem[] = [ + { key: "home", path: "", Icon: Home }, + { key: "about", path: "/about", Icon: User }, + { key: "portfolio", path: "/portfolio", Icon: FolderKanban }, + { key: "products", path: "/products", Icon: Package, disabled: true }, + { key: "contact", path: "/contact", Icon: Mail }, +]; + +function isNavItemActive(currentPath: string, itemPath: string) { + if (itemPath === "/") { + return currentPath === "/"; + } + return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`); +} + +function MenuClock({ locale }: { locale: string }) { + const [time, setTime] = useState(""); + + useEffect(() => { + const format = () => + setTime( + new Date().toLocaleTimeString(locale === "ar" ? "ar" : locale, { + hour: "2-digit", + minute: "2-digit", + }), + ); + format(); + const id = setInterval(format, 20_000); + return () => clearInterval(id); + }, [locale]); + + return ( + + {time || "--:--"} + + ); +} + +export function SiteDock({ + lightLogoUrl, + darkLogoUrl, + defaultLocale, + isSuperAdmin = false, +}: SiteDockProps) { + const locale = useLocale(); + const pathname = usePathname(); + const t = useTranslations("navigation"); + const { theme, resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const isArabic = locale === "ar"; + const currentPath = stripLocalePrefix(pathname); + const homeHref = getLocalizedPath(locale, "/", defaultLocale); + const adminHref = buildAdminUrl("/"); + + const activeTheme = theme === "system" ? resolvedTheme : theme; + const isDark = mounted && activeTheme === "dark"; + const logoSrc = isDark + ? darkLogoUrl || lightLogoUrl || "/logos/dark-primary.svg" + : lightLogoUrl || darkLogoUrl || "/logos/light-primary.svg"; + + const brandSubtitle = isArabic + ? "مطوّر ويب · فريلانسر" + : "Webdesigner Bremen · Freelancer"; + + const controlButtonClassName = + "h-8 w-8 rounded-full border border-transparent text-foreground/70 hover:bg-accent hover:text-foreground"; + + return ( + <> + {/* ===== Top menu bar (macOS-style utility strip) ===== */} +
+
+ + + + {isArabic ? ( + <> + محمد{" "} + فرواتي + + ) : ( + <> + MOH + FARAWATI + + )} + + + + + {brandSubtitle} + + +
+ + + + + {isSuperAdmin ? ( + + + Admin + + ) : null} +
+
+
+ + {/* ===== Bottom dock (Magic UI) — primary navigation ===== */} +
+ + {/* Logo — first icon in the dock */} + + + mohfarawati + + + + +
+ + {navItems.map(({ key, path, Icon, disabled }) => { + const itemPath = path || "/"; + const isActive = isNavItemActive(currentPath, itemPath); + const label = t(key); + + const inner = ( + + + + ); + + return ( + + {disabled ? ( + + {inner} + + ) : ( + + {inner} + + )} + + + + {isActive ? ( + + ) : null} + + ); + })} + +
+ + ); +} + +function DockTip({ label, soon }: { label: string; soon?: string }) { + return ( + + {label} + {soon ? {soon} : null} + + ); +} diff --git a/components/ui/dock.tsx b/components/ui/dock.tsx new file mode 100644 index 0000000..696c5b9 --- /dev/null +++ b/components/ui/dock.tsx @@ -0,0 +1,150 @@ +"use client"; + +import React, { useRef, type PropsWithChildren } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { + motion, + useMotionValue, + useSpring, + useTransform, + type MotionValue, + type MotionProps, +} from "framer-motion"; + +import { cn } from "@/lib/utils"; + +export interface DockProps extends VariantProps { + className?: string; + iconSize?: number; + iconMagnification?: number; + disableMagnification?: boolean; + iconDistance?: number; + direction?: "top" | "middle" | "bottom"; + children: React.ReactNode; +} + +const DEFAULT_SIZE = 40; +const DEFAULT_MAGNIFICATION = 60; +const DEFAULT_DISTANCE = 140; +const DEFAULT_DISABLEMAGNIFICATION = false; + +const dockVariants = cva( + "supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto flex w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md", +); + +const Dock = React.forwardRef( + ( + { + className, + children, + iconSize = DEFAULT_SIZE, + iconMagnification = DEFAULT_MAGNIFICATION, + disableMagnification = DEFAULT_DISABLEMAGNIFICATION, + iconDistance = DEFAULT_DISTANCE, + direction = "bottom", + ...props + }, + ref, + ) => { + const mouseX = useMotionValue(Infinity); + + const renderChildren = () => { + return React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.type === DockIcon) { + return React.cloneElement(child, { + ...child.props, + mouseX: mouseX, + size: iconSize, + magnification: iconMagnification, + disableMagnification: disableMagnification, + distance: iconDistance, + }); + } + return child; + }); + }; + + return ( + mouseX.set(e.pageX)} + onMouseLeave={() => mouseX.set(Infinity)} + {...props} + className={cn(dockVariants({ className }), { + "items-start": direction === "top", + "items-center": direction === "middle", + "items-end": direction === "bottom", + })} + > + {renderChildren()} + + ); + }, +); + +Dock.displayName = "Dock"; + +export interface DockIconProps + extends Omit, "children"> { + size?: number; + magnification?: number; + disableMagnification?: boolean; + distance?: number; + mouseX?: MotionValue; + className?: string; + children?: React.ReactNode; + props?: PropsWithChildren; +} + +const DockIcon = ({ + size = DEFAULT_SIZE, + magnification = DEFAULT_MAGNIFICATION, + disableMagnification, + distance = DEFAULT_DISTANCE, + mouseX, + className, + children, + ...props +}: DockIconProps) => { + const ref = useRef(null); + const padding = Math.max(6, size * 0.2); + const defaultMouseX = useMotionValue(Infinity); + + const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => { + const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; + return val - bounds.x - bounds.width / 2; + }); + + const targetSize = disableMagnification ? size : magnification; + + const sizeTransform = useTransform( + distanceCalc, + [-distance, 0, distance], + [size, targetSize, size], + ); + + const scaleSize = useSpring(sizeTransform, { + mass: 0.1, + stiffness: 150, + damping: 12, + }); + + return ( + + {children} + + ); +}; + +DockIcon.displayName = "DockIcon"; + +export { Dock, DockIcon, dockVariants };