From 0c76bf2c8b71d59b72dbe8a7dbb919b415a85804 Mon Sep 17 00:00:00 2001 From: Reece Date: Thu, 5 Jun 2025 21:59:18 +0100 Subject: [PATCH] Homepage refactor --- frontend/src/components/DeepLinks.tsx | 6 +- frontend/src/components/ToolRenderer.tsx | 74 +++++ frontend/src/components/TopControls.tsx | 106 +++++++ frontend/src/hooks/useToolParams.ts | 130 +++++++++ frontend/src/pages/HomePage.tsx | 357 +++-------------------- frontend/src/styles/HomePage.module.css | 95 ++++++ 6 files changed, 453 insertions(+), 315 deletions(-) create mode 100644 frontend/src/components/ToolRenderer.tsx create mode 100644 frontend/src/components/TopControls.tsx create mode 100644 frontend/src/hooks/useToolParams.ts create mode 100644 frontend/src/styles/HomePage.module.css diff --git a/frontend/src/components/DeepLinks.tsx b/frontend/src/components/DeepLinks.tsx index 53a4ab7b5..79f1fc2ef 100644 --- a/frontend/src/components/DeepLinks.tsx +++ b/frontend/src/components/DeepLinks.tsx @@ -6,17 +6,17 @@ const DeepLinks: React.FC = () => { const commonLinks = [ { name: "Split PDF Pages 1-5", - url: "/?tool=split&splitMode=byPages&pages=1-5&view=viewer", + url: "/?t=split&mode=byPages&p=1-5&v=viewer", description: "Split a PDF and extract pages 1-5" }, { name: "Compress PDF (High)", - url: "/?tool=compress&level=9&grayscale=true&view=viewer", + url: "/?t=compress&level=9&gray=true&v=viewer", description: "Compress a PDF with high compression level" }, { name: "Merge PDFs", - url: "/?tool=merge&view=fileManager", + url: "/?t=merge&v=fileManager", description: "Combine multiple PDF files into one" } ]; diff --git a/frontend/src/components/ToolRenderer.tsx b/frontend/src/components/ToolRenderer.tsx new file mode 100644 index 000000000..18f2742e4 --- /dev/null +++ b/frontend/src/components/ToolRenderer.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { FileWithUrl } from "../types/file"; + +interface ToolRendererProps { + selectedToolKey: string; + selectedTool: any; + pdfFile: any; + files: FileWithUrl[]; + downloadUrl: string | null; + setDownloadUrl: (url: string | null) => void; + toolParams: any; + updateParams: (params: any) => void; +} + +const ToolRenderer: React.FC = ({ + selectedToolKey, + selectedTool, + pdfFile, + files, + downloadUrl, + setDownloadUrl, + toolParams, + updateParams, +}) => { + if (!selectedTool || !selectedTool.component) { + return
Tool not found
; + } + + const ToolComponent = selectedTool.component; + + // Pass tool-specific props + switch (selectedToolKey) { + case "split": + return ( + + ); + case "compress": + return ( + {}} // TODO: Add loading state + params={toolParams} + updateParams={updateParams} + /> + ); + case "merge": + return ( + + ); + default: + return ( + + ); + } +}; + +export default ToolRenderer; \ No newline at end of file diff --git a/frontend/src/components/TopControls.tsx b/frontend/src/components/TopControls.tsx new file mode 100644 index 000000000..1b9822bf3 --- /dev/null +++ b/frontend/src/components/TopControls.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { Button, SegmentedControl } from "@mantine/core"; +import { useMantineColorScheme } from "@mantine/core"; +import LanguageSelector from "./LanguageSelector"; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import LightModeIcon from '@mui/icons-material/LightMode'; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import EditNoteIcon from "@mui/icons-material/EditNote"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; +import { Group } from "@mantine/core"; + +const VIEW_OPTIONS = [ + { + label: ( + + + + ), + value: "viewer", + }, + { + label: ( + + + + ), + value: "pageEditor", + }, + { + label: ( + + + + ), + value: "fileManager", + }, +]; + +interface TopControlsProps { + currentView: string; + setCurrentView: (view: string) => void; +} + +const TopControls: React.FC = ({ + currentView, + setCurrentView, +}) => { + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + + return ( +
+
+ + +
+
+ +
+
+ ); +}; + +export default TopControls; \ No newline at end of file diff --git a/frontend/src/hooks/useToolParams.ts b/frontend/src/hooks/useToolParams.ts new file mode 100644 index 000000000..be6145b3e --- /dev/null +++ b/frontend/src/hooks/useToolParams.ts @@ -0,0 +1,130 @@ +import { useSearchParams } from "react-router-dom"; +import { useEffect } from "react"; + +// Tool parameter definitions (shortened URLs) +const TOOL_PARAMS = { + split: [ + "mode", "p", "hd", "vd", "m", + "type", "val", "level", "meta", "dupes" + ], + compress: [ + "level", "gray", "rmeta", "size", "agg" + ], + merge: [ + "order", "rdupes" + ] +}; + +// Extract params for a specific tool from URL +function getToolParams(toolKey: string, searchParams: URLSearchParams) { + switch (toolKey) { + case "split": + return { + mode: searchParams.get("mode") || "byPages", + pages: searchParams.get("p") || "", + hDiv: searchParams.get("hd") || "", + vDiv: searchParams.get("vd") || "", + merge: searchParams.get("m") === "true", + splitType: searchParams.get("type") || "size", + splitValue: searchParams.get("val") || "", + bookmarkLevel: searchParams.get("level") || "0", + includeMetadata: searchParams.get("meta") === "true", + allowDuplicates: searchParams.get("dupes") === "true", + }; + case "compress": + return { + compressionLevel: parseInt(searchParams.get("level") || "5"), + grayscale: searchParams.get("gray") === "true", + removeMetadata: searchParams.get("rmeta") === "true", + expectedSize: searchParams.get("size") || "", + aggressive: searchParams.get("agg") === "true", + }; + case "merge": + return { + order: searchParams.get("order") || "default", + removeDuplicates: searchParams.get("rdupes") === "true", + }; + default: + return {}; + } +} + +// Update tool-specific params in URL +function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) { + const params = new URLSearchParams(searchParams); + + // Clear tool-specific params + if (toolKey === "split") { + ["mode", "p", "hd", "vd", "m", "type", "val", "level", "meta", "dupes"].forEach((k) => params.delete(k)); + // Set new split params + const merged = { ...getToolParams("split", searchParams), ...newParams }; + params.set("mode", merged.mode); + if (merged.mode === "byPages") params.set("p", merged.pages); + else if (merged.mode === "bySections") { + params.set("hd", merged.hDiv); + params.set("vd", merged.vDiv); + params.set("m", String(merged.merge)); + } else if (merged.mode === "bySizeOrCount") { + params.set("type", merged.splitType); + params.set("val", merged.splitValue); + } else if (merged.mode === "byChapters") { + params.set("level", merged.bookmarkLevel); + params.set("meta", String(merged.includeMetadata)); + params.set("dupes", String(merged.allowDuplicates)); + } + } else if (toolKey === "compress") { + ["level", "gray", "rmeta", "size", "agg"].forEach((k) => params.delete(k)); + const merged = { ...getToolParams("compress", searchParams), ...newParams }; + params.set("level", String(merged.compressionLevel)); + params.set("gray", String(merged.grayscale)); + params.set("rmeta", String(merged.removeMetadata)); + if (merged.expectedSize) params.set("size", merged.expectedSize); + params.set("agg", String(merged.aggressive)); + } else if (toolKey === "merge") { + ["order", "rdupes"].forEach((k) => params.delete(k)); + const merged = { ...getToolParams("merge", searchParams), ...newParams }; + params.set("order", merged.order); + params.set("rdupes", String(merged.removeDuplicates)); + } + + setSearchParams(params, { replace: true }); +} + +export function useToolParams(selectedToolKey: string, currentView: string) { + const [searchParams, setSearchParams] = useSearchParams(); + + const toolParams = getToolParams(selectedToolKey, searchParams); + + const updateParams = (newParams: any) => + updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams); + + // Update URL when core state changes + useEffect(() => { + const params = new URLSearchParams(searchParams); + + // Remove all tool-specific params except for the current tool + Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => { + if (tool !== selectedToolKey) { + keys.forEach((k) => params.delete(k)); + } + }); + + // Collect all params except 'v' + const entries = Array.from(params.entries()).filter(([key]) => key !== "v"); + + // Rebuild params with 'v' first + const newParams = new URLSearchParams(); + newParams.set("v", currentView); + newParams.set("t", selectedToolKey); + entries.forEach(([key, value]) => { + if (key !== "t") newParams.set(key, value); + }); + + setSearchParams(newParams, { replace: true }); + }, [selectedToolKey, currentView, setSearchParams, searchParams]); + + return { + toolParams, + updateParams, + }; +} \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 427dd6645..9ceec9948 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,13 +1,11 @@ import React, { useState, useCallback, useEffect } from "react"; import { useTranslation } from 'react-i18next'; import { useSearchParams } from "react-router-dom"; +import { useToolParams } from "../hooks/useToolParams"; import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; import ContentCutIcon from "@mui/icons-material/ContentCut"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; -import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; -import VisibilityIcon from "@mui/icons-material/Visibility"; -import EditNoteIcon from "@mui/icons-material/EditNote"; -import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core"; +import { Group, Paper, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core"; import ToolPicker from "../components/ToolPicker"; import FileManager from "../components/FileManager"; @@ -16,9 +14,9 @@ import CompressPdfPanel from "../tools/Compress"; import MergePdfPanel from "../tools/Merge"; import PageEditor from "../components/PageEditor"; import Viewer from "../components/Viewer"; -import LanguageSelector from "../components/LanguageSelector"; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; +import TopControls from "../components/TopControls"; +import ToolRenderer from "../components/ToolRenderer"; +import styles from "../styles/HomePage.module.css"; type ToolRegistryEntry = { icon: React.ReactNode; @@ -38,134 +36,25 @@ const baseToolRegistry = { merge: { icon: , component: MergePdfPanel, view: "fileManager" }, }; -const VIEW_OPTIONS = [ - { - label: ( - - - - ), - value: "viewer", - }, - { - label: ( - - - - ), - value: "pageEditor", - }, - { - label: ( - - - - ), - value: "fileManager", - }, -]; -// Utility to extract params for a tool from searchParams -function getToolParams(toolKey: string, searchParams: URLSearchParams) { - switch (toolKey) { - case "split": - return { - mode: searchParams.get("splitMode") || "byPages", - pages: searchParams.get("pages") || "", - hDiv: searchParams.get("hDiv") || "", - vDiv: searchParams.get("vDiv") || "", - merge: searchParams.get("merge") === "true", - splitType: searchParams.get("splitType") || "size", - splitValue: searchParams.get("splitValue") || "", - bookmarkLevel: searchParams.get("bookmarkLevel") || "0", - includeMetadata: searchParams.get("includeMetadata") === "true", - allowDuplicates: searchParams.get("allowDuplicates") === "true", - }; - case "compress": - return { - compressionLevel: parseInt(searchParams.get("compressionLevel") || "5"), - grayscale: searchParams.get("grayscale") === "true", - removeMetadata: searchParams.get("removeMetadata") === "true", - expectedSize: searchParams.get("expectedSize") || "", - aggressive: searchParams.get("aggressive") === "true", - }; - case "merge": - return { - order: searchParams.get("mergeOrder") || "default", - removeDuplicates: searchParams.get("removeDuplicates") === "true", - }; - // Add more tools here as needed - default: - return {}; - } -} - -// Utility to update params for a tool -function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) { - const params = new URLSearchParams(searchParams); - - // Clear tool-specific params - if (toolKey === "split") { - [ - "splitMode", "pages", "hDiv", "vDiv", "merge", - "splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates" - ].forEach((k) => params.delete(k)); - // Set new split params - const merged = { ...getToolParams("split", searchParams), ...newParams }; - params.set("splitMode", merged.mode); - if (merged.mode === "byPages") params.set("pages", merged.pages); - else if (merged.mode === "bySections") { - params.set("hDiv", merged.hDiv); - params.set("vDiv", merged.vDiv); - params.set("merge", String(merged.merge)); - } else if (merged.mode === "bySizeOrCount") { - params.set("splitType", merged.splitType); - params.set("splitValue", merged.splitValue); - } else if (merged.mode === "byChapters") { - params.set("bookmarkLevel", merged.bookmarkLevel); - params.set("includeMetadata", String(merged.includeMetadata)); - params.set("allowDuplicates", String(merged.allowDuplicates)); - } - } else if (toolKey === "compress") { - ["compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive"].forEach((k) => params.delete(k)); - const merged = { ...getToolParams("compress", searchParams), ...newParams }; - params.set("compressionLevel", String(merged.compressionLevel)); - params.set("grayscale", String(merged.grayscale)); - params.set("removeMetadata", String(merged.removeMetadata)); - if (merged.expectedSize) params.set("expectedSize", merged.expectedSize); - params.set("aggressive", String(merged.aggressive)); - } else if (toolKey === "merge") { - ["mergeOrder", "removeDuplicates"].forEach((k) => params.delete(k)); - const merged = { ...getToolParams("merge", searchParams), ...newParams }; - params.set("mergeOrder", merged.order); - params.set("removeDuplicates", String(merged.removeDuplicates)); - } - // Add more tools as needed - - setSearchParams(params, { replace: true }); -} - -// List of all tool-specific params -const TOOL_PARAMS = { - split: [ - "splitMode", "pages", "hDiv", "vDiv", "merge", - "splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates" - ], - compress: [ - "compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive" - ], - merge: [ - "mergeOrder", "removeDuplicates" - ] - // Add more tools as needed -}; export default function HomePage() { const { t } = useTranslation(); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const theme = useMantineTheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + // Core app state + const [selectedToolKey, setSelectedToolKey] = useState(searchParams.get("t") || "split"); + const [currentView, setCurrentView] = useState(searchParams.get("v") || "viewer"); + const [pdfFile, setPdfFile] = useState(null); + const [files, setFiles] = useState([]); + const [downloadUrl, setDownloadUrl] = useState(null); + const [sidebarsVisible, setSidebarsVisible] = useState(true); + + // URL parameter management + const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView); + // Create translated tool registry const toolRegistry: ToolRegistry = { split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") }, @@ -173,43 +62,6 @@ export default function HomePage() { merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") }, }; - // Core app state - const [selectedToolKey, setSelectedToolKey] = useState(searchParams.get("tool") || "split"); - const [currentView, setCurrentView] = useState(searchParams.get("view") || "viewer"); - const [pdfFile, setPdfFile] = useState(null); - const [files, setFiles] = useState([]); - const [downloadUrl, setDownloadUrl] = useState(null); - const [sidebarsVisible, setSidebarsVisible] = useState(true); - - const toolParams = getToolParams(selectedToolKey, searchParams); - - const updateParams = (newParams: any) => - updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams); - - // Update URL when core state changes - useEffect(() => { - const params = new URLSearchParams(searchParams); - - // Remove all tool-specific params except for the current tool - Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => { - if (tool !== selectedToolKey) { - keys.forEach((k) => params.delete(k)); - } - }); - - // Collect all params except 'view' - const entries = Array.from(params.entries()).filter(([key]) => key !== "view"); - - // Rebuild params with 'view' first - const newParams = new URLSearchParams(); - newParams.set("view", currentView); - newParams.set("tool", selectedToolKey); - entries.forEach(([key, value]) => { - if (key !== "tool") newParams.set(key, value); - }); - - setSearchParams(newParams, { replace: true }); - }, [selectedToolKey, currentView, setSearchParams, searchParams]); // Handle tool selection const handleToolSelect = useCallback( @@ -222,73 +74,18 @@ export default function HomePage() { const selectedTool = toolRegistry[selectedToolKey]; - // Tool component rendering - const renderTool = () => { - if (!selectedTool || !selectedTool.component) { - return
Tool not found
; - } - - // Pass tool-specific props - switch (selectedToolKey) { - case "split": - return React.createElement(selectedTool.component, { - file: pdfFile, - downloadUrl, - setDownloadUrl, - params: toolParams, - updateParams, - }); - case "compress": - return React.createElement(selectedTool.component, { - files, - setDownloadUrl, - setLoading: (loading: boolean) => {}, // TODO: Add loading state - params: toolParams, - updateParams, - }); - case "merge": - return React.createElement(selectedTool.component, { - files, - setDownloadUrl, - params: toolParams, - updateParams, - }); - default: - return React.createElement(selectedTool.component, { - files, - setDownloadUrl, - params: toolParams, - updateParams, - }); - } - }; - return ( {/* Left: Tool Picker */} {sidebarsVisible && ( - {/* Overlayed View Switcher + Theme Toggle */} -
-
- - -
-
- -
-
+ {/* Top Controls */} + {/* Main content area */} - + {(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? ( - {selectedTool && selectedTool.component && renderTool()} + )} @@ -439,7 +172,7 @@ export default function HomePage() { variant="light" color="blue" size="xs" - style={{ position: "fixed", top: 16, right: 16, zIndex: 200 }} + className={styles.sidebarToggle} onClick={() => setSidebarsVisible((v) => !v)} > {t("sidebar.toggle", sidebarsVisible ? "Hide Sidebars" : "Show Sidebars")} diff --git a/frontend/src/styles/HomePage.module.css b/frontend/src/styles/HomePage.module.css new file mode 100644 index 000000000..5ffd91e12 --- /dev/null +++ b/frontend/src/styles/HomePage.module.css @@ -0,0 +1,95 @@ +/* Main container */ +.container { + min-height: 100vh; + width: 100vw; + overflow: hidden; + flex-wrap: nowrap; + display: flex; +} + +/* Left sidebar */ +.leftSidebar { + min-width: 180px; + max-width: 240px; + width: 16vw; + height: 100vh; + z-index: 101; + display: flex; + flex-direction: column; +} + +.leftSidebarLight { + border-right: 1px solid #e9ecef; + background: #fff; +} + +.leftSidebarDark { + border-right: 1px solid var(--mantine-color-dark-4); + background: var(--mantine-color-dark-7); +} + +/* Main content area */ +.mainContent { + flex: 1; + height: 100vh; + min-width: 20rem; + position: relative; + display: flex; + flex-direction: column; + transition: all 0.3s; +} + +.mainContentLight { + background: #f8f9fa; +} + +.mainContentDark { + background: var(--mantine-color-dark-6); +} + +/* Main paper container */ +.mainPaper { + flex: 1; + min-height: 0; + margin-top: 0; + box-sizing: border-box; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.mainPaperInner { + flex: 1; + min-height: 0; +} + +/* Right sidebar */ +.rightSidebar { + min-width: 260px; + max-width: 400px; + width: 22vw; + height: 100vh; + padding: 24px; + gap: 16px; + z-index: 100; + display: flex; + flex-direction: column; +} + +.rightSidebarLight { + border-left: 1px solid #e9ecef; + background: #fff; +} + +.rightSidebarDark { + border-left: 1px solid var(--mantine-color-dark-4); + background: var(--mantine-color-dark-7); +} + +/* Sidebar toggle button */ +.sidebarToggle { + position: fixed; + top: 16px; + right: 16px; + z-index: 200; +} \ No newline at end of file