diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index 663ea9618..328972c79 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, useReducer, useCallback } from 'react'; +import { useNavigationUrlSync } from '../hooks/useUrlSync'; /** * NavigationContext - Complete navigation management system @@ -92,7 +93,10 @@ const NavigationStateContext = createContext(undefined); // Provider component -export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const NavigationProvider: React.FC<{ + children: React.ReactNode; + enableUrlSync?: boolean; +}> = ({ children, enableUrlSync = true }) => { const [state, dispatch] = useReducer(navigationReducer, initialState); const actions: NavigationContextActions = { @@ -149,6 +153,9 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch actions }; + // Enable URL synchronization + useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync); + return ( diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 637906a22..4ca66e61c 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -7,6 +7,7 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } fr import { useToolManagement } from '../hooks/useToolManagement'; import { PageEditorFunctions } from '../types/pageEditor'; import { ToolRegistryEntry } from '../data/toolsTaxonomy'; +import { useToolWorkflowUrlSync } from '../hooks/useUrlSync'; // State interface interface ToolWorkflowState { @@ -101,9 +102,11 @@ interface ToolWorkflowProviderProps { children: React.ReactNode; /** Handler for view changes (passed from parent) */ onViewChange?: (view: string) => void; + /** Enable URL synchronization for tool selection */ + enableUrlSync?: boolean; } -export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) { +export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = true }: ToolWorkflowProviderProps) { const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); // Tool management hook @@ -182,6 +185,9 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro [state.sidebarsVisible, state.readerMode] ); + // Enable URL synchronization for tool selection + useToolWorkflowUrlSync(selectedToolKey, selectTool, clearToolSelection, enableUrlSync); + // Simple context value with basic memoization const contextValue = useMemo((): ToolWorkflowContextValue => ({ // State diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts new file mode 100644 index 000000000..ef181c04b --- /dev/null +++ b/frontend/src/hooks/useUrlSync.ts @@ -0,0 +1,125 @@ +/** + * URL synchronization hooks for tool routing + */ + +import { useEffect, useCallback } from 'react'; +import { ModeType } from '../contexts/NavigationContext'; +import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; + +/** + * Hook to sync navigation mode with URL + */ +export function useNavigationUrlSync( + currentMode: ModeType, + setMode: (mode: ModeType) => void, + enableSync: boolean = true +) { + // Initialize mode from URL on mount + useEffect(() => { + if (!enableSync) return; + + const route = parseToolRoute(); + if (route.mode !== currentMode) { + setMode(route.mode); + } + }, []); // Only run on mount + + // Update URL when mode changes + useEffect(() => { + if (!enableSync) return; + + if (currentMode === 'pageEditor') { + clearToolRoute(); + } else { + updateToolRoute(currentMode, currentMode); + } + }, [currentMode, enableSync]); + + // Handle browser back/forward navigation + useEffect(() => { + if (!enableSync) return; + + const handlePopState = () => { + const route = parseToolRoute(); + if (route.mode !== currentMode) { + setMode(route.mode); + } + }; + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, [currentMode, setMode, enableSync]); +} + +/** + * Hook to sync tool workflow with URL + */ +export function useToolWorkflowUrlSync( + selectedToolKey: string | null, + selectTool: (toolKey: string) => void, + clearTool: () => void, + enableSync: boolean = true +) { + // Initialize tool from URL on mount + useEffect(() => { + if (!enableSync) return; + + const route = parseToolRoute(); + if (route.toolKey && route.toolKey !== selectedToolKey) { + selectTool(route.toolKey); + } else if (!route.toolKey && selectedToolKey) { + clearTool(); + } + }, []); // Only run on mount + + // Update URL when tool changes + useEffect(() => { + if (!enableSync) return; + + if (selectedToolKey) { + const route = parseToolRoute(); + if (route.toolKey !== selectedToolKey) { + updateToolRoute(selectedToolKey as ModeType, selectedToolKey); + } + } + }, [selectedToolKey, enableSync]); +} + +/** + * Hook to get current URL route information + */ +export function useCurrentRoute() { + const getCurrentRoute = useCallback(() => { + return parseToolRoute(); + }, []); + + return getCurrentRoute; +} + +/** + * Hook to programmatically navigate to tools + */ +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 } + })); + }, []); + + const navigateToHome = useCallback(() => { + clearToolRoute(); + + // Dispatch a custom event to notify other components + window.dispatchEvent(new CustomEvent('toolNavigation', { + detail: { toolKey: null } + })); + }, []); + + return { + navigateToTool, + navigateToHome + }; +} \ No newline at end of file diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts new file mode 100644 index 000000000..fbf6c1c3c --- /dev/null +++ b/frontend/src/utils/urlRouting.ts @@ -0,0 +1,180 @@ +/** + * URL routing utilities for tool navigation + * Provides clean URL routing for the V2 tool system + */ + +import { ModeType } from '../contexts/NavigationContext'; + +export interface ToolRoute { + mode: ModeType; + toolKey?: string; +} + +/** + * Parse the current URL to extract tool routing information + */ +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 = { + 'split': { mode: 'split', toolKey: 'split' }, + 'merge': { mode: 'merge', toolKey: 'merge' }, + 'compress': { mode: 'compress', toolKey: 'compress' }, + 'convert': { mode: 'convert', toolKey: 'convert' }, + 'add-password': { mode: 'addPassword', toolKey: 'addPassword' }, + 'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' }, + 'sanitize': { mode: 'sanitize', toolKey: 'sanitize' }, + 'ocr': { mode: 'ocr', toolKey: 'ocr' } + }; + + const mapping = toolMappings[toolKey]; + if (mapping) { + return { + mode: mapping.mode, + toolKey: mapping.toolKey + }; + } + } + + // Check for query parameter fallback (e.g., ?tool=split) + const toolParam = searchParams.get('tool'); + if (toolParam && isValidMode(toolParam)) { + return { + mode: toolParam as ModeType, + toolKey: toolParam + }; + } + + // Default to page editor for home page + return { + mode: 'pageEditor' + }; +} + +/** + * Update the URL to reflect the current tool selection + * Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs + */ +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 + if (currentPath !== '/') { + clearToolRoute(); + } + return; + } + + let newPath = '/'; + + // Map modes to URL paths (only for actual tools) + if (toolKey) { + const pathMappings: Record = { + 'split': '/split-pdf', + 'merge': '/merge-pdf', + 'compress': '/compress-pdf', + 'convert': '/convert-pdf', + 'addPassword': '/add-password-pdf', + 'changePermissions': '/change-permissions-pdf', + 'sanitize': '/sanitize-pdf', + 'ocr': '/ocr-pdf' + }; + + 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); + } +} + +/** + * Clear tool routing and return to home page + */ +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); +} + +/** + * Get clean tool name for display purposes + */ +export function getToolDisplayName(toolKey: string): string { + const displayNames: Record = { + 'split': 'Split PDF', + 'merge': 'Merge PDF', + 'compress': 'Compress PDF', + 'convert': 'Convert PDF', + 'addPassword': 'Add Password', + 'changePermissions': 'Change Permissions', + 'sanitize': 'Sanitize PDF', + 'ocr': 'OCR PDF' + }; + + return displayNames[toolKey] || toolKey; +} + +/** + * Check if a mode is valid + */ +function isValidMode(mode: string): mode is ModeType { + const validModes: ModeType[] = [ + 'viewer', 'pageEditor', 'fileEditor', 'merge', 'split', + 'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize' + ]; + return validModes.includes(mode as ModeType); +} + +/** + * Generate shareable URL for current tool state + * Only generates URLs for actual tools, not internal UI modes + */ +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 = { + 'split': '/split-pdf', + 'merge': '/merge-pdf', + 'compress': '/compress-pdf', + 'convert': '/convert-pdf', + 'addPassword': '/add-password-pdf', + 'changePermissions': '/change-permissions-pdf', + 'sanitize': '/sanitize-pdf', + 'ocr': '/ocr-pdf' + }; + + const path = pathMappings[toolKey] || `/${toolKey}`; + return `${baseUrl}${path}`; + } + + return baseUrl; +} \ No newline at end of file