Homepage refactor

This commit is contained in:
Reece 2025-06-05 21:59:18 +01:00
parent 7dd79aa2a1
commit 0c76bf2c8b
6 changed files with 453 additions and 315 deletions

View File

@ -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"
}
];

View File

@ -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<ToolRendererProps> = ({
selectedToolKey,
selectedTool,
pdfFile,
files,
downloadUrl,
setDownloadUrl,
toolParams,
updateParams,
}) => {
if (!selectedTool || !selectedTool.component) {
return <div>Tool not found</div>;
}
const ToolComponent = selectedTool.component;
// Pass tool-specific props
switch (selectedToolKey) {
case "split":
return (
<ToolComponent
file={pdfFile}
downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl}
params={toolParams}
updateParams={updateParams}
/>
);
case "compress":
return (
<ToolComponent
files={files}
setDownloadUrl={setDownloadUrl}
setLoading={(loading: boolean) => {}} // TODO: Add loading state
params={toolParams}
updateParams={updateParams}
/>
);
case "merge":
return (
<ToolComponent
files={files}
setDownloadUrl={setDownloadUrl}
params={toolParams}
updateParams={updateParams}
/>
);
default:
return (
<ToolComponent
files={files}
setDownloadUrl={setDownloadUrl}
params={toolParams}
updateParams={updateParams}
/>
);
}
};
export default ToolRenderer;

View File

@ -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: (
<Group gap={4}>
<VisibilityIcon fontSize="small" />
</Group>
),
value: "viewer",
},
{
label: (
<Group gap={4}>
<EditNoteIcon fontSize="small" />
</Group>
),
value: "pageEditor",
},
{
label: (
<Group gap={4}>
<InsertDriveFileIcon fontSize="small" />
</Group>
),
value: "fileManager",
},
];
interface TopControlsProps {
currentView: string;
setCurrentView: (view: string) => void;
}
const TopControls: React.FC<TopControlsProps> = ({
currentView,
setCurrentView,
}) => {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return (
<div
style={{
position: "absolute",
left: 0,
width: "100%",
top: 0,
zIndex: 30,
pointerEvents: "none",
}}
>
<div
style={{
position: "absolute",
left: 16,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "auto",
display: "flex",
gap: 12,
alignItems: "center",
}}
>
<Button
onClick={toggleColorScheme}
variant="subtle"
size="md"
aria-label="Toggle theme"
>
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
</Button>
<LanguageSelector />
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
pointerEvents: "auto",
}}
>
<SegmentedControl
data={VIEW_OPTIONS}
value={currentView}
onChange={setCurrentView}
color="blue"
radius="xl"
size="md"
fullWidth
/>
</div>
</div>
);
};
export default TopControls;

View File

@ -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,
};
}

View File

@ -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: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
};
const VIEW_OPTIONS = [
{
label: (
<Group gap={4}>
<VisibilityIcon fontSize="small" />
</Group>
),
value: "viewer",
},
{
label: (
<Group gap={4}>
<EditNoteIcon fontSize="small" />
</Group>
),
value: "pageEditor",
},
{
label: (
<Group gap={4}>
<InsertDriveFileIcon fontSize="small" />
</Group>
),
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<string>(searchParams.get("t") || "split");
const [currentView, setCurrentView] = useState<string>(searchParams.get("v") || "viewer");
const [pdfFile, setPdfFile] = useState<any>(null);
const [files, setFiles] = useState<any[]>([]);
const [downloadUrl, setDownloadUrl] = useState<string | null>(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<string>(searchParams.get("tool") || "split");
const [currentView, setCurrentView] = useState<string>(searchParams.get("view") || "viewer");
const [pdfFile, setPdfFile] = useState<any>(null);
const [files, setFiles] = useState<any[]>([]);
const [downloadUrl, setDownloadUrl] = useState<string | null>(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 <div>Tool not found</div>;
}
// 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 (
<Group
align="flex-start"
gap={0}
style={{
minHeight: "100vh",
width: "100vw",
overflow: "hidden",
flexWrap: "nowrap",
display: "flex",
}}
className={styles.container}
>
{/* Left: Tool Picker */}
{sidebarsVisible && (
<Box
style={{
minWidth: 180,
maxWidth: 240,
width: "16vw",
height: "100vh",
borderRight: `1px solid ${colorScheme === "dark" ? theme.colors.dark[4] : "#e9ecef"}`,
background: colorScheme === "dark" ? theme.colors.dark[7] : "#fff",
zIndex: 101,
display: "flex",
flexDirection: "column",
}}
className={`${styles.leftSidebar} ${
colorScheme === "dark" ? styles.leftSidebarDark : styles.leftSidebarLight
}`}
>
<ToolPicker
selectedToolKey={selectedToolKey}
@ -300,86 +97,23 @@ export default function HomePage() {
{/* Middle: Main View */}
<Box
style={{
flex: 1,
height: "100vh",
minWidth: "20rem",
position: "relative",
display: "flex",
flexDirection: "column",
transition: "all 0.3s",
background: colorScheme === "dark" ? theme.colors.dark[6] : "#f8f9fa",
}}
className={`${styles.mainContent} ${
colorScheme === "dark" ? styles.mainContentDark : styles.mainContentLight
}`}
>
{/* Overlayed View Switcher + Theme Toggle */}
<div
style={{
position: "absolute",
left: 0,
width: "100%",
top: 0,
zIndex: 30,
pointerEvents: "none",
}}
>
<div
style={{
position: "absolute",
left: 16,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "auto",
display: "flex",
gap: 12,
alignItems: "center",
}}
>
<Button
onClick={toggleColorScheme}
variant="subtle"
size="md"
aria-label="Toggle theme"
>
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
</Button>
<LanguageSelector />
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
pointerEvents: "auto",
}}
>
<SegmentedControl
data={VIEW_OPTIONS}
value={currentView}
onChange={setCurrentView}
color="blue"
radius="xl"
size="md"
fullWidth
/>
</div>
</div>
{/* Top Controls */}
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
/>
{/* Main content area */}
<Paper
radius="0 0 xl xl"
shadow="sm"
p={0}
style={{
flex: 1,
minHeight: 0,
marginTop: 0,
boxSizing: "border-box",
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
className={styles.mainPaper}
>
<Box style={{ flex: 1, minHeight: 0 }}>
<Box className={styles.mainPaperInner}>
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
<FileManager
files={files}
@ -416,21 +150,20 @@ export default function HomePage() {
{/* Right: Tool Interaction */}
{sidebarsVisible && (
<Box
style={{
minWidth: 260,
maxWidth: 400,
width: "22vw",
height: "100vh",
borderLeft: `1px solid ${colorScheme === "dark" ? theme.colors.dark[4] : "#e9ecef"}`,
background: colorScheme === "dark" ? theme.colors.dark[7] : "#fff",
padding: 24,
gap: 16,
zIndex: 100,
display: "flex",
flexDirection: "column",
}}
className={`${styles.rightSidebar} ${
colorScheme === "dark" ? styles.rightSidebarDark : styles.rightSidebarLight
}`}
>
{selectedTool && selectedTool.component && renderTool()}
<ToolRenderer
selectedToolKey={selectedToolKey}
selectedTool={selectedTool}
pdfFile={pdfFile}
files={files}
downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl}
toolParams={toolParams}
updateParams={updateParams}
/>
</Box>
)}
@ -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")}

View File

@ -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;
}