- Viewer overhaul

-Dark mode toggle
-URL params improvements
-app.js set up fix
- UI clean up
This commit is contained in:
Reece 2025-05-27 19:22:26 +01:00
parent 41c82b15da
commit d216811317
9 changed files with 740 additions and 348 deletions

View File

@ -1,31 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import './index.css';
import HomePage from './pages/HomePage';
import SplitPdfPanel from './tools/Split';
import reportWebVitals from './reportWebVitals';
export default function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/split" element={<SplitPdfPanel />} />
</Routes>
);
export default function App({ colorScheme, toggleColorScheme }) {
return <HomePage colorScheme={colorScheme} toggleColorScheme={toggleColorScheme} />;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ColorSchemeScript />
<MantineProvider withGlobalStyles withNormalizeCSS>
<BrowserRouter>
<App />
</BrowserRouter>
</MantineProvider>
</React.StrictMode>
);
reportWebVitals();

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex } from "@mantine/core";
import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex, ThemeIcon } from "@mantine/core";
import { Dropzone, MIME_TYPES } from "@mantine/dropzone";
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
GlobalWorkerOptions.workerSrc =
(import.meta as any).env?.PUBLIC_URL
@ -93,7 +94,15 @@ function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) {
{thumb ? (
<Image src={thumb} alt="PDF thumbnail" height={110} width={80} fit="contain" radius="sm" />
) : (
<Image src="/images/pdf-placeholder.svg" alt="PDF" height={60} width={60} fit="contain" radius="sm" />
<ThemeIcon
variant="light"
color="red"
size={60}
radius="sm"
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
>
<PictureAsPdfIcon style={{ fontSize: 40 }} />
</ThemeIcon>
)}
</Box>
<Text fw={500} size="sm" lineClamp={1} ta="center">
@ -145,21 +154,22 @@ const FileManager: React.FC<FileManagerProps> = ({
};
return (
<div style={{ width: "100%", margin: "0 auto" }}>
<div style={{ width: "100%", margin: "0 auto", justifyContent: "center", display: "flex", flexDirection: "column", alignItems: "center", padding: "20px" }}>
<Dropzone
onDrop={handleDrop}
accept={[MIME_TYPES.pdf]}
multiple={allowMultiple}
maxSize={20 * 1024 * 1024}
style={{
marginTop: 16,
marginBottom: 16,
border: "2px dashed rgb(202, 202, 202)",
background: "#f8fafc",
borderRadius: 8,
minHeight: 120,
display: "flex",
alignItems: "center",
justifyContent: "center",
width:"90%"
}}
>
<Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>

View File

@ -27,7 +27,6 @@ const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, tool
<Box
style={{
width: 220,
background: "#f8f9fa",
borderRight: "1px solid #e9ecef",
minHeight: "100vh",
padding: 16,

View File

@ -1,18 +1,121 @@
import React, { useEffect, useState } from "react";
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group } from "@mantine/core";
import React, { useEffect, useState, useRef } from "react";
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme } from "@mantine/core";
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import FirstPageIcon from "@mui/icons-material/FirstPage";
import LastPageIcon from "@mui/icons-material/LastPage";
import ViewSidebarIcon from "@mui/icons-material/ViewSidebar";
import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book)
import DescriptionIcon from "@mui/icons-material/Description"; // for single page
import { useLocalStorage } from "@mantine/hooks";
GlobalWorkerOptions.workerSrc = `${process.env.PUBLIC_URL}/pdf.worker.js`;
export interface ViewerProps {
pdfFile: { file: File; url: string } | null;
setPdfFile: (file: { file: File; url: string } | null) => void;
sidebarsVisible: boolean;
setSidebarsVisible: (v: boolean) => void;
}
const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
const Viewer: React.FC<ViewerProps> = ({
pdfFile,
setPdfFile,
sidebarsVisible,
setSidebarsVisible,
}) => {
const theme = useMantineTheme();
const [numPages, setNumPages] = useState<number>(0);
const [pageImages, setPageImages] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [currentPage, setCurrentPage] = useState<number | null>(null);
const [dualPage, setDualPage] = useState(false);
const [zoom, setZoom] = useState(1); // 1 = 100%
const pageRefs = useRef<(HTMLImageElement | null)[]>([]);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const userInitiatedRef = useRef(false);
const suppressScrollRef = useRef(false);
// Listen for hash changes and update currentPage
useEffect(() => {
function handleHashChange() {
if (window.location.hash.startsWith("#page=")) {
const page = parseInt(window.location.hash.replace("#page=", ""), 10);
if (!isNaN(page) && page >= 1 && page <= numPages) {
setCurrentPage(page);
}
}
userInitiatedRef.current = false;
}
window.addEventListener("hashchange", handleHashChange);
handleHashChange(); // Run on mount
return () => window.removeEventListener("hashchange", handleHashChange);
}, [numPages]);
// Scroll to the current page when it changes
useEffect(() => {
if (currentPage && pageRefs.current[currentPage - 1]) {
suppressScrollRef.current = true;
const el = pageRefs.current[currentPage - 1];
el?.scrollIntoView({ behavior: "smooth", block: "center" });
// Try to use scrollend if supported
const viewport = scrollAreaRef.current;
let timeout: NodeJS.Timeout | null = null;
let scrollEndHandler: (() => void) | null = null;
if (viewport && "onscrollend" in viewport) {
scrollEndHandler = () => {
suppressScrollRef.current = false;
viewport.removeEventListener("scrollend", scrollEndHandler!);
};
viewport.addEventListener("scrollend", scrollEndHandler);
} else {
// Fallback for non-Chromium browsers
timeout = setTimeout(() => {
suppressScrollRef.current = false;
}, 1000);
}
return () => {
if (viewport && scrollEndHandler) {
viewport.removeEventListener("scrollend", scrollEndHandler);
}
if (timeout) clearTimeout(timeout);
};
}
}, [currentPage, pageImages]);
// Detect visible page on scroll and update hash
const handleScroll = () => {
if (suppressScrollRef.current) return;
const scrollArea = scrollAreaRef.current;
if (!scrollArea || !pageRefs.current.length) return;
const areaRect = scrollArea.getBoundingClientRect();
let closestIdx = 0;
let minDist = Infinity;
pageRefs.current.forEach((img, idx) => {
if (img) {
const imgRect = img.getBoundingClientRect();
const dist = Math.abs(imgRect.top - areaRect.top);
if (dist < minDist) {
minDist = dist;
closestIdx = idx;
}
}
});
if (currentPage !== closestIdx + 1) {
setCurrentPage(closestIdx + 1);
if (window.location.hash !== `#page=${closestIdx + 1}`) {
window.location.hash = `#page=${closestIdx + 1}`;
}
}
};
useEffect(() => {
let cancelled = false;
@ -49,12 +152,31 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
return () => { cancelled = true; };
}, [pdfFile]);
useEffect(() => {
const viewport = scrollAreaRef.current;
if (!viewport) return;
const handler = () => {
handleScroll();
};
viewport.addEventListener("scroll", handler);
return () => viewport.removeEventListener("scroll", handler);
}, [pageImages]);
return (
<Paper shadow="xs" radius="md" p="md" style={{ height: "100%", minHeight: 400, display: "flex", flexDirection: "column" }}>
<Paper
shadow="xs"
radius="md"
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
position: "relative",
}}
>
{!pdfFile ? (
<Center style={{ flex: 1 }}>
<Stack align="center">
<Text color="dimmed">No PDF loaded. Click to upload a PDF.</Text>
<Text c="dimmed">No PDF loaded. Click to upload a PDF.</Text>
<Button
component="label"
variant="outline"
@ -81,39 +203,222 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
<Loader size="lg" />
</Center>
) : (
<ScrollArea style={{ flex: 1, height: "100%" }}>
<ScrollArea
style={{ flex: 1, height: "100%", position: "relative"}}
viewportRef={scrollAreaRef}
>
<Stack gap="xl" align="center" >
{pageImages.length === 0 && (
<Text color="dimmed">No pages to display.</Text>
)}
{pageImages.map((img, idx) => (
{dualPage
? Array.from({ length: Math.ceil(pageImages.length / 2) }).map((_, i) => (
<Group key={i} gap="md" align="flex-start" style={{ width: "100%", justifyContent: "center" }}>
<img
ref={el => { pageRefs.current[i * 2] = el; }}
src={pageImages[i * 2]}
alt={`Page ${i * 2 + 1}`}
style={{
width: `${100 * zoom}%`,
maxWidth: 700 * zoom,
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
borderRadius: 8,
marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row
}}
/>
{pageImages[i * 2 + 1] && (
<img
ref={el => { pageRefs.current[i * 2 + 1] = el; }}
src={pageImages[i * 2 + 1]}
alt={`Page ${i * 2 + 2}`}
style={{
width: `${100 * zoom}%`,
maxWidth: 700 * zoom,
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
borderRadius: 8,
marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row
}}
/>
)}
</Group>
))
: pageImages.map((img, idx) => (
<img
key={idx}
ref={el => { pageRefs.current[idx] = el; }}
src={img}
alt={`Page ${idx + 1}`}
style={{
width: "100%",
maxWidth: 700,
width: `${100 * zoom}%`,
maxWidth: 700 * zoom,
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
borderRadius: 8,
background: "#fff"
marginTop: idx === 0 ? theme.spacing.xl : 0, // <-- add gap to first page
}}
/>
))}
</Stack>
{/* Navigation bar overlays the scroll area */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
zIndex: 50,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
background: "transparent",
}}
>
<Paper
radius="xl xl 0 0"
shadow="sm"
p={12}
style={{
display: "flex",
alignItems: "center",
gap: 10,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
pointerEvents: "auto",
minWidth: 420,
maxWidth: 700,
flexWrap: "wrap",
}}
>
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={() => {
window.location.hash = `#page=1`;
}}
disabled={currentPage === 1}
style={{ minWidth: 36 }}
>
<FirstPageIcon fontSize="small" />
</Button>
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={() => {
window.location.hash = `#page=${Math.max(1, (currentPage || 1) - 1)}`;
}}
disabled={currentPage === 1}
style={{ minWidth: 36 }}
>
<ArrowBackIosNewIcon fontSize="small" />
</Button>
<NumberInput
value={currentPage || 1}
onChange={value => {
const page = Number(value);
if (!isNaN(page) && page >= 1 && page <= numPages) {
window.location.hash = `#page=${page}`;
}
}}
min={1}
max={numPages}
hideControls
styles={{
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16},
}}
/>
<span style={{ fontWeight: 500, fontSize: 16 }}>
/ {numPages}
</span>
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={() => {
window.location.hash = `#page=${Math.min(numPages, (currentPage || 1) + 1)}`;
}}
disabled={currentPage === numPages}
style={{ minWidth: 36 }}
>
<ArrowForwardIosIcon fontSize="small" />
</Button>
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={() => {
window.location.hash = `#page=${numPages}`;
}}
disabled={currentPage === numPages}
style={{ minWidth: 36 }}
>
<LastPageIcon fontSize="small" />
</Button>
<Button
variant={dualPage ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={() => setDualPage(v => !v)}
style={{ minWidth: 36 }}
title={dualPage ? "Single Page View" : "Dual Page View"}
>
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
</Button>
<Button
variant="subtle"
color="blue"
size="md"
radius="xl"
onClick={() => setSidebarsVisible(!sidebarsVisible)}
style={{ minWidth: 36 }}
title={sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
>
<ViewSidebarIcon
fontSize="small"
style={{
transform: sidebarsVisible ? "none" : "scaleX(-1)",
transition: "transform 0.2s"
}}
/>
</Button>
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
<Button
variant="subtle"
color="blue"
size="md"
radius="xl"
onClick={() => setZoom(z => Math.max(0.1, z - 0.1))}
style={{ minWidth: 32, padding: 0 }}
title="Zoom out"
></Button>
<span style={{ minWidth: 40, textAlign: "center" }}>{Math.round(zoom * 100)}%</span>
<Button
variant="subtle"
color="blue"
size="md"
radius="xl"
onClick={() => setZoom(z => Math.min(5, z + 0.1))}
style={{ minWidth: 32, padding: 0 }}
title="Zoom in"
>+</Button>
</Group>
</Paper>
</div>
</ScrollArea>
)}
{pdfFile && (
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="red"
onClick={() => setPdfFile(null)}
>
Close PDF
</Button>
</Group>
)}
</Paper>
);
};

View File

@ -7,6 +7,7 @@ import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
root.render(
<React.StrictMode>

View File

@ -6,7 +6,7 @@ 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 } from "@mantine/core";
import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core";
import ToolPicker from "../components/ToolPicker";
import FileManager from "../components/FileManager";
@ -15,6 +15,8 @@ import CompressPdfPanel from "../tools/Compress";
import MergePdfPanel from "../tools/Merge";
import PageEditor from "../components/PageEditor";
import Viewer from "../components/Viewer";
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
type ToolRegistryEntry = {
icon: React.ReactNode;
@ -60,18 +62,11 @@ const VIEW_OPTIONS = [
},
];
export default function HomePage() {
const [searchParams, setSearchParams] = useSearchParams();
// 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);
// Tool-specific parameters
const [splitParams, setSplitParams] = useState({
// 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") || "0",
@ -82,15 +77,121 @@ export default function HomePage() {
bookmarkLevel: searchParams.get("bookmarkLevel") || "0",
includeMetadata: searchParams.get("includeMetadata") === "true",
allowDuplicates: searchParams.get("allowDuplicates") === "true",
});
};
case "compress":
return {
level: searchParams.get("compressLevel") || "medium",
keepQuality: searchParams.get("keepQuality") === "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") {
["compressLevel", "keepQuality"].forEach((k) => params.delete(k));
const merged = { ...getToolParams("compress", searchParams), ...newParams };
params.set("compressLevel", merged.level);
params.set("keepQuality", String(merged.keepQuality));
} 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: [
"compressLevel", "keepQuality"
],
merge: [
"mergeOrder", "removeDuplicates"
]
// Add more tools as needed
};
export default function HomePage() {
const [searchParams, setSearchParams] = useSearchParams();
const theme = useMantineTheme();
// 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);
params.set("tool", selectedToolKey);
params.set("view", currentView);
setSearchParams(params, { replace: true });
}, [selectedToolKey, currentView, setSearchParams]);
// 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(
@ -101,51 +202,6 @@ export default function HomePage() {
[toolRegistry]
);
// Handle split parameter updates
const updateSplitParams = useCallback((newParams: Partial<typeof splitParams>) => {
setSplitParams(prev => {
const updated = { ...prev, ...newParams };
// Update URL with split params
const params = new URLSearchParams(searchParams);
// Clear old parameters when mode changes
if (newParams.mode && newParams.mode !== prev.mode) {
params.delete("pages");
params.delete("hDiv");
params.delete("vDiv");
params.delete("merge");
params.delete("splitType");
params.delete("splitValue");
params.delete("bookmarkLevel");
params.delete("includeMetadata");
params.delete("allowDuplicates");
}
// Set the mode
params.set("splitMode", updated.mode);
// Set mode-specific parameters
if (updated.mode === "byPages" && updated.pages) {
params.set("pages", updated.pages);
} else if (updated.mode === "bySections") {
params.set("hDiv", updated.hDiv);
params.set("vDiv", updated.vDiv);
params.set("merge", String(updated.merge));
} else if (updated.mode === "bySizeOrCount") {
params.set("splitType", updated.splitType);
if (updated.splitValue) params.set("splitValue", updated.splitValue);
} else if (updated.mode === "byChapters") {
params.set("bookmarkLevel", updated.bookmarkLevel);
params.set("includeMetadata", String(updated.includeMetadata));
params.set("allowDuplicates", String(updated.allowDuplicates));
}
setSearchParams(params, { replace: true });
return updated;
});
}, [searchParams, setSearchParams]);
const selectedTool = toolRegistry[selectedToolKey];
// Tool component rendering
@ -154,78 +210,80 @@ export default function HomePage() {
return <div>Tool not found</div>;
}
// Pass appropriate props based on tool type
if (selectedToolKey === "split") {
// Pass only the necessary props
return React.createElement(selectedTool.component, {
file: pdfFile,
setPdfFile,
downloadUrl,
setDownloadUrl,
// Tool-specific params and update function
params: splitParams,
updateParams: updateSplitParams
});
}
// For other tools, pass standard props
return React.createElement(selectedTool.component, {
file: pdfFile,
setPdfFile,
files,
setFiles,
downloadUrl,
setDownloadUrl,
params: toolParams,
updateParams,
});
};
return (
<Group align="flex-start" gap={0} style={{ minHeight: "100vh" }}>
{/* Left: Tool Picker */}
{sidebarsVisible && (
<ToolPicker
selectedToolKey={selectedToolKey}
onSelect={handleToolSelect}
toolRegistry={toolRegistry}
/>
)}
{/* Middle: Main View (Viewer, Editor, Manager) */}
<Box
style={{
width: "calc(100vw - 220px - 380px)",
marginLeft: 220,
marginRight: 380,
padding: 24,
background: "#fff",
position: "relative",
minHeight: "100vh",
width: sidebarsVisible
? "calc(100vw - 220px - 380px)"
: "100vw",
marginLeft: sidebarsVisible ? 220 : 0,
marginRight: sidebarsVisible ? 380 : 0,
position: "relative", // <-- important for absolute overlay
height: "100vh",
overflowY: "auto",
display: "flex",
flexDirection: "column",
transition: "all 0.3s",
}}
>
<Center>
<Paper
radius="xl"
shadow="sm"
p={4}
{/* Overlayed View Switcher */}
<div
style={{
display: "inline-block",
marginTop: 8,
marginBottom: 24,
background: "#f8f9fa",
zIndex: 10,
position: "absolute",
left: 0,
width: "100%",
display: "flex",
justifyContent: "center",
zIndex: 30,
}}
>
<div style={{ pointerEvents: "auto" }}>
<SegmentedControl
data={VIEW_OPTIONS}
value={currentView}
onChange={setCurrentView} // Using the state setter directly
onChange={setCurrentView}
color="blue"
radius="xl"
size="md"
fullWidth
/>
</Paper>
</Center>
<Box>
</div>
</div>
{/* Main content area with matching Paper */}
<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",
}}
>
<Box style={{ flex: 1, minHeight: 0 }}>
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
<FileManager
files={files}
@ -237,6 +295,8 @@ export default function HomePage() {
<Viewer
pdfFile={pdfFile}
setPdfFile={setPdfFile}
sidebarsVisible={sidebarsVisible}
setSidebarsVisible={setSidebarsVisible}
/>
) : currentView === "pageEditor" ? (
<PageEditor
@ -254,14 +314,15 @@ export default function HomePage() {
/>
)}
</Box>
</Paper>
</Box>
{/* Right: Tool Interaction */}
{sidebarsVisible && (
<Box
style={{
width: 380,
background: "#f8f9fa",
borderLeft: "1px solid #e9ecef",
minHeight: "100vh",
height: "100vh",
padding: 24,
gap: 16,
position: "fixed",
@ -269,7 +330,6 @@ export default function HomePage() {
top: 0,
bottom: 0,
zIndex: 100,
overflowY: "auto",
}}
>
{selectedTool && selectedTool.component && (
@ -278,6 +338,16 @@ export default function HomePage() {
</>
)}
</Box>
)}
<Button
variant="light"
color="blue"
size="xs"
style={{ position: "absolute", top: 16, right: 16, zIndex: 200 }}
onClick={() => setSidebarsVisible(v => !v)}
>
{sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
</Button>
</Group>
);
}

View File

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useSearchParams } from "react-router-dom";
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
export interface CompressProps {
@ -12,6 +13,9 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
setDownloadUrl,
setLoading,
}) => {
const [searchParams] = useSearchParams();
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
const [compressionLevel, setCompressionLevel] = useState<number>(5);
const [grayscale, setGrayscale] = useState<boolean>(false);
@ -56,8 +60,8 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
}
};
return (
<Paper shadow="xs" p="md" radius="md" withBorder>
<Stack>
<Text fw={500} mb={4}>Select files to compress:</Text>
<Stack gap={4}>
@ -118,7 +122,6 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
</Button>
</Stack>
</Paper>
);
};

