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

@ -4,9 +4,9 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
/**
* NavigationContext - Complete navigation management system
*
*
* Handles navigation modes, navigation guards for unsaved changes,
* and breadcrumb/history navigation. Separated from FileContext to
* and breadcrumb/history navigation. Separated from FileContext to
* maintain clear separation of concerns.
*/
@ -32,19 +32,19 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
switch (action.type) {
case 'SET_MODE':
return { ...state, currentMode: action.payload.mode };
case 'SET_UNSAVED_CHANGES':
return { ...state, hasUnsavedChanges: action.payload.hasChanges };
case 'SET_PENDING_NAVIGATION':
return { ...state, pendingNavigation: action.payload.navigationFn };
case 'SHOW_NAVIGATION_WARNING':
return { ...state, showNavigationWarning: action.payload.show };
case 'SET_SELECTED_TOOL':
return { ...state, selectedToolKey: action.payload.toolKey };
default:
return state;
}
@ -90,7 +90,7 @@ const NavigationStateContext = createContext<NavigationContextStateValue | undef
const NavigationActionsContext = createContext<NavigationContextActionsValue | undefined>(undefined);
// Provider component
export const NavigationProvider: React.FC<{
export const NavigationProvider: React.FC<{
children: React.ReactNode;
enableUrlSync?: boolean;
}> = ({ children, enableUrlSync = true }) => {
@ -126,7 +126,7 @@ export const NavigationProvider: React.FC<{
if (state.pendingNavigation) {
state.pendingNavigation();
}
// Clear navigation state
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
@ -144,12 +144,14 @@ export const NavigationProvider: React.FC<{
clearToolSelection: useCallback(() => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
}, []),
handleToolSelect: useCallback((toolId: string) => {
// Handle special cases
if (toolId === 'allTools') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
return;
}
@ -216,7 +218,7 @@ export const useNavigation = () => {
export const useNavigationGuard = () => {
const state = useNavigationState();
const { actions } = useNavigationActions();
return {
pendingNavigation: state.pendingNavigation,
showNavigationWarning: state.showNavigationWarning,
@ -234,7 +236,7 @@ export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation'
// TODO: This will be expanded for URL-based routing system
// - URL parsing utilities
// - Route definitions
// - Route definitions
// - Navigation hooks with URL sync
// - History management
// - Breadcrumb restoration from URL params
// - Breadcrumb restoration from URL params

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

@ -17,7 +17,7 @@ export function useNavigationUrlSync(
// Initialize mode from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
@ -27,10 +27,10 @@ export function useNavigationUrlSync(
// Update URL when mode changes
useEffect(() => {
if (!enableSync) return;
if (currentMode === 'pageEditor') {
clearToolRoute();
} else {
// Only update URL for actual tool modes, not internal UI modes
// URL clearing is handled by useToolWorkflowUrlSync when selectedToolKey becomes null
if (currentMode !== 'fileEditor' && currentMode !== 'pageEditor' && currentMode !== 'viewer') {
updateToolRoute(currentMode, currentMode);
}
}, [currentMode, enableSync]);
@ -38,7 +38,7 @@ export function useNavigationUrlSync(
// Handle browser back/forward navigation
useEffect(() => {
if (!enableSync) return;
const handlePopState = () => {
const route = parseToolRoute();
if (route.mode !== currentMode) {
@ -63,7 +63,7 @@ export function useToolWorkflowUrlSync(
// Initialize tool from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.toolKey && route.toolKey !== selectedToolKey) {
selectTool(route.toolKey);
@ -75,12 +75,15 @@ export function useToolWorkflowUrlSync(
// Update URL when tool changes
useEffect(() => {
if (!enableSync) return;
if (selectedToolKey) {
const route = parseToolRoute();
if (route.toolKey !== selectedToolKey) {
updateToolRoute(selectedToolKey as ModeType, selectedToolKey);
}
} else {
// Clear URL when no tool is selected - always clear regardless of current URL
clearToolRoute();
}
}, [selectedToolKey, enableSync]);
}
@ -102,19 +105,19 @@ export function useCurrentRoute() {
export function useToolNavigation() {
const navigateToTool = useCallback((toolKey: string) => {
updateToolRoute(toolKey as ModeType, toolKey);
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey }
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey }
}));
}, []);
const navigateToHome = useCallback(() => {
clearToolRoute();
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey: null }
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey: null }
}));
}, []);
@ -122,4 +125,4 @@ export function useToolNavigation() {
navigateToTool,
navigateToHome
};
}
}

View File

@ -11,12 +11,12 @@ import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } f
export function parseToolRoute(): ToolRoute {
const path = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search);
// Extract tool from URL path (e.g., /split-pdf -> split)
const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/);
if (toolMatch) {
const toolKey = toolMatch[1].toLowerCase();
// Map URL paths to tool keys and modes (excluding internal UI modes)
const toolMappings: Record<string, { mode: ModeType; toolKey: string }> = {
'split-pdfs': { mode: 'split', toolKey: 'split' },
@ -48,7 +48,7 @@ export function parseToolRoute(): ToolRoute {
'remove-certificate-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' },
'remove-cert-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' }
};
const mapping = toolMappings[toolKey];
if (mapping) {
return {
@ -57,7 +57,7 @@ export function parseToolRoute(): ToolRoute {
};
}
}
// Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool');
if (toolParam && isValidModeType(toolParam)) {
@ -66,7 +66,7 @@ export function parseToolRoute(): ToolRoute {
toolKey: toolParam
};
}
// Default to page editor for home page
return {
mode: getDefaultMode(),
@ -81,7 +81,7 @@ export function parseToolRoute(): ToolRoute {
export function updateToolRoute(mode: ModeType, toolKey?: string): void {
const currentPath = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search);
// Don't create URLs for internal UI modes
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
// If we're switching to an internal mode, clear any existing tool URL
@ -90,32 +90,38 @@ export function updateToolRoute(mode: ModeType, toolKey?: string): void {
}
return;
}
let newPath = '/';
// Map modes to URL paths (only for actual tools)
if (toolKey) {
const pathMappings: Record<string, string> = {
'split': '/split-pdf',
'merge': '/merge-pdf',
'split': '/split-pdfs',
'merge': '/merge-pdf',
'compress': '/compress-pdf',
'convert': '/convert-pdf',
'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-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}`;
}
// Remove tool query parameter since we're using path-based routing
searchParams.delete('tool');
// Construct final URL
const queryString = searchParams.toString();
const fullUrl = newPath + (queryString ? `?${queryString}` : '');
// Update URL without triggering page reload
if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) {
window.history.replaceState(null, '', fullUrl);
@ -128,10 +134,10 @@ export function updateToolRoute(mode: ModeType, toolKey?: string): void {
export function clearToolRoute(): void {
const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('tool');
const queryString = searchParams.toString();
const url = '/' + (queryString ? `?${queryString}` : '');
window.history.replaceState(null, '', url);
}
@ -142,14 +148,14 @@ export function getToolDisplayName(toolKey: string): string {
const displayNames: Record<string, string> = {
'split': 'Split PDF',
'merge': 'Merge PDF',
'compress': 'Compress PDF',
'compress': 'Compress PDF',
'convert': 'Convert PDF',
'addPassword': 'Add Password',
'changePermissions': 'Change Permissions',
'sanitize': 'Sanitize PDF',
'ocr': 'OCR PDF'
};
return displayNames[toolKey] || toolKey;
}
@ -161,27 +167,33 @@ export function getToolDisplayName(toolKey: string): string {
*/
export function generateShareableUrl(mode: ModeType, toolKey?: string): string {
const baseUrl = window.location.origin;
// Don't generate URLs for internal UI modes
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
return baseUrl;
}
if (toolKey) {
const pathMappings: Record<string, string> = {
'split': '/split-pdf',
'merge': '/merge-pdf',
'compress': '/compress-pdf',
'compress': '/compress-pdf',
'convert': '/convert-pdf',
'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-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}`;
return `${baseUrl}${path}`;
}
return baseUrl;
}
}