fix docs-menu, leftbar, sublink

This commit is contained in:
Wildan Nursahidan
2025-05-17 18:55:25 +07:00
parent 53e97c574c
commit 1a3ce7f2b9
5 changed files with 1561 additions and 39 deletions

37
.gitignore vendored
View File

@@ -1 +1,36 @@
node_modules # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

1418
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,21 +4,42 @@ import { ROUTES } from "@/lib/routes-config";
import SubLink from "./sublink"; import SubLink from "./sublink";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
export default function DocsMenu({ isSheet = false }) { interface DocsMenuProps {
isSheet?: boolean;
className?: string;
}
export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuProps) {
const pathname = usePathname(); const pathname = usePathname();
// Skip rendering if not on a docs page
if (!pathname.startsWith("/docs")) return null; if (!pathname.startsWith("/docs")) return null;
return ( return (
<div className="flex flex-col gap-3.5 mt-5 pr-2 pb-6"> <nav
{ROUTES.map((item, index) => { aria-label="Documentation navigation"
const modifiedItems = { className={className}
...item, >
href: `/docs${item.href}`, <ul className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
level: 0, {ROUTES.map((item, index) => {
isSheet, // Normalize href - hapus leading/trailing slashes
}; const normalizedHref = `/${item.href.replace(/^\/+|\/+$/g, '')}`;
return <SubLink key={item.title + index} {...modifiedItems} />; const itemHref = `/docs${normalizedHref}`;
})}
</div> const modifiedItems = {
...item,
href: itemHref,
level: 0,
isSheet,
};
return (
<li key={`${item.title}-${index}`}>
<SubLink {...modifiedItems} />
</li>
);
})}
</ul>
</nav>
); );
} }

View File

@@ -25,7 +25,7 @@ export function Leftbar() {
${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`} ${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`}
> >
{/* Toggle Button */} {/* Toggle Button */}
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-10"> <div className="absolute top-0 right-0 py-6 px-0 ml-6 z-10 -mt-4">
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"

View File

@@ -8,9 +8,15 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { SheetClose } from "@/components/ui/sheet"; import { SheetClose } from "@/components/ui/sheet";
import { ChevronDown, ChevronRight } from "lucide-react"; import { ChevronDown, ChevronRight } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
interface SubLinkProps extends EachRoute {
level: number;
isSheet: boolean;
parentHref?: string;
}
export default function SubLink({ export default function SubLink({
title, title,
href, href,
@@ -18,19 +24,45 @@ export default function SubLink({
noLink, noLink,
level, level,
isSheet, isSheet,
}: EachRoute & { level: number; isSheet: boolean }) { parentHref = "",
}: SubLinkProps) {
const path = usePathname(); const path = usePathname();
const [isOpen, setIsOpen] = useState(level == 0); const [isOpen, setIsOpen] = useState(level === 0);
// Full path including parent's href
const fullHref = `${parentHref}${href}`;
// Check if current path exactly matches this link's href
const isExactActive = useMemo(() => path === fullHref, [path, fullHref]);
// Check if any child is active (for parent items)
const hasActiveChild = useMemo(() => {
if (!items) return false;
return items.some(item => {
const childHref = `${fullHref}${item.href}`;
return path.startsWith(childHref) && path !== fullHref;
});
}, [items, path, fullHref]);
// Auto-expand if current path is a child of this item
useEffect(() => { useEffect(() => {
if (path == href || path.includes(href)) setIsOpen(true); if (items && (path.startsWith(fullHref) && path !== fullHref)) {
}, [href, path]); setIsOpen(true);
}
}, [path, fullHref, items]);
const Comp = ( // Only apply active styles if it's an exact match and not a parent with active children
<Anchor activeClassName="text-primary font-medium" href={href}> const Comp = useMemo(() => (
<Anchor
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
href={fullHref}
className={cn(
hasActiveChild && "font-medium text-foreground"
)}
>
{title} {title}
</Anchor> </Anchor>
); ), [title, fullHref, hasActiveChild]);
const titleOrLink = !noLink ? ( const titleOrLink = !noLink ? (
isSheet ? ( isSheet ? (
@@ -39,7 +71,12 @@ export default function SubLink({
Comp Comp
) )
) : ( ) : (
<h4 className="font-medium sm:text-sm text-primary">{title}</h4> <h4 className={cn(
"font-medium sm:text-sm",
hasActiveChild ? "text-foreground" : "text-primary"
)}>
{title}
</h4>
); );
if (!items) { if (!items) {
@@ -47,36 +84,47 @@ export default function SubLink({
} }
return ( return (
<div className="flex flex-col gap-1 w-full"> <div className={cn("flex flex-col gap-1 w-full")}>
<Collapsible open={isOpen} onOpenChange={setIsOpen}> <Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="w-full pr-5"> <CollapsibleTrigger
<div className="flex items-center justify-between cursor-pointer w-full"> className="w-full pr-5 text-left"
aria-expanded={isOpen}
aria-controls={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
>
<div className="flex items-center justify-between w-full">
{titleOrLink} {titleOrLink}
<span> <span className="ml-2">
{!isOpen ? ( {!isOpen ? (
<ChevronRight className="h-[0.9rem] w-[0.9rem]" /> <ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
) : ( ) : (
<ChevronDown className="h-[0.9rem] w-[0.9rem]" /> <ChevronDown className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
)} )}
</span> </span>
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent
id={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
className={cn(
"overflow-hidden transition-all duration-200 ease-in-out",
isOpen ? "animate-collapsible-down" : "animate-collapsible-up"
)}
>
<div <div
className={cn( className={cn(
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3", "flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
level > 0 && "pl-4 border-l ml-1.5" level > 0 && "pl-4 border-l ml-1.5"
)} )}
> >
{items?.map((innerLink) => { {items?.map((innerLink) => (
const modifiedItems = { <SubLink
...innerLink, key={`${fullHref}${innerLink.href}`}
href: `${href + innerLink.href}`, {...innerLink}
level: level + 1, href={innerLink.href}
isSheet, level={level + 1}
}; isSheet={isSheet}
return <SubLink key={modifiedItems.href} {...modifiedItems} />; parentHref={fullHref}
})} />
))}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>