docu version 1.8.5

This commit is contained in:
Wildan Nursahidan
2025-05-17 19:35:31 +07:00
parent 82c3a03d3a
commit a3fcae0112
23 changed files with 866 additions and 6395 deletions

37
components/Sponsor.tsx Normal file
View File

@@ -0,0 +1,37 @@
import docuConfig from "@/docu.json";
import Image from "next/image";
import Link from "next/link";
export function Sponsor() {
const sponsor = docuConfig.sponsor;
const item = sponsor?.item;
if (!item) return null;
return (
<div className="mt-4">
<h2 className="mb-4 text-sm font-medium">{sponsor.title || "Sponsor"}</h2>
<Link
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col justify-center gap-2 p-4 border rounded-lg hover:shadow transition-shadow"
>
<div className="relative w-8 h-8 flex-shrink-0">
<Image
src={item.image}
alt={item.title}
fill
className="object-contain"
/>
</div>
<div className="text-center sm:text-left">
<h3 className="text-sm font-medium">{item.title}</h3>
<p className="text-muted-foreground text-sm">{item.description}</p>
</div>
</Link>
</div>
);
}
export default Sponsor;

View File

@@ -51,7 +51,7 @@ export function FloatingVersionToc({ versions }: FloatingVersionTocProps) {
};
return (
<div className="fixed bottom-4 right-4 lg:hidden z-50">
<div className="fixed bottom-4 right-4 md:hidden z-50">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className="rounded-full shadow-lg px-4 py-2 flex items-center gap-2">

View File

@@ -24,13 +24,13 @@ interface VersionEntryProps {
isLast?: boolean;
}
export function VersionEntry({
version,
date,
export function VersionEntry({
version,
date,
description,
image,
image,
changes,
isLast
isLast
}: VersionEntryProps) {
const [expanded, setExpanded] = useState(false);
@@ -45,7 +45,7 @@ export function VersionEntry({
{formatDate2(date)}
</time>
</div>
{description && (
<p className="text-dark text-xl">{description}</p>
)}
@@ -69,8 +69,8 @@ export function VersionEntry({
<div className="space-y-6">
{Object.entries(changes).map(([type, items]) => (
items && items.length > 0 && (
<ChangeGroup
key={type}
<ChangeGroup
key={type}
type={type as keyof typeof changes}
changes={items}
expanded={expanded}
@@ -82,10 +82,10 @@ export function VersionEntry({
{/* Show more/less button */}
{Object.values(changes).some(items => items && items.length > 5) && (
<Button
variant="ghost"
variant="outline"
size="sm"
onClick={() => setExpanded(!expanded)}
className="mt-4 text-muted-foreground hover:text-foreground"
className="mt-4 text-muted-foreground hover:bg-transparent hover:text-accent border-none"
>
{expanded ? (
<>
@@ -110,4 +110,4 @@ export function VersionEntry({
)}
</div>
);
}
}

View File

@@ -2,8 +2,9 @@
import { useEffect, useState } from "react";
import { cn, formatDate2 } from "@/lib/utils";
import { History } from "lucide-react";
import { History, PanelLeftOpen, PanelLeftClose } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
interface VersionTocProps {
versions: Array<{
@@ -14,33 +15,30 @@ interface VersionTocProps {
export function VersionToc({ versions }: VersionTocProps) {
const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
// Handle initial hash
const hash = window.location.hash.slice(1);
if (hash) {
setActiveId(hash);
}
// Set up intersection observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
setActiveId(id);
// Use pushState instead of replaceState to maintain history
window.history.pushState(null, '', `#${id}`);
window.history.pushState(null, "", `#${id}`);
}
});
},
{
threshold: 0.2,
rootMargin: '-20% 0px -60% 0px'
rootMargin: "-20% 0px -60% 0px",
}
);
// Observe version elements
versions.forEach(({ version }) => {
const element = document.getElementById(`v${version}`);
if (element) observer.observe(element);
@@ -50,41 +48,64 @@ export function VersionToc({ versions }: VersionTocProps) {
}, [versions]);
return (
<aside className="lg:flex hidden toc flex-[1.5] min-w-[238px] pt-8 sticky top-16 h-[calc(100vh-4rem)]">
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2 mb-2">
<History className="w-4 h-4" />
<h3 className="font-medium text-sm">Version History</h3>
</div>
<ScrollArea className="h-full">
<div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
{versions.map(({ version, date }) => (
<a
key={version}
href={`#v${version}`}
className={cn(
"hover:text-foreground transition-colors py-1",
activeId === `v${version}` && "font-medium text-primary"
)}
onClick={(e) => {
e.preventDefault();
const element = document.getElementById(`v${version}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
setActiveId(`v${version}`);
window.history.pushState(null, '', `#v${version}`);
}
}}
>
v{version}
<span className="text-xs text-muted-foreground ml-2">
{formatDate2(date)}
</span>
</a>
))}
</div>
</ScrollArea>
<aside
className={cn(
"sticky top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300 z-20 hidden md:flex",
collapsed ? "w-[48px]" : "w-[250px]"
)}
>
{/* Toggle Button */}
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-30">
<Button
size="icon"
variant="outline"
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
</Button>
</div>
{/* Content */}
{!collapsed && (
<div className="flex flex-col gap-2 w-full pt-8 pr-2">
<div className="flex mb-2">
<h2 className="font-semibold text-lg">Changelog</h2>
</div>
<div className="flex items-center gap-2 mb-2">
<History className="w-4 h-4" />
<h3 className="font-medium text-sm">Version History</h3>
</div>
<ScrollArea className="h-full pr-2">
<div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
{versions.map(({ version, date }) => (
<a
key={version}
href={`#v${version}`}
className={cn(
"hover:text-foreground transition-colors py-1",
activeId === `v${version}` && "font-medium text-primary"
)}
onClick={(e) => {
e.preventDefault();
const element = document.getElementById(`v${version}`);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
setActiveId(`v${version}`);
window.history.pushState(null, "", `#v${version}`);
}
}}
>
v{version}
<span className="text-xs text-muted-foreground ml-2">
{formatDate2(date)}
</span>
</a>
))}
</div>
</ScrollArea>
</div>
)}
</aside>
);
}

View File

@@ -17,7 +17,7 @@ export default function DocsBreadcrumb({ paths }: { paths: string[] }) {
<BreadcrumbLink>Docs</BreadcrumbLink>
</BreadcrumbItem>
{paths.map((path, index) => (
<Fragment key={path}>
<Fragment key={`${path}-${index}`}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{index < paths.length - 1 ? (

View File

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

View File

@@ -9,24 +9,22 @@ interface EditThisPageProps {
const EditThisPage: React.FC<EditThisPageProps> = ({ filePath }) => {
const { repository } = docuConfig;
if (!repository?.editLink || !repository.url || !repository.editPathTemplate) return null;
const editUrl = `${repository.url}${repository.editPathTemplate.replace("{filePath}", filePath)}`;
return (
<div style={{ textAlign: 'right' }}>
<div className="text-right">
<Link
href={editUrl}
target='_blank'
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
textDecoration: 'none',
fontWeight: 'bold',
}}
aria-label="Edit this page on Git"
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground no-underline"
>
<span className='text-primary text-sm max-[480px]:hidden'>Edit this page</span>
<SquarePenIcon className="w-4 h-4 text-primary" />
<span className="flex justify-start items-center gap-1">Edit this page
<SquarePenIcon className="w-4 h-4" /></span>
</Link>
</div>
);

View File

@@ -7,7 +7,7 @@ export function Footer() {
const { footer } = docuConfig;
const { meta } = docuConfig;
return (
<footer className="w-full py-4 border-t lg:py-8 bg-background">
<footer className="w-full py-4 px-2 border-t lg:py-8 bg-background">
<div className="container flex flex-wrap items-center justify-between text-sm">
<div className="items-start justify-center hidden gap-4 lg:flex-col lg:flex lg:w-3/5">
<h3 className="text-lg font-bold font-code">{meta.title}</h3>
@@ -16,12 +16,9 @@ export function Footer() {
<FooterButtons />
</div>
</div>
<div className="flex flex-col items-start justify-center w-full gap-4 mt-4 xl:items-end lg:w-2/5">
<div className="flex flex-col items-center justify-center w-full gap-4 mt-4 lg:items-end lg:w-2/5">
<p className="text-center text-muted-foreground">
Copyright © {new Date().getFullYear()} {footer.copyright} - Made with{" "}
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">
DocuBook
</Link>
Copyright © {new Date().getFullYear()} {footer.copyright} - <MadeWith />
</p>
<div className="hidden lg:flex">
<ModeToggle />
@@ -33,12 +30,20 @@ export function Footer() {
}
export function FooterButtons() {
const { footer } = docuConfig;
const footer = docuConfig?.footer;
// Jangan render apapun jika tidak ada data sosial
if (!footer?.social || !Array.isArray(footer.social) || footer.social.length === 0) {
return null;
}
return (
<>
{footer.social?.map((item) => {
const IconComponent = (LucideIcons[item.iconName as keyof typeof LucideIcons] ?? LucideIcons["Globe"]) as unknown as React.FC<{ className?: string }>;
{footer.social.map((item) => {
const IconComponent =
(LucideIcons[item.iconName as keyof typeof LucideIcons] ??
LucideIcons["Globe"]) as React.FC<{ className?: string }>;
return (
<Link
key={item.name}
@@ -54,3 +59,15 @@ export function FooterButtons() {
</>
);
}
export function MadeWith() {
return (
<>
<span className="text-muted-foreground">Made with </span>
<span className="text-primary">
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2 text-muted-foreground">
DocuBook
</Link></span>
</>
);
}

View File

@@ -1,3 +1,5 @@
"use client"
import { useState } from "react";
import {
Sheet,
SheetClose,
@@ -5,20 +7,42 @@ import {
SheetHeader,
SheetTrigger,
} from "@/components/ui/sheet";
import { Logo, NavMenu } from "./navbar";
import { Button } from "./ui/button";
import { AlignLeftIcon } from "lucide-react";
import { FooterButtons } from "./footer";
import { DialogTitle } from "./ui/dialog";
import { Logo, NavMenu } from "@/components/navbar";
import { Button } from "@/components/ui/button";
import { AlignLeftIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { FooterButtons } from "@/components/footer";
import { DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import DocsMenu from "./docs-menu";
import { ModeToggle } from "./theme-toggle";
import DocsMenu from "@/components/docs-menu";
import { ModeToggle } from "@/components/theme-toggle";
export function Leftbar() {
const [collapsed, setCollapsed] = useState(false);
return (
<aside className="lg:flex hidden flex-[1.5] min-w-[238px] sticky top-16 flex-col h-[93.75vh] overflow-y-auto">
<ScrollArea className="py-4">
<DocsMenu />
<aside
className={`sticky lg:flex hidden top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300
${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`}
>
{/* Toggle Button */}
<div className="absolute top-0 right-0 py-6 px-0 ml-6 z-10 -mt-4">
<Button
size="icon"
variant="outline"
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? (
<PanelLeftOpen size={18} />
) : (
<PanelLeftClose size={18} />
)}
</Button>
</div>
{/* Scrollable DocsMenu */}
<ScrollArea className="flex-1 px-2 pb-4">
{!collapsed && <DocsMenu />}
</ScrollArea>
</aside>
);
@@ -36,7 +60,7 @@ export function SheetLeftbar() {
<DialogTitle className="sr-only">Menu</DialogTitle>
<SheetHeader>
<SheetClose className="px-5" asChild>
<Logo />
<span className="px-2"><Logo /></span>
</SheetClose>
</SheetHeader>
<div className="flex flex-col gap-4 overflow-y-auto">

View File

@@ -16,7 +16,7 @@ export function Navbar() {
<div className="flex items-center gap-5">
<SheetLeftbar />
<div className="flex items-center gap-6">
<div className="hidden sm:flex">
<div className="hidden lg:flex">
<Logo />
</div>
</div>
@@ -25,7 +25,7 @@ export function Navbar() {
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
<NavMenu />
</div>
<Separator className="hidden sm:flex my-4 h-9" orientation="vertical" />
<Separator className="hidden lg:flex my-4 h-9" orientation="vertical" />
<Search />
</div>
</div>

View File

@@ -8,9 +8,15 @@ import {
import { cn } from "@/lib/utils";
import { SheetClose } from "@/components/ui/sheet";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { usePathname } from "next/navigation";
interface SubLinkProps extends EachRoute {
level: number;
isSheet: boolean;
parentHref?: string;
}
export default function SubLink({
title,
href,
@@ -18,19 +24,45 @@ export default function SubLink({
noLink,
level,
isSheet,
}: EachRoute & { level: number; isSheet: boolean }) {
parentHref = "",
}: SubLinkProps) {
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(() => {
if (path == href || path.includes(href)) setIsOpen(true);
}, [href, path]);
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
setIsOpen(true);
}
}, [path, fullHref, items]);
const Comp = (
<Anchor activeClassName="text-primary font-medium" href={href}>
// Only apply active styles if it's an exact match and not a parent with active children
const Comp = useMemo(() => (
<Anchor
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
href={fullHref}
className={cn(
hasActiveChild && "font-medium text-foreground"
)}
>
{title}
</Anchor>
);
), [title, fullHref, hasActiveChild]);
const titleOrLink = !noLink ? (
isSheet ? (
@@ -39,7 +71,12 @@ export default function SubLink({
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) {
@@ -47,36 +84,47 @@ export default function SubLink({
}
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}>
<CollapsibleTrigger className="w-full pr-5">
<div className="flex items-center justify-between cursor-pointer w-full">
<CollapsibleTrigger
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}
<span>
<span className="ml-2">
{!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>
</div>
</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
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",
level > 0 && "pl-4 border-l ml-1.5"
)}
>
{items?.map((innerLink) => {
const modifiedItems = {
...innerLink,
href: `${href + innerLink.href}`,
level: level + 1,
isSheet,
};
return <SubLink key={modifiedItems.href} {...modifiedItems} />;
})}
{items?.map((innerLink) => (
<SubLink
key={`${fullHref}${innerLink.href}`}
{...innerLink}
href={innerLink.href}
level={level + 1}
isSheet={isSheet}
parentHref={fullHref}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>

View File

@@ -2,19 +2,25 @@ import { getDocsTocs } from "@/lib/markdown";
import TocObserver from "./toc-observer";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ListIcon } from "lucide-react";
import Sponsor from "./Sponsor";
export default async function Toc({ path }: { path: string }) {
const tocs = await getDocsTocs(path);
return (
<div className="lg:flex hidden toc flex-[1.5] min-w-[238px] py-9 sticky top-16 h-[96.95vh]">
<div className="flex flex-col gap-3 w-full pl-2">
<div className="flex items-center gap-2">
<ListIcon className="w-5 h-5" /><h3 className="font-medium text-sm">On this page</h3>
<div className="flex flex-col gap-6 w-full pl-2 h-full">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<ListIcon className="w-5 h-5" />
<h3 className="font-medium text-sm">On this page</h3>
</div>
<ScrollArea className="pb-2 pt-0.5 overflow-y-auto">
<TocObserver data={tocs} />
</ScrollArea>
</div>
<ScrollArea className="pb-2 pt-0.5 overflow-y-auto">
<TocObserver data={tocs} />
</ScrollArea>
<Sponsor />
</div>
</div>
);