mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	Mantine overhaul
This commit is contained in:
		
							parent
							
								
									d669964975
								
							
						
					
					
						commit
						f789533c83
					
				
							
								
								
									
										911
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										911
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -6,6 +6,8 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@emotion/react": "^11.14.0", |     "@emotion/react": "^11.14.0", | ||||||
|     "@emotion/styled": "^11.14.0", |     "@emotion/styled": "^11.14.0", | ||||||
|  |     "@mantine/core": "^8.0.1", | ||||||
|  |     "@mantine/hooks": "^8.0.1", | ||||||
|     "@mui/icons-material": "^7.1.0", |     "@mui/icons-material": "^7.1.0", | ||||||
|     "@mui/material": "^7.1.0", |     "@mui/material": "^7.1.0", | ||||||
|     "@testing-library/dom": "^10.4.0", |     "@testing-library/dom": "^10.4.0", | ||||||
| @ -42,5 +44,11 @@ | |||||||
|       "last 1 firefox version", |       "last 1 firefox version", | ||||||
|       "last 1 safari version" |       "last 1 safari version" | ||||||
|     ] |     ] | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "postcss": "^8.5.3", | ||||||
|  |     "postcss-cli": "^11.0.1", | ||||||
|  |     "postcss-preset-mantine": "^1.17.0", | ||||||
|  |     "postcss-simple-vars": "^7.0.1" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,5 +2,6 @@ module.exports = { | |||||||
|   plugins: { |   plugins: { | ||||||
|     tailwindcss: {}, |     tailwindcss: {}, | ||||||
|     autoprefixer: {}, |     autoprefixer: {}, | ||||||
|  | 
 | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,16 +1,31 @@ | |||||||
| import React from "react"; | import React from 'react'; | ||||||
| import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; | 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'; | ||||||
| 
 | 
 | ||||||
| import HomePage from "./pages/HomePage"; | export default function App() { | ||||||
| 
 |  | ||||||
| function App() { |  | ||||||
|   return ( |   return ( | ||||||
|     <Router> |     <Routes> | ||||||
|       <Routes> |       <Route path="/" element={<HomePage />} /> | ||||||
|         <Route path="/" element={<HomePage />} /> |       <Route path="/split" element={<SplitPdfPanel />} /> | ||||||
|       </Routes> |     </Routes> | ||||||
|     </Router> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default App; | const root = ReactDOM.createRoot(document.getElementById('root')); | ||||||
|  | root.render( | ||||||
|  |   <React.StrictMode> | ||||||
|  |     <ColorSchemeScript /> | ||||||
|  |     <MantineProvider withGlobalStyles withNormalizeCSS> | ||||||
|  |       <BrowserRouter> | ||||||
|  |         <App /> | ||||||
|  |       </BrowserRouter> | ||||||
|  |     </MantineProvider> | ||||||
|  |   </React.StrictMode> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | reportWebVitals(); | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								frontend/src/components/FileManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/components/FileManager.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | export default function FileManager({ files, setFiles, allowMultiple = true }) { | ||||||
|  |   const handleFileUpload = (e) => { | ||||||
|  |     const uploadedFiles = Array.from(e.target.files); | ||||||
|  |     setFiles((prevFiles) => (allowMultiple ? [...prevFiles, ...uploadedFiles] : uploadedFiles)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleRemoveFile = (index) => { | ||||||
|  |     setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="space-y-4 w-full max-w-3xl"> | ||||||
|  |       <input | ||||||
|  |         type="file" | ||||||
|  |         accept="application/pdf" | ||||||
|  |         multiple={allowMultiple} | ||||||
|  |         onChange={handleFileUpload} | ||||||
|  |         className="block" | ||||||
|  |       /> | ||||||
|  |       <ul className="list-disc pl-5 text-sm"> | ||||||
|  |         {files.map((file, index) => ( | ||||||
|  |           <li key={index} className="flex justify-between items-center"> | ||||||
|  |             {file.name} | ||||||
|  |             <button | ||||||
|  |               onClick={() => handleRemoveFile(index)} | ||||||
|  |               className="text-red-600 hover:underline text-xs" | ||||||
|  |             > | ||||||
|  |               Remove | ||||||
|  |             </button> | ||||||
|  |           </li> | ||||||
|  |         ))} | ||||||
|  |       </ul> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								frontend/src/components/PageEditor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/components/PageEditor.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | export default function PageEditor({ pdfFile }) { | ||||||
|  |   return ( | ||||||
|  |     <div className="w-full h-full flex items-center justify-center"> | ||||||
|  |       <p className="text-gray-500">Page Editor is under construction.</p> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								frontend/src/components/Viewer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/components/Viewer.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | export default function Viewer({ pdfFile, setPdfFile }) { | ||||||
|  |   return pdfFile ? ( | ||||||
|  |     <iframe | ||||||
|  |       src={pdfFile.url} | ||||||
|  |       title="PDF Viewer" | ||||||
|  |       className="w-full h-full border-none" | ||||||
|  |     /> | ||||||
|  |   ) : ( | ||||||
|  |     <label className="cursor-pointer text-blue-600 underline"> | ||||||
|  |       Click to upload a PDF | ||||||
|  |       <input | ||||||
|  |         type="file" | ||||||
|  |         accept="application/pdf" | ||||||
|  |         onChange={(e) => { | ||||||
|  |           const file = e.target.files[0]; | ||||||
|  |           if (file && file.type === "application/pdf") { | ||||||
|  |             const fileUrl = URL.createObjectURL(file); | ||||||
|  |             setPdfFile({ file, url: fileUrl }); | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |         className="hidden" | ||||||
|  |       /> | ||||||
|  |     </label> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -1,17 +1,22 @@ | |||||||
|  | import '@mantine/core/styles.css'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import ReactDOM from 'react-dom/client'; | import ReactDOM from 'react-dom/client'; | ||||||
|  | import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core'; | ||||||
|  | import { BrowserRouter } from 'react-router-dom'; | ||||||
| import './index.css'; | import './index.css'; | ||||||
| import App from './App'; | import App from './App'; | ||||||
| import reportWebVitals from './reportWebVitals'; | import reportWebVitals from './reportWebVitals'; | ||||||
| 
 | 
 | ||||||
| const root = ReactDOM.createRoot(document.getElementById('root')); | const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
 | ||||||
| root.render( | root.render( | ||||||
|   <React.StrictMode> |   <React.StrictMode> | ||||||
|     <App /> |     <ColorSchemeScript /> | ||||||
|  |     <MantineProvider withGlobalStyles withNormalizeCSS> | ||||||
|  |       <BrowserRouter> | ||||||
|  |         <App /> | ||||||
|  |       </BrowserRouter> | ||||||
|  |     </MantineProvider> | ||||||
|   </React.StrictMode> |   </React.StrictMode> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| // If you want to start measuring performance in your app, pass a function
 |  | ||||||
| // to log results (for example: reportWebVitals(console.log))
 |  | ||||||
| // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
 |  | ||||||
| reportWebVitals(); | reportWebVitals(); | ||||||
|  | |||||||
							
								
								
									
										362
									
								
								frontend/src/output.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								frontend/src/output.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,362 @@ | |||||||
|  | *, ::before, ::after { | ||||||
|  |     --tw-border-spacing-x: 0; | ||||||
|  |     --tw-border-spacing-y: 0; | ||||||
|  |     --tw-translate-x: 0; | ||||||
|  |     --tw-translate-y: 0; | ||||||
|  |     --tw-rotate: 0; | ||||||
|  |     --tw-skew-x: 0; | ||||||
|  |     --tw-skew-y: 0; | ||||||
|  |     --tw-scale-x: 1; | ||||||
|  |     --tw-scale-y: 1; | ||||||
|  |     --tw-pan-x:  ; | ||||||
|  |     --tw-pan-y:  ; | ||||||
|  |     --tw-pinch-zoom:  ; | ||||||
|  |     --tw-scroll-snap-strictness: proximity; | ||||||
|  |     --tw-gradient-from-position:  ; | ||||||
|  |     --tw-gradient-via-position:  ; | ||||||
|  |     --tw-gradient-to-position:  ; | ||||||
|  |     --tw-ordinal:  ; | ||||||
|  |     --tw-slashed-zero:  ; | ||||||
|  |     --tw-numeric-figure:  ; | ||||||
|  |     --tw-numeric-spacing:  ; | ||||||
|  |     --tw-numeric-fraction:  ; | ||||||
|  |     --tw-ring-inset:  ; | ||||||
|  |     --tw-ring-offset-width: 0px; | ||||||
|  |     --tw-ring-offset-color: #fff; | ||||||
|  |     --tw-ring-color: rgb(59 130 246 / 0.5); | ||||||
|  |     --tw-ring-offset-shadow: 0 0 #0000; | ||||||
|  |     --tw-ring-shadow: 0 0 #0000; | ||||||
|  |     --tw-shadow: 0 0 #0000; | ||||||
|  |     --tw-shadow-colored: 0 0 #0000; | ||||||
|  |     --tw-blur:  ; | ||||||
|  |     --tw-brightness:  ; | ||||||
|  |     --tw-contrast:  ; | ||||||
|  |     --tw-grayscale:  ; | ||||||
|  |     --tw-hue-rotate:  ; | ||||||
|  |     --tw-invert:  ; | ||||||
|  |     --tw-saturate:  ; | ||||||
|  |     --tw-sepia:  ; | ||||||
|  |     --tw-drop-shadow:  ; | ||||||
|  |     --tw-backdrop-blur:  ; | ||||||
|  |     --tw-backdrop-brightness:  ; | ||||||
|  |     --tw-backdrop-contrast:  ; | ||||||
|  |     --tw-backdrop-grayscale:  ; | ||||||
|  |     --tw-backdrop-hue-rotate:  ; | ||||||
|  |     --tw-backdrop-invert:  ; | ||||||
|  |     --tw-backdrop-opacity:  ; | ||||||
|  |     --tw-backdrop-saturate:  ; | ||||||
|  |     --tw-backdrop-sepia:  ; | ||||||
|  |     --tw-contain-size:  ; | ||||||
|  |     --tw-contain-layout:  ; | ||||||
|  |     --tw-contain-paint:  ; | ||||||
|  |     --tw-contain-style:   | ||||||
|  | } | ||||||
|  | ::backdrop { | ||||||
|  |     --tw-border-spacing-x: 0; | ||||||
|  |     --tw-border-spacing-y: 0; | ||||||
|  |     --tw-translate-x: 0; | ||||||
|  |     --tw-translate-y: 0; | ||||||
|  |     --tw-rotate: 0; | ||||||
|  |     --tw-skew-x: 0; | ||||||
|  |     --tw-skew-y: 0; | ||||||
|  |     --tw-scale-x: 1; | ||||||
|  |     --tw-scale-y: 1; | ||||||
|  |     --tw-pan-x:  ; | ||||||
|  |     --tw-pan-y:  ; | ||||||
|  |     --tw-pinch-zoom:  ; | ||||||
|  |     --tw-scroll-snap-strictness: proximity; | ||||||
|  |     --tw-gradient-from-position:  ; | ||||||
|  |     --tw-gradient-via-position:  ; | ||||||
|  |     --tw-gradient-to-position:  ; | ||||||
|  |     --tw-ordinal:  ; | ||||||
|  |     --tw-slashed-zero:  ; | ||||||
|  |     --tw-numeric-figure:  ; | ||||||
|  |     --tw-numeric-spacing:  ; | ||||||
|  |     --tw-numeric-fraction:  ; | ||||||
|  |     --tw-ring-inset:  ; | ||||||
|  |     --tw-ring-offset-width: 0px; | ||||||
|  |     --tw-ring-offset-color: #fff; | ||||||
|  |     --tw-ring-color: rgb(59 130 246 / 0.5); | ||||||
|  |     --tw-ring-offset-shadow: 0 0 #0000; | ||||||
|  |     --tw-ring-shadow: 0 0 #0000; | ||||||
|  |     --tw-shadow: 0 0 #0000; | ||||||
|  |     --tw-shadow-colored: 0 0 #0000; | ||||||
|  |     --tw-blur:  ; | ||||||
|  |     --tw-brightness:  ; | ||||||
|  |     --tw-contrast:  ; | ||||||
|  |     --tw-grayscale:  ; | ||||||
|  |     --tw-hue-rotate:  ; | ||||||
|  |     --tw-invert:  ; | ||||||
|  |     --tw-saturate:  ; | ||||||
|  |     --tw-sepia:  ; | ||||||
|  |     --tw-drop-shadow:  ; | ||||||
|  |     --tw-backdrop-blur:  ; | ||||||
|  |     --tw-backdrop-brightness:  ; | ||||||
|  |     --tw-backdrop-contrast:  ; | ||||||
|  |     --tw-backdrop-grayscale:  ; | ||||||
|  |     --tw-backdrop-hue-rotate:  ; | ||||||
|  |     --tw-backdrop-invert:  ; | ||||||
|  |     --tw-backdrop-opacity:  ; | ||||||
|  |     --tw-backdrop-saturate:  ; | ||||||
|  |     --tw-backdrop-sepia:  ; | ||||||
|  |     --tw-contain-size:  ; | ||||||
|  |     --tw-contain-layout:  ; | ||||||
|  |     --tw-contain-paint:  ; | ||||||
|  |     --tw-contain-style:   | ||||||
|  | } | ||||||
|  | .mb-3 { | ||||||
|  |     margin-bottom: 0.75rem | ||||||
|  | } | ||||||
|  | .mb-4 { | ||||||
|  |     margin-bottom: 1rem | ||||||
|  | } | ||||||
|  | .mr-2 { | ||||||
|  |     margin-right: 0.5rem | ||||||
|  | } | ||||||
|  | .mt-2 { | ||||||
|  |     margin-top: 0.5rem | ||||||
|  | } | ||||||
|  | .mt-4 { | ||||||
|  |     margin-top: 1rem | ||||||
|  | } | ||||||
|  | .block { | ||||||
|  |     display: block | ||||||
|  | } | ||||||
|  | .flex { | ||||||
|  |     display: flex | ||||||
|  | } | ||||||
|  | .hidden { | ||||||
|  |     display: none | ||||||
|  | } | ||||||
|  | .h-6 { | ||||||
|  |     height: 1.5rem | ||||||
|  | } | ||||||
|  | .h-full { | ||||||
|  |     height: 100% | ||||||
|  | } | ||||||
|  | .h-screen { | ||||||
|  |     height: 100vh | ||||||
|  | } | ||||||
|  | .w-6 { | ||||||
|  |     width: 1.5rem | ||||||
|  | } | ||||||
|  | .w-64 { | ||||||
|  |     width: 16rem | ||||||
|  | } | ||||||
|  | .w-72 { | ||||||
|  |     width: 18rem | ||||||
|  | } | ||||||
|  | .w-full { | ||||||
|  |     width: 100% | ||||||
|  | } | ||||||
|  | .max-w-3xl { | ||||||
|  |     max-width: 48rem | ||||||
|  | } | ||||||
|  | .flex-1 { | ||||||
|  |     flex: 1 1 0% | ||||||
|  | } | ||||||
|  | .cursor-pointer { | ||||||
|  |     cursor: pointer | ||||||
|  | } | ||||||
|  | .list-disc { | ||||||
|  |     list-style-type: disc | ||||||
|  | } | ||||||
|  | .flex-col { | ||||||
|  |     flex-direction: column | ||||||
|  | } | ||||||
|  | .items-center { | ||||||
|  |     align-items: center | ||||||
|  | } | ||||||
|  | .justify-center { | ||||||
|  |     justify-content: center | ||||||
|  | } | ||||||
|  | .justify-between { | ||||||
|  |     justify-content: space-between | ||||||
|  | } | ||||||
|  | .space-x-2 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |     --tw-space-x-reverse: 0; | ||||||
|  |     margin-right: calc(0.5rem * var(--tw-space-x-reverse)); | ||||||
|  |     margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))) | ||||||
|  | } | ||||||
|  | .space-x-3 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |     --tw-space-x-reverse: 0; | ||||||
|  |     margin-right: calc(0.75rem * var(--tw-space-x-reverse)); | ||||||
|  |     margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))) | ||||||
|  | } | ||||||
|  | .space-y-2 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |     --tw-space-y-reverse: 0; | ||||||
|  |     margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|  |     margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)) | ||||||
|  | } | ||||||
|  | .space-y-3 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |     --tw-space-y-reverse: 0; | ||||||
|  |     margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|  |     margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)) | ||||||
|  | } | ||||||
|  | .space-y-4 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |     --tw-space-y-reverse: 0; | ||||||
|  |     margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|  |     margin-bottom: calc(1rem * var(--tw-space-y-reverse)) | ||||||
|  | } | ||||||
|  | .overflow-hidden { | ||||||
|  |     overflow: hidden | ||||||
|  | } | ||||||
|  | .overflow-y-auto { | ||||||
|  |     overflow-y: auto | ||||||
|  | } | ||||||
|  | .rounded { | ||||||
|  |     border-radius: 0.25rem | ||||||
|  | } | ||||||
|  | .rounded-md { | ||||||
|  |     border-radius: 0.375rem | ||||||
|  | } | ||||||
|  | .border { | ||||||
|  |     border-width: 1px | ||||||
|  | } | ||||||
|  | .border-b { | ||||||
|  |     border-bottom-width: 1px | ||||||
|  | } | ||||||
|  | .border-l { | ||||||
|  |     border-left-width: 1px | ||||||
|  | } | ||||||
|  | .border-r { | ||||||
|  |     border-right-width: 1px | ||||||
|  | } | ||||||
|  | .border-none { | ||||||
|  |     border-style: none | ||||||
|  | } | ||||||
|  | .bg-blue-600 { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .bg-gray-100 { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .bg-gray-50 { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .bg-green-600 { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .bg-white { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .p-2 { | ||||||
|  |     padding: 0.5rem | ||||||
|  | } | ||||||
|  | .p-4 { | ||||||
|  |     padding: 1rem | ||||||
|  | } | ||||||
|  | .px-2 { | ||||||
|  |     padding-left: 0.5rem; | ||||||
|  |     padding-right: 0.5rem | ||||||
|  | } | ||||||
|  | .px-4 { | ||||||
|  |     padding-left: 1rem; | ||||||
|  |     padding-right: 1rem | ||||||
|  | } | ||||||
|  | .py-1 { | ||||||
|  |     padding-top: 0.25rem; | ||||||
|  |     padding-bottom: 0.25rem | ||||||
|  | } | ||||||
|  | .py-2 { | ||||||
|  |     padding-top: 0.5rem; | ||||||
|  |     padding-bottom: 0.5rem | ||||||
|  | } | ||||||
|  | .pl-5 { | ||||||
|  |     padding-left: 1.25rem | ||||||
|  | } | ||||||
|  | .text-left { | ||||||
|  |     text-align: left | ||||||
|  | } | ||||||
|  | .text-center { | ||||||
|  |     text-align: center | ||||||
|  | } | ||||||
|  | .text-lg { | ||||||
|  |     font-size: 1.125rem; | ||||||
|  |     line-height: 1.75rem | ||||||
|  | } | ||||||
|  | .text-sm { | ||||||
|  |     font-size: 0.875rem; | ||||||
|  |     line-height: 1.25rem | ||||||
|  | } | ||||||
|  | .text-xl { | ||||||
|  |     font-size: 1.25rem; | ||||||
|  |     line-height: 1.75rem | ||||||
|  | } | ||||||
|  | .text-xs { | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     line-height: 1rem | ||||||
|  | } | ||||||
|  | .font-medium { | ||||||
|  |     font-weight: 500 | ||||||
|  | } | ||||||
|  | .font-semibold { | ||||||
|  |     font-weight: 600 | ||||||
|  | } | ||||||
|  | .leading-none { | ||||||
|  |     line-height: 1 | ||||||
|  | } | ||||||
|  | .text-blue-600 { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(37 99 235 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .text-gray-500 { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(107 114 128 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .text-gray-600 { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(75 85 99 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .text-red-500 { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(239 68 68 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .text-red-600 { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(220 38 38 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .text-white { | ||||||
|  |     --tw-text-opacity: 1; | ||||||
|  |     color: rgb(255 255 255 / var(--tw-text-opacity, 1)) | ||||||
|  | } | ||||||
|  | .underline { | ||||||
|  |     text-decoration-line: underline | ||||||
|  | } | ||||||
|  | .shadow { | ||||||
|  |     --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | ||||||
|  |     --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); | ||||||
|  |     box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) | ||||||
|  | } | ||||||
|  | .shadow-sm { | ||||||
|  |     --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); | ||||||
|  |     --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); | ||||||
|  |     box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) | ||||||
|  | } | ||||||
|  | .grayscale { | ||||||
|  |     --tw-grayscale: grayscale(100%); | ||||||
|  |     filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) | ||||||
|  | } | ||||||
|  | .filter { | ||||||
|  |     filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) | ||||||
|  | } | ||||||
|  | .hover\:bg-blue-700:hover { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .hover\:bg-gray-200:hover { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .hover\:bg-green-700:hover { | ||||||
|  |     --tw-bg-opacity: 1; | ||||||
|  |     background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1)) | ||||||
|  | } | ||||||
|  | .hover\:underline:hover { | ||||||
|  |     text-decoration-line: underline | ||||||
|  | } | ||||||
| @ -1,230 +1,180 @@ | |||||||
| import React, { useState } from "react"; | import React, { useState } from "react"; | ||||||
| import ConstructionIcon from '@mui/icons-material/Construction'; | import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; | ||||||
| import AddToPhotosIcon from '@mui/icons-material/AddToPhotos'; | import ContentCutIcon from "@mui/icons-material/ContentCut"; | ||||||
| import ContentCutIcon from '@mui/icons-material/ContentCut'; | import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; | ||||||
| import RotateRightIcon from '@mui/icons-material/RotateRight'; | import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; | ||||||
| import CropIcon from '@mui/icons-material/Crop'; | import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||||
| import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; | import EditNoteIcon from "@mui/icons-material/EditNote"; | ||||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import { Group, SegmentedControl, Paper, Center, Stack, Button, Text, Box } from "@mantine/core"; | ||||||
| import DashboardIcon from '@mui/icons-material/Dashboard'; |  | ||||||
| import FullscreenIcon from '@mui/icons-material/Fullscreen'; |  | ||||||
| import FileUploadIcon from '@mui/icons-material/FileUpload'; |  | ||||||
| import LooksOneIcon from '@mui/icons-material/LooksOne'; |  | ||||||
| import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; |  | ||||||
| import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; |  | ||||||
| import LinkIcon from '@mui/icons-material/Link'; |  | ||||||
| import CodeIcon from '@mui/icons-material/Code'; |  | ||||||
| import TableChartIcon from '@mui/icons-material/TableChart'; |  | ||||||
| import IntegrationInstructionsIcon from '@mui/icons-material/IntegrationInstructions'; |  | ||||||
| import LockIcon from '@mui/icons-material/Lock'; |  | ||||||
| import LockOpenIcon from '@mui/icons-material/LockOpen'; |  | ||||||
| import EditNoteIcon from '@mui/icons-material/EditNote'; |  | ||||||
| import WorkspacePremiumIcon from '@mui/icons-material/WorkspacePremium'; |  | ||||||
| import VerifiedIcon from '@mui/icons-material/Verified'; |  | ||||||
| import RemoveModeratorIcon from '@mui/icons-material/RemoveModerator'; |  | ||||||
| import SanitizerIcon from '@mui/icons-material/Sanitizer'; |  | ||||||
| import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; |  | ||||||
| import DrawIcon from '@mui/icons-material/Draw'; |  | ||||||
| import ApprovalIcon from '@mui/icons-material/Approval'; |  | ||||||
| import WaterDropIcon from '@mui/icons-material/WaterDrop'; |  | ||||||
| import MenuBookIcon from '@mui/icons-material/MenuBook'; |  | ||||||
| import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate'; |  | ||||||
| import AssignmentIcon from '@mui/icons-material/Assignment'; |  | ||||||
| import CollectionsIcon from '@mui/icons-material/Collections'; |  | ||||||
| import LayersClearIcon from '@mui/icons-material/LayersClear'; |  | ||||||
| import ScannerIcon from '@mui/icons-material/Scanner'; |  | ||||||
| import NoteAltIcon from '@mui/icons-material/NoteAlt'; |  | ||||||
| import CompareIcon from '@mui/icons-material/Compare'; |  | ||||||
| import InfoIcon from '@mui/icons-material/Info'; |  | ||||||
| import HighlightOffIcon from '@mui/icons-material/HighlightOff'; |  | ||||||
| import InvertColorsIcon from '@mui/icons-material/InvertColors'; |  | ||||||
| import AccountTreeIcon from '@mui/icons-material/AccountTree'; |  | ||||||
| import PaletteIcon from '@mui/icons-material/Palette'; |  | ||||||
| import ZoomInMapIcon from '@mui/icons-material/ZoomInMap'; |  | ||||||
| import BuildIcon from '@mui/icons-material/Build'; |  | ||||||
| import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline'; |  | ||||||
| import JavascriptIcon from '@mui/icons-material/Javascript'; |  | ||||||
| import SegmentIcon from '@mui/icons-material/Segment'; |  | ||||||
| import LayersIcon from '@mui/icons-material/Layers'; |  | ||||||
| import GridOnIcon from '@mui/icons-material/GridOn'; |  | ||||||
| import AutoStoriesIcon from '@mui/icons-material/AutoStories'; |  | ||||||
| import Icon from '@mui/material/Icon'; |  | ||||||
| 
 | 
 | ||||||
|  | import FileManager from "../components/FileManager"; | ||||||
| import SplitPdfPanel from "../tools/Split"; | import SplitPdfPanel from "../tools/Split"; | ||||||
| import CompressPdfPanel from "../tools/Compress-pdf"; | import CompressPdfPanel from "../tools/Compress"; | ||||||
|  | import MergePdfPanel from "../tools/Merge"; | ||||||
|  | import PageEditor from "../components/PageEditor"; | ||||||
|  | import Viewer from "../components/Viewer"; | ||||||
| 
 | 
 | ||||||
| const toolRegistry = { | const toolRegistry = { | ||||||
|   "split-pdf": { icon: <PictureAsPdfIcon />, name: "Split PDF", component: SplitPdfPanel }, |   split: { icon: <ContentCutIcon />, name: "Split PDF", component: SplitPdfPanel, view: "viewer" }, | ||||||
|   "compress-pdf": { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel } |   compress: { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel, view: "viewer" }, | ||||||
|  |   merge: { icon: <AddToPhotosIcon />, name: "Merge PDFs", component: MergePdfPanel, view: "fileManager" }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const tools = Object.entries(toolRegistry).map(([id, { icon, name }]) => ({ id, icon, name })); | const VIEW_OPTIONS = [ | ||||||
| 
 |   { | ||||||
| // Example tool panels
 |     label: ( | ||||||
| function ToolPanel({ selectedTool }) { |       <Group gap={4}> | ||||||
|   if (!selectedTool) { |         <VisibilityIcon fontSize="small" /> | ||||||
|     return ( |       </Group> | ||||||
|       <div className="p-2 border rounded bg-white shadow-sm"> |     ), | ||||||
|         <p className="text-sm">Select a tool to begin interacting with the PDF.</p> |     value: "viewer", | ||||||
|       </div> |   }, | ||||||
|     ); |   { | ||||||
|   } |     label: ( | ||||||
|   return ( |       <Group gap={4}> | ||||||
|     <div className="p-2 border rounded bg-white shadow-sm"> |         <EditNoteIcon fontSize="small" /> | ||||||
|       <h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3> |       </Group> | ||||||
|       <p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p> |     ), | ||||||
|     </div> |     value: "pageEditor", | ||||||
|   ); |   }, | ||||||
| } |   { | ||||||
| 
 |     label: ( | ||||||
| export default function HomePage() { |       <Group gap={4}> | ||||||
| const tools = [ |         <InsertDriveFileIcon fontSize="small" /> | ||||||
|   { id: "multi-tool", icon: <ConstructionIcon />, name: "Multi-Tool" }, |       </Group> | ||||||
|   { id: "merge-pdfs", icon: <AddToPhotosIcon />, name: "Merge PDFs" }, |     ), | ||||||
|   { id: "split-pdf", icon: <ContentCutIcon />, name: "Split PDF" }, |     value: "fileManager", | ||||||
|   { id: "rotate-pdf", icon: <RotateRightIcon />, name: "Rotate Pages" }, |   }, | ||||||
|   { id: "crop", icon: <CropIcon />, name: "Crop PDF" }, |  | ||||||
|   { id: "pdf-organizer", icon: <FormatListBulletedIcon />, name: "PDF Organizer" }, |  | ||||||
|   { id: "remove-pages", icon: <DeleteIcon />, name: "Remove Pages" }, |  | ||||||
|   { id: "multi-page-layout", icon: <DashboardIcon />, name: "Page Layout" }, |  | ||||||
|   { id: "scale-pages", icon: <FullscreenIcon />, name: "Scale Pages" }, |  | ||||||
|   { id: "extract-page", icon: <FileUploadIcon />, name: "Extract Page" }, |  | ||||||
|   { id: "pdf-to-single-page", icon: <LooksOneIcon />, name: "PDF to Single Page" }, |  | ||||||
|   { id: "img-to-pdf", icon: <PictureAsPdfIcon />, name: "Image to PDF" }, |  | ||||||
|   { id: "file-to-pdf", icon: <InsertDriveFileIcon />, name: "File to PDF" }, |  | ||||||
|   { id: "url-to-pdf", icon: <LinkIcon />, name: "URL to PDF" }, |  | ||||||
|   { id: "html-to-pdf", icon: <CodeIcon />, name: "HTML to PDF" }, |  | ||||||
|   { id: "markdown-to-pdf", icon: <IntegrationInstructionsIcon />, name: "Markdown to PDF" }, |  | ||||||
|   { id: "pdf-to-img", icon: <CollectionsIcon />, name: "PDF to Image" }, |  | ||||||
|   { id: "pdf-to-pdfa", icon: <PictureAsPdfIcon />, name: "PDF to PDF/A" }, |  | ||||||
|   { id: "pdf-to-word", icon: <InsertDriveFileIcon />, name: "PDF to Word" }, |  | ||||||
|   { id: "pdf-to-presentation", icon: <DashboardIcon />, name: "PDF to Presentation" }, |  | ||||||
|   { id: "pdf-to-text", icon: <AssignmentIcon />, name: "PDF to Text" }, |  | ||||||
|   { id: "pdf-to-html", icon: <CodeIcon />, name: "PDF to HTML" }, |  | ||||||
|   { id: "pdf-to-xml", icon: <CodeIcon />, name: "PDF to XML" }, |  | ||||||
|   { id: "pdf-to-csv", icon: <TableChartIcon />, name: "PDF to CSV" }, |  | ||||||
|   { id: "pdf-to-markdown", icon: <IntegrationInstructionsIcon />, name: "PDF to Markdown" }, |  | ||||||
|   { id: "add-password", icon: <LockIcon />, name: "Add Password" }, |  | ||||||
|   { id: "remove-password", icon: <LockOpenIcon />, name: "Remove Password" }, |  | ||||||
|   { id: "change-permissions", icon: <LockIcon />, name: "Change Permissions" }, |  | ||||||
|   { id: "sign", icon: <EditNoteIcon />, name: "Sign PDF" }, |  | ||||||
|   { id: "cert-sign", icon: <WorkspacePremiumIcon />, name: "Certify Signature" }, |  | ||||||
|   { id: "validate-signature", icon: <VerifiedIcon />, name: "Validate Signature" }, |  | ||||||
|   { id: "remove-cert-sign", icon: <RemoveModeratorIcon />, name: "Remove Cert Signature" }, |  | ||||||
|   { id: "sanitize-pdf", icon: <SanitizerIcon />, name: "Sanitize PDF" }, |  | ||||||
|   { id: "auto-redact", icon: <VisibilityOffIcon />, name: "Auto Redact" }, |  | ||||||
|   { id: "redact", icon: <DrawIcon />, name: "Manual Redact" }, |  | ||||||
|   { id: "stamp", icon: <ApprovalIcon />, name: "Add Stamp" }, |  | ||||||
|   { id: "add-watermark", icon: <WaterDropIcon />, name: "Add Watermark" }, |  | ||||||
|   { id: "view-pdf", icon: <MenuBookIcon />, name: "View PDF" }, |  | ||||||
|   { id: "add-page-numbers", icon: <LooksOneIcon />, name: "Add Page Numbers" }, |  | ||||||
|   { id: "add-image", icon: <AddPhotoAlternateIcon />, name: "Add Image" }, |  | ||||||
|   { id: "change-metadata", icon: <AssignmentIcon />, name: "Change Metadata" }, |  | ||||||
|   { id: "ocr-pdf", icon: <LayersIcon />, name: "OCR PDF" }, |  | ||||||
|   { id: "extract-images", icon: <CollectionsIcon />, name: "Extract Images" }, |  | ||||||
|   { id: "flatten", icon: <LayersClearIcon />, name: "Flatten PDF" }, |  | ||||||
|   { id: "remove-blanks", icon: <ScannerIcon />, name: "Remove Blank Pages" }, |  | ||||||
|   { id: "remove-annotations", icon: <NoteAltIcon />, name: "Remove Annotations" }, |  | ||||||
|   { id: "compare", icon: <CompareIcon />, name: "Compare PDFs" }, |  | ||||||
|   { id: "get-info-on-pdf", icon: <InfoIcon />, name: "PDF Info" }, |  | ||||||
|   { id: "remove-image-pdf", icon: <HighlightOffIcon />, name: "Remove Images from PDF" }, |  | ||||||
|   { id: "replace-and-invert-color-pdf", icon: <InvertColorsIcon />, name: "Invert Colors" }, |  | ||||||
|   { id: "unlock-pdf-forms", icon: <LayersIcon />, name: "Unlock PDF Forms" }, |  | ||||||
|   { id: "pipeline", icon: <AccountTreeIcon />, name: "Pipeline" }, |  | ||||||
|   { id: "adjust-contrast", icon: <PaletteIcon />, name: "Adjust Contrast" }, |  | ||||||
|   { id: "compress-pdf", icon: <ZoomInMapIcon />, name: "Compress PDF" }, |  | ||||||
|   { id: "extract-image-scans", icon: <ScannerIcon />, name: "Extract Image Scans" }, |  | ||||||
|   { id: "repair", icon: <BuildIcon />, name: "Repair PDF" }, |  | ||||||
|   { id: "auto-rename", icon: <DriveFileRenameOutlineIcon />, name: "Auto Rename" }, |  | ||||||
|   { id: "show-javascript", icon: <JavascriptIcon />, name: "Show JavaScript" }, |  | ||||||
|   { id: "overlay-pdf", icon: <LayersIcon />, name: "Overlay PDF" }, |  | ||||||
| 
 |  | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const [selectedTool, setSelectedTool] = useState(null); | export default function HomePage() { | ||||||
| const [search, setSearch] = useState(""); |   const [selectedToolKey, setSelectedToolKey] = useState("split"); | ||||||
| const [pdfFile, setPdfFile] = useState(null); |   const [currentView, setCurrentView] = useState("viewer"); | ||||||
| const SelectedComponent = selectedTool ? toolRegistry[selectedTool.id]?.component : null; |   const [pdfFile, setPdfFile] = useState(null); | ||||||
| const [downloadUrl, setDownloadUrl] = useState(null); |   const [files, setFiles] = useState([]); | ||||||
|  |   const [downloadUrl, setDownloadUrl] = useState(null); | ||||||
| 
 | 
 | ||||||
| const filteredTools = tools.filter(tool => |   const selectedTool = toolRegistry[selectedToolKey]; | ||||||
|   tool.name.toLowerCase().includes(search.toLowerCase()) |  | ||||||
| ); |  | ||||||
| 
 | 
 | ||||||
| function handleFileUpload(e) { |   return ( | ||||||
|   const file = e.target.files[0]; |     <Group align="flex-start" spacing={0} style={{ minHeight: "100vh" }}> | ||||||
|   if (file && file.type === "application/pdf") { |       {/* Left: Tool Picker */} | ||||||
|     const fileUrl = URL.createObjectURL(file); |       <Box | ||||||
|     setPdfFile({ file, url: fileUrl }); |         style={{ | ||||||
|   } |           width: 220, | ||||||
| } |           background: "#f8f9fa", | ||||||
| 
 |           borderRight: "1px solid #e9ecef", | ||||||
| return (    <div className="flex h-screen overflow-hidden"> |           minHeight: "100vh", | ||||||
|   {/* Left Sidebar */} |           padding: 16, | ||||||
|   <div className="w-64 bg-gray-100 p-4 flex flex-col space-y-2 overflow-y-auto border-r"> |         }} | ||||||
|     <input |  | ||||||
|       type="text" |  | ||||||
|       placeholder="Search tools..." |  | ||||||
|       value={search} |  | ||||||
|       onChange={(e) => setSearch(e.target.value)} |  | ||||||
|       className="mb-3 px-2 py-1 border rounded text-sm" |  | ||||||
|     /> |  | ||||||
|     {filteredTools.map(tool => ( |  | ||||||
|       <button |  | ||||||
|         key={tool.id} |  | ||||||
|         title={tool.name} |  | ||||||
|         onClick={() => setSelectedTool(tool)} |  | ||||||
|         className="flex items-center space-x-3 p-2 hover:bg-gray-200 rounded text-left" |  | ||||||
|       > |       > | ||||||
|         <div className="text-xl leading-none flex items-center justify-center h-6 w-6"> |         <Text size="lg" weight={500} mb="md"> | ||||||
|           {tool.icon} |           Tools | ||||||
|         </div> |         </Text> | ||||||
|         <span className="text-sm font-medium">{tool.name}</span> |         <Stack spacing="sm"> | ||||||
|       </button> |           {Object.entries(toolRegistry).map(([id, { icon, name }]) => ( | ||||||
|     ))} |             <Button | ||||||
|   </div> |               key={id} | ||||||
|  |               variant={selectedToolKey === id ? "filled" : "subtle"} | ||||||
|  |               leftIcon={icon} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setSelectedToolKey(id); | ||||||
|  |                 if (toolRegistry[id].view) setCurrentView(toolRegistry[id].view); | ||||||
|  |               }} | ||||||
|  |               fullWidth | ||||||
|  |               size="md" | ||||||
|  |               radius="md" | ||||||
|  |             > | ||||||
|  |               {name} | ||||||
|  |             </Button> | ||||||
|  |           ))} | ||||||
|  |         </Stack> | ||||||
|  |       </Box> | ||||||
| 
 | 
 | ||||||
| {/* Central PDF Viewer Area */} |       {/* Middle: Main View (Viewer, Editor, Manager) */} | ||||||
| <div className="flex-1 bg-white flex items-center justify-center overflow-hidden"> |       <Box | ||||||
|   <div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center"> |         style={{ | ||||||
|     {!pdfFile ? ( |           flex: 1, | ||||||
|       <label className="cursor-pointer text-blue-600 underline"> |           minWidth: 0, | ||||||
|         Click to upload a PDF |           padding: 24, | ||||||
|         <input |           background: "#fff", | ||||||
|           type="file" |           minHeight: "100vh", | ||||||
|           accept="application/pdf" |           position: "relative", | ||||||
|           onChange={handleFileUpload} |         }} | ||||||
|           className="hidden" |       > | ||||||
|         /> |         <Center> | ||||||
|       </label> |           <Paper | ||||||
|     ) : ( |             radius="xl" | ||||||
|       <iframe |             shadow="sm" | ||||||
|         src={pdfFile.url} |             p={4} | ||||||
|         title="PDF Viewer" |             style={{ | ||||||
|         className="w-full h-full border-none" |               display: "inline-block", | ||||||
|       /> |               marginTop: 8, | ||||||
|     )} |               marginBottom: 24, | ||||||
|   </div> |               background: "#f8f9fa", | ||||||
| </div> |               zIndex: 10, | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             <SegmentedControl | ||||||
|  |               data={VIEW_OPTIONS} | ||||||
|  |               value={currentView} | ||||||
|  |               onChange={setCurrentView} | ||||||
|  |               color="blue" | ||||||
|  |               radius="xl" | ||||||
|  |               size="md" | ||||||
|  |             /> | ||||||
|  |           </Paper> | ||||||
|  |         </Center> | ||||||
|  |         <Box> | ||||||
|  |           {currentView === "viewer" && ( | ||||||
|  |             <Viewer | ||||||
|  |               file={pdfFile} | ||||||
|  |               setFile={setPdfFile} | ||||||
|  |               downloadUrl={downloadUrl} | ||||||
|  |               setDownloadUrl={setDownloadUrl} | ||||||
|  |             /> | ||||||
|  |           )} | ||||||
|  |           {currentView === "pageEditor" && ( | ||||||
|  |             <PageEditor | ||||||
|  |               file={pdfFile} | ||||||
|  |               setFile={setPdfFile} | ||||||
|  |               downloadUrl={downloadUrl} | ||||||
|  |               setDownloadUrl={setDownloadUrl} | ||||||
|  |             /> | ||||||
|  |           )} | ||||||
|  |           {currentView === "fileManager" && ( | ||||||
|  |             <FileManager | ||||||
|  |               files={files} | ||||||
|  |               setFiles={setFiles} | ||||||
|  |               setPdfFile={setPdfFile} | ||||||
|  |             /> | ||||||
|  |           )} | ||||||
|  |         </Box> | ||||||
|  |       </Box> | ||||||
| 
 | 
 | ||||||
|   {/* Right Sidebar: Tool Interactions */} |       {/* Right: Tool Interaction */} | ||||||
|   <div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto"> |       <Box | ||||||
|     <h2 className="text-lg font-semibold mb-4">Tool Panel</h2> |         style={{ | ||||||
|     <div className="space-y-3"> |           width: 380, | ||||||
|       {SelectedComponent ? ( |           background: "#f8f9fa", | ||||||
|         <SelectedComponent file={pdfFile} downloadUrl setDownloadUrl /> |           borderLeft: "1px solid #e9ecef", | ||||||
|       ) : selectedTool ? ( |           minHeight: "100vh", | ||||||
|         <div className="p-2 border rounded bg-white shadow-sm"> |           padding: 24, | ||||||
|           <h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3> |         }} | ||||||
|           <p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p> |       > | ||||||
|         </div> |         {selectedTool && selectedTool.component && ( | ||||||
|       ) : ( |           <Paper p="md" radius="md" shadow="xs"> | ||||||
|         <div className="p-2 border rounded bg-white shadow-sm"> |             {React.createElement(selectedTool.component, { | ||||||
|           <p className="text-sm">Select a tool to begin interacting with the PDF.</p> |               file: pdfFile, | ||||||
|         </div> |               setPdfFile, | ||||||
|       )} |               files, | ||||||
|     </div> |               setFiles, | ||||||
|   </div> |               downloadUrl, | ||||||
| </div> |               setDownloadUrl, | ||||||
| ); |             })} | ||||||
|  |           </Paper> | ||||||
|  |         )} | ||||||
|  |       </Box> | ||||||
|  |     </Group> | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,91 +0,0 @@ | |||||||
| import React, { useState } from "react"; |  | ||||||
| import axios from "axios"; |  | ||||||
| 
 |  | ||||||
| export default function CompressPdfPanel({file}) { |  | ||||||
|   const [optimizeLevel, setOptimizeLevel] = useState("5"); |  | ||||||
|   const [grayscale, setGrayscale] = useState(false); |  | ||||||
|   const [expectedOutputSize, setExpectedOutputSize] = useState(""); |  | ||||||
|   const [status, setStatus] = useState(""); |  | ||||||
| 
 |  | ||||||
|   const handleSubmit = async (e) => { |  | ||||||
|     e.preventDefault(); |  | ||||||
| 
 |  | ||||||
|     if (!file) { |  | ||||||
|       setStatus("Please select a file."); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const formData = new FormData(); |  | ||||||
|     formData.append("fileInput", file.file); |  | ||||||
|     formData.append("optimizeLevel", optimizeLevel); |  | ||||||
|     formData.append("grayscale", grayscale); |  | ||||||
|     if (expectedOutputSize) { |  | ||||||
|       formData.append("expectedOutputSize", expectedOutputSize); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     setStatus("Compressing..."); |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       const response = await axios.post("/api/v1/misc/compress-pdf", formData, { |  | ||||||
|         responseType: "blob", |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       const url = window.URL.createObjectURL(new Blob([response.data])); |  | ||||||
|       const link = document.createElement("a"); |  | ||||||
|       link.href = url; |  | ||||||
|       link.setAttribute("download", "compressed.pdf"); |  | ||||||
|       document.body.appendChild(link); |  | ||||||
|       link.click(); |  | ||||||
| 
 |  | ||||||
|       setStatus("Download ready!"); |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error(error); |  | ||||||
|       setStatus("Failed to compress PDF."); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <form onSubmit={handleSubmit} className="space-y-4 text-sm"> |  | ||||||
| 
 |  | ||||||
|       <div> |  | ||||||
|         <label className="block font-medium">Compression Level (1-9)</label> |  | ||||||
|         <select |  | ||||||
|           value={optimizeLevel} |  | ||||||
|           onChange={(e) => setOptimizeLevel(e.target.value)} |  | ||||||
|           className="w-full border px-2 py-1 rounded" |  | ||||||
|         > |  | ||||||
|           {[...Array(9)].map((_, i) => ( |  | ||||||
|             <option key={i + 1} value={i + 1}>{i + 1}</option> |  | ||||||
|           ))} |  | ||||||
|         </select> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div className="flex items-center"> |  | ||||||
|         <input |  | ||||||
|           type="checkbox" |  | ||||||
|           id="grayscale" |  | ||||||
|           checked={grayscale} |  | ||||||
|           onChange={(e) => setGrayscale(e.target.checked)} |  | ||||||
|           className="mr-2" |  | ||||||
|         /> |  | ||||||
|         <label htmlFor="grayscale">Convert images to grayscale</label> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div> |  | ||||||
|         <label className="block font-medium">Expected Output Size (e.g. 2MB)</label> |  | ||||||
|         <input |  | ||||||
|           type="text" |  | ||||||
|           value={expectedOutputSize} |  | ||||||
|           onChange={(e) => setExpectedOutputSize(e.target.value)} |  | ||||||
|           className="w-full border px-2 py-1 rounded" |  | ||||||
|         /> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded"> |  | ||||||
|         Compress PDF |  | ||||||
|       </button> |  | ||||||
| 
 |  | ||||||
|       {status && <p className="text-xs text-gray-600 mt-2">{status}</p>} |  | ||||||
|     </form> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
							
								
								
									
										113
									
								
								frontend/src/tools/Compress.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								frontend/src/tools/Compress.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | |||||||
|  | import React, { useState } from "react"; | ||||||
|  | import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core"; | ||||||
|  | 
 | ||||||
|  | export default function CompressPdfPanel({ files = [], setDownloadUrl, setLoading }) { | ||||||
|  |   const [selected, setSelected] = useState(files.map(() => false)); | ||||||
|  |   const [compressionLevel, setCompressionLevel] = useState(5); // 1-9, default 5
 | ||||||
|  |   const [grayscale, setGrayscale] = useState(false); | ||||||
|  |   const [removeMetadata, setRemoveMetadata] = useState(false); | ||||||
|  |   const [expectedSize, setExpectedSize] = useState(""); | ||||||
|  |   const [aggressive, setAggressive] = useState(false); | ||||||
|  |   const [localLoading, setLocalLoading] = useState(false); | ||||||
|  | 
 | ||||||
|  |   // Update selection state if files prop changes
 | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     setSelected(files.map(() => false)); | ||||||
|  |   }, [files]); | ||||||
|  | 
 | ||||||
|  |   const handleCheckbox = idx => { | ||||||
|  |     setSelected(sel => sel.map((v, i) => (i === idx ? !v : v))); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleCompress = async () => { | ||||||
|  |     const selectedFiles = files.filter((_, i) => selected[i]); | ||||||
|  |     if (selectedFiles.length === 0) return; | ||||||
|  |     setLocalLoading(true); | ||||||
|  |     setLoading?.(true); | ||||||
|  | 
 | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     selectedFiles.forEach(file => formData.append("fileInput", file)); | ||||||
|  |     formData.append("compressionLevel", compressionLevel); | ||||||
|  |     formData.append("grayscale", grayscale); | ||||||
|  |     formData.append("removeMetadata", removeMetadata); | ||||||
|  |     formData.append("aggressive", aggressive); | ||||||
|  |     if (expectedSize) formData.append("expectedSize", expectedSize); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const res = await fetch("/api/v1/general/compress-pdf", { | ||||||
|  |         method: "POST", | ||||||
|  |         body: formData, | ||||||
|  |       }); | ||||||
|  |       const blob = await res.blob(); | ||||||
|  |       setDownloadUrl(URL.createObjectURL(blob)); | ||||||
|  |     } finally { | ||||||
|  |       setLocalLoading(false); | ||||||
|  |       setLoading?.(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Paper shadow="xs" p="md" radius="md" withBorder> | ||||||
|  |       <Stack> | ||||||
|  |         <Text weight={500} mb={4}>Select files to compress:</Text> | ||||||
|  |         <Stack spacing={4}> | ||||||
|  |           {files.length === 0 && <Text color="dimmed" size="sm">No files loaded.</Text>} | ||||||
|  |           {files.map((file, idx) => ( | ||||||
|  |             <Checkbox | ||||||
|  |               key={file.name + idx} | ||||||
|  |               label={file.name} | ||||||
|  |               checked={selected[idx] || false} | ||||||
|  |               onChange={() => handleCheckbox(idx)} | ||||||
|  |             /> | ||||||
|  |           ))} | ||||||
|  |         </Stack> | ||||||
|  |         <Stack spacing={4} mb={14}> | ||||||
|  |           <Text size="sm" style={{ minWidth: 140 }}>Compression Level</Text> | ||||||
|  |           <Slider | ||||||
|  |             min={1} | ||||||
|  |             max={9} | ||||||
|  |             step={1} | ||||||
|  |             value={compressionLevel} | ||||||
|  |             onChange={setCompressionLevel} | ||||||
|  |             marks={[ | ||||||
|  |               { value: 1, label: "1" }, | ||||||
|  |               { value: 5, label: "5" }, | ||||||
|  |               { value: 9, label: "9" }, | ||||||
|  |             ]} | ||||||
|  |             style={{ flex: 1 }} | ||||||
|  |           /> | ||||||
|  |         </Stack > | ||||||
|  |         <Checkbox | ||||||
|  |           label="Convert images to grayscale" | ||||||
|  |           checked={grayscale} | ||||||
|  |           onChange={e => setGrayscale(e.currentTarget.checked)} | ||||||
|  |         /> | ||||||
|  |         <Checkbox | ||||||
|  |           label="Remove PDF metadata" | ||||||
|  |           checked={removeMetadata} | ||||||
|  |           onChange={e => setRemoveMetadata(e.currentTarget.checked)} | ||||||
|  |         /> | ||||||
|  |         <Checkbox | ||||||
|  |           label="Aggressive compression (may reduce quality)" | ||||||
|  |           checked={aggressive} | ||||||
|  |           onChange={e => setAggressive(e.currentTarget.checked)} | ||||||
|  |         /> | ||||||
|  |         <TextInput | ||||||
|  |           label="Expected output size (e.g. 2MB, 500KB)" | ||||||
|  |           placeholder="Optional" | ||||||
|  |           value={expectedSize} | ||||||
|  |           onChange={e => setExpectedSize(e.currentTarget.value)} | ||||||
|  |         /> | ||||||
|  |         <Button | ||||||
|  |           onClick={handleCompress} | ||||||
|  |           loading={localLoading} | ||||||
|  |           disabled={selected.every(v => !v)} | ||||||
|  |           fullWidth | ||||||
|  |           mt="md" | ||||||
|  |         > | ||||||
|  |           Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""} | ||||||
|  |         </Button> | ||||||
|  |       </Stack> | ||||||
|  |     </Paper> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										101
									
								
								frontend/src/tools/Merge.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								frontend/src/tools/Merge.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | |||||||
|  | import React, { useState, useEffect } from "react"; | ||||||
|  | 
 | ||||||
|  | export default function MergePdfPanel({ files, setDownloadUrl }) { | ||||||
|  |   const [selectedFiles, setSelectedFiles] = useState([]); | ||||||
|  |   const [downloadUrl, setLocalDownloadUrl] = useState(null); // Local state for download URL
 | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); // Loading state
 | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(null); // Error message state
 | ||||||
|  | 
 | ||||||
|  |   // Sync selectedFiles with files whenever files change
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     setSelectedFiles(files.map(() => true)); // Select all files by default
 | ||||||
|  |   }, [files]); | ||||||
|  | 
 | ||||||
|  |   const handleMerge = async () => { | ||||||
|  |     const filesToMerge = files.filter((_, index) => selectedFiles[index]); | ||||||
|  | 
 | ||||||
|  |     if (filesToMerge.length < 2) { | ||||||
|  |       alert("Please select at least two PDFs to merge."); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     filesToMerge.forEach((file) => formData.append("fileInput", file)); // Use "fileInput" as the key
 | ||||||
|  | 
 | ||||||
|  |     setIsLoading(true); // Start loading
 | ||||||
|  |     setErrorMessage(null); // Clear previous errors
 | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const response = await fetch("/api/v1/general/merge-pdfs", { | ||||||
|  |         method: "POST", | ||||||
|  |         body: formData, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (!response.ok) { | ||||||
|  |         const errorText = await response.text(); | ||||||
|  |         throw new Error(`Failed to merge PDFs: ${errorText}`); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const blob = await response.blob(); | ||||||
|  |       const downloadUrl = URL.createObjectURL(blob); | ||||||
|  |       setDownloadUrl(downloadUrl); // Pass to parent component
 | ||||||
|  |       setLocalDownloadUrl(downloadUrl); // Store locally for download button
 | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error("Error merging PDFs:", error); | ||||||
|  |       setErrorMessage(error.message); // Set error message
 | ||||||
|  |     } finally { | ||||||
|  |       setIsLoading(false); // Stop loading
 | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleCheckboxChange = (index) => { | ||||||
|  |     setSelectedFiles((prevSelectedFiles) => | ||||||
|  |       prevSelectedFiles.map((selected, i) => (i === index ? !selected : selected)) | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="space-y-4"> | ||||||
|  |       <h3 className="font-semibold text-lg">Merge PDFs</h3> | ||||||
|  |       <ul className="list-disc pl-5 text-sm"> | ||||||
|  |         {files.map((file, index) => ( | ||||||
|  |           <li key={index} className="flex items-center space-x-2"> | ||||||
|  |             <input | ||||||
|  |               type="checkbox" | ||||||
|  |               checked={selectedFiles[index]} | ||||||
|  |               onChange={() => handleCheckboxChange(index)} | ||||||
|  |               className="form-checkbox" | ||||||
|  |             /> | ||||||
|  |             <span>{file.name}</span> | ||||||
|  |           </li> | ||||||
|  |         ))} | ||||||
|  |       </ul> | ||||||
|  |       {files.filter((_, index) => selectedFiles[index]).length < 2 && ( | ||||||
|  |         <p className="text-sm text-red-500"> | ||||||
|  |           Please select at least two PDFs to merge. | ||||||
|  |         </p> | ||||||
|  |       )} | ||||||
|  |       <button | ||||||
|  |         onClick={handleMerge} | ||||||
|  |         className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" | ||||||
|  |         disabled={files.filter((_, index) => selectedFiles[index]).length < 2 || isLoading} | ||||||
|  |       > | ||||||
|  |         {isLoading ? "Merging..." : "Merge PDFs"} | ||||||
|  |       </button> | ||||||
|  |       {errorMessage && ( | ||||||
|  |         <p className="text-sm text-red-500 mt-2"> | ||||||
|  |           {errorMessage} | ||||||
|  |         </p> | ||||||
|  |       )} | ||||||
|  |       {downloadUrl && ( | ||||||
|  |         <a | ||||||
|  |           href={downloadUrl} | ||||||
|  |           download="merged.pdf" | ||||||
|  |           className="block mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-center" | ||||||
|  |         > | ||||||
|  |           Download Merged PDF | ||||||
|  |         </a> | ||||||
|  |       )} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -1,10 +1,33 @@ | |||||||
| import React, { useState } from "react"; | import React, { useState } from "react"; | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
| import DownloadIcon from '@mui/icons-material/Download'; | import { | ||||||
|  |   Button, | ||||||
|  |   Select, | ||||||
|  |   TextInput, | ||||||
|  |   Checkbox, | ||||||
|  |   Notification, | ||||||
|  |   Stack, | ||||||
|  | } from "@mantine/core"; | ||||||
|  | import DownloadIcon from "@mui/icons-material/Download"; | ||||||
| 
 | 
 | ||||||
| export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) { | export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) { | ||||||
|   const [mode, setMode] = useState("byPages"); |   const [mode, setMode] = useState("byPages"); | ||||||
|  |   const [pageNumbers, setPageNumbers] = useState(""); | ||||||
|  | 
 | ||||||
|  |   const [horizontalDivisions, setHorizontalDivisions] = useState("0"); | ||||||
|  |   const [verticalDivisions, setVerticalDivisions] = useState("1"); | ||||||
|  |   const [mergeSections, setMergeSections] = useState(false); | ||||||
|  | 
 | ||||||
|  |   const [splitType, setSplitType] = useState("size"); | ||||||
|  |   const [splitValue, setSplitValue] = useState(""); | ||||||
|  | 
 | ||||||
|  |   const [bookmarkLevel, setBookmarkLevel] = useState("0"); | ||||||
|  |   const [includeMetadata, setIncludeMetadata] = useState(false); | ||||||
|  |   const [allowDuplicates, setAllowDuplicates] = useState(false); | ||||||
|  | 
 | ||||||
|   const [status, setStatus] = useState(""); |   const [status, setStatus] = useState(""); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(null); | ||||||
| 
 | 
 | ||||||
|   const handleSubmit = async (e) => { |   const handleSubmit = async (e) => { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
| @ -17,175 +40,168 @@ export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) { | |||||||
|     formData.append("fileInput", file.file); |     formData.append("fileInput", file.file); | ||||||
| 
 | 
 | ||||||
|     let endpoint = ""; |     let endpoint = ""; | ||||||
|     if (mode === "byPages") { | 
 | ||||||
|       const pageNumbers = document.getElementById("pagesInput").value; |     switch (mode) { | ||||||
|       formData.append("pageNumbers", pageNumbers); |       case "byPages": | ||||||
|       endpoint = "/api/v1/general/split-pages"; |         formData.append("pageNumbers", pageNumbers); | ||||||
|     } else if (mode === "bySections") { |         endpoint = "/api/v1/general/split-pages"; | ||||||
|       const horizontal = document.getElementById("horizontalDivisions").value; |         break; | ||||||
|       const vertical = document.getElementById("verticalDivisions").value; |       case "bySections": | ||||||
|       const merge = document.getElementById("merge").checked; |         formData.append("horizontalDivisions", horizontalDivisions); | ||||||
|       formData.append("horizontalDivisions", horizontal); |         formData.append("verticalDivisions", verticalDivisions); | ||||||
|       formData.append("verticalDivisions", vertical); |         formData.append("merge", mergeSections); | ||||||
|       formData.append("merge", merge); |         endpoint = "/api/v1/general/split-pdf-by-sections"; | ||||||
|       endpoint = "/api/v1/general/split-pdf-by-sections"; |         break; | ||||||
|     } else if (mode === "bySizeOrCount") { |       case "bySizeOrCount": | ||||||
|       const splitType = document.getElementById("splitType").value; |         formData.append("splitType", splitType === "size" ? 0 : splitType === "pages" ? 1 : 2); | ||||||
|       const splitValue = document.getElementById("splitValue").value; |         formData.append("splitValue", splitValue); | ||||||
|       formData.append("splitType", splitType === "size" ? 0 : splitType === "pages" ? 1 : 2); |         endpoint = "/api/v1/general/split-by-size-or-count"; | ||||||
|       formData.append("splitValue", splitValue); |         break; | ||||||
|       endpoint = "/api/v1/general/split-by-size-or-count"; |       case "byChapters": | ||||||
|     } else if (mode === "byChapters") { |         formData.append("bookmarkLevel", bookmarkLevel); | ||||||
|       const bookmarkLevel = document.getElementById("bookmarkLevel").value; |         formData.append("includeMetadata", includeMetadata); | ||||||
|       const includeMetadata = document.getElementById("includeMetadata").checked; |         formData.append("allowDuplicates", allowDuplicates); | ||||||
|       const allowDuplicates = document.getElementById("allowDuplicates").checked; |         endpoint = "/api/v1/general/split-pdf-by-chapters"; | ||||||
|       formData.append("bookmarkLevel", bookmarkLevel); |         break; | ||||||
|       formData.append("includeMetadata", includeMetadata); |       default: | ||||||
|       formData.append("allowDuplicates", allowDuplicates); |         return; | ||||||
|       endpoint = "/api/v1/general/split-pdf-by-chapters"; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     setStatus("Processing split..."); |     setStatus("Processing split..."); | ||||||
|  |     setIsLoading(true); | ||||||
|  |     setErrorMessage(null); | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|       const response = await axios.post(endpoint, formData, { responseType: "blob" }); |       const response = await axios.post(endpoint, formData, { responseType: "blob" }); | ||||||
|       const link = document.createElement("a"); |  | ||||||
|       link.href = url; |  | ||||||
|       link.setAttribute("download", "split_output.zip"); |  | ||||||
|       document.body.appendChild(link); |  | ||||||
|       const blob = new Blob([response.data], { type: "application/zip" }); |       const blob = new Blob([response.data], { type: "application/zip" }); | ||||||
|       const url = window.URL.createObjectURL(blob); |       const url = window.URL.createObjectURL(blob); | ||||||
|       setDownloadUrl(url); |       setDownloadUrl(url); | ||||||
|       setStatus("Download ready."); |       setStatus("Download ready."); | ||||||
|         } catch (error) { |     } catch (error) { | ||||||
|       console.error(error); |       console.error(error); | ||||||
|  |       setErrorMessage(error.response?.data || "An error occurred while splitting the PDF."); | ||||||
|       setStatus("Split failed."); |       setStatus("Split failed."); | ||||||
|  |     } finally { | ||||||
|  |       setIsLoading(false); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <form onSubmit={handleSubmit} className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm"> |     <form onSubmit={handleSubmit} > | ||||||
|       <h3 className="font-semibold">Split PDF</h3> |       <h3 className="font-semibold">Split PDF</h3> | ||||||
| 
 | 
 | ||||||
|       <div> |       <Select | ||||||
|         <label className="block mb-1 font-medium">Split Mode</label> |         label="Split Mode" | ||||||
|         <select |         value={mode} | ||||||
|           value={mode} |         onChange={setMode} | ||||||
|           onChange={(e) => setMode(e.target.value)} |         data={[ | ||||||
|           className="w-full border px-2 py-1 rounded" |           { value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" }, | ||||||
|         > |           { value: "bySections", label: "Split by Grid Sections" }, | ||||||
|           <option value="byPages">Split by Pages (e.g. 1,3,5-10)</option> |           { value: "bySizeOrCount", label: "Split by Size or Count" }, | ||||||
|           <option value="bySections">Split by Grid Sections</option> |           { value: "byChapters", label: "Split by Chapters" }, | ||||||
|           <option value="bySizeOrCount">Split by Size or Count</option> |         ]} | ||||||
|           <option value="byChapters">Split by Chapters</option> |       /> | ||||||
|         </select> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       {mode === "byPages" && ( |       {mode === "byPages" && ( | ||||||
|         <div> |         <TextInput | ||||||
|           <label className="block font-medium mb-1">Pages</label> |           label="Pages" | ||||||
|           <input |           placeholder="e.g. 1,3,5-10" | ||||||
|             type="text" |           value={pageNumbers} | ||||||
|             id="pagesInput" |           onChange={(e) => setPageNumbers(e.target.value)} | ||||||
|             className="w-full border px-2 py-1 rounded" |         /> | ||||||
|             placeholder="e.g. 1,3,5-10" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {mode === "bySections" && ( |       {mode === "bySections" && ( | ||||||
|         <div className="space-y-2"> |         <Stack spacing="sm"> | ||||||
|           <div> |           <TextInput | ||||||
|             <label className="block font-medium mb-1">Horizontal Divisions</label> |             label="Horizontal Divisions" | ||||||
|             <input |             type="number" | ||||||
|               type="number" |             min="0" | ||||||
|               id="horizontalDivisions" |             max="300" | ||||||
|               className="w-full border px-2 py-1 rounded" |             value={horizontalDivisions} | ||||||
|               min="0" |             onChange={(e) => setHorizontalDivisions(e.target.value)} | ||||||
|               max="300" |           /> | ||||||
|               defaultValue="0" |           <TextInput | ||||||
|             /> |             label="Vertical Divisions" | ||||||
|           </div> |             type="number" | ||||||
|           <div> |             min="0" | ||||||
|             <label className="block font-medium mb-1">Vertical Divisions</label> |             max="300" | ||||||
|             <input |             value={verticalDivisions} | ||||||
|               type="number" |             onChange={(e) => setVerticalDivisions(e.target.value)} | ||||||
|               id="verticalDivisions" |           /> | ||||||
|               className="w-full border px-2 py-1 rounded" |           <Checkbox | ||||||
|               min="0" |             label="Merge sections into one PDF" | ||||||
|               max="300" |             checked={mergeSections} | ||||||
|               defaultValue="1" |             onChange={(e) => setMergeSections(e.currentTarget.checked)} | ||||||
|             /> |           /> | ||||||
|           </div> |         </Stack> | ||||||
|           <div className="flex items-center space-x-2"> |  | ||||||
|             <input type="checkbox" id="merge" /> |  | ||||||
|             <label htmlFor="merge">Merge sections into one PDF</label> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {mode === "bySizeOrCount" && ( |       {mode === "bySizeOrCount" && ( | ||||||
|         <div className="space-y-2"> |         <Stack spacing="sm"> | ||||||
|           <div> |           <Select | ||||||
|             <label className="block font-medium mb-1">Split Type</label> |             label="Split Type" | ||||||
|             <select id="splitType" className="w-full border px-2 py-1 rounded"> |             value={splitType} | ||||||
|               <option value="size">By Size</option> |             onChange={setSplitType} | ||||||
|               <option value="pages">By Page Count</option> |             data={[ | ||||||
|               <option value="docs">By Document Count</option> |               { value: "size", label: "By Size" }, | ||||||
|             </select> |               { value: "pages", label: "By Page Count" }, | ||||||
|           </div> |               { value: "docs", label: "By Document Count" }, | ||||||
|           <div> |             ]} | ||||||
|             <label className="block font-medium mb-1">Split Value</label> |           /> | ||||||
|             <input |           <TextInput | ||||||
|               type="text" |             label="Split Value" | ||||||
|               id="splitValue" |             placeholder="e.g. 10MB or 5 pages" | ||||||
|               className="w-full border px-2 py-1 rounded" |             value={splitValue} | ||||||
|               placeholder="e.g. 10MB or 5 pages" |             onChange={(e) => setSplitValue(e.target.value)} | ||||||
|             /> |           /> | ||||||
|           </div> |         </Stack> | ||||||
|         </div> |  | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {mode === "byChapters" && ( |       {mode === "byChapters" && ( | ||||||
|         <div className="space-y-2"> |         <Stack spacing="sm"> | ||||||
|           <div> |           <TextInput | ||||||
|             <label className="block font-medium mb-1">Bookmark Level</label> |             label="Bookmark Level" | ||||||
|             <input |             type="number" | ||||||
|               type="number" |             value={bookmarkLevel} | ||||||
|               id="bookmarkLevel" |             onChange={(e) => setBookmarkLevel(e.target.value)} | ||||||
|               className="w-full border px-2 py-1 rounded" |           /> | ||||||
|               defaultValue="0" |           <Checkbox | ||||||
|               min="0" |             label="Include Metadata" | ||||||
|             /> |             checked={includeMetadata} | ||||||
|           </div> |             onChange={(e) => setIncludeMetadata(e.currentTarget.checked)} | ||||||
|           <div className="flex items-center space-x-2"> |           /> | ||||||
|             <input type="checkbox" id="includeMetadata" /> |           <Checkbox | ||||||
|             <label htmlFor="includeMetadata">Include Metadata</label> |             label="Allow Duplicate Bookmarks" | ||||||
|           </div> |             checked={allowDuplicates} | ||||||
|           <div className="flex items-center space-x-2"> |             onChange={(e) => setAllowDuplicates(e.currentTarget.checked)} | ||||||
|             <input type="checkbox" id="allowDuplicates" /> |           /> | ||||||
|             <label htmlFor="allowDuplicates">Allow Duplicate Bookmarks</label> |         </Stack> | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded mt-2"> |       <Button type="submit" loading={isLoading} fullWidth> | ||||||
|         Split PDF |         {isLoading ? "Processing..." : "Split PDF"} | ||||||
|       </button> |       </Button> | ||||||
| 
 | 
 | ||||||
|       {status && <p className="text-xs text-gray-600">{status}</p>} |       {status && <p className="text-xs text-gray-600">{status}</p>} | ||||||
| 
 | 
 | ||||||
| {status === "Download ready." && downloadUrl && ( |       {errorMessage && ( | ||||||
|   <a |         <Notification color="red" title="Error" onClose={() => setErrorMessage(null)}> | ||||||
|     href={downloadUrl} |           {errorMessage} | ||||||
|     download="split_output.zip" |         </Notification> | ||||||
|     className="inline-flex items-center bg-green-600 text-white px-4 py-2 rounded shadow hover:bg-green-700 transition mt-2" |       )} | ||||||
|   > |  | ||||||
|     <DownloadIcon className="mr-2" /> |  | ||||||
|     Download Split PDF |  | ||||||
|   </a> |  | ||||||
| )} |  | ||||||
| 
 | 
 | ||||||
|  |       {status === "Download ready." && downloadUrl && ( | ||||||
|  |         <Button | ||||||
|  |           component="a" | ||||||
|  |           href={downloadUrl} | ||||||
|  |           download="split_output.zip" | ||||||
|  |           leftIcon={<DownloadIcon />} | ||||||
|  |           color="green" | ||||||
|  |           fullWidth | ||||||
|  |         > | ||||||
|  |           Download Split PDF | ||||||
|  |         </Button> | ||||||
|  |       )} | ||||||
|     </form> |     </form> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
|  |   corePlugins: { | ||||||
|  |     preflight: false, | ||||||
|  |   }, | ||||||
|   content: [ |   content: [ | ||||||
|     "./src/**/*.{js,jsx,ts,tsx}" |     "./src/**/*.{js,jsx,ts,tsx}" | ||||||
|   ], |   ], | ||||||
|  | |||||||
							
								
								
									
										264
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										264
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,264 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "Stirling-PDF", |  | ||||||
|   "lockfileVersion": 3, |  | ||||||
|   "requires": true, |  | ||||||
|   "packages": { |  | ||||||
|     "": { |  | ||||||
|       "devDependencies": { |  | ||||||
|         "autoprefixer": "^10.4.21", |  | ||||||
|         "postcss": "^8.5.3", |  | ||||||
|         "tailwindcss": "^4.1.6" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/autoprefixer": { |  | ||||||
|       "version": "10.4.21", |  | ||||||
|       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", |  | ||||||
|       "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "opencollective", |  | ||||||
|           "url": "https://opencollective.com/postcss/" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "tidelift", |  | ||||||
|           "url": "https://tidelift.com/funding/github/npm/autoprefixer" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "browserslist": "^4.24.4", |  | ||||||
|         "caniuse-lite": "^1.0.30001702", |  | ||||||
|         "fraction.js": "^4.3.7", |  | ||||||
|         "normalize-range": "^0.1.2", |  | ||||||
|         "picocolors": "^1.1.1", |  | ||||||
|         "postcss-value-parser": "^4.2.0" |  | ||||||
|       }, |  | ||||||
|       "bin": { |  | ||||||
|         "autoprefixer": "bin/autoprefixer" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": "^10 || ^12 || >=14" |  | ||||||
|       }, |  | ||||||
|       "peerDependencies": { |  | ||||||
|         "postcss": "^8.1.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/browserslist": { |  | ||||||
|       "version": "4.24.5", |  | ||||||
|       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", |  | ||||||
|       "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "opencollective", |  | ||||||
|           "url": "https://opencollective.com/browserslist" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "tidelift", |  | ||||||
|           "url": "https://tidelift.com/funding/github/npm/browserslist" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "caniuse-lite": "^1.0.30001716", |  | ||||||
|         "electron-to-chromium": "^1.5.149", |  | ||||||
|         "node-releases": "^2.0.19", |  | ||||||
|         "update-browserslist-db": "^1.1.3" |  | ||||||
|       }, |  | ||||||
|       "bin": { |  | ||||||
|         "browserslist": "cli.js" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/caniuse-lite": { |  | ||||||
|       "version": "1.0.30001718", |  | ||||||
|       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", |  | ||||||
|       "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "opencollective", |  | ||||||
|           "url": "https://opencollective.com/browserslist" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "tidelift", |  | ||||||
|           "url": "https://tidelift.com/funding/github/npm/caniuse-lite" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "CC-BY-4.0" |  | ||||||
|     }, |  | ||||||
|     "node_modules/electron-to-chromium": { |  | ||||||
|       "version": "1.5.152", |  | ||||||
|       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", |  | ||||||
|       "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "ISC" |  | ||||||
|     }, |  | ||||||
|     "node_modules/escalade": { |  | ||||||
|       "version": "3.2.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", |  | ||||||
|       "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=6" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/fraction.js": { |  | ||||||
|       "version": "4.3.7", |  | ||||||
|       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", |  | ||||||
|       "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "engines": { |  | ||||||
|         "node": "*" |  | ||||||
|       }, |  | ||||||
|       "funding": { |  | ||||||
|         "type": "patreon", |  | ||||||
|         "url": "https://github.com/sponsors/rawify" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/nanoid": { |  | ||||||
|       "version": "3.3.11", |  | ||||||
|       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", |  | ||||||
|       "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "MIT", |  | ||||||
|       "bin": { |  | ||||||
|         "nanoid": "bin/nanoid.cjs" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/node-releases": { |  | ||||||
|       "version": "2.0.19", |  | ||||||
|       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", |  | ||||||
|       "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |  | ||||||
|     }, |  | ||||||
|     "node_modules/normalize-range": { |  | ||||||
|       "version": "0.1.2", |  | ||||||
|       "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", |  | ||||||
|       "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=0.10.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/picocolors": { |  | ||||||
|       "version": "1.1.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", |  | ||||||
|       "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "ISC" |  | ||||||
|     }, |  | ||||||
|     "node_modules/postcss": { |  | ||||||
|       "version": "8.5.3", |  | ||||||
|       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", |  | ||||||
|       "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "opencollective", |  | ||||||
|           "url": "https://opencollective.com/postcss/" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "tidelift", |  | ||||||
|           "url": "https://tidelift.com/funding/github/npm/postcss" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "nanoid": "^3.3.8", |  | ||||||
|         "picocolors": "^1.1.1", |  | ||||||
|         "source-map-js": "^1.2.1" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": "^10 || ^12 || >=14" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/postcss-value-parser": { |  | ||||||
|       "version": "4.2.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", |  | ||||||
|       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |  | ||||||
|     }, |  | ||||||
|     "node_modules/source-map-js": { |  | ||||||
|       "version": "1.2.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", |  | ||||||
|       "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "BSD-3-Clause", |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=0.10.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/tailwindcss": { |  | ||||||
|       "version": "4.1.6", |  | ||||||
|       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", |  | ||||||
|       "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |  | ||||||
|     }, |  | ||||||
|     "node_modules/update-browserslist-db": { |  | ||||||
|       "version": "1.1.3", |  | ||||||
|       "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", |  | ||||||
|       "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", |  | ||||||
|       "dev": true, |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "opencollective", |  | ||||||
|           "url": "https://opencollective.com/browserslist" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "tidelift", |  | ||||||
|           "url": "https://tidelift.com/funding/github/npm/browserslist" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/ai" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "escalade": "^3.2.0", |  | ||||||
|         "picocolors": "^1.1.1" |  | ||||||
|       }, |  | ||||||
|       "bin": { |  | ||||||
|         "update-browserslist-db": "cli.js" |  | ||||||
|       }, |  | ||||||
|       "peerDependencies": { |  | ||||||
|         "browserslist": ">= 4.21.0" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "devDependencies": { |  | ||||||
|     "autoprefixer": "^10.4.21", |  | ||||||
|     "postcss": "^8.5.3", |  | ||||||
|     "tailwindcss": "^4.1.6" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,156 +1,72 @@ | |||||||
| import React, { useState } from "react"; | <!DOCTYPE html> | ||||||
| import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; | <html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" | ||||||
|  |   xmlns:th="https://www.thymeleaf.org"> | ||||||
| 
 | 
 | ||||||
| const tools = [ | <head> | ||||||
|   { id: "split-pdf", icon: <PictureAsPdfIcon />, name: "Split PDF" } |   <th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}"> | ||||||
| ]; |   </th:block> | ||||||
|  | </head> | ||||||
| 
 | 
 | ||||||
| function SplitPdfPanel() { | <body> | ||||||
|   const [mode, setMode] = useState("byPages"); |   <div id="page-container"> | ||||||
|   return ( |     <div id="content-wrap"> | ||||||
|     <div className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm"> |       <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> | ||||||
|       <h3 className="font-semibold">Split PDF</h3> |       <br><br> | ||||||
| 
 |       <div class="container"> | ||||||
|       <div> |         <div class="row justify-content-center"> | ||||||
|         <label className="block mb-1 font-medium">Split Mode</label> |           <div class="col-md-6 bg-card"> | ||||||
|         <select |             <div class="tool-header"> | ||||||
|           value={mode} |               <svg class="material-symbols-rounded tool-header-icon advance"> | ||||||
|           onChange={(e) => setMode(e.target.value)} |                 <use xlink:href="/images/split-chapters.svg#icon-split-chapters"></use> | ||||||
|           className="w-full border px-2 py-1 rounded" |               </svg> | ||||||
|         > |               <span class="tool-header-text" th:text="#{splitByChapters.header}"></span> | ||||||
|           <option value="byPages">Split by Pages (e.g. 1,3,5-10)</option> |  | ||||||
|           <option value="bySections">Split by Grid Sections</option> |  | ||||||
|           <option value="bySizeOrCount">Split by Size or Count</option> |  | ||||||
|           <option value="byChapters">Split by Chapters</option> |  | ||||||
|         </select> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       {mode === "byPages" && ( |  | ||||||
|         <div> |  | ||||||
|           <label className="block font-medium mb-1">Pages</label> |  | ||||||
|           <input |  | ||||||
|             type="text" |  | ||||||
|             className="w-full border px-2 py-1 rounded" |  | ||||||
|             placeholder="e.g. 1,3,5-10" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       {mode === "bySections" && ( |  | ||||||
|         <div className="space-y-2"> |  | ||||||
|           <div> |  | ||||||
|             <label className="block font-medium mb-1">Horizontal Divisions</label> |  | ||||||
|             <input type="number" className="w-full border px-2 py-1 rounded" min="0" max="300" defaultValue="0" /> |  | ||||||
|           </div> |  | ||||||
|           <div> |  | ||||||
|             <label className="block font-medium mb-1">Vertical Divisions</label> |  | ||||||
|             <input type="number" className="w-full border px-2 py-1 rounded" min="0" max="300" defaultValue="1" /> |  | ||||||
|           </div> |  | ||||||
|           <div className="flex items-center space-x-2"> |  | ||||||
|             <input type="checkbox" id="merge" /> |  | ||||||
|             <label htmlFor="merge">Merge sections into one PDF</label> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       {mode === "bySizeOrCount" && ( |  | ||||||
|         <div className="space-y-2"> |  | ||||||
|           <div> |  | ||||||
|             <label className="block font-medium mb-1">Split Type</label> |  | ||||||
|             <select className="w-full border px-2 py-1 rounded"> |  | ||||||
|               <option value="size">By Size</option> |  | ||||||
|               <option value="pages">By Page Count</option> |  | ||||||
|               <option value="docs">By Document Count</option> |  | ||||||
|             </select> |  | ||||||
|           </div> |  | ||||||
|           <div> |  | ||||||
|             <label className="block font-medium mb-1">Split Value</label> |  | ||||||
|             <input type="text" className="w-full border px-2 py-1 rounded" placeholder="e.g. 10MB or 5 pages" /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       {mode === "byChapters" && ( |  | ||||||
|         <div className="space-y-2"> |  | ||||||
|           <div> |  | ||||||
|             <label className="block font-medium mb-1">Bookmark Level</label> |  | ||||||
|             <input type="number" className="w-full border px-2 py-1 rounded" defaultValue="0" min="0" /> |  | ||||||
|           </div> |  | ||||||
|           <div className="flex items-center space-x-2"> |  | ||||||
|             <input type="checkbox" id="includeMetadata" /> |  | ||||||
|             <label htmlFor="includeMetadata">Include Metadata</label> |  | ||||||
|           </div> |  | ||||||
|           <div className="flex items-center space-x-2"> |  | ||||||
|             <input type="checkbox" id="allowDuplicates" /> |  | ||||||
|             <label htmlFor="allowDuplicates">Allow Duplicate Bookmarks</label> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       <button className="bg-blue-600 text-white px-4 py-2 rounded mt-2">Split PDF</button> |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default function HomePage() { |  | ||||||
|   const [selectedTool, setSelectedTool] = useState(null); |  | ||||||
|   const [search, setSearch] = useState(""); |  | ||||||
| 
 |  | ||||||
|   const filteredTools = tools.filter(tool => |  | ||||||
|     tool.name.toLowerCase().includes(search.toLowerCase()) |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div className="flex h-screen overflow-hidden"> |  | ||||||
|       {/* Left Sidebar */} |  | ||||||
|       <div className="w-64 bg-gray-100 p-4 flex flex-col space-y-2 overflow-y-auto border-r"> |  | ||||||
|         <input |  | ||||||
|           type="text" |  | ||||||
|           placeholder="Search tools..." |  | ||||||
|           value={search} |  | ||||||
|           onChange={(e) => setSearch(e.target.value)} |  | ||||||
|           className="mb-3 px-2 py-1 border rounded text-sm" |  | ||||||
|         /> |  | ||||||
|         {filteredTools.map(tool => ( |  | ||||||
|           <button |  | ||||||
|             key={tool.id} |  | ||||||
|             title={tool.name} |  | ||||||
|             onClick={() => setSelectedTool(tool)} |  | ||||||
|             className="flex items-center space-x-3 p-2 hover:bg-gray-200 rounded text-left" |  | ||||||
|           > |  | ||||||
|             <div className="text-xl leading-none flex items-center justify-center h-6 w-6"> |  | ||||||
|               {tool.icon} |  | ||||||
|             </div> |             </div> | ||||||
|             <span className="text-sm font-medium">{tool.name}</span> |             <form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data"> | ||||||
|           </button> |               <div | ||||||
|         ))} |                 th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"> | ||||||
|       </div> |               </div> | ||||||
| 
 | 
 | ||||||
|       {/* Central PDF Viewer Area */} |               <div class="mb-3"> | ||||||
|       <div className="flex-1 bg-white flex items-center justify-center overflow-hidden"> |                 <label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label> | ||||||
|         <div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center"> |                 <input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0" | ||||||
|           <span className="text-gray-400 text-lg">PDF Viewer Placeholder</span> |                   required> | ||||||
|         </div> |               </div> | ||||||
|       </div> |  | ||||||
| 
 | 
 | ||||||
|       {/* Right Sidebar: Tool Interactions */} |               <div class="mb-3 form-check"> | ||||||
|       <div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto"> |                 <input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata"> | ||||||
|         <h2 className="text-lg font-semibold mb-4">Tool Panel</h2> |                 <label class="form-check-label" for="includeMetadata" | ||||||
|         <div className="space-y-3"> |                   th:text="#{splitByChapters.includeMetadata}"></label> | ||||||
|           {selectedTool?.id === "split-pdf" ? ( |                 <input type="hidden" name="includeMetadata" value="false" /> | ||||||
|             <SplitPdfPanel /> |               </div> | ||||||
|           ) : selectedTool ? ( | 
 | ||||||
|             <div className="p-2 border rounded bg-white shadow-sm"> |               <div class="mb-3 form-check"> | ||||||
|               <h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3> |                 <input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates"> | ||||||
|               <p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p> |                 <label class="form-check-label" for="allowDuplicates" | ||||||
|             </div> |                   th:text="#{splitByChapters.allowDuplicates}"></label> | ||||||
|           ) : ( |                 <input type="hidden" name="allowDuplicates" value="false" /> | ||||||
|             <div className="p-2 border rounded bg-white shadow-sm"> |               </div> | ||||||
|               <p className="text-sm">Select a tool to begin interacting with the PDF.</p> | 
 | ||||||
|             </div> |               <p> | ||||||
|           )} |                 <a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button" | ||||||
|  |                   aria-expanded="false" aria-controls="info" th:text="#{info}"></a> | ||||||
|  |               </p> | ||||||
|  |               <div class="collapse" id="info"> | ||||||
|  |                 <p th:text="#{splitByChapters.desc.1}"></p> | ||||||
|  |                 <p th:text="#{splitByChapters.desc.2}"></p> | ||||||
|  |                 <p th:text="#{splitByChapters.desc.3}"></p> | ||||||
|  |                 <p th:text="#{splitByChapters.desc.4}"></p> | ||||||
|  |               </div> | ||||||
|  | 
 | ||||||
|  |               <br> | ||||||
|  |               <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button> | ||||||
|  |             </form> | ||||||
|  | 
 | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |     <th:block th:insert="~{fragments/footer.html :: footer}"></th:block> | ||||||
| } |   </div> | ||||||
|  | </body> | ||||||
|  | 
 | ||||||
|  | </html> | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user