Basic homepage with compress and split

This commit is contained in:
Reece Browne 2025-05-13 23:32:54 +01:00
parent b567f4b110
commit d669964975
13 changed files with 1499 additions and 179 deletions

View File

@ -8,10 +8,15 @@
"name": "frontend",
"version": "0.1.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.9.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
@ -2352,6 +2357,167 @@
"postcss-selector-parser": "^6.0.10"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@ -2964,6 +3130,251 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"license": "MIT"
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
"integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz",
"integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
"integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/core-downloads-tracker": "^7.1.0",
"@mui/system": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
"license": "MIT"
},
"node_modules/@mui/private-theming": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
"integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/private-theming": "^7.1.0",
"@mui/styled-engine": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
"license": "MIT"
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@ -3088,6 +3499,16 @@
}
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -3813,6 +4234,12 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/q": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
@ -3831,6 +4258,25 @@
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -4895,6 +5341,32 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -5633,6 +6105,15 @@
"wrap-ansi": "^7.0.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -6363,6 +6844,12 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -6689,6 +7176,16 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -8157,6 +8654,12 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@ -8842,6 +9345,21 @@
"he": "bin/he"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@ -13627,6 +14145,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@ -14038,6 +14562,22 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -15614,6 +16154,12 @@
"postcss": "^8.2.15"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",

View File

@ -4,10 +4,15 @@
"private": true,
"proxy": "http://localhost:8080",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.9.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -10,34 +10,13 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@ -1,97 +1,230 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import ConstructionIcon from '@mui/icons-material/Construction';
import AddToPhotosIcon from '@mui/icons-material/AddToPhotos';
import ContentCutIcon from '@mui/icons-material/ContentCut';
import RotateRightIcon from '@mui/icons-material/RotateRight';
import CropIcon from '@mui/icons-material/Crop';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import DeleteIcon from '@mui/icons-material/Delete';
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';
export default function HomePage() {
const [homeText, setHomeText] = useState("");
const [showSurvey, setShowSurvey] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [favoritesVisible, setFavoritesVisible] = useState(true);
import SplitPdfPanel from "../tools/Split";
import CompressPdfPanel from "../tools/Compress-pdf";
useEffect(() => {
setHomeText("Your document tools in one secure place");
}, []);
const toolRegistry = {
"split-pdf": { icon: <PictureAsPdfIcon />, name: "Split PDF", component: SplitPdfPanel },
"compress-pdf": { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel }
};
const features = [
{
id: "redact",
icon: "🖊️",
title: "Redact PDF",
description: "Remove sensitive content",
tags: ["security"]
},
{
id: "multi-tool",
icon: "🛠️",
title: "Multi-Tool",
description: "Bundle many tools together",
tags: ["organize"]
},
{
id: "validate-signature",
icon: "✔️",
title: "Validate Signature",
description: "Check document authenticity",
tags: ["security"]
}
];
const filteredFeatures = features.filter(f =>
f.title.toLowerCase().includes(searchTerm.toLowerCase())
);
const tools = Object.entries(toolRegistry).map(([id, { icon, name }]) => ({ id, icon, name }));
// Example tool panels
function ToolPanel({ selectedTool }) {
if (!selectedTool) {
return (
<div style={{ padding: "2rem" }}>
<h1>{homeText}</h1>
<div style={{ margin: "1rem 0" }}>
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ padding: "0.5rem", width: "200px" }}
/>
<select style={{ marginLeft: "1rem", padding: "0.5rem" }}>
<option value="alphabetical">Alphabetical</option>
<option value="global">Global Popularity</option>
</select>
<button
onClick={() => setFavoritesVisible(!favoritesVisible)}
style={{ marginLeft: "1rem", padding: "0.5rem" }}
>
{favoritesVisible ? "Hide" : "Show"} Favorites
</button>
</div>
{favoritesVisible && (
<div style={{ margin: "1rem 0" }}>
<h2>Favorite Tools</h2>
<p>(You can add favorites here later)</p>
</div>
)}
<div>
<h2>Recent Features</h2>
<div style={{ display: "flex", flexWrap: "wrap", gap: "1rem" }}>
{filteredFeatures.map((f) => (
<div
key={f.id}
style={{
border: "1px solid #ccc",
padding: "1rem",
borderRadius: "8px",
width: "200px"
}}
>
<div style={{ fontSize: "2rem" }}>{f.icon}</div>
<h3>{f.title}</h3>
<p>{f.description}</p>
<small>{f.tags.join(", ")}</small>
</div>
))}
</div>
<div className="p-2 border rounded bg-white shadow-sm">
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
</div>
);
}
return (
<div className="p-2 border rounded bg-white shadow-sm">
<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>
);
}
export default function HomePage() {
const tools = [
{ id: "multi-tool", icon: <ConstructionIcon />, name: "Multi-Tool" },
{ id: "merge-pdfs", icon: <AddToPhotosIcon />, name: "Merge PDFs" },
{ id: "split-pdf", icon: <ContentCutIcon />, name: "Split PDF" },
{ 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);
const [search, setSearch] = useState("");
const [pdfFile, setPdfFile] = useState(null);
const SelectedComponent = selectedTool ? toolRegistry[selectedTool.id]?.component : null;
const [downloadUrl, setDownloadUrl] = useState(null);
const filteredTools = tools.filter(tool =>
tool.name.toLowerCase().includes(search.toLowerCase())
);
function handleFileUpload(e) {
const file = e.target.files[0];
if (file && file.type === "application/pdf") {
const fileUrl = URL.createObjectURL(file);
setPdfFile({ file, url: fileUrl });
}
}
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>
<span className="text-sm font-medium">{tool.name}</span>
</button>
))}
</div>
{/* Central PDF Viewer Area */}
<div className="flex-1 bg-white flex items-center justify-center overflow-hidden">
<div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center">
{!pdfFile ? (
<label className="cursor-pointer text-blue-600 underline">
Click to upload a PDF
<input
type="file"
accept="application/pdf"
onChange={handleFileUpload}
className="hidden"
/>
</label>
) : (
<iframe
src={pdfFile.url}
title="PDF Viewer"
className="w-full h-full border-none"
/>
)}
</div>
</div>
{/* Right Sidebar: Tool Interactions */}
<div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">Tool Panel</h2>
<div className="space-y-3">
{SelectedComponent ? (
<SelectedComponent file={pdfFile} downloadUrl setDownloadUrl />
) : selectedTool ? (
<div className="p-2 border rounded bg-white shadow-sm">
<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>
) : (
<div className="p-2 border rounded bg-white shadow-sm">
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,91 @@
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>
);
}

