refactor: docubook@latest template nextjs-docker

This commit is contained in:
gitfromwildan
2026-05-30 18:52:21 +07:00
parent bf2ef37f49
commit 80eb49d968
101 changed files with 1759 additions and 4165 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useLayoutEffect, useRef } from "react";
import { ROUTES, EachRoute } from "@/lib/routes";
import { cn } from "@/lib/utils";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -41,8 +41,20 @@ function getIcon(name: string) {
export default function ContextPopover({ className }: ContextPopoverProps) {
const pathname = usePathname();
const router = useRouter();
const [mounted, setMounted] = useState(false);
const [activeRoute, setActiveRoute] = useState<EachRoute>();
const [useDefaultTitle, setUseDefaultTitle] = useState(false);
const [triggerWidth, setTriggerWidth] = useState<number | null>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const contextRoutes = getContextRoutes();
const fallbackRoute = ROUTES[0];
const displayRoute = useDefaultTitle ? fallbackRoute : activeRoute;
useEffect(() => {
// Mount-only state (used for client-only rendering) and intentionally set after first render.
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
useEffect(() => {
if (pathname.startsWith("/docs")) {
@@ -53,7 +65,66 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
}
}, [pathname]);
if (!pathname.startsWith("/docs") || contextRoutes.length === 0) {
useEffect(() => {
const hasTitle = Boolean(
activeRoute?.context?.title ||
activeRoute?.title
);
if (!hasTitle) {
const timer = window.setTimeout(() => {
setUseDefaultTitle(true);
}, 300);
return () => window.clearTimeout(timer);
}
// Avoid calling setState synchronously inside the effect body.
// Using a micro task to reset state avoids the react-hooks/set-state-in-effect lint error.
const resetTimer = window.setTimeout(() => {
setUseDefaultTitle(false);
}, 0);
return () => window.clearTimeout(resetTimer);
}, [activeRoute?.context?.title, activeRoute?.title]);
// Keep the popover width in sync with the trigger width when the trigger text changes
// (e.g. when navigating between docs contexts) and when the window/resizing changes.
useLayoutEffect(() => {
if (!triggerRef.current) return;
const updateWidth = () => {
if (triggerRef.current) {
setTriggerWidth(triggerRef.current.offsetWidth);
}
};
// Make sure the width is updated when the trigger text/route changes.
updateWidth();
if (typeof ResizeObserver !== "undefined") {
const observer = new ResizeObserver(() => {
updateWidth();
});
observer.observe(triggerRef.current);
return () => {
observer.disconnect();
};
} else {
const handleResize = () => {
updateWidth();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}
}, [displayRoute]);
if (!mounted || !pathname.startsWith("/docs") || contextRoutes.length === 0) {
return null;
}
@@ -61,6 +132,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
<Popover>
<PopoverTrigger asChild>
<Button
ref={triggerRef}
variant="ghost"
className={cn(
"w-full cursor-pointer flex items-center justify-between font-semibold text-foreground px-2 py-4 border border-muted",
@@ -69,22 +141,29 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
)}
>
<div className="flex items-center gap-2">
{activeRoute?.context?.icon && (
{displayRoute?.context?.icon && (
<span className="text-primary bg-primary/10 border border-primary dark:border dark:border-accent dark:bg-accent/10 dark:text-accent rounded p-0.5">
{getIcon(activeRoute.context.icon)}
{getIcon(displayRoute.context.icon)}
</span>
)}
<span className="truncate text-sm">
{activeRoute?.context?.title || activeRoute?.title || <Skeleton className="h-3.5 w-24" />}
{displayRoute?.context?.title ||
displayRoute?.title ||
(useDefaultTitle
? fallbackRoute?.context?.title || fallbackRoute?.title
: <Skeleton className="h-3.5 w-24" />)}
</span>
</div>
<ChevronsUpDown className="h-4 w-4 text-foreground/50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-64 p-2"
className="p-2"
align="start"
sideOffset={6}
style={{
width: triggerWidth !== null ? `${triggerWidth}px` : "auto"
}}
>
<div className="space-y-1">
{contextRoutes.map((route) => {