Skip to content

Commit

Permalink
Add-new-bottom-navigation-menu-&-home-page (#50)
Browse files Browse the repository at this point in the history
* ✨ add new floating dock menu

* 🎨 improve scrollarea to accept viewport

* ✨ display sidemenu corresponding to category selected

* ✅ remove outdated HomePage tests and update Sidemenu tests to include InfoMenuList

* 🎨 add components to preview only variants

* ✨ update dev script to use turbo mode for improved performance
  • Loading branch information
damien-schneider authored Oct 31, 2024
1 parent 205f56e commit 3bc4536
Show file tree
Hide file tree
Showing 30 changed files with 820 additions and 561 deletions.
35 changes: 0 additions & 35 deletions apps/website/__tests__/home-h1.test.tsx

This file was deleted.

13 changes: 11 additions & 2 deletions apps/website/__tests__/sidemenu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import NavigationMenu from "#/src/components/navigation/navigation-menu";
import InfoMenuList from "#/src/components/navigation/info-menu-list";

const gettingStartedRegex = /getting-started$/;

vi.mock("next/navigation", () => ({
usePathname: () => [],
usePathname: () => {
return "/common-ui";
},
useSelectedLayoutSegments: () => [],
}));

describe("Sidemenu component", () => {
render(<NavigationMenu />);
render(
<div>
<InfoMenuList />
<NavigationMenu />
</div>,
);

it("should have href attribute of the 'Contribute' element set to https://cuicui.featurebase.app/", () => {
const contributeElement = screen.getByTestId("navigation-link-Contribute");

Expand Down
2 changes: 1 addition & 1 deletion apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"publisher": "Damien Schneider",
"scripts": {
"build": "next build",
"dev": "next dev",
"dev": "next dev --turbo",
"start": "next start",
"format:check": "biome format --check ./src",
"format:write": "biome format --write ./src",
Expand Down
26 changes: 24 additions & 2 deletions apps/website/src/app/(components)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { DesktopSideMenu } from "#/src/components/navigation/desktop-menu";

import { AddressBar } from "#/src/ui/address-bar";
import StarGithubProjectButton from "#/src/ui/star-github-project-button";
import type React from "react";

export default function ComponentsLayout({
children,
children,
}: Readonly<{ children: React.ReactNode }>) {
return <div className="space-y-10">{children}</div>;
return (
<div className="mx-auto max-w-screen-2xl">
<DesktopSideMenu />

<div className="lg:ml-80">
{/* Add overflow-auto if layout width problems */}

<AddressBar />

<main className=" p-4 pt-12 pb-20 md:p-6">
<div className="space-y-10">{children}</div>
</main>

<div className="flex sm:hidden fixed bottom-6 left-2">
<StarGithubProjectButton />
</div>
</div>
</div>
);
}
261 changes: 261 additions & 0 deletions apps/website/src/app/floating-docks-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
"use client";
import { useOnClickOutside } from "@/cuicui/hooks/use-click-outside/use-click-outside";
/**
* Note: Use position fixed according to your needs
* Desktop navbar is better positioned at the bottom
* Mobile navbar is better positioned at bottom right.
**/

import { cn } from "@/cuicui/utils/cn/cn";
import {
AnimatePresence,
type MotionValue,
motion,
useMotionValue,
useSpring,
useTransform,
} from "framer-motion";
import { ListCollapseIcon, type LucideIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";

export const FloatingDock = ({
items,
desktopClassName,
mobileClassName,
}: {
items: {
title: string;
Icon: LucideIcon;
href: string;
}[];
desktopClassName?: string;
mobileClassName?: string;
}) => {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile items={items} className={mobileClassName} />
</>
);
};

const FloatingDockMobile = ({
items,
className,
}: {
items: {
title: string;
Icon: LucideIcon;
href: string;
}[];
className?: string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent | TouchEvent | FocusEvent) => {
setOpen(false);
};

const pathname = usePathname();

useEffect(() => {
if (!pathname) {
return;
}
setOpen(false);
}, [pathname]);

useOnClickOutside(ref, handleClickOutside);
const [open, setOpen] = useState(false);
return (
<div ref={ref} className={cn("relative block md:hidden ", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute bottom-full mb-2 inset-x-0 flex flex-col items-end gap-2"
>
{items.map((item, idx) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
transition: {
delay: idx * 0.05,
},
}}
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
>
<Link
href={item.href}
key={item.title}
className={cn(
"rounded-full w-fit px-4 py-4 bg-neutral-50 border border-neutral-400/20 flex items-center justify-center gap-2",
pathname.includes(item.href)
? "bg-neutral-700 dark:bg-neutral-200 text-neutral-100 dark:text-neutral-800"
: "dark:bg-neutral-900 text-neutral-500 dark:text-neutral-300",
)}
onClick={() => setOpen(false)}
>
<item.Icon className="size-6" />
<p className=" text-nowrap tracking-tighter font-medium">
{item.title}
</p>
</Link>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
type="button"
onClick={() => setOpen(!open)}
className="size-16 rounded-full border border-neutral-400/20 bg-neutral-50 dark:bg-neutral-800 flex items-center justify-center"
>
<ListCollapseIcon className="size-7 text-neutral-500 dark:text-neutral-400" />
</button>
</div>
);
};

const FloatingDockDesktop = ({
items,
className,
}: {
items: { title: string; Icon: LucideIcon; href: string }[];
className?: string;
}) => {
const mouseX = useMotionValue(Number.POSITIVE_INFINITY);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Number.POSITIVE_INFINITY)}
className={cn(
"mx-auto hidden md:flex h-14 gap-2 items-end rounded-full bg-neutral-50 dark:bg-neutral-900 px-2 pb-2 border border-neutral-500/20",
className,
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
};

function IconContainer({
mouseX,
title,
Icon,
href,
}: Readonly<{
mouseX: MotionValue;
title: string;
Icon: LucideIcon;
href: string;
}>) {
const pathname = usePathname();
const [isActive, setIsActive] = useState(false);

useEffect(() => {
if (href === "/") {
setIsActive(href === pathname);
} else {
setIsActive(pathname.includes(href));
}
}, [pathname, href]);

const ref = useRef<HTMLDivElement>(null);

const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };

return val - bounds.x - bounds.width / 2;
});

const widthTransform = useTransform(distance, [-150, 0, 150], [60, 80, 60]);
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 60, 40]);

const widthTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);
const heightTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);

const width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 300,
damping: 12,
});
const height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 300,
damping: 12,
});

const widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 300,
damping: 12,
});
const heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 300,
damping: 12,
});

const [hovered, setHovered] = useState(false);

return (
<Link href={href}>
<motion.div
ref={ref}
style={{ width, height }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={cn(
"aspect-square rounded-full border border-neutral-400/20 backdrop-blur-2xl flex items-center justify-center relative",
isActive
? "bg-neutral-800 dark:bg-neutral-100"
: "bg-neutral-100 dark:bg-neutral-800",
)}
>
<AnimatePresence>
{hovered && (
// ------ Tooltip ------ //
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
className="px-2 py-0.5 whitespace-pre rounded-md bg-neutral-100 dark:bg-neutral-800 dark:text-white border border-neutral-500/20 text-neutral-700 absolute left-1/2 -translate-x-1/2 -top-8 w-fit text-xs"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className={cn(
"flex items-center justify-center",
isActive
? "*:text-neutral-100 *:dark:text-neutral-800"
: "*:text-neutral-800 *:dark:text-neutral-100",
)}
>
<Icon className="size-6" />
</motion.div>
</motion.div>
</Link>
);
}
Loading

0 comments on commit 3bc4536

Please sign in to comment.