From ac8746a287a6abf1bbb5c8e8ac806cc3d1c7606b Mon Sep 17 00:00:00 2001 From: moh Date: Sun, 20 Sep 2026 19:12:44 +0200 Subject: [PATCH] IMPROVED - Use swappable image icons and move controls to a corner dock - Dock icons are now real SVG files under public/dock/ (macOS squircle look, active variants), so they can be replaced with custom art later. - Full-bleed icons (DockIcon padding 0), fixed logo src to stop the refresh flash. - Remove the top menu bar entirely; relocate theme/language/sound (+admin) into a corner control dock that expands on click. --- app/[locale]/(site)/layout.tsx | 12 +- components/layout/dock-app-icon.tsx | 119 ----------------- components/layout/floating-controls.tsx | 114 ++++++++++++++++ components/layout/site-dock.tsx | 169 ++++-------------------- components/ui/dock.tsx | 2 +- public/dock/about-active.svg | 12 ++ public/dock/about.svg | 12 ++ public/dock/contact-active.svg | 12 ++ public/dock/contact.svg | 12 ++ public/dock/home-active.svg | 12 ++ public/dock/home.svg | 12 ++ public/dock/portfolio-active.svg | 13 ++ public/dock/portfolio.svg | 13 ++ public/dock/products-active.svg | 12 ++ public/dock/products.svg | 12 ++ 15 files changed, 267 insertions(+), 271 deletions(-) delete mode 100644 components/layout/dock-app-icon.tsx create mode 100644 components/layout/floating-controls.tsx create mode 100644 public/dock/about-active.svg create mode 100644 public/dock/about.svg create mode 100644 public/dock/contact-active.svg create mode 100644 public/dock/contact.svg create mode 100644 public/dock/home-active.svg create mode 100644 public/dock/home.svg create mode 100644 public/dock/portfolio-active.svg create mode 100644 public/dock/portfolio.svg create mode 100644 public/dock/products-active.svg create mode 100644 public/dock/products.svg diff --git a/app/[locale]/(site)/layout.tsx b/app/[locale]/(site)/layout.tsx index 6f964a3..1e27e7a 100644 --- a/app/[locale]/(site)/layout.tsx +++ b/app/[locale]/(site)/layout.tsx @@ -8,7 +8,7 @@ import { SiteDock } from "@/components/layout/site-dock"; import { SiteFooter } from "@/components/layout/site-footer"; import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider"; import { isSuperAdmin } from "@/lib/admin-auth"; -import { getMaintenanceMode, getSiteSettings, getSiteSettingsMediaBindings } from "@/lib/app-config"; +import { getMaintenanceMode, getSiteSettings } from "@/lib/app-config"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; type SiteLayoutProps = { @@ -24,9 +24,8 @@ export const revalidate = 0; export default async function SiteLayout({ children, params }: SiteLayoutProps) { noStore(); await params; - const [maintenanceEnabled, mediaBindings, siteSettings] = await Promise.all([ + const [maintenanceEnabled, siteSettings] = await Promise.all([ getMaintenanceMode(), - getSiteSettingsMediaBindings(), getSiteSettings(), ]); const localeKey = resolveLocale(await getLocale().catch(() => siteSettings.defaultLocale), siteSettings.defaultLocale); @@ -44,12 +43,7 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps) <> - +
diff --git a/components/layout/dock-app-icon.tsx b/components/layout/dock-app-icon.tsx deleted file mode 100644 index 568558d..0000000 --- a/components/layout/dock-app-icon.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { useId } from "react"; - -export type DockSymbol = "home" | "about" | "portfolio" | "products" | "contact"; - -/** - * macOS Big Sur–style squircle app icon, drawn entirely in SVG so it stays - * crisp at every magnification step. Inactive icons use a neutral graphite - * gradient; the active icon lights up in the brand coral. - */ -export function DockAppIcon({ - symbol, - active = false, - disabled = false, -}: { - symbol: DockSymbol; - active?: boolean; - disabled?: boolean; -}) { - const uid = useId().replace(/[:]/g, ""); - const bg = `bg-${uid}`; - const gloss = `gloss-${uid}`; - - return ( - - - - {active ? ( - <> - - - - ) : ( - <> - - - - )} - - - - - - - - {/* tile */} - - - - - {/* symbol */} - - {symbol === "home" && ( - <> - - - - )} - {symbol === "about" && ( - <> - - - - )} - {symbol === "contact" && ( - <> - - - - )} - - - {/* filled symbols */} - {symbol === "portfolio" && ( - - - - - - - )} - {symbol === "products" && ( - - - - - - )} - - ); -} diff --git a/components/layout/floating-controls.tsx b/components/layout/floating-controls.tsx new file mode 100644 index 0000000..5b69e97 --- /dev/null +++ b/components/layout/floating-controls.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import { ShieldCheck, SlidersHorizontal, X } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useState, type ReactNode } from "react"; + +import { LocaleToggle } from "@/components/layout/locale-toggle"; +import { SoundToggle } from "@/components/sound-toggle"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { buildAdminUrl } from "@/lib/admin-routing"; +import type { AppLocale } from "@/lib/locale"; +import { cn } from "@/lib/utils"; + +type FloatingControlsProps = { + locale: string; + defaultLocale: AppLocale; + isSuperAdmin?: boolean; +}; + +const pill = + "flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-background/80 text-foreground/80 shadow-panel backdrop-blur-chrome transition-colors hover:bg-accent hover:text-foreground"; + +const itemVariants = { + hidden: { opacity: 0, y: 12, scale: 0.8 }, + show: { opacity: 1, y: 0, scale: 1 }, +}; + +export function FloatingControls({ + locale, + defaultLocale, + isSuperAdmin = false, +}: FloatingControlsProps) { + const [open, setOpen] = useState(false); + const t = useTranslations("navigation"); + const reducedMotion = useReducedMotion(); + + const controls: ReactNode[] = [ + , + , + , + ]; + + if (isSuperAdmin) { + controls.push( + + + , + ); + } + + return ( +
+ + {open ? ( + + {controls.map((control, index) => ( + + {control} + + ))} + + ) : null} + + + +
+ ); +} diff --git a/components/layout/site-dock.tsx b/components/layout/site-dock.tsx index 5b0be07..aea5c41 100644 --- a/components/layout/site-dock.tsx +++ b/components/layout/site-dock.tsx @@ -1,25 +1,16 @@ "use client"; -import { ShieldCheck } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useTheme } from "next-themes"; import { useLocale, useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; -import { DockAppIcon, type DockSymbol } from "@/components/layout/dock-app-icon"; -import { LocaleToggle } from "@/components/layout/locale-toggle"; -import { SoundToggle } from "@/components/sound-toggle"; -import { ThemeToggle } from "@/components/theme-toggle"; +import { FloatingControls } from "@/components/layout/floating-controls"; import { Dock, DockIcon } from "@/components/ui/dock"; -import { buildAdminUrl } from "@/lib/admin-routing"; import { getLocalizedPath, stripLocalePrefix, type AppLocale } from "@/lib/locale"; import { cn } from "@/lib/utils"; type SiteDockProps = { - lightLogoUrl?: string | null; - darkLogoUrl?: string | null; defaultLocale: AppLocale; /** * Server-computed via `isSuperAdmin()` in the parent layout. This client @@ -30,8 +21,10 @@ type SiteDockProps = { isSuperAdmin?: boolean; }; +type NavKey = "home" | "about" | "portfolio" | "products" | "contact"; + type NavItem = { - key: DockSymbol; + key: NavKey; path: string; disabled?: boolean; }; @@ -51,134 +44,16 @@ function isNavItemActive(currentPath: string, itemPath: string) { 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) { +export function SiteDock({ 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 */} + {/* Logo — first icon in the dock (fixed src, no theme swap → no flash) */} mohfarawati @@ -220,8 +92,18 @@ export function SiteDock({ const itemPath = path || "/"; const isActive = isNavItemActive(currentPath, itemPath); const label = t(key); + const src = `/dock/${key}${isActive ? "-active" : ""}.svg`; - const icon = ; + const icon = ( + {label} + ); return (
+ + {/* ===== Corner controls — theme / language / sound / admin ===== */} + ); } diff --git a/components/ui/dock.tsx b/components/ui/dock.tsx index 0b5d53d..7dd36d6 100644 --- a/components/ui/dock.tsx +++ b/components/ui/dock.tsx @@ -107,7 +107,7 @@ const DockIcon = ({ ...props }: DockIconProps) => { const ref = useRef(null); - const padding = Math.max(2, size * 0.05); + const padding = 0; const defaultMouseX = useMotionValue(Infinity); const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => { diff --git a/public/dock/about-active.svg b/public/dock/about-active.svg new file mode 100644 index 0000000..8619f15 --- /dev/null +++ b/public/dock/about-active.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/about.svg b/public/dock/about.svg new file mode 100644 index 0000000..d38b0be --- /dev/null +++ b/public/dock/about.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/contact-active.svg b/public/dock/contact-active.svg new file mode 100644 index 0000000..7018726 --- /dev/null +++ b/public/dock/contact-active.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/contact.svg b/public/dock/contact.svg new file mode 100644 index 0000000..710dc97 --- /dev/null +++ b/public/dock/contact.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/home-active.svg b/public/dock/home-active.svg new file mode 100644 index 0000000..d9eb81c --- /dev/null +++ b/public/dock/home-active.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/home.svg b/public/dock/home.svg new file mode 100644 index 0000000..09fb01f --- /dev/null +++ b/public/dock/home.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/portfolio-active.svg b/public/dock/portfolio-active.svg new file mode 100644 index 0000000..7e170fa --- /dev/null +++ b/public/dock/portfolio-active.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/portfolio.svg b/public/dock/portfolio.svg new file mode 100644 index 0000000..e289f12 --- /dev/null +++ b/public/dock/portfolio.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/products-active.svg b/public/dock/products-active.svg new file mode 100644 index 0000000..6c4db6c --- /dev/null +++ b/public/dock/products-active.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/dock/products.svg b/public/dock/products.svg new file mode 100644 index 0000000..e77e661 --- /dev/null +++ b/public/dock/products.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file