import { useCallback, useEffect, useMemo, useState } from 'react'; import type { NavigationContext } from '../lib/navigationHelpers'; /** * Maximum history entries to prevent unbounded growth in long sessions. * Matches typical browser behavior (50 entries). */ const MAX_HISTORY_LENGTH = 50; /** * Minimal page interface for navigation */ export interface NavigablePage { id: string; sort_order?: number; slug?: string; } export interface UsePageNavigationOptions { pages: TPage[]; defaultPageId?: string; trackHistory?: boolean; onPageChange?: (pageId: string, isBack: boolean) => void; } export interface UsePageNavigationResult { currentPageId: string | null; currentPage: TPage | null; pageHistory: string[]; previousPageId: string | null; defaultPage: TPage | null; setCurrentPageId: (pageId: string) => void; applyPageSelection: (targetPageId: string, isBack?: boolean) => void; isBackNavigation: (targetPageId: string) => boolean; goBack: () => boolean; resetHistory: () => void; /** * Get navigation context for history-based back navigation. * Provides currentPageSlug and previousPageId for resolveNavigationTarget(). */ getNavigationContext: () => NavigationContext; } /** * Hook for managing page navigation state with optional history tracking. * Default page is the first page by sort_order. * * @example * // Basic usage (runtime) * const nav = usePageNavigation({ * pages, * trackHistory: true, * }); * * // Editor usage (constructor) - no history * const nav = usePageNavigation({ * pages, * defaultPageId: initialPageId, * trackHistory: false, * }); */ export function usePageNavigation( options: UsePageNavigationOptions, ): UsePageNavigationResult { const { pages, defaultPageId, trackHistory = true, onPageChange } = options; const [currentPageId, setCurrentPageIdState] = useState(null); const [pageHistory, setPageHistory] = useState([]); // Compute default page (by ID or first by sort order) const defaultPage = useMemo(() => { if (!pages.length) return null; // Try explicit default page ID if (defaultPageId) { const byId = pages.find((p) => p.id === defaultPageId); if (byId) return byId; } // Fall back to first by sort order return [...pages].sort( (a, b) => (a.sort_order || 0) - (b.sort_order || 0), )[0]; }, [pages, defaultPageId]); // Get current page object const currentPage = useMemo(() => { if (!pages.length) return null; if (currentPageId) { return pages.find((p) => p.id === currentPageId) || null; } return defaultPage; }, [pages, currentPageId, defaultPage]); // Previous page ID from history const previousPageId = useMemo(() => { if (pageHistory.length < 2) return null; return pageHistory[pageHistory.length - 2]; }, [pageHistory]); // Check if navigating to a page would be a "back" navigation const isBackNavigation = useCallback( (targetPageId: string): boolean => { return previousPageId === targetPageId; }, [previousPageId], ); // Apply page selection with history tracking const applyPageSelection = useCallback( (targetPageId: string, isBack = false) => { setCurrentPageIdState(targetPageId); if (trackHistory) { setPageHistory((prev) => { if (!prev.length) return [targetPageId]; const currentId = prev[prev.length - 1]; if (currentId === targetPageId) return prev; // If going back and target matches previous, pop history (browser-like behavior) if ( isBack && prev.length > 1 && prev[prev.length - 2] === targetPageId ) { return prev.slice(0, -1); } // Add to history and trim to max length (keep most recent entries) const newHistory = [...prev, targetPageId]; return newHistory.slice(-MAX_HISTORY_LENGTH); }); } onPageChange?.(targetPageId, isBack); }, [trackHistory, onPageChange], ); // Simple setter (no history tracking) const setCurrentPageId = useCallback( (pageId: string) => { setCurrentPageIdState(pageId); if (trackHistory && pageHistory.length === 0) { setPageHistory([pageId]); } }, [trackHistory, pageHistory.length], ); // Go back in history const goBack = useCallback((): boolean => { if (!trackHistory || pageHistory.length < 2) return false; const targetPageId = pageHistory[pageHistory.length - 2]; applyPageSelection(targetPageId, true); return true; }, [trackHistory, pageHistory, applyPageSelection]); // Reset history const resetHistory = useCallback(() => { setPageHistory(currentPageId ? [currentPageId] : []); }, [currentPageId]); // Get navigation context for history-based back navigation const getNavigationContext = useCallback((): NavigationContext => { return { currentPageSlug: currentPage?.slug, previousPageId, }; }, [currentPage?.slug, previousPageId]); // Initialize to default page useEffect(() => { if (defaultPage?.id && !currentPageId) { setCurrentPageIdState(defaultPage.id); if (trackHistory) { setPageHistory((prev) => (prev.length ? prev : [defaultPage.id])); } } }, [defaultPage, currentPageId, trackHistory]); return { currentPageId, currentPage, pageHistory, previousPageId, defaultPage, setCurrentPageId, applyPageSelection, isBackNavigation, goBack, resetHistory, getNavigationContext, }; }