docu version 1.8.5

This commit is contained in:
Wildan Nursahidan
2025-05-17 21:07:08 +07:00
parent 11ff2a86ed
commit e25ee4cb93
24 changed files with 1900 additions and 7006 deletions

View File

@@ -1,3 +1,31 @@
## [1.8.5] - 2025-05-10
> Add sponsor card on single docs page
### Added
- Expandables Leftbar
- sponsor badges or ads
- boolean show/hide `edit on github`
- with the same code run anywhere (bun or nodejs)
- add fronmatter (metadata) to playground editor
### Improved
- adjusment docu.json
- adjustment navbar, footer and components
### Fixed
- bun compability rename .js to common js
- cli manage packageManager on package.json
- inconsistent design moved to better UI/UX
- error handle render footer.social
### Removed
- remove confused and verbose cli on installer
## [1.8.0] - 2025-03-01 ## [1.8.0] - 2025-03-01
> Now looks more modern and clean which is a big change in layout and design > Now looks more modern and clean which is a big change in layout and design

View File

@@ -4,6 +4,8 @@ import { formatDate2, stringToDate } from "@/lib/utils";
import { getMetadata } from "@/app/layout"; import { getMetadata } from "@/app/layout";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { AuroraText } from "@/components/ui/aurora";
import { ShineBorder } from "@/components/ui/shine-border";
import docuConfig from "@/docu.json"; import docuConfig from "@/docu.json";
export const metadata = getMetadata({ export const metadata = getMetadata({
@@ -16,16 +18,17 @@ export default async function BlogIndexPage() {
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime() (a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
); );
return ( return (
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] py-2"> <div className="flex flex-col items-center justify-center px-2 py-8 text-center sm:py-36">
<div className="mb-7 flex flex-col gap-2"> <div className="w-full max-w-[800px] pb-8">
<h1 className="text-2xl font-extrabold"> <AuroraText className="text-lg"># Stay Informed, Stay Ahead</AuroraText>
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">
Blog Posts Blog Posts
</h1> </h1>
<p className="text-lg text-muted-foreground mt-2"> <p className="mb-8 sm:text-xl text-muted-foreground">
Discover the latest updates, tutorials, and insights on {meta.title}. Explore updates, tips, and deep dives from the {meta.title}.
</p> </p>
</div> </div>
<div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 sm:gap-8 gap-4 mb-5"> <div className="text-left grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 sm:gap-8 gap-4 my-6">
{blogs.map((blog) => ( {blogs.map((blog) => (
<BlogCard {...blog} slug={blog.slug} key={blog.slug} /> <BlogCard {...blog} slug={blog.slug} key={blog.slug} />
))} ))}
@@ -45,9 +48,8 @@ function BlogCard({
return ( return (
<Link <Link
href={`/blog/${slug}`} href={`/blog/${slug}`}
className="flex flex-col gap-2 items-start border rounded-md py-5 px-3 min-h-[400px]" className="flex flex-col gap-2 items-start border rounded-md max-h-[420px] min-h-[420px]"
> >
<h3 className="text-md font-semibold -mt-1 pr-7">{title}</h3>
<div className="w-full"> <div className="w-full">
<Image <Image
src={cover} src={cover}
@@ -55,11 +57,14 @@ function BlogCard({
width={400} width={400}
height={150} height={150}
quality={80} quality={80}
className="w-full rounded-md object-cover h-[180px] border" className="w-full rounded-md object-cover h-[200px]"
/> />
</div> </div>
<p className="text-sm text-muted-foreground">{description}</p> <div className="flex flex-col items-start px-3 py-3 gap-2 mb-auto">
<div className="flex items-center justify-between w-full mt-auto"> <h3 className="text-md font-semibold line-clamp-2">{title}</h3>
<p className="text-sm text-muted-foreground line-clamp-3">{description}</p>
</div>
<div className="flex items-center justify-between w-full px-3 mb-6">
<p className="text-[13px] text-muted-foreground"> <p className="text-[13px] text-muted-foreground">
Published on {formatDate2(date)} Published on {formatDate2(date)}
</p> </p>

View File

@@ -2,42 +2,23 @@ import { Suspense } from "react";
import { getChangelogEntries } from "@/lib/changelog"; import { getChangelogEntries } from "@/lib/changelog";
import { VersionEntry } from "@/components/changelog/version-entry"; import { VersionEntry } from "@/components/changelog/version-entry";
import { VersionToc } from "@/components/changelog/version-toc"; import { VersionToc } from "@/components/changelog/version-toc";
import { getMetadata } from "@/app/layout";
import docuConfig from "@/docu.json";
import { FloatingVersionToc } from "@/components/changelog/floating-version"; import { FloatingVersionToc } from "@/components/changelog/floating-version";
export const metadata = getMetadata({
title: "Changelog",
description: "Latest updates and improvements to DocuBook",
image: "release-note.png",
});
export default async function ChangelogPage() { export default async function ChangelogPage() {
const entries = await getChangelogEntries(); const entries = await getChangelogEntries();
const { meta } = docuConfig;
return (
<div className="flex flex-col w-full">
<div className="border-b">
<div className="py-8">
<h1 className="text-2xl font-extrabold">Changelog</h1>
<p className="text-lg text-muted-foreground mt-2">
Latest updates and improvements to {meta.title}
</p>
</div>
</div>
<div className="md:container py-8"> return (
<div className="flex items-start gap-8"> <div className="flex items-start">
<Suspense fallback={<div className="lg:flex hidden flex-[1.5] min-w-[238px]" />}> <Suspense fallback={<div className="lg:flex hidden flex-[1.5]" />}>
<VersionToc <VersionToc
versions={entries.map(({ version, date }) => ({ version, date }))} versions={entries.map(({ version, date }) => ({ version, date }))}
/> />
</Suspense> </Suspense>
<main className="flex-1 lg:flex-[5.25] min-w-0"> <main className="flex-1 md:flex-[5.25] min-w-0 max-w-[800px]">
<div className="relative"> <div className="relative">
<div className="absolute left-0 top-0 h-full w-px bg-border lg:block hidden" /> <div className="absolute left-0 top-0 h-full w-px bg-border md:block hidden" />
<div className="lg:pl-12 pl-0 lg:pt-8"> <div className="md:px-12 md:py-8 max-md:py-10">
{entries.map((entry, index) => ( {entries.map((entry, index) => (
<section <section
id={`version-${entry.version}`} id={`version-${entry.version}`}
@@ -50,8 +31,6 @@ export default async function ChangelogPage() {
</div> </div>
</div> </div>
</main> </main>
</div>
</div>
{/* Floating TOC for smaller screens */} {/* Floating TOC for smaller screens */}
{entries.length > 0 && ( {entries.length > 0 && (
<FloatingVersionToc <FloatingVersionToc

View File

@@ -87,13 +87,17 @@ export default async function DocsPage({ params: { slug = [] } }: PageProps) {
<h1 className="text-3xl !-mt-0.5">{title}</h1> <h1 className="text-3xl !-mt-0.5">{title}</h1>
<p className="-mt-4 text-muted-foreground text-[16.5px]">{description}</p> <p className="-mt-4 text-muted-foreground text-[16.5px]">{description}</p>
<div>{res.content}</div> <div>{res.content}</div>
<div className="my-8 flex justify-end items-center border-b-2 border-x-muted-foreground"> <div
className={`my-8 flex items-center border-b-2 border-dashed border-x-muted-foreground ${
docuConfig.repository?.editLink ? "justify-between" : "justify-end"
}`}
>
{docuConfig.repository?.editLink && <EditThisPage filePath={filePath} />}
{date && ( {date && (
<p className="text-[13px] text-muted-foreground"> <p className="text-[13px] text-muted-foreground">
Published on {formatDate2(date)} Published on {formatDate2(date)}
</p> </p>
)} )}
{/* <EditThisPage filePath={filePath} /> */}
</div> </div>
<Pagination pathname={pathName} /> <Pagination pathname={pathName} />
</Typography> </Typography>

View File

@@ -47,6 +47,7 @@ export default function Home() {
</Link> </Link>
<Link <Link
href="https://www.youtube.com/channel/UC5H-2U68EuVAH9Ehz5eYHNg?sub_confirmation=1" href="https://www.youtube.com/channel/UC5H-2U68EuVAH9Ehz5eYHNg?sub_confirmation=1"
target="_blank"
className={buttonVariants({ className={buttonVariants({
variant: "secondary", variant: "secondary",
className: "px-6 bg-gray-200 text-gray-900 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700", className: "px-6 bg-gray-200 text-gray-900 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700",

View File

@@ -193,18 +193,19 @@ export default function PlaygroundPage() {
const before = markdown.substring(0, start); const before = markdown.substring(0, start);
const after = markdown.substring(end); const after = markdown.substring(end);
// Menambahkan satu baris kosong sebelum dan sesudah komponen const needsLeadingNewline = before && !before.endsWith('\n\n') ? '\n\n' : '';
const newText = `${before}${text}\n${after}`; const needsTrailingNewline = after && !after.startsWith('\n\n') ? '\n\n' : '';
const newText = `${before}${needsLeadingNewline}${text}${needsTrailingNewline}${after}`;
setMarkdown(newText); setMarkdown(newText);
requestAnimationFrame(() => { requestAnimationFrame(() => {
textArea.focus(); textArea.focus();
const newPosition = start + text.length + 1; const newPosition = before.length + needsLeadingNewline.length + text.length + 1;
textArea.setSelectionRange(newPosition, newPosition); textArea.setSelectionRange(newPosition, newPosition);
}); });
}; };
if (isMobile) { if (isMobile) {
return <MobileMessage />; return <MobileMessage />;
} }

69
components/Sponsor.tsx Normal file
View File

@@ -0,0 +1,69 @@
import docuData from "@/docu.json";
import Image from "next/image";
import Link from "next/link";
// Define types for docu.json
interface SponsorItem {
url: string;
image: string;
title: string;
description?: string;
}
interface DocuConfig {
sponsor?: {
title?: string;
item?: SponsorItem;
};
navbar: any; // Anda bisa mendefinisikan tipe yang lebih spesifik jika diperlukan
footer: any;
meta: any;
repository: any;
routes: any[];
}
// Type assertion for docu.json
const docuConfig = docuData as DocuConfig;
export function Sponsor() {
// Safely get sponsor data with optional chaining and default values
const sponsor = docuConfig?.sponsor || {};
const item = sponsor?.item;
// Return null if required fields are missing
if (!item?.url || !item?.image || !item?.title) {
return null;
}
return (
<div className="mt-4">
{sponsor?.title && (
<h2 className="mb-4 text-sm font-medium">{sponsor.title}</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"
sizes="32px"
/>
</div>
<div className="text-center sm:text-left">
<h3 className="text-sm font-medium">{item.title}</h3>
{item.description && (
<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 ( 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}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="rounded-full shadow-lg px-4 py-2 flex items-center gap-2"> <Button variant="outline" className="rounded-full shadow-lg px-4 py-2 flex items-center gap-2">

View File

@@ -82,10 +82,10 @@ export function VersionEntry({
{/* Show more/less button */} {/* Show more/less button */}
{Object.values(changes).some(items => items && items.length > 5) && ( {Object.values(changes).some(items => items && items.length > 5) && (
<Button <Button
variant="ghost" variant="outline"
size="sm" size="sm"
onClick={() => setExpanded(!expanded)} 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 ? ( {expanded ? (
<> <>

View File

@@ -2,8 +2,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn, formatDate2 } from "@/lib/utils"; 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 { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
interface VersionTocProps { interface VersionTocProps {
versions: Array<{ versions: Array<{
@@ -14,33 +15,30 @@ interface VersionTocProps {
export function VersionToc({ versions }: VersionTocProps) { export function VersionToc({ versions }: VersionTocProps) {
const [activeId, setActiveId] = useState<string | null>(null); const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => { useEffect(() => {
// Handle initial hash
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
if (hash) { if (hash) {
setActiveId(hash); setActiveId(hash);
} }
// Set up intersection observer
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
const id = entry.target.id; const id = entry.target.id;
setActiveId(id); setActiveId(id);
// Use pushState instead of replaceState to maintain history window.history.pushState(null, "", `#${id}`);
window.history.pushState(null, '', `#${id}`);
} }
}); });
}, },
{ {
threshold: 0.2, threshold: 0.2,
rootMargin: '-20% 0px -60% 0px' rootMargin: "-20% 0px -60% 0px",
} }
); );
// Observe version elements
versions.forEach(({ version }) => { versions.forEach(({ version }) => {
const element = document.getElementById(`v${version}`); const element = document.getElementById(`v${version}`);
if (element) observer.observe(element); if (element) observer.observe(element);
@@ -50,13 +48,35 @@ export function VersionToc({ versions }: VersionTocProps) {
}, [versions]); }, [versions]);
return ( return (
<aside className="lg:flex hidden toc flex-[1.5] min-w-[238px] pt-8 sticky top-16 h-[calc(100vh-4rem)]"> <aside
<div className="flex flex-col gap-2 w-full"> 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"> <div className="flex items-center gap-2 mb-2">
<History className="w-4 h-4" /> <History className="w-4 h-4" />
<h3 className="font-medium text-sm">Version History</h3> <h3 className="font-medium text-sm">Version History</h3>
</div> </div>
<ScrollArea className="h-full"> <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"> <div className="flex flex-col gap-1.5 text-sm dark:text-stone-300/85 text-stone-800 pr-4">
{versions.map(({ version, date }) => ( {versions.map(({ version, date }) => (
<a <a
@@ -70,9 +90,9 @@ export function VersionToc({ versions }: VersionTocProps) {
e.preventDefault(); e.preventDefault();
const element = document.getElementById(`v${version}`); const element = document.getElementById(`v${version}`);
if (element) { if (element) {
element.scrollIntoView({ behavior: 'smooth' }); element.scrollIntoView({ behavior: "smooth" });
setActiveId(`v${version}`); setActiveId(`v${version}`);
window.history.pushState(null, '', `#v${version}`); window.history.pushState(null, "", `#v${version}`);
} }
}} }}
> >
@@ -85,6 +105,7 @@ export function VersionToc({ versions }: VersionTocProps) {
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>
)}
</aside> </aside>
); );
} }

View File

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

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
aria-label="Documentation navigation"
className={className}
>
<ul className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
{ROUTES.map((item, index) => { {ROUTES.map((item, index) => {
// Normalize href - hapus leading/trailing slashes
const normalizedHref = `/${item.href.replace(/^\/+|\/+$/g, '')}`;
const itemHref = `/docs${normalizedHref}`;
const modifiedItems = { const modifiedItems = {
...item, ...item,
href: `/docs${item.href}`, href: itemHref,
level: 0, level: 0,
isSheet, isSheet,
}; };
return <SubLink key={item.title + index} {...modifiedItems} />;
return (
<li key={`${item.title}-${index}`}>
<SubLink {...modifiedItems} />
</li>
);
})} })}
</div> </ul>
</nav>
); );
} }

View File

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

View File

@@ -1,7 +1,37 @@
import Link from "next/link"; import Link from "next/link";
import { ModeToggle } from "@/components/theme-toggle"; import { ModeToggle } from "@/components/theme-toggle";
import docuConfig from "@/docu.json"; import docuData from "@/docu.json";
import * as LucideIcons from "lucide-react"; // Import all icons import * as LucideIcons from "lucide-react";
// Define types for docu.json
interface SocialItem {
name: string;
url: string;
iconName: string;
}
interface FooterConfig {
copyright: string;
social?: SocialItem[];
}
interface MetaConfig {
title: string;
description: string;
baseURL: string;
favicon: string;
}
interface DocuConfig {
footer: FooterConfig;
meta: MetaConfig;
navbar: any;
repository: any;
routes: any[];
}
// Type assertion for docu.json
const docuConfig = docuData as DocuConfig;
export function Footer() { export function Footer() {
const { footer } = docuConfig; const { footer } = docuConfig;
@@ -13,10 +43,10 @@ export function Footer() {
<h3 className="text-lg font-bold font-code">{meta.title}</h3> <h3 className="text-lg font-bold font-code">{meta.title}</h3>
<span className="w-3/4 text-base text-wrap text-muted-foreground">{meta.description}</span> <span className="w-3/4 text-base text-wrap text-muted-foreground">{meta.description}</span>
<div className="flex items-center gap-6 mt-2"> <div className="flex items-center gap-6 mt-2">
{/* <FooterButtons /> */} <FooterButtons />
</div> </div>
</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"> <p className="text-center text-muted-foreground">
Copyright © {new Date().getFullYear()} {footer.copyright} - <MadeWith /> Copyright © {new Date().getFullYear()} {footer.copyright} - <MadeWith />
</p> </p>
@@ -30,12 +60,20 @@ export function Footer() {
} }
export function FooterButtons() { export function FooterButtons() {
const { footer } = docuConfig; const footer = docuConfig?.footer;
// Jangan render apapun jika tidak ada data sosial
if (!footer || !Array.isArray(footer.social) || footer.social.length === 0) {
return null;
}
return ( return (
<> <>
{footer.social?.map((item) => { {footer.social.map((item) => {
const IconComponent = (LucideIcons[item.iconName as keyof typeof LucideIcons] ?? LucideIcons["Globe"]) as unknown as React.FC<{ className?: string }>; const IconComponent =
(LucideIcons[item.iconName as keyof typeof LucideIcons] ??
LucideIcons["Globe"]) as React.FC<{ className?: string }>;
return ( return (
<Link <Link
key={item.name} key={item.name}

View File

@@ -1,3 +1,5 @@
"use client"
import { useState } from "react";
import { import {
Sheet, Sheet,
SheetClose, SheetClose,
@@ -5,20 +7,42 @@ import {
SheetHeader, SheetHeader,
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Logo, NavMenu } from "./navbar"; import { Logo, NavMenu } from "@/components/navbar";
import { Button } from "./ui/button"; import { Button } from "@/components/ui/button";
import { AlignLeftIcon } from "lucide-react"; import { AlignLeftIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { FooterButtons } from "./footer"; import { FooterButtons } from "@/components/footer";
import { DialogTitle } from "./ui/dialog"; import { DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import DocsMenu from "./docs-menu"; import DocsMenu from "@/components/docs-menu";
import { ModeToggle } from "./theme-toggle"; import { ModeToggle } from "@/components/theme-toggle";
export function Leftbar() { export function Leftbar() {
const [collapsed, setCollapsed] = useState(false);
return ( return (
<aside className="lg:flex hidden flex-[1.5] min-w-[238px] sticky top-16 flex-col h-[93.75vh] overflow-y-auto"> <aside
<ScrollArea className="py-4"> className={`sticky lg:flex hidden top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300
<DocsMenu /> ${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> </ScrollArea>
</aside> </aside>
); );
@@ -36,7 +60,7 @@ export function SheetLeftbar() {
<DialogTitle className="sr-only">Menu</DialogTitle> <DialogTitle className="sr-only">Menu</DialogTitle>
<SheetHeader> <SheetHeader>
<SheetClose className="px-5" asChild> <SheetClose className="px-5" asChild>
<Logo /> <span className="px-2"><Logo /></span>
</SheetClose> </SheetClose>
</SheetHeader> </SheetHeader>
<div className="flex flex-col gap-4 overflow-y-auto"> <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"> <div className="flex items-center gap-5">
<SheetLeftbar /> <SheetLeftbar />
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="hidden sm:flex"> <div className="hidden lg:flex">
<Logo /> <Logo />
</div> </div>
</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"> <div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
<NavMenu /> <NavMenu />
</div> </div>
<Separator className="hidden sm:flex my-4 h-9" orientation="vertical" /> <Separator className="hidden lg:flex my-4 h-9" orientation="vertical" />
<Search /> <Search />
</div> </div>
</div> </div>

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>

View File

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

View File

@@ -12,19 +12,7 @@
] ]
}, },
"footer": { "footer": {
"copyright": "TutorAddons", "copyright": "TutorAddons"
"social": [
{
"name": "Gitlab",
"url": "https://gitlab.com/mywildancloud/docubook",
"iconName": "GitlabIcon"
},
{
"name": "Instagram",
"url": "https://www.instagram.com/wildan.nrs/",
"iconName": "InstagramIcon"
}
]
}, },
"meta": { "meta": {
"baseURL": "https://docs.tutoraddons.com", "baseURL": "https://docs.tutoraddons.com",
@@ -34,7 +22,8 @@
}, },
"repository": { "repository": {
"url": "https://gitlab.com/mywildancloud/docubook", "url": "https://gitlab.com/mywildancloud/docubook",
"editPathTemplate": "/blob/main/{filePath}" "editPathTemplate": "/blob/main/{filePath}",
"editLink": false
}, },
"routes": [ "routes": [
{ {

2036
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "docubook", "name": "docubook",
"version": "1.8.0", "version": "1.8.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@@ -12,7 +12,7 @@
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-scroll-area": "^1.2.0",
@@ -23,7 +23,7 @@
"@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "1.0.0",
"framer-motion": "^12.4.1", "framer-motion": "^12.4.1",
"geist": "^1.3.1", "geist": "^1.3.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@@ -55,5 +55,5 @@
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"typescript": "^5" "typescript": "^5"
}, },
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af" "packageManager": "npm@11.3.0"
} }

6199
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -118,3 +118,39 @@ pre>code {
.highlight-comp>code { .highlight-comp>code {
background-color: transparent !important; background-color: transparent !important;
} }
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
@layer utilities {
.animate-shine {
--animate-shine: shine var(--duration) infinite linear;
animation: var(--animate-shine);
background-size: 200% 200%;
}
@keyframes shine {
0% {
background-position: 0% 0%;
}
50% {
background-position: 100% 100%;
}
100% {
background-position: 0% 0%;
}
}
}

View File

@@ -9,12 +9,13 @@ const config = {
"./src/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}",
], ],
prefix: "", prefix: "",
safelist: ["line-clamp-3","line-clam-2"],
theme: { theme: {
container: { container: {
center: true, center: true,
padding: '2rem', padding: '2rem',
screens: { screens: {
'2xl': '1300px' '2xl': '1440px'
} }
}, },
extend: { extend: {