Reset route on all tools

This commit is contained in:
Connor Yoh 2025-08-27 14:15:09 +01:00
parent 79c50f4bc5
commit 83400dc6a7
4 changed files with 70 additions and 182 deletions

View File

@ -144,12 +144,14 @@ export const NavigationProvider: React.FC<{
clearToolSelection: useCallback(() => { clearToolSelection: useCallback(() => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
}, []), }, []),
handleToolSelect: useCallback((toolId: string) => { handleToolSelect: useCallback((toolId: string) => {
// Handle special cases // Handle special cases
if (toolId === 'allTools') { if (toolId === 'allTools') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
return; return;
} }

View File

@ -1,129 +0,0 @@
// src/hooks/useToolUrlRouting.ts
// Focused hook for URL <-> tool-key mapping and browser history sync.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
export interface UseToolUrlRoutingOpts {
/** Currently selected tool key (from context). */
selectedToolKey: string | null;
/** Registry of available tools (key -> tool metadata). */
toolRegistry: Record<string, any> | null | undefined;
/** Select a tool (no extra side-effects). */
selectTool: (toolKey: string) => void;
/** Clear selection. */
clearToolSelection: () => void;
/** Called once during initialization if URL contains a tool; may trigger UI changes. */
onInitSelect?: (toolKey: string) => void;
/** Called when navigating via back/forward (popstate). Defaults to selectTool. */
onPopStateSelect?: (toolKey: string) => void;
/** Optional base path if the app isn't served at "/" (no trailing slash). Default: "" (root). */
basePath?: string;
}
export function useToolUrlRouting(opts: UseToolUrlRoutingOpts) {
const {
selectedToolKey,
toolRegistry,
selectTool,
clearToolSelection,
onInitSelect,
onPopStateSelect,
basePath = '',
} = opts;
// Central slug map; keep here to co-locate routing policy.
const urlMap = useMemo(
() =>
new Map<string, string>([
['compress', 'compress-pdf'],
['split', 'split-pdf'],
['convert', 'convert-pdf'],
['ocr', 'ocr-pdf'],
['merge', 'merge-pdf'],
['rotate', 'rotate-pdf'],
]),
[]
);
const getToolUrlSlug = useCallback(
(toolKey: string) => urlMap.get(toolKey) ?? toolKey,
[urlMap]
);
const getToolKeyFromSlug = useCallback(
(slug: string) => {
for (const [key, value] of urlMap) {
if (value === slug) return key;
}
return slug; // fall back to raw key
},
[urlMap]
);
// Internal flag to avoid clearing URL on initial mount.
const [hasInitialized, setHasInitialized] = useState(false);
// Normalize a pathname by stripping basePath and leading slash.
const normalizePath = useCallback(
(fullPath: string) => {
let p = fullPath;
if (basePath && p.startsWith(basePath)) {
p = p.slice(basePath.length);
}
if (p.startsWith('/')) p = p.slice(1);
return p;
},
[basePath]
);
// Update URL when tool changes (but not on first paint before any selection happens).
useEffect(() => {
if (selectedToolKey) {
const slug = getToolUrlSlug(selectedToolKey);
const newUrl = `${basePath}/${slug}`.replace(/\/+/, '/');
window.history.replaceState({}, '', newUrl);
setHasInitialized(true);
} else if (hasInitialized) {
const rootUrl = basePath || '/';
window.history.replaceState({}, '', rootUrl);
}
}, [selectedToolKey, getToolUrlSlug, hasInitialized, basePath]);
// Initialize from URL when the registry is ready and nothing is selected yet.
useEffect(() => {
if (!toolRegistry || Object.keys(toolRegistry).length === 0) return;
if (selectedToolKey) return; // don't override explicit selection
const currentPath = normalizePath(window.location.pathname);
if (currentPath) {
const toolKey = getToolKeyFromSlug(currentPath);
if (toolRegistry[toolKey]) {
(onInitSelect ?? selectTool)(toolKey);
}
}
}, [toolRegistry, selectedToolKey, getToolKeyFromSlug, selectTool, onInitSelect, normalizePath]);
// Handle browser back/forward. NOTE: useRef needs an initial value in TS.
const popHandlerRef = useRef<((this: Window, ev: PopStateEvent) => any) | null>(null);
useEffect(() => {
popHandlerRef.current = () => {
const path = normalizePath(window.location.pathname);
if (path) {
const toolKey = getToolKeyFromSlug(path);
if (toolRegistry && toolRegistry[toolKey]) {
(onPopStateSelect ?? selectTool)(toolKey);
return;
}
}
clearToolSelection();
};
const handler = (e: PopStateEvent) => popHandlerRef.current?.call(window, e);
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, [toolRegistry, selectTool, clearToolSelection, getToolKeyFromSlug, onPopStateSelect, normalizePath]);
// Expose pure helpers if you want them elsewhere (optional).
return { getToolUrlSlug, getToolKeyFromSlug };
}

View File

@ -28,9 +28,9 @@ export function useNavigationUrlSync(
useEffect(() => { useEffect(() => {
if (!enableSync) return; if (!enableSync) return;
if (currentMode === 'pageEditor') { // Only update URL for actual tool modes, not internal UI modes
clearToolRoute(); // URL clearing is handled by useToolWorkflowUrlSync when selectedToolKey becomes null
} else { if (currentMode !== 'fileEditor' && currentMode !== 'pageEditor' && currentMode !== 'viewer') {
updateToolRoute(currentMode, currentMode); updateToolRoute(currentMode, currentMode);
} }
}, [currentMode, enableSync]); }, [currentMode, enableSync]);
@ -81,6 +81,9 @@ export function useToolWorkflowUrlSync(
if (route.toolKey !== selectedToolKey) { if (route.toolKey !== selectedToolKey) {
updateToolRoute(selectedToolKey as ModeType, selectedToolKey); updateToolRoute(selectedToolKey as ModeType, selectedToolKey);
} }
} else {
// Clear URL when no tool is selected - always clear regardless of current URL
clearToolRoute();
} }
}, [selectedToolKey, enableSync]); }, [selectedToolKey, enableSync]);
} }