191
frontend/src/tools/Split.js Normal file
View File

@ -0,0 +1,191 @@
import React, { useState } from "react";
import axios from "axios";
import DownloadIcon from '@mui/icons-material/Download';
export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) {
const [mode, setMode] = useState("byPages");
const [status, setStatus] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) {
setStatus("Please upload a PDF first.");
return;
}
const formData = new FormData();
formData.append("fileInput", file.file);
let endpoint = "";
if (mode === "byPages") {
const pageNumbers = document.getElementById("pagesInput").value;
formData.append("pageNumbers", pageNumbers);
endpoint = "/api/v1/general/split-pages";
} else if (mode === "bySections") {
const horizontal = document.getElementById("horizontalDivisions").value;
const vertical = document.getElementById("verticalDivisions").value;
const merge = document.getElementById("merge").checked;
formData.append("horizontalDivisions", horizontal);
formData.append("verticalDivisions", vertical);
formData.append("merge", merge);
endpoint = "/api/v1/general/split-pdf-by-sections";
} else if (mode === "bySizeOrCount") {
const splitType = document.getElementById("splitType").value;
const splitValue = document.getElementById("splitValue").value;
formData.append("splitType", splitType === "size" ? 0 : splitType === "pages" ? 1 : 2);
formData.append("splitValue", splitValue);
endpoint = "/api/v1/general/split-by-size-or-count";
} else if (mode === "byChapters") {
const bookmarkLevel = document.getElementById("bookmarkLevel").value;
const includeMetadata = document.getElementById("includeMetadata").checked;
const allowDuplicates = document.getElementById("allowDuplicates").checked;
formData.append("bookmarkLevel", bookmarkLevel);
formData.append("includeMetadata", includeMetadata);
formData.append("allowDuplicates", allowDuplicates);
endpoint = "/api/v1/general/split-pdf-by-chapters";
}
setStatus("Processing split...");
try {
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 url = window.URL.createObjectURL(blob);
setDownloadUrl(url);
setStatus("Download ready.");
} catch (error) {
console.error(error);
setStatus("Split failed.");
}
};
return (
<form onSubmit={handleSubmit} className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm">
<h3 className="font-semibold">Split PDF</h3>
<div>
<label className="block mb-1 font-medium">Split Mode</label>
<select
value={mode}
onChange={(e) => setMode(e.target.value)}
className="w-full border px-2 py-1 rounded"
>
<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"
id="pagesInput"
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"
id="horizontalDivisions"
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"
id="verticalDivisions"
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 id="splitType" 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"
id="splitValue"
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"
id="bookmarkLevel"
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 type="submit" className="bg-blue-600 text-white px-4 py-2 rounded mt-2">
Split PDF
</button>
{status && <p className="text-xs text-gray-600">{status}</p>}
{status === "Download ready." && downloadUrl && (
<a
href={downloadUrl}
download="split_output.zip"
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>
)}
</form>
);
}

