refactor: docubook@latest template nextjs-docker
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user