View File

@ -96,14 +96,20 @@ export function updateToolRoute(mode: ModeType, toolKey?: string): void {
// Map modes to URL paths (only for actual tools) // Map modes to URL paths (only for actual tools)
if (toolKey) { if (toolKey) {
const pathMappings: Record<string, string> = { const pathMappings: Record<string, string> = {
'split': '/split-pdf', 'split': '/split-pdfs',
'merge': '/merge-pdf', 'merge': '/merge-pdf',
'compress': '/compress-pdf', 'compress': '/compress-pdf',
'convert': '/convert-pdf', 'convert': '/convert-pdf',
'addPassword': '/add-password-pdf', 'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-pdf', 'changePermissions': '/change-permissions-pdf',
'sanitize': '/sanitize-pdf', 'sanitize': '/sanitize-pdf',
'ocr': '/ocr-pdf' 'ocr': '/ocr-pdf',
'addWatermark': '/watermark',
'removePassword': '/remove-password',
'single-large-page': '/single-large-page',
'repair': '/repair',
'unlockPdfForms': '/unlock-pdf-forms',
'removeCertificateSign': '/remove-certificate-sign'
}; };
newPath = pathMappings[toolKey] || `/${toolKey}`; newPath = pathMappings[toolKey] || `/${toolKey}`;
@ -176,7 +182,13 @@ export function generateShareableUrl(mode: ModeType, toolKey?: string): string {
'addPassword': '/add-password-pdf', 'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-pdf', 'changePermissions': '/change-permissions-pdf',
'sanitize': '/sanitize-pdf', 'sanitize': '/sanitize-pdf',
'ocr': '/ocr-pdf' 'ocr': '/ocr-pdf',
'addWatermark': '/watermark',
'removePassword': '/remove-password',
'single-large-page': '/single-large-page',
'repair': '/repair',
'unlockPdfForms': '/unlock-pdf-forms',
'removeCertificateSign': '/remove-certificate-sign'
}; };
const path = pathMappings[toolKey] || `/${toolKey}`; const path = pathMappings[toolKey] || `/${toolKey}`;