Posthog, scarf and url navigation overhaul (#4318)

Added post hog project - always enabled
Added scarf pixel - Always enabled 
Reworked Url navigation 
Forward and back now works without reloading page

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
ConnorYoh 2025-08-28 15:42:33 +01:00 committed by GitHub
parent 5b20f11e20
commit a7d5c80188
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 945 additions and 882 deletions

View File

@ -32,6 +32,7 @@
"jszip": "^3.10.1",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"posthog-js": "^1.261.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.2",
@ -1774,6 +1775,12 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@posthog/core": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz",
"integrity": "sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
@ -3813,6 +3820,17 @@
"node": ">=18"
}
},
"node_modules/core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -4614,6 +4632,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
@ -7388,6 +7412,47 @@
"postcss": "^8.2.9"
}
},
"node_modules/posthog-js": {
"version": "1.261.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.0.tgz",
"integrity": "sha512-jyiXqyrCU+VlpbNNVRA6OQYAVut0XZMYNELCZH+XvTd981VqbE4jXn4XCBreo7XCL2gdPgDVxUVOuzNvEuKcmw==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "1.0.2",
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.4"
},
"peerDependencies": {
"@rrweb/types": "2.0.0-alpha.17",
"rrweb-snapshot": "2.0.0-alpha.17"
},
"peerDependenciesMeta": {
"@rrweb/types": {
"optional": true
},
"rrweb-snapshot": {
"optional": true
}
}
},
"node_modules/posthog-js/node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/preact": {
"version": "10.27.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz",
"integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/precinct": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz",

View File

@ -28,6 +28,7 @@
"jszip": "^3.10.1",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"posthog-js": "^1.261.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.2",

View File

@ -182,6 +182,7 @@ function getLicenseUrl(licenseType) {
const licenseUrls = {
'MIT': 'https://opensource.org/licenses/MIT',
'MIT*': 'https://opensource.org/licenses/MIT',
'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause',
@ -270,7 +271,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
// Known good licenses (no warnings needed)
const goodLicenses = new Set([
'MIT', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0',
'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0',
'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE'

View File

@ -412,9 +412,9 @@ const FileEditor = ({
if (record) {
// Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]);
navActions.setMode('viewer');
navActions.setWorkbench('viewer');
}
}, [activeFileRecords, setSelectedFiles, navActions.setMode]);
}, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: FileId) => {
const startIndex = activeFileRecords.findIndex(r => r.id === fileId);

View File

@ -6,13 +6,13 @@ import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileState, useFileActions } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { useToolManagement } from '../../hooks/useToolManagement';
import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor';
import PageEditor from '../pageEditor/PageEditor';
import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer';
import ToolRenderer from '../tools/ToolRenderer';
import LandingPage from '../shared/LandingPage';
// No props needed - component uses contexts directly
@ -23,9 +23,9 @@ export default function Workbench() {
// Use context-based hooks to eliminate all prop drilling
const { state } = useFileState();
const { actions } = useFileActions();
const { currentMode: currentView } = useNavigationState();
const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setMode;
const setCurrentView = navActions.setWorkbench;
const activeFiles = state.files.ids;
const {
previewFile,
@ -36,7 +36,14 @@ export default function Workbench() {
setSidebarsVisible
} = useToolWorkflow();
const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow();
const { handleToolSelect } = useToolWorkflow();
// Get navigation state - this is the source of truth
const { selectedTool: selectedToolId } = useNavigationState();
// Get tool registry to look up selected tool
const { toolRegistry } = useToolManagement();
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
const { addToActiveFiles } = useFileHandler();
const handlePreviewClose = () => {
@ -69,11 +76,11 @@ export default function Workbench() {
case "fileEditor":
return (
<FileEditor
toolMode={!!selectedToolKey}
toolMode={!!selectedToolId}
showUpload={true}
showBulkActions={!selectedToolKey}
showBulkActions={!selectedToolId}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
{...(!selectedToolKey && {
{...(!selectedToolId && {
onOpenPageEditor: (file) => {
setCurrentView("pageEditor");
},
@ -127,14 +134,6 @@ export default function Workbench() {
);
default:
// Check if it's a tool view
if (selectedToolKey && selectedTool) {
return (
<ToolRenderer
selectedToolKey={selectedToolKey}
/>
);
}
return (
<LandingPage/>
);
@ -154,7 +153,7 @@ export default function Workbench() {
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
selectedToolKey={selectedToolKey}
selectedToolKey={selectedToolId}
/>
{/* Main content area */}

View File

@ -6,7 +6,6 @@ import {
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
import { ModeType } from "../../contexts/NavigationContext";
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
import { pdfExportService } from "../../services/pdfExportService";

View File

@ -25,7 +25,7 @@ export default function RightRail() {
const [csvInput, setCsvInput] = useState<string>("");
// Navigation view
const { currentMode: currentView } = useNavigationState();
const { workbench: currentView } = useNavigationState();
// File state and selection
const { state, selectors } = useFileState();

View File

@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css';
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote";
import FolderIcon from "@mui/icons-material/Folder";
import { ModeType, isValidMode } from '../../contexts/NavigationContext';
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
import { Tooltip } from "./Tooltip";
const viewOptionStyle = {
@ -19,7 +19,7 @@ const viewOptionStyle = {
// Build view options showing text only for current view; others icon-only with tooltip
const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) => [
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [
{
label: (
<div style={viewOptionStyle as React.CSSProperties}>
@ -70,8 +70,8 @@ const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null)
];
interface TopControlsProps {
currentView: ModeType;
setCurrentView: (view: ModeType) => void;
currentView: WorkbenchType;
setCurrentView: (view: WorkbenchType) => void;
selectedToolKey?: string | null;
}
@ -81,25 +81,25 @@ const TopControls = ({
selectedToolKey,
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<ModeType | null>(null);
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
const isToolSelected = selectedToolKey !== null;
const handleViewChange = useCallback((view: string) => {
if (!isValidMode(view)) {
// Ignore invalid values defensively
if (!isValidWorkbench(view)) {
return;
}
const mode = view as ModeType;
const workbench = view;
// Show immediate feedback
setSwitchingTo(mode as ModeType);
setSwitchingTo(workbench);
// Defer the heavy view change to next frame so spinner can render
requestAnimationFrame(() => {
// Give the spinner one more frame to show
requestAnimationFrame(() => {
setCurrentView(mode as ModeType);
setCurrentView(workbench);
// Clear the loading state after view change completes
setTimeout(() => setSwitchingTo(null), 300);

View File

@ -11,7 +11,7 @@ import {
Modal
} from '@mantine/core';
import CheckIcon from '@mui/icons-material/Check';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import ToolConfigurationModal from './ToolConfigurationModal';
import ToolList from './ToolList';
import IconSelector from './IconSelector';
@ -24,7 +24,7 @@ interface AutomationCreationProps {
existingAutomation?: AutomationConfig;
onBack: () => void;
onComplete: (automation: AutomationConfig) => void;
toolRegistry: Record<string, ToolRegistryEntry>;
toolRegistry: ToolRegistry;
}
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {

View File

@ -33,7 +33,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
React.useEffect(() => {
if (automation?.operations) {
const steps = automation.operations.map((op: any, index: number) => {
const tool = toolRegistry[op.operation];
const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
return {
id: `${op.operation}-${index}`,
operation: op.operation,

View File

@ -35,7 +35,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
const [isValid, setIsValid] = useState(true);
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation];
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const SettingsComponent = toolInfo?.settingsComponent;
// Initialize parameters from tool (which should contain defaults from registry)

View File

@ -1,6 +1,7 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
import { ToolId, isValidToolId } from '../types/toolId';
import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
/**
* NavigationContext - Complete navigation management system
@ -11,27 +12,38 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
*/
// Navigation state
interface NavigationState {
currentMode: ModeType;
interface NavigationContextState {
workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;
selectedToolKey: string | null; // Add tool selection to navigation state
}
// Navigation actions
type NavigationAction =
| { type: 'SET_MODE'; payload: { mode: ModeType } }
| { type: 'SET_WORKBENCH'; payload: { workbench: WorkbenchType } }
| { type: 'SET_SELECTED_TOOL'; payload: { toolId: ToolId | null } }
| { type: 'SET_TOOL_AND_WORKBENCH'; payload: { toolId: ToolId | null; workbench: WorkbenchType } }
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }
| { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } };
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } };
// Navigation reducer
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => {
switch (action.type) {
case 'SET_MODE':
return { ...state, currentMode: action.payload.mode };
case 'SET_WORKBENCH':
return { ...state, workbench: action.payload.workbench };
case 'SET_SELECTED_TOOL':
return { ...state, selectedTool: action.payload.toolId };
case 'SET_TOOL_AND_WORKBENCH':
return {
...state,
selectedTool: action.payload.toolId,
workbench: action.payload.workbench
};
case 'SET_UNSAVED_CHANGES':
return { ...state, hasUnsavedChanges: action.payload.hasChanges };
@ -42,43 +54,41 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
case 'SHOW_NAVIGATION_WARNING':
return { ...state, showNavigationWarning: action.payload.show };
case 'SET_SELECTED_TOOL':
return { ...state, selectedToolKey: action.payload.toolKey };
default:
return state;
}
};
// Initial state
const initialState: NavigationState = {
currentMode: getDefaultMode(),
const initialState: NavigationContextState = {
workbench: getDefaultWorkbench(),
selectedTool: null,
hasUnsavedChanges: false,
pendingNavigation: null,
showNavigationWarning: false,
selectedToolKey: null
showNavigationWarning: false
};
// Navigation context actions interface
export interface NavigationContextActions {
setMode: (mode: ModeType) => void;
setWorkbench: (workbench: WorkbenchType) => void;
setSelectedTool: (toolId: ToolId | null) => void;
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void;
cancelNavigation: () => void;
selectTool: (toolKey: string) => void;
clearToolSelection: () => void;
handleToolSelect: (toolId: string) => void;
}
// Split context values
// Context state values
export interface NavigationContextStateValue {
currentMode: ModeType;
workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;
selectedToolKey: string | null;
}
export interface NavigationContextActionsValue {
@ -95,10 +105,19 @@ export const NavigationProvider: React.FC<{
enableUrlSync?: boolean;
}> = ({ children, enableUrlSync = true }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState);
const toolRegistry = useFlatToolRegistry();
const actions: NavigationContextActions = {
setMode: useCallback((mode: ModeType) => {
dispatch({ type: 'SET_MODE', payload: { mode } });
setWorkbench: useCallback((workbench: WorkbenchType) => {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
}, []),
setSelectedTool: useCallback((toolId: ToolId | null) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
}, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
}, []),
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
@ -110,75 +129,67 @@ export const NavigationProvider: React.FC<{
}, []),
requestNavigation: useCallback((navigationFn: () => void) => {
// If no unsaved changes, navigate immediately
if (!state.hasUnsavedChanges) {
navigationFn();
return;
}
// Otherwise, store the navigation and show warning
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
}, [state.hasUnsavedChanges]),
confirmNavigation: useCallback(() => {
// Execute pending navigation
if (state.pendingNavigation) {
state.pendingNavigation();
}
// Clear navigation state
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
}, [state.pendingNavigation]),
cancelNavigation: useCallback(() => {
// Clear navigation without executing
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
}, []),
selectTool: useCallback((toolKey: string) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } });
}, []),
clearToolSelection: useCallback(() => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
}, []),
handleToolSelect: useCallback((toolId: string) => {
// Handle special cases
if (toolId === 'allTools') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
return;
}
// Special-case: if tool is a dedicated reader tool, enter reader mode
if (toolId === 'read' || toolId === 'view-pdf') {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } });
return;
}
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } });
dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } });
}, [])
// Look up the tool in the registry to get its proper workbench
const tool = isValidToolId(toolId)? toolRegistry[toolId] : null;
const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench();
// Validate toolId and convert to ToolId type
const validToolId = isValidToolId(toolId) ? toolId : null;
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } });
}, [toolRegistry])
};
const stateValue: NavigationContextStateValue = {
currentMode: state.currentMode,
workbench: state.workbench,
selectedTool: state.selectedTool,
hasUnsavedChanges: state.hasUnsavedChanges,
pendingNavigation: state.pendingNavigation,
showNavigationWarning: state.showNavigationWarning,
selectedToolKey: state.selectedToolKey
showNavigationWarning: state.showNavigationWarning
};
const actionsValue: NavigationContextActionsValue = {
actions
};
// Enable URL synchronization
useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync);
return (
<NavigationStateContext.Provider value={stateValue}>
<NavigationActionsContext.Provider value={actionsValue}>
@ -228,13 +239,3 @@ export const useNavigationGuard = () => {
setShowNavigationWarning: actions.showNavigationWarning
};
};
// Re-export utility functions from types for backward compatibility
export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation';
// TODO: This will be expanded for URL-based routing system
// - URL parsing utilities
// - Route definitions
// - Navigation hooks with URL sync
// - History management
// - Breadcrumb restoration from URL params

View File

@ -6,9 +6,11 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
import { ToolRegistryEntry, ToolRegistry } from '../data/toolsTaxonomy';
import { useNavigationActions, useNavigationState } from './NavigationContext';
import { ToolId, isValidToolId } from '../types/toolId';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { getDefaultWorkbench } from '../types/workbench';
// State interface
interface ToolWorkflowState {
@ -83,7 +85,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
setSearchQuery: (query: string) => void;
// Tool Actions
selectTool: (toolId: string) => void;
selectTool: (toolId: ToolId | null) => void;
clearToolSelection: () => void;
// Tool Reset Actions
@ -124,7 +126,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
} = useToolManagement();
// Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
const selectedTool = getSelectedTool(navigationState.selectedTool);
// UI Action creators
const setSidebarsVisible = useCallback((visible: boolean) => {
@ -142,7 +144,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const setPreviewFile = useCallback((file: File | null) => {
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
if (file) {
actions.setMode('viewer');
actions.setWorkbench('viewer');
}
}, [actions]);
@ -172,7 +174,17 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => {
actions.handleToolSelect(toolId);
// Set the selected tool and determine the appropriate workbench
const validToolId = isValidToolId(toolId) ? toolId : null;
actions.setSelectedTool(validToolId);
// Get the tool from registry to determine workbench
const tool = getSelectedTool(toolId);
if (tool && tool.workbench) {
actions.setWorkbench(tool.workbench);
} else {
actions.setWorkbench(getDefaultWorkbench());
}
// Clear search query when selecting a tool
setSearchQuery('');
@ -189,13 +201,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools
}
}, [actions, setLeftPanelView, setReaderMode, setSearchQuery]);
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
setReaderMode(false);
actions.clearToolSelection();
}, [setLeftPanelView, setReaderMode, actions]);
actions.setSelectedTool(null);
}, [setLeftPanelView, setReaderMode, actions.setSelectedTool]);
const handleReaderToggle = useCallback(() => {
setReaderMode(true);
@ -214,14 +226,20 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
[state.sidebarsVisible, state.readerMode]
);
// Enable URL synchronization for tool selection
useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true);
// URL sync for proper tool navigation
useNavigationUrlSync(
navigationState.selectedTool,
handleToolSelect,
handleBackToTools,
toolRegistry as ToolRegistry,
true
);
// Properly memoized context value
const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State
...state,
selectedToolKey: navigationState.selectedToolKey,
selectedToolKey: navigationState.selectedTool,
selectedTool,
toolRegistry,
@ -232,8 +250,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
selectTool: actions.selectTool,
clearToolSelection: actions.clearToolSelection,
selectTool: actions.setSelectedTool,
clearToolSelection: () => actions.setSelectedTool(null),
// Tool Reset Actions
registerToolReset,
@ -249,7 +267,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
isPanelVisible,
}), [
state,
navigationState.selectedToolKey,
navigationState.selectedTool,
selectedTool,
toolRegistry,
setSidebarsVisible,
@ -258,8 +276,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setPreviewFile,
setPageEditorFunctions,
setSearchQuery,
actions.selectTool,
actions.clearToolSelection,
actions.setSelectedTool,
registerToolReset,
resetTool,
handleToolSelect,

View File

@ -1,8 +1,9 @@
import { type TFunction } from 'i18next';
import React from 'react';
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
import { BaseParameters } from '../types/parameters';
import { WorkbenchType } from '../types/workbench';
import { ToolId } from '../types/toolId';
export enum SubcategoryId {
SIGNING = 'signing',
@ -28,7 +29,6 @@ export type ToolRegistryEntry = {
icon: React.ReactNode;
name: string;
component: React.ComponentType<BaseToolProps> | null;
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
description: string;
categoryId: ToolCategoryId;
subcategoryId: SubcategoryId;
@ -37,13 +37,17 @@ export type ToolRegistryEntry = {
endpoints?: string[];
link?: string;
type?: string;
// URL path for routing (e.g., '/split-pdfs', '/compress-pdf')
urlPath?: string;
// Workbench type for navigation
workbench?: WorkbenchType;
// Operation configuration for automation
operationConfig?: ToolOperationConfig<any>;
// Settings component for automation configuration
settingsComponent?: React.ComponentType<any>;
}
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>;
export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
export const SUBCATEGORY_ORDER: SubcategoryId[] = [
SubcategoryId.SIGNING,
@ -107,3 +111,30 @@ export const getAllApplicationEndpoints = (
const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
return Array.from(new Set([...toolEp, ...convEp]));
};
/**
* Default workbench for tools that don't specify one
* Returns null to trigger the default case in Workbench component (ToolRenderer)
*/
export const getDefaultToolWorkbench = (): WorkbenchType => 'fileEditor';
/**
* Get workbench type for a tool
*/
export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => {
return tool.workbench || getDefaultToolWorkbench();
};
/**
* Get URL path for a tool
*/
export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => {
return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
};
/**
* Check if a tool ID exists in the registry
*/
export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => {
return toolId in registry;
};

View File

@ -1,66 +1,128 @@
import React, { useMemo } from 'react';
import LocalIcon from '../components/shared/LocalIcon';
import { useTranslation } from 'react-i18next';
import React, { useMemo } from "react";
import LocalIcon from "../components/shared/LocalIcon";
import { useTranslation } from "react-i18next";
import SplitPdfPanel from "../tools/Split";
import CompressPdfPanel from "../tools/Compress";
import OCRPanel from '../tools/OCR';
import ConvertPanel from '../tools/Convert';
import Sanitize from '../tools/Sanitize';
import AddPassword from '../tools/AddPassword';
import ChangePermissions from '../tools/ChangePermissions';
import RemovePassword from '../tools/RemovePassword';
import { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy';
import AddWatermark from '../tools/AddWatermark';
import Repair from '../tools/Repair';
import SingleLargePage from '../tools/SingleLargePage';
import UnlockPdfForms from '../tools/UnlockPdfForms';
import RemoveCertificateSign from '../tools/RemoveCertificateSign';
import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation';
import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation';
import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation';
import { removePasswordOperationConfig } from '../hooks/tools/removePassword/useRemovePasswordOperation';
import { sanitizeOperationConfig } from '../hooks/tools/sanitize/useSanitizeOperation';
import { repairOperationConfig } from '../hooks/tools/repair/useRepairOperation';
import { addWatermarkOperationConfig } from '../hooks/tools/addWatermark/useAddWatermarkOperation';
import { unlockPdfFormsOperationConfig } from '../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation';
import { singleLargePageOperationConfig } from '../hooks/tools/singleLargePage/useSingleLargePageOperation';
import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation';
import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation';
import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation';
import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation';
import CompressSettings from '../components/tools/compress/CompressSettings';
import SplitSettings from '../components/tools/split/SplitSettings';
import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings';
import RemovePasswordSettings from '../components/tools/removePassword/RemovePasswordSettings';
import SanitizeSettings from '../components/tools/sanitize/SanitizeSettings';
import RepairSettings from '../components/tools/repair/RepairSettings';
import UnlockPdfFormsSettings from '../components/tools/unlockPdfForms/UnlockPdfFormsSettings';
import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/AddWatermarkSingleStepSettings';
import OCRSettings from '../components/tools/ocr/OCRSettings';
import ConvertSettings from '../components/tools/convert/ConvertSettings';
import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings';
import OCRPanel from "../tools/OCR";
import ConvertPanel from "../tools/Convert";
import Sanitize from "../tools/Sanitize";
import AddPassword from "../tools/AddPassword";
import ChangePermissions from "../tools/ChangePermissions";
import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import AddWatermark from "../tools/AddWatermark";
import Repair from "../tools/Repair";
import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
import { removePasswordOperationConfig } from "../hooks/tools/removePassword/useRemovePasswordOperation";
import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOperation";
import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation";
import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from "../components/tools/split/SplitSettings";
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
import RepairSettings from "../components/tools/repair/RepairSettings";
import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings";
import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings";
import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
import { ToolId } from "../types/toolId";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
// Convert tool supported file formats
export const CONVERT_SUPPORTED_FORMATS = [
// Microsoft Office
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
"doc",
"docx",
"dot",
"dotx",
"csv",
"xls",
"xlsx",
"xlt",
"xltx",
"slk",
"dif",
"ppt",
"pptx",
// OpenDocument
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
"odt",
"ott",
"ods",
"ots",
"odp",
"otp",
"odg",
"otg",
// Text formats
"txt", "text", "xml", "rtf", "html", "lwp", "md",
"txt",
"text",
"xml",
"rtf",
"html",
"lwp",
"md",
// Images
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
"bmp",
"gif",
"jpeg",
"jpg",
"png",
"tif",
"tiff",
"pbm",
"pgm",
"ppm",
"ras",
"xbm",
"xpm",
"svg",
"svm",
"wmf",
"webp",
// StarOffice
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
"sda",
"sdc",
"sdd",
"sdw",
"stc",
"std",
"sti",
"stw",
"sxd",
"sxg",
"sxi",
"sxw",
// Email formats
"eml",
// Archive formats
"zip",
// Other
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
];
"dbf",
"fods",
"vsd",
"vor",
"vor3",
"vor4",
"uop",
"pct",
"ps",
"pdf",
];
// Hook to get the translated tool registry
export function useFlatToolRegistry(): ToolRegistry {
@ -70,119 +132,111 @@ export function useFlatToolRegistry(): ToolRegistry {
const allTools: ToolRegistry = {
// Signing
"certSign": {
certSign: {
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.certSign.title", "Sign with Certificate"),
component: null,
view: "sign",
description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING
subcategoryId: SubcategoryId.SIGNING,
},
"sign": {
sign: {
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sign.title", "Sign"),
component: null,
view: "sign",
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING
subcategoryId: SubcategoryId.SIGNING,
},
// Document Security
"addPassword": {
addPassword: {
icon: <LocalIcon icon="password-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPassword.title", "Add Password"),
component: AddPassword,
view: "security",
description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig,
settingsComponent: AddPasswordSettings
settingsComponent: AddPasswordSettings,
},
"watermark": {
addWatermark: {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
view: "format",
maxFiles: -1,
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig,
settingsComponent: AddWatermarkSingleStepSettings
settingsComponent: AddWatermarkSingleStepSettings,
},
"add-stamp": {
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.AddStampRequest.title", "Add Stamp to PDF"),
component: null,
view: "format",
description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
},
"sanitize": {
sanitize: {
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sanitize.title", "Sanitize"),
component: Sanitize,
view: "security",
maxFiles: -1,
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig,
settingsComponent: SanitizeSettings
settingsComponent: SanitizeSettings,
},
"flatten": {
flatten: {
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.flatten.title", "Flatten"),
component: null,
view: "format",
description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
},
"unlock-pdf-forms": {
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms,
view: "security",
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["unlock-pdf-forms"],
operationConfig: unlockPdfFormsOperationConfig,
settingsComponent: UnlockPdfFormsSettings
settingsComponent: UnlockPdfFormsSettings,
},
"manage-certificates": {
icon: <LocalIcon icon="license-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.manageCertificates.title", "Manage Certificates"),
component: null,
view: "security",
description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."),
description: t(
"home.manageCertificates.desc",
"Import, export, or delete digital certificate files used for signing PDFs."
),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
},
"change-permissions": {
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions,
view: "security",
description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig,
settingsComponent: ChangePermissionsSettings
settingsComponent: ChangePermissionsSettings,
},
// Verification
@ -190,422 +244,390 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
component: null,
view: "extract",
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION
subcategoryId: SubcategoryId.VERIFICATION,
},
"validate-pdf-signature": {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null,
view: "security",
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION
subcategoryId: SubcategoryId.VERIFICATION,
},
// Document Review
"read": {
read: {
icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.read.title", "Read"),
component: null,
view: "view",
description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."),
workbench: "viewer",
description: t(
"home.read.desc",
"View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
},
"change-metadata": {
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.changeMetadata.title", "Change Metadata"),
component: null,
view: "format",
description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
},
// Page Formatting
"cropPdf": {
cropPdf: {
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.crop.title", "Crop PDF"),
component: null,
view: "format",
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"rotate": {
rotate: {
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.rotate.title", "Rotate"),
component: null,
view: "format",
description: t("home.rotate.desc", "Easily rotate your PDFs."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"splitPdf": {
split: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.split.title", "Split"),
component: SplitPdfPanel,
view: "split",
description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
operationConfig: splitOperationConfig,
settingsComponent: SplitSettings
settingsComponent: SplitSettings,
},
"reorganize-pages": {
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.reorganizePages.title", "Reorganize Pages"),
component: null,
view: "pageEditor",
description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."),
workbench: "pageEditor",
description: t(
"home.reorganizePages.desc",
"Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"adjust-page-size-scale": {
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.scalePages.title", "Adjust page size/scale"),
component: null,
view: "format",
description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"addPageNumbers": {
addPageNumbers: {
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: null,
view: "format",
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"multi-page-layout": {
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pageLayout.title", "Multi-Page Layout"),
component: null,
view: "format",
description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
"single-large-page": {
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage,
view: "format",
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["pdf-to-single-page"],
operationConfig: singleLargePageOperationConfig
operationConfig: singleLargePageOperationConfig,
},
"add-attachments": {
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.attachments.title", "Add Attachments"),
component: null,
view: "format",
description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
// Extraction
"extractPages": {
"extract-page": {
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.extractPages.title", "Extract Pages"),
component: null,
view: "extract",
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION
subcategoryId: SubcategoryId.EXTRACTION,
},
"extract-images": {
icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />,
name: t("home.extractImages.title", "Extract Images"),
component: null,
view: "extract",
description: t("home.extractImages.desc", "Extract images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION
subcategoryId: SubcategoryId.EXTRACTION,
},
// Removal
"removePages": {
removePages: {
icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePages.title", "Remove Pages"),
component: null,
view: "remove",
description: t("home.removePages.desc", "Remove specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL
subcategoryId: SubcategoryId.REMOVAL,
},
"remove-blank-pages": {
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeBlanks.title", "Remove Blank Pages"),
component: null,
view: "remove",
description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL
subcategoryId: SubcategoryId.REMOVAL,
},
"remove-annotations": {
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeAnnotations.title", "Remove Annotations"),
component: null,
view: "remove",
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL
subcategoryId: SubcategoryId.REMOVAL,
},
"remove-image": {
icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeImagePdf.title", "Remove Image"),
component: null,
view: "format",
description: t("home.removeImagePdf.desc", "Remove images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL
subcategoryId: SubcategoryId.REMOVAL,
},
"remove-password": {
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword,
view: "security",
description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
endpoints: ["remove-password"],
maxFiles: -1,
operationConfig: removePasswordOperationConfig,
settingsComponent: RemovePasswordSettings
settingsComponent: RemovePasswordSettings,
},
"remove-certificate-sign": {
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign,
view: "security",
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1,
endpoints: ["remove-certificate-sign"],
operationConfig: removeCertificateSignOperationConfig
operationConfig: removeCertificateSignOperationConfig,
},
// Automation
"automate": {
automate: {
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
name: t("home.automate.title", "Automate"),
component: React.lazy(() => import('../tools/Automate')),
view: "format",
description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."),
component: React.lazy(() => import("../tools/Automate")),
description: t(
"home.automate.desc",
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION,
maxFiles: -1,
supportedFormats: CONVERT_SUPPORTED_FORMATS,
endpoints: ["handleData"]
endpoints: ["handleData"],
},
"auto-rename-pdf-file": {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.auto-rename.title", "Auto Rename PDF File"),
component: null,
view: "format",
description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION
subcategoryId: SubcategoryId.AUTOMATION,
},
"auto-split-pages": {
icon: <LocalIcon icon="split-scene-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.autoSplitPDF.title", "Auto Split Pages"),
component: null,
view: "format",
description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION
subcategoryId: SubcategoryId.AUTOMATION,
},
"auto-split-by-size-count": {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"),
component: null,
view: "format",
description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION
subcategoryId: SubcategoryId.AUTOMATION,
},
// Advanced Formatting
"adjustContrast": {
"adjust-contrast": {
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null,
view: "format",
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"repair": {
repair: {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.repair.title", "Repair"),
component: Repair,
view: "format",
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1,
endpoints: ["repair"],
operationConfig: repairOperationConfig,
settingsComponent: RepairSettings
settingsComponent: RepairSettings,
},
"detect-split-scanned-photos": {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"),
component: null,
view: "format",
description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"overlay-pdfs": {
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: null,
view: "format",
description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"replace-and-invert-color": {
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.replaceColorPdf.title", "Replace & Invert Color"),
component: null,
view: "format",
description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"add-image": {
icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addImage.title", "Add Image"),
component: null,
view: "format",
description: t("home.addImage.desc", "Add images to PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"edit-table-of-contents": {
icon: <LocalIcon icon="bookmark-add-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.editTableOfContents.title", "Edit Table of Contents"),
component: null,
view: "format",
description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
"scanner-effect": {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.fakeScan.title", "Scanner Effect"),
component: null,
view: "format",
description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
},
// Developer Tools
"show-javascript": {
icon: <LocalIcon icon="javascript-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.showJS.title", "Show JavaScript"),
component: null,
view: "extract",
description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
},
"dev-api": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devApi.title", "API"),
component: null,
view: "external",
description: t("home.devApi.desc", "Link to API documentation"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html"
link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
},
"dev-folder-scanning": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devFolderScanning.title", "Automated Folder Scanning"),
component: null,
view: "external",
description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/"
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
},
"dev-sso-guide": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devSsoGuide.title", "SSO Guide"),
component: null,
view: "external",
description: t("home.devSsoGuide.desc", "Link to SSO guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
},
"dev-airgapped": {
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: '#2F7BF6' }} />,
icon: <LocalIcon icon="open-in-new-rounded" width="1.5rem" height="1.5rem" style={{ color: "#2F7BF6" }} />,
name: t("home.devAirgapped.title", "Air-gapped Setup"),
component: null,
view: "external",
description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Pro/#activation"
link: "https://docs.stirlingpdf.com/Pro/#activation",
},
// Recommended Tools
"compare": {
compare: {
icon: <LocalIcon icon="compare-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compare.title", "Compare"),
component: null,
view: "format",
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL
subcategoryId: SubcategoryId.GENERAL,
},
"compress": {
compress: {
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compress.title", "Compress"),
component: CompressPdfPanel,
view: "compress",
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
operationConfig: compressOperationConfig,
settingsComponent: CompressSettings
settingsComponent: CompressSettings,
},
"convert": {
convert: {
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.convert.title", "Convert"),
component: ConvertPanel,
view: "convert",
description: t("home.convert.desc", "Convert files to and from PDF format"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
@ -625,52 +647,50 @@ export function useFlatToolRegistry(): ToolRegistry {
"pdf-to-csv",
"pdf-to-markdown",
"pdf-to-pdfa",
"eml-to-pdf"
"eml-to-pdf",
],
operationConfig: convertOperationConfig,
settingsComponent: ConvertSettings
settingsComponent: ConvertSettings,
},
"mergePdfs": {
mergePdfs: {
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.merge.title", "Merge"),
component: null,
view: "merge",
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1
maxFiles: -1,
},
"multi-tool": {
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.multiTool.title", "Multi-Tool"),
component: null,
view: "pageEditor",
workbench: "pageEditor",
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1
maxFiles: -1,
},
"ocr": {
ocr: {
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.ocr.title", "OCR"),
component: OCRPanel,
view: "convert",
description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
operationConfig: ocrOperationConfig,
settingsComponent: OCRSettings
settingsComponent: OCRSettings,
},
"redact": {
redact: {
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.redact.title", "Redact"),
component: null,
view: "redact",
description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL
subcategoryId: SubcategoryId.GENERAL,
},
};
@ -678,9 +698,9 @@ export function useFlatToolRegistry(): ToolRegistry {
return allTools;
}
const filteredTools = Object.keys(allTools)
.filter(key => allTools[key].component !== null || allTools[key].link)
.filter((key) => allTools[key as ToolId].component !== null || allTools[key as ToolId].link)
.reduce((obj, key) => {
obj[key] = allTools[key];
obj[key as ToolId] = allTools[key as ToolId];
return obj;
}, {} as ToolRegistry);
return filteredTools;

View File

@ -2,12 +2,12 @@ import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface UseAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
toolRegistry: Record<string, ToolRegistryEntry>;
toolRegistry: ToolRegistry;
}
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
@ -19,12 +19,12 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation] as any;
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
const config = toolRegistry[operation]?.operationConfig;
const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
import { ToolId } from '../types/toolId';
// Material UI Icons
import CompressIcon from '@mui/icons-material/Compress';
@ -9,7 +10,7 @@ import CropIcon from '@mui/icons-material/Crop';
import TextFieldsIcon from '@mui/icons-material/TextFields';
export interface SuggestedTool {
id: string /* FIX ME: Should be ToolId */;
id: ToolId;
title: string;
icon: React.ComponentType<any>;
navigate: () => void;
@ -32,7 +33,7 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
icon: CleaningServicesIcon
},
{
id: 'splitPdf',
id: 'split',
title: 'Split',
icon: CropIcon
},
@ -45,16 +46,16 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
export function useSuggestedTools(): SuggestedTool[] {
const { actions } = useNavigationActions();
const { selectedToolKey } = useNavigationState();
const { selectedTool } = useNavigationState();
return useMemo(() => {
// Filter out the current tool
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey);
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedTool);
// Add navigation function to each tool
return filteredTools.map(tool => ({
...tool,
navigate: () => actions.handleToolSelect(tool.id)
navigate: () => actions.setSelectedTool(tool.id)
}));
}, [selectedToolKey, actions]);
}, [selectedTool, actions]);
}

View File

@ -35,7 +35,7 @@ export const useToolManagement = (): ToolManagementResult => {
const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true;
const endpoints = baseRegistry[toolKey]?.endpoints || [];
const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]);

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

@ -1,120 +1,107 @@
/**
* URL synchronization hooks for tool routing
* URL synchronization hooks for tool routing with registry support
*/
import { useEffect, useCallback } from 'react';
import { ModeType } from '../types/navigation';
import { useEffect, useCallback, useRef } from 'react';
import { ToolId } from '../types/toolId';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy';
import { firePixel } from '../utils/scarfTracking';
/**
* Hook to sync navigation mode with URL
* Hook to sync workbench and tool with URL using registry
*/
export function useNavigationUrlSync(
currentMode: ModeType,
setMode: (mode: ModeType) => void,
selectedTool: ToolId | null,
handleToolSelect: (toolId: string) => void,
clearToolSelection: () => void,
registry: ToolRegistry,
enableSync: boolean = true
) {
// Initialize mode from URL on mount
const hasInitialized = useRef(false);
const prevSelectedTool = useRef<ToolId | null>(null);
// Initialize workbench and tool from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
// Fire pixel for initial page load
const currentPath = window.location.pathname;
firePixel(currentPath);
const route = parseToolRoute(registry);
if (route.toolId !== selectedTool) {
if (route.toolId) {
handleToolSelect(route.toolId);
} else if (selectedTool !== null) {
// Only clear selection if we actually had a tool selected
// Don't clear on initial load when selectedTool starts as null
clearToolSelection();
}
}
hasInitialized.current = true;
}, []); // Only run on mount
// Update URL when mode changes
// Update URL when tool or workbench changes
useEffect(() => {
if (!enableSync) return;
if (currentMode === 'pageEditor') {
clearToolRoute();
} else {
updateToolRoute(currentMode, currentMode);
if (selectedTool) {
updateToolRoute(selectedTool, registry, false); // Use pushState for user navigation
} else if (prevSelectedTool.current !== null) {
// Only clear URL if we had a tool before (user navigated away)
// Don't clear on initial load when both current and previous are null
if (window.location.pathname !== '/') {
clearToolRoute(false); // Use pushState for user navigation
}
}, [currentMode, enableSync]);
}
prevSelectedTool.current = selectedTool;
}, [selectedTool, registry, enableSync]);
// Handle browser back/forward navigation
useEffect(() => {
if (!enableSync) return;
const handlePopState = () => {
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
const route = parseToolRoute(registry);
if (route.toolId !== selectedTool) {
// Fire pixel for back/forward navigation
const currentPath = window.location.pathname;
firePixel(currentPath);
if (route.toolId) {
handleToolSelect(route.toolId);
} else {
clearToolSelection();
}
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [currentMode, setMode, enableSync]);
}, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync]);
}
/**
* Hook to sync tool workflow with URL
* Hook to programmatically navigate to tools with registry support
*/
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);
export function useToolNavigation(registry: ToolRegistry) {
const navigateToTool = useCallback((toolId: ToolId) => {
updateToolRoute(toolId, registry);
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey }
detail: { toolId }
}));
}, []);
}, [registry]);
const navigateToHome = useCallback(() => {
clearToolRoute();
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey: null }
detail: { toolId: null }
}));
}, []);
@ -123,3 +110,14 @@ export function useToolNavigation() {
navigateToHome
};
}
/**
* Hook to get current URL route information with registry support
*/
export function useCurrentRoute(registry: ToolRegistry) {
const getCurrentRoute = useCallback(() => {
return parseToolRoute(registry);
}, [registry]);
return getCurrentRoute;
}

View File

@ -1,4 +1,5 @@
import '@mantine/core/styles.css';
import '../vite-env.d.ts';
import './index.css'; // Import Tailwind CSS
import React from 'react';
import ReactDOM from 'react-dom/client';
@ -6,6 +7,7 @@ import { ColorSchemeScript } from '@mantine/core';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './i18n'; // Initialize i18next
import { PostHogProvider } from 'posthog-js/react';
// Compute initial color scheme
function getInitialScheme(): 'light' | 'dark' {
@ -27,9 +29,18 @@ const root = ReactDOM.createRoot(container); // Finds the root DOM element
root.render(
<React.StrictMode>
<ColorSchemeScript defaultColorScheme={getInitialScheme()} />
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-05-24',
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
debug: import.meta.env.MODE === 'development',
}}
>
<BrowserRouter>
<App />
</BrowserRouter>
</PostHogProvider>
</React.StrictMode>
);

View File

@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useFileContext } from "../contexts/FileContext";
import { useFileSelection } from "../contexts/FileContext";
import { useNavigation } from "../contexts/NavigationContext";
import { useNavigationActions } from "../contexts/NavigationContext";
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation";
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const { setMode } = useNavigation();
const { actions } = useNavigationActions();
const { registerToolReset } = useToolWorkflow();
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
@ -161,7 +161,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesPlaceholder = useMemo(() => {
if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) {
const firstOperation = stepData.automation.operations[0];
const toolConfig = toolRegistry[firstOperation.operation];
const toolConfig = toolRegistry[firstOperation.operation as keyof typeof toolRegistry];
// Check if the tool has supportedFormats that include non-PDF formats
if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) {
@ -223,7 +223,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
title: t('automate.reviewTitle', 'Automation Results'),
onFileClick: (file: File) => {
onPreviewFile?.(file);
setMode('viewer');
actions.setWorkbench('viewer');
}
}
});

View File

@ -1,42 +1,19 @@
/**
* Shared navigation types to avoid circular dependencies
* Navigation types for workbench and tool separation
*/
// Navigation mode types - complete list to match contexts
export type ModeType =
| 'viewer'
| 'pageEditor'
| 'fileEditor'
| 'merge'
| 'split'
| 'compress'
| 'ocr'
| 'convert'
| 'sanitize'
| 'addPassword'
| 'changePermissions'
| 'addWatermark'
| 'removePassword'
| 'single-large-page'
| 'repair'
| 'unlockPdfForms'
| 'removeCertificateSign';
import { WorkbenchType } from './workbench';
import { ToolId } from './toolId';
// Utility functions for mode handling
export const isValidMode = (mode: string): mode is ModeType => {
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions',
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
'repair', 'unlockPdfForms', 'removeCertificateSign'
];
return validModes.includes(mode as ModeType);
};
// Navigation state
export interface NavigationState {
workbench: WorkbenchType;
selectedTool: ToolId | null;
}
export const getDefaultMode = (): ModeType => 'fileEditor';
// Route parsing result
export interface ToolRoute {
mode: ModeType;
toolKey: string | null;
workbench: WorkbenchType;
toolId: ToolId | null;
}

View File

@ -2,10 +2,12 @@
* Navigation action interfaces to break circular dependencies
*/
import { ModeType } from './navigation';
import { WorkbenchType } from './workbench';
import { ToolId } from './toolId';
export interface NavigationActions {
setMode: (mode: ModeType) => void;
setWorkbench: (workbench: WorkbenchType) => void;
setSelectedTool: (toolId: ToolId | null) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void;
@ -14,7 +16,8 @@ export interface NavigationActions {
}
export interface NavigationState {
currentMode: ModeType;
workbench: WorkbenchType;
selectedTool: ToolId | null;
hasUnsavedChanges: boolean;
pendingNavigation: (() => void) | null;
showNavigationWarning: boolean;

View File

@ -0,0 +1,25 @@
// Define all possible tool IDs as source of truth
const TOOL_IDS = [
'certSign', 'sign', 'addPassword', 'remove-password', 'removePages', 'remove-blank-pages', 'remove-annotations', 'remove-image',
'change-permissions', 'addWatermark',
'sanitize', 'auto-split-pages', 'auto-split-by-size-count', 'split', 'mergePdfs',
'convert', 'ocr', 'add-image', 'rotate',
'detect-split-scanned-photos',
'edit-table-of-contents',
'scanner-effect',
'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
'repair', 'compare', 'addPageNumbers', 'redact',
'flatten', 'remove-certificate-sign',
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs',
'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color',
'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped'
] as const;
// Tool identity - what PDF operation we're performing (type-safe)
export type ToolId = typeof TOOL_IDS[number];
// Type guard using the same source of truth
export const isValidToolId = (value: string): value is ToolId => {
return TOOL_IDS.includes(value as ToolId);
};

View File

@ -0,0 +1,12 @@
// Define workbench values once as source of truth
const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
// Workbench types - how the user interacts with content
export type WorkbenchType = typeof WORKBENCH_TYPES[number];
export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
// Type guard using the same source of truth
export const isValidWorkbench = (value: string): value is WorkbenchType => {
return WORKBENCH_TYPES.includes(value as WorkbenchType);
};

View File

@ -31,7 +31,7 @@ export const executeToolOperationWithPrefix = async (
): Promise<File[]> => {
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
const config = toolRegistry[operationName]?.operationConfig;
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
if (!config) {
console.error(`❌ Tool operation not supported: ${operationName}`);
throw new Error(`Tool operation not supported: ${operationName}`);

View File

@ -0,0 +1,28 @@
let lastFiredPathname: string | null = null;
let lastFiredTime = 0;
/**
* Fire scarf pixel for analytics tracking
* Only fires if pathname is different from last call or enough time has passed
*/
export function firePixel(pathname: string): void {
const now = Date.now();
// Only fire if pathname changed or it's been at least 1 second since last fire
if (pathname === lastFiredPathname && now - lastFiredTime < 250) {
return;
}
lastFiredPathname = pathname;
lastFiredTime = now;
const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7'
+ '&path=' + encodeURIComponent(pathname)
const img = new Image();
img.referrerPolicy = "no-referrer-when-downgrade";
img.src = url;
console.log("ScarfPixel: Fire to... " + pathname);
}

View File

@ -0,0 +1,33 @@
import { ToolId } from '../types/toolId';
// Map URL paths to tool keys (multiple URLs can map to same tool)
export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/split-pdfs': 'split',
'/split': 'split',
'/merge-pdfs': 'mergePdfs',
'/compress-pdf': 'compress',
'/convert': 'convert',
'/convert-pdf': 'convert',
'/file-to-pdf': 'convert',
'/eml-to-pdf': 'convert',
'/html-to-pdf': 'convert',
'/markdown-to-pdf': 'convert',
'/pdf-to-csv': 'convert',
'/pdf-to-img': 'convert',
'/pdf-to-markdown': 'convert',
'/pdf-to-pdfa': 'convert',
'/pdf-to-word': 'convert',
'/pdf-to-xml': 'convert',
'/add-password': 'addPassword',
'/change-permissions': 'change-permissions',
'/sanitize-pdf': 'sanitize',
'/ocr': 'ocr',
'/ocr-pdf': 'ocr',
'/add-watermark': 'addWatermark',
'/remove-password': 'remove-password',
'/single-large-page': 'single-large-page',
'/repair': 'repair',
'/unlock-pdf-forms': 'unlock-pdf-forms',
'/remove-certificate-sign': 'remove-certificate-sign',
'/remove-cert-sign': 'remove-certificate-sign'
};

View File

@ -1,167 +1,127 @@
/**
* URL routing utilities for tool navigation
* Provides clean URL routing for the V2 tool system
* URL routing utilities for tool navigation with registry support
*/
import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation';
import { ToolRoute } from '../types/navigation';
import { ToolId, isValidToolId } from '../types/toolId';
import { getDefaultWorkbench } from '../types/workbench';
import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy';
import { firePixel } from './scarfTracking';
import { URL_TO_TOOL_MAP } from './urlMapping';
/**
* Parse the current URL to extract tool routing information
*/
export function parseToolRoute(): ToolRoute {
export function parseToolRoute(registry: ToolRegistry): 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': { 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) {
// First, check URL mapping for multiple URL aliases
const mappedToolId = URL_TO_TOOL_MAP[path];
if (mappedToolId && registry[mappedToolId]) {
const tool = registry[mappedToolId];
return {
mode: mapping.mode,
toolKey: mapping.toolKey
workbench: getToolWorkbench(tool),
toolId: mappedToolId
};
}
// Fallback: Try to find tool by primary URL path in registry
for (const [toolId, tool] of Object.entries(registry)) {
const toolUrlPath = getToolUrlPath(toolId, tool);
if (path === toolUrlPath && isValidToolId(toolId)) {
return {
workbench: getToolWorkbench(tool),
toolId
};
}
}
// Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool');
if (toolParam && isValidModeType(toolParam)) {
if (toolParam && isValidToolId(toolParam) && registry[toolParam]) {
const tool = registry[toolParam];
return {
mode: toolParam as ModeType,
toolKey: toolParam
workbench: getToolWorkbench(tool),
toolId: toolParam
};
}
// Default to page editor for home page
// Default to fileEditor workbench for home page
return {
mode: getDefaultMode(),
toolKey: null
workbench: getDefaultWorkbench(),
toolId: null
};
}
/**
* Update the URL to reflect the current tool selection
* Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs
* Update URL and fire analytics pixel
*/
export function updateToolRoute(mode: ModeType, toolKey?: string): void {
function updateUrl(newPath: string, searchParams: URLSearchParams, replace: boolean = false): void {
const currentPath = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search);
const queryString = searchParams.toString();
const fullUrl = newPath + (queryString ? `?${queryString}` : '');
// 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();
// Only update URL and fire pixel if something actually changed
if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) {
if (replace) {
window.history.replaceState(null, '', fullUrl);
} else {
window.history.pushState(null, '', fullUrl);
}
firePixel(newPath);
}
}
/**
* Update the URL to reflect the current tool selection
*/
export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace: boolean = false): void {
const tool = registry[toolId];
if (!tool) {
console.warn(`Tool ${toolId} not found in registry`);
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',
'compress': '/compress-pdf',
'convert': '/convert-pdf',
'addPassword': '/add-password-pdf',
'changePermissions': '/change-permissions-pdf',
'sanitize': '/sanitize-pdf',
'ocr': '/ocr-pdf'
};
newPath = pathMappings[toolKey] || `/${toolKey}`;
}
const newPath = getToolUrlPath(toolId, tool);
const searchParams = new URLSearchParams(window.location.search);
// 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);
}
updateUrl(newPath, searchParams, replace);
}
/**
* Clear tool routing and return to home page
*/
export function clearToolRoute(): void {
export function clearToolRoute(replace: boolean = false): void {
const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('tool');
const queryString = searchParams.toString();
const url = '/' + (queryString ? `?${queryString}` : '');
window.history.replaceState(null, '', url);
updateUrl('/', searchParams, replace);
}
/**
* Get clean tool name for display purposes
* Get clean tool name for display purposes using registry
*/
export function getToolDisplayName(toolKey: string): string {
const displayNames: Record<string, string> = {
'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;
export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): string {
const tool = registry[toolId];
return tool ? tool.name : toolId;
}
// Note: isValidMode is now imported from types/navigation.ts
/**
* Generate shareable URL for current tool state
* Only generates URLs for actual tools, not internal UI modes
* Generate shareable URL for current tool state using registry
*/
export function generateShareableUrl(mode: ModeType, toolKey?: string): string {
export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string {
const baseUrl = window.location.origin;
// Don't generate URLs for internal UI modes
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
if (!toolId || !registry[toolId]) {
return baseUrl;
}
if (toolKey) {
const pathMappings: Record<string, string> = {
'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 tool = registry[toolId];
const path = pathMappings[toolKey] || `/${toolKey}`;
const path = getToolUrlPath(toolId, tool);
return `${baseUrl}${path}`;
}
return baseUrl;
}

10
frontend/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PUBLIC_POSTHOG_KEY: string;
readonly VITE_PUBLIC_POSTHOG_HOST: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}