View File

@ -0,0 +1,9 @@
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}"
],
theme: {
extend: {},
},
plugins: [],
}

264
package-lock.json generated Normal file
View File

@ -0,0 +1,264 @@
{
"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"
}
}
}
}

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"devDependencies": {
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.6"
}
}

View File

@ -6,12 +6,13 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ReactRoutingController {
@GetMapping(
value = {
"/{path:^(?!api|static|robots\\.txt|favicon\\.ico).*}",
"/**/{path:^(?!.*\\.).*}"
})
public String forwardToIndex() {
@GetMapping("/{path:^(?!api|static|robots\\.txt|favicon\\.ico)[^\\.]*$}")
public String forwardRootPaths() {
return "forward:/index.html";
}
@GetMapping("/{path:^(?!api|static)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
public String forwardNestedPaths() {
return "forward:/index.html";
}
}

View File

@ -1,72 +1,156 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
import React, { useState } from "react";
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
<head>
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}">
</th:block>
</head>
const tools = [
{ id: "split-pdf", icon: <PictureAsPdfIcon />, name: "Split PDF" }
];
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<svg class="material-symbols-rounded tool-header-icon advance">
<use xlink:href="/images/split-chapters.svg#icon-split-chapters"></use>
</svg>
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
</div>
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
function SplitPdfPanel() {
const [mode, setMode] = useState("byPages");
return (
<div className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm">
<h3 className="font-semibold">Split PDF</h3>
<div>
<label className="block mb-1 font-medium">Split Mode</label>
<select
value={mode}
onChange={(e) => setMode(e.target.value)}
className="w-full border px-2 py-1 rounded"
>
<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>
<div class="mb-3">
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0"
required>
{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>
<span className="text-sm font-medium">{tool.name}</span>
</button>
))}
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
<label class="form-check-label" for="includeMetadata"
th:text="#{splitByChapters.includeMetadata}"></label>
<input type="hidden" name="includeMetadata" value="false" />
{/* Central PDF Viewer Area */}
<div className="flex-1 bg-white flex items-center justify-center overflow-hidden">
<div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center">
<span className="text-gray-400 text-lg">PDF Viewer Placeholder</span>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
<label class="form-check-label" for="allowDuplicates"
th:text="#{splitByChapters.allowDuplicates}"></label>
<input type="hidden" name="allowDuplicates" value="false" />
{/* Right Sidebar: Tool Interactions */}
<div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">Tool Panel</h2>
<div className="space-y-3">
{selectedTool?.id === "split-pdf" ? (
<SplitPdfPanel />
) : selectedTool ? (
<div className="p-2 border rounded bg-white shadow-sm">
<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>
<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 className="p-2 border rounded bg-white shadow-sm">
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
</form>
)}
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
);
}