Skip to content

Commit 695d4c6

Browse files
committed
feat(ui): add back-to-top and update navigation components
1 parent cc3e99f commit 695d4c6

File tree

5 files changed

+209
-92
lines changed

5 files changed

+209
-92
lines changed

src/app/icon.svg

Lines changed: 12 additions & 0 deletions
Loading

src/components/layout/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { default as Navigation } from "./navigation";
2-
export { default as Footer } from "./footer";
2+
export { default as Sidebar } from "./sidebar";

src/components/layout/navigation.tsx

Lines changed: 113 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,131 @@
11
"use client";
22

3+
import { useState, useEffect, useCallback } from "react";
34
import Link from "next/link";
5+
import { usePathname } from "next/navigation";
46
import {
57
Navbar,
68
NavbarBrand,
79
NavbarContent,
810
NavbarItem,
911
Button,
10-
DropdownItem,
11-
DropdownTrigger,
12-
Dropdown,
13-
DropdownMenu,
12+
useDisclosure,
13+
Input,
1414
} from "@heroui/react";
15-
import { ChevronDown, Origami, User } from "lucide-react";
15+
import { ArrowBigLeft, Menu, Origami, Search, X } from "lucide-react";
16+
import { ThemeSwitcher } from "@/components/ui";
17+
import { PROJECT_NAME } from "@/config/constants";
18+
19+
export default function Navigation() {
20+
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
21+
const [search, setSearch] = useState<string>("");
22+
const pathname = usePathname();
23+
const { isOpen, onOpen, onOpenChange } = useDisclosure();
24+
25+
useEffect(() => {
26+
setIsMenuOpen(false);
27+
}, [pathname]);
28+
29+
useEffect(() => {
30+
if (isMenuOpen) {
31+
document.body.style.overflow = "hidden";
32+
} else {
33+
document.body.style.overflow = "";
34+
}
35+
36+
return () => {
37+
document.body.style.overflow = "";
38+
};
39+
}, [isMenuOpen]);
40+
41+
const toggleMenu = useCallback(() => {
42+
setIsMenuOpen((prev) => !prev);
43+
}, []);
1644

17-
export default function App() {
1845
return (
19-
<Navbar maxWidth="full">
20-
<NavbarBrand>
21-
<Link href="/" className="flex items-center gap-1">
22-
<Origami size={30} />
23-
<p className="text-2xl font-bold text-inherit">Code Atlas</p>
24-
</Link>
25-
</NavbarBrand>
26-
<NavbarContent className="hidden sm:flex gap-4" justify="center">
27-
<Dropdown>
28-
<NavbarItem>
29-
<DropdownTrigger>
30-
<Button
31-
disableRipple
32-
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
33-
endContent={<ChevronDown size={20} />}
34-
radius="sm"
35-
variant="light"
36-
>
37-
Explore
38-
</Button>
39-
</DropdownTrigger>
46+
<>
47+
<Navbar
48+
maxWidth="full"
49+
className="fixed z-50"
50+
classNames={{
51+
base: "bg-white dark:bg-neutral-950 h-12 border-b-2 border-neutral-200 dark:border-neutral-800",
52+
}}
53+
>
54+
<NavbarContent>
55+
<Link href="/">
56+
<NavbarBrand>
57+
<Origami size={24} />
58+
<span className="font-bold ml-2">
59+
{PROJECT_NAME.toUpperCase()}
60+
</span>
61+
</NavbarBrand>
62+
</Link>
63+
</NavbarContent>
64+
<NavbarContent justify="center">
65+
<NavbarItem className="hidden sm:flex">
66+
{/* TODO: Add search bar component */}
67+
<Input
68+
size="sm"
69+
variant="bordered"
70+
placeholder="Search..."
71+
value={search}
72+
onChange={(e) => setSearch(e.target.value)}
73+
className="w-[500px]"
74+
classNames={{
75+
inputWrapper:
76+
"border-2 border-neutral-200 dark:border-neutral-800 text-neutral-950 dark:text-white shadow-none",
77+
}}
78+
/>
4079
</NavbarItem>
41-
<DropdownMenu
42-
aria-label="CodeAtlas features"
43-
itemClasses={{
44-
base: "gap-4",
45-
}}
46-
>
47-
<DropdownItem
48-
key="autoscaling"
49-
description="ACME scales apps based on demand and load"
50-
startContent={<User size={20} />}
51-
>
52-
Autoscaling
53-
</DropdownItem>
54-
<DropdownItem
55-
key="usage_metrics"
56-
description="Real-time metrics to debug issues"
57-
startContent={<User size={20} />}
58-
>
59-
Usage Metrics
60-
</DropdownItem>
61-
<DropdownItem
62-
key="production_ready"
63-
description="ACME runs on ACME, join us at web scale"
64-
startContent={<User size={20} />}
65-
>
66-
Production Ready
67-
</DropdownItem>
68-
<DropdownItem
69-
key="99_uptime"
70-
description="High availability and uptime guarantees"
71-
startContent={<User size={20} />}
80+
</NavbarContent>
81+
82+
<NavbarContent justify="end">
83+
<ThemeSwitcher />
84+
<NavbarItem className="sm:hidden">
85+
<Button
86+
isIconOnly
87+
type="button"
88+
size="sm"
89+
variant="light"
90+
onPress={onOpen}
91+
aria-label="Search documentation"
7292
>
73-
+99% Uptime
74-
</DropdownItem>
75-
<DropdownItem
76-
key="supreme_support"
77-
description="Support team ready to respond"
78-
startContent={<User size={20} />}
93+
<Search size={20} />
94+
</Button>
95+
</NavbarItem>
96+
<NavbarItem className="hidden sm:flex sm:gap-2">
97+
<Button as={Link} href="/auth/login" variant="light" size="sm">
98+
Log In
99+
</Button>
100+
<Button
101+
as={Link}
102+
href="/auth/login"
103+
variant="solid"
104+
size="sm"
105+
className="bg-neutral-950 dark:bg-white text-white dark:text-neutral-950 font-medium"
79106
>
80-
+Supreme Support
81-
</DropdownItem>
82-
</DropdownMenu>
83-
</Dropdown>
84-
<NavbarItem isActive>
85-
<Link aria-current="page" href="/authors">
86-
Authors
87-
</Link>
88-
</NavbarItem>
89-
<NavbarItem>
90-
<Link color="foreground" href="/blog">
91-
Blog
92-
</Link>
93-
</NavbarItem>
94-
</NavbarContent>
95-
<NavbarContent justify="end">
96-
<NavbarItem className="hidden lg:flex">
97-
<Button as={Link} href="/auth/login" variant="bordered">
98-
Login
99-
</Button>
100-
</NavbarItem>
101-
<NavbarItem>
102-
<Button as={Link} href="/auth/signup" className="bg-black text-white">
103-
Sign Up
107+
Sign Up
108+
</Button>
109+
</NavbarItem>
110+
<Button
111+
isIconOnly
112+
data-menu-toggle
113+
size="sm"
114+
type="button"
115+
variant="light"
116+
onPress={toggleMenu}
117+
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
118+
className="sm:hidden"
119+
>
120+
{isMenuOpen ? <X size={20} /> : <Menu size={20} />}
104121
</Button>
105-
</NavbarItem>
106-
</NavbarContent>
107-
</Navbar>
122+
</NavbarContent>
123+
</Navbar>
124+
125+
{/* <MobileNavigation
126+
isOpen={isMenuOpen}
127+
data={data}
128+
/> */}
129+
</>
108130
);
109131
}

src/components/ui/back-to-top.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import {
5+
motion,
6+
AnimatePresence,
7+
useScroll,
8+
useMotionValueEvent,
9+
} from "framer-motion";
10+
import { Button } from "@heroui/react";
11+
import { ChevronUp } from "lucide-react";
12+
13+
export default function BackToTop() {
14+
const { scrollYProgress } = useScroll();
15+
const [visible, setVisible] = useState<boolean>(false);
16+
const [showing, setShowing] = useState<boolean>(false);
17+
18+
useMotionValueEvent(scrollYProgress, "change", (current) => {
19+
if (typeof current === "number") {
20+
const direction = current! - scrollYProgress.getPrevious()!;
21+
22+
if (scrollYProgress.get() < 0.05) {
23+
setVisible(false);
24+
setShowing(true);
25+
} else {
26+
if (direction < 0) {
27+
setVisible(true);
28+
setShowing(true);
29+
} else {
30+
setVisible(false);
31+
}
32+
}
33+
}
34+
});
35+
36+
useEffect(() => {
37+
if (showing) {
38+
const timer = setTimeout(() => {
39+
setVisible(false);
40+
setShowing(false);
41+
}, 5000);
42+
43+
return () => clearTimeout(timer);
44+
}
45+
}, [showing]);
46+
47+
const scrollToTop = () => {
48+
window.scrollTo({
49+
top: 0,
50+
behavior: "smooth",
51+
});
52+
};
53+
54+
return (
55+
<AnimatePresence mode="wait">
56+
<motion.div
57+
initial={{
58+
opacity: 1,
59+
y: 200,
60+
}}
61+
animate={{
62+
y: visible ? 0 : 200,
63+
opacity: visible ? 1 : 0,
64+
}}
65+
transition={{
66+
duration: 0.2,
67+
}}
68+
className="flex max-w-fit z-30 fixed bottom-4 inset-x-0 mx-auto"
69+
>
70+
<Button
71+
size="sm"
72+
onPress={scrollToTop}
73+
aria-label="Back to top"
74+
className="backdrop-blur-xl backdrop-saturate-150 bg-neutral-950 border-2 border-neutral-200 text-white"
75+
>
76+
<ChevronUp />
77+
</Button>
78+
</motion.div>
79+
</AnimatePresence>
80+
);
81+
}

src/components/ui/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as BackToTop } from "./back-to-top";
2+
export { default as ThemeSwitcher } from "./theme-switcher";

0 commit comments

Comments
 (0)