View File

@ -1,12 +1,24 @@
import React, { useState, useEffect } from "react";
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
import { useSearchParams } from "react-router-dom";
export interface MergePdfPanelProps {
files: File[];
setDownloadUrl: (url: string) => void;
params: {
order: string;
removeDuplicates: boolean;
};
updateParams: (newParams: Partial<MergePdfPanelProps["params"]>) => void;
}
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl }) => {
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
files,
setDownloadUrl,
params,
updateParams,
}) => {
const [searchParams] = useSearchParams();
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
@ -59,8 +71,9 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
const selectedCount = selectedFiles.filter(Boolean).length;
const { order, removeDuplicates } = params;
return (
<Paper shadow="xs" radius="md" p="md" withBorder>
<Stack>
<Text fw={500} size="lg">Merge PDFs</Text>
<Stack gap={4}>
@ -104,8 +117,12 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
Download Merged PDF
</Button>
)}
<Checkbox
label="Remove Duplicates"
checked={removeDuplicates}
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
/>
</Stack>
</Paper>
);
};

View File

@ -7,7 +7,9 @@ import {
Checkbox,
Notification,
Stack,
Paper,
} from "@mantine/core";
import { useSearchParams } from "react-router-dom";
import DownloadIcon from "@mui/icons-material/Download";
export interface SplitPdfPanelProps {
@ -26,7 +28,7 @@ export interface SplitPdfPanelProps {
includeMetadata: boolean;
allowDuplicates: boolean;
};
updateParams: (newParams: Partial<SplitPdfPanelProps['params']>) => void;
updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void;
}
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
@ -36,16 +38,26 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
params,
updateParams,
}) => {
const [searchParams] = useSearchParams();
const [status, setStatus] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const {
mode, pages, hDiv, vDiv, merge,
splitType, splitValue, bookmarkLevel,
includeMetadata, allowDuplicates
mode,
pages,
hDiv,
vDiv,
merge,
splitType,
splitValue,
bookmarkLevel,
includeMetadata,
allowDuplicates,
} = params;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {