mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-17 11:45:05 +02:00
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 <[email protected]>
This commit is contained in:
@@ -2,29 +2,29 @@ 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) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [automationName, setAutomationName] = useState('');
|
||||
const [automationDescription, setAutomationDescription] = useState('');
|
||||
const [automationIcon, setAutomationIcon] = useState<string>('');
|
||||
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 };
|
||||
}
|
||||
@@ -119,4 +119,4 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
||||
getToolName,
|
||||
getToolDefaultParameters
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
export function useToolNavigation(registry: ToolRegistry) {
|
||||
const navigateToTool = useCallback((toolId: ToolId) => {
|
||||
updateToolRoute(toolId, registry);
|
||||
|
||||
// Update URL when tool changes
|
||||
useEffect(() => {
|
||||
if (!enableSync) return;
|
||||
|
||||
if (selectedToolKey) {
|
||||
const route = parseToolRoute();
|
||||
if (route.toolKey !== selectedToolKey) {
|
||||
updateToolRoute(selectedToolKey as ModeType, selectedToolKey);
|
||||
}
|
||||
}
|
||||
}, [selectedToolKey, enableSync]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get current URL route information
|
||||
*/
|
||||
export function useCurrentRoute() {
|
||||
const getCurrentRoute = useCallback(() => {
|
||||
return parseToolRoute();
|
||||
}, []);
|
||||
|
||||
return getCurrentRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to programmatically navigate to tools
|
||||
*/
|
||||
export function useToolNavigation() {
|
||||
const navigateToTool = useCallback((toolKey: string) => {
|
||||
updateToolRoute(toolKey as ModeType, toolKey);
|
||||
|
||||
// Dispatch a custom event to notify other components
|
||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||
detail: { toolKey }
|
||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||
detail: { toolId }
|
||||
}));
|
||||
}, []);
|
||||
}, [registry]);
|
||||
|
||||
const navigateToHome = useCallback(() => {
|
||||
clearToolRoute();
|
||||
|
||||
|
||||
// Dispatch a custom event to notify other components
|
||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||
detail: { toolKey: null }
|
||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||
detail: { toolId: null }
|
||||
}));
|
||||
}, []);
|
||||
|
||||
@@ -122,4 +109,15 @@ export function useToolNavigation() {
|
||||
navigateToTool,
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user