refactor: streamline HTML structure and enhance UI components

- Cleaned up the HTML structure in index.html and pdf-multi-tool.html for better readability and maintainability.
- Improved the user interface of the PDF multi-tool with responsive button designs and better layout.
- Added new CSS styles for button states and cursor behavior.
- Updated README with corrected Docker deployment link.
- Refactored JavaScript logic to utilize new helper functions for formatting star counts.
- Commented out unused attachment functionalities in the logic files for future integration.
This commit is contained in:
abdullahalam123
2025-11-10 21:54:41 +05:30
parent 0634600073
commit 7e14c83ab8
11 changed files with 1161 additions and 1321 deletions

View File

@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
### 🚀 Quick Start with Docker ### 🚀 Quick Start with Docker
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/K4AU2B)
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository: You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:

View File

@@ -1,26 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BentoPDF - The Privacy First PDF Toolkit</title> <title>BentoPDF - The Privacy First PDF Toolkit</title>
<link rel="icon" type="image/png" href="./images/favicon.svg" /> <link rel="icon" type="image/png" href="./images/favicon.svg" />
<link href="/src/css/styles.css" rel="stylesheet" /> <link href="/src/css/styles.css" rel="stylesheet" />
</head> </head>
<body class="antialiased"> <body class="antialiased">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
class="flex-shrink-0 flex items-center cursor-pointer" <img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
id="home-logo"
>
<img
src="images/favicon.svg"
alt="Bento PDF Logo"
class="h-8 w-8"
/>
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -36,47 +30,19 @@
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <div class="md:hidden flex items-center">
<button <button id="mobile-menu-button" type="button"
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu" aria-controls="mobile-menu" aria-expanded="false">
aria-expanded="false"
>
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- Hamburger Icon -->
<svg <svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
id="menu-icon" stroke="currentColor" aria-hidden="true">
class="block h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg <svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
id="close-icon" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
class="hidden h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -84,17 +50,12 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link" <a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
>All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -103,63 +64,47 @@
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4"> <h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span class="marker-slanted"> PDF Toolkit </span> built for The <span class="marker-slanted"> PDF Toolkit </span> built for
privacy<span privacy<span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500" class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
>.</span
>
</h1> </h1>
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p> <p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
<div <div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8"
>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
No Signups No Signups
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Unlimited Use Unlimited Use
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Works Offline Works Offline
</span> </span>
</div> </div>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<a <a href="#tools-header"
href="#tools-header" class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1" Using - Forever Free</a>
>Start Using - Forever Free</a <a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
> inline-flex items-center gap-1.5 text-sm font-medium
<a bg-white text-gray-800 border border-gray-300
href="https://github.com/alam00000/bentopdf" dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
target="_blank" pl-2.5 pr-3 py-1
rel="noopener noreferrer" rounded-full transition-colors duration-200
class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-indigo-400 underline transition-colors" shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
> ">
<svg <svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
class="w-4 h-4" aria-hidden="true">
fill="currentColor" <path fill-rule="evenodd"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
<span>View on GitHub</span>
<span class="inline-flex items-center gap-1">
<i data-lucide="star" class="w-3 h-3"></i>
<span id="github-stars">-</span> <span id="github-stars">-</span>
</span>
</a> </a>
</div> </div>
</section> </section>
@@ -173,10 +118,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="user-plus"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Signup</h3> <h3 class="text-xl font-bold text-white">No Signup</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -185,10 +127,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="shield"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Uploads</h3> <h3 class="text-xl font-bold text-white">No Uploads</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -197,10 +136,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="badge-dollar-sign"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Forever Free</h3> <h3 class="text-xl font-bold text-white">Forever Free</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -209,10 +145,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="infinity"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Limits</h3> <h3 class="text-xl font-bold text-white">No Limits</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -221,20 +154,14 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="layers"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Batch Processing</h3> <h3 class="text-xl font-bold text-white">Batch Processing</h3>
</div> </div>
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p> <p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="zap"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Lightning Fast</h3> <h3 class="text-xl font-bold text-white">Lightning Fast</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -259,47 +186,30 @@
<span class="absolute inset-y-0 left-0 flex items-center pl-3"> <span class="absolute inset-y-0 left-0 flex items-center pl-3">
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i> <i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
</span> </span>
<input <input type="text" id="search-bar"
type="text"
id="search-bar"
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500" class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)" placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
/> <span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2">
<span
class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2"
>
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd> <kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
</span> </span>
</div> </div>
</div> </div>
<div <div id="tool-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"></div>
id="tool-grid"
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"
></div>
</div> </div>
<div <div id="tool-interface"
id="tool-interface" class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700">
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700" <button id="back-to-grid"
> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<button
id="back-to-grid"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
>
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span> <span class="cursor-pointer"> Back to Tools </span>
</button> </button>
<div id="tool-content"></div> <div id="tool-content"></div>
</div> </div>
<div <div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
id="loader-modal" <div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
>
<div
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
>
<div class="solid-spinner"></div> <div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium"> <p id="loader-text" class="text-white text-lg font-medium">
Processing... Processing...
@@ -307,48 +217,33 @@
</div> </div>
</div> </div>
<div <div id="alert-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4">
id="alert-modal" <div class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
>
<div
class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center"
>
<h3 id="alert-title" class="text-xl font-bold text-white mb-2"> <h3 id="alert-title" class="text-xl font-bold text-white mb-2">
Alert Alert
</h3> </h3>
<p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line"> <p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line">
This is an alert message. This is an alert message.
</p> </p>
<button <button id="alert-ok"
id="alert-ok" class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
>
OK OK
</button> </button>
</div> </div>
</div> </div>
<div <div id="preview-modal"
id="preview-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4" <div class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col">
>
<div
class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col"
>
<div class="p-4 border-b flex justify-between items-center"> <div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">Document Preview</h3> <h3 class="text-xl font-bold">Document Preview</h3>
<div class="flex gap-4"> <div class="flex gap-4">
<button <button id="preview-download-btn"
id="preview-download-btn" class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
>
Download as PDF Download as PDF
</button> </button>
<button <button id="preview-close-btn"
id="preview-close-btn" class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700"
>
Close Close
</button> </button>
</div> </div>
@@ -362,96 +257,62 @@
<!-- COMPLIANCE SECTION START --> <!-- COMPLIANCE SECTION START -->
<section id="security-compliance-section" class="py-20 hide-section"> <section id="security-compliance-section" class="py-20 hide-section">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<h2 <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance"
>
Your data never leaves your device Your data never leaves your device
<span class="inline-flex items-center gap-2"> <span class="inline-flex items-center gap-2">
<i <i data-lucide="shield" class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
data-lucide="shield"
class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"
></i>
We keep We keep
</span> </span>
<br class="hidden sm:block" /> <br class="hidden sm:block" />
<span <span
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold" class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
>
your information safe your information safe
</span> </span>
by following global security standards. by following global security standards.
</h2> </h2>
</div> </div>
<div class="mb-16 text-center"> <div class="mb-16 text-center">
<span <span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors"
>
All the processing happens locally on your device. All the processing happens locally on your device.
</span> </span>
</div> </div>
<div <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4"
>
<!-- GDPR Compliance --> <!-- GDPR Compliance -->
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/gdpr.svg" alt="GDPR compliance" class="w-full h-full" />
>
<img
src="/images/gdpr.svg"
alt="GDPR compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
GDPR compliance GDPR compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Protects the personal data and privacy of individuals within the Protects the personal data and privacy of individuals within the
European Union. European Union.
</p> </p>
</div> </div>
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/ccpa.svg" alt="CCPA compliance" class="w-full h-full" />
>
<img
src="/images/ccpa.svg"
alt="CCPA compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
CCPA compliance CCPA compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Gives California residents rights over how their personal Gives California residents rights over how their personal
information is collected, used, and shared. information is collected, used, and shared.
</p> </p>
</div> </div>
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/hipaa.svg" alt="HIPAA compliance" class="w-full h-full" />
>
<img
src="/images/hipaa.svg"
alt="HIPAA compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
HIPAA compliance HIPAA compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Sets safeguards for handling sensitive health information in the Sets safeguards for handling sensitive health information in the
United States healthcare system. United States healthcare system.
</p> </p>
@@ -464,27 +325,16 @@
<div class="section-divider hide-section"></div> <div class="section-divider hide-section"></div>
<section id="faq-accordion" class="space-y-4 hide-section"> <section id="faq-accordion" class="space-y-4 hide-section">
<h2 <h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8"
>
Frequently Asked <span class="marker-slanted">Questions</span> Frequently Asked <span class="marker-slanted">Questions</span>
</h2> </h2>
<!-- Existing FAQs here... --> <!-- Existing FAQs here... -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Is BentoPDF really free?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes, absolutely. All tools on BentoPDF are 100% free to use, with Yes, absolutely. All tools on BentoPDF are 100% free to use, with
no file limits, no sign-ups, and no watermarks. We believe no file limits, no sign-ups, and no watermarks. We believe
@@ -495,20 +345,11 @@
</div> </div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Are my files secure? Where are they processed?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Are my files secure? Where are they processed?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Your files are as secure as possible because they **never leave Your files are as secure as possible because they **never leave
your computer**. All processing happens directly in your web your computer**. All processing happens directly in your web
@@ -519,20 +360,11 @@
</div> </div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Does it work on Mac, Windows, and Mobile?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Does it work on Mac, Windows, and Mobile?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes! Since BentoPDF runs entirely in your browser, it works on any Yes! Since BentoPDF runs entirely in your browser, it works on any
operating system with a modern web browser, including Windows, operating system with a modern web browser, including Windows,
@@ -543,20 +375,11 @@
<!-- New FAQ: GDPR Compliance --> <!-- New FAQ: GDPR Compliance -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Is BentoPDF GDPR compliant?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes. BentoPDF is fully GDPR compliant. Since all file processing Yes. BentoPDF is fully GDPR compliant. Since all file processing
happens locally in your browser and we never collect or transmit happens locally in your browser and we never collect or transmit
@@ -568,20 +391,11 @@
<!-- New FAQ: Data Storage --> <!-- New FAQ: Data Storage -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Do you store or track any of my files?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
No. We never store, track, or log your files. Everything you do on No. We never store, track, or log your files. Everything you do on
BentoPDF happens in your browser memory and disappears once you BentoPDF happens in your browser memory and disappears once you
@@ -593,20 +407,11 @@
<!-- New FAQ: Privacy --> <!-- New FAQ: Privacy -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF tools?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>What makes BentoPDF different from other PDF tools?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Most PDF tools upload your files to a server for processing. Most PDF tools upload your files to a server for processing.
BentoPDF never does that. We use secure, modern web technology to BentoPDF never does that. We use secure, modern web technology to
@@ -618,20 +423,11 @@
<!-- New FAQ: Browser-Based --> <!-- New FAQ: Browser-Based -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">How does browser-based processing keep me safe?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>How does browser-based processing keep me safe?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
By running entirely inside your browser, BentoPDF ensures that By running entirely inside your browser, BentoPDF ensures that
your files never leave your device. This eliminates the risks of your files never leave your device. This eliminates the risks of
@@ -643,30 +439,16 @@
<!-- New FAQ: Analytics --> <!-- New FAQ: Analytics -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Do you use cookies or analytics to track me?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
We care about your privacy. BentoPDF does not track personal We care about your privacy. BentoPDF does not track personal
information. We use information. We use
<a <a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
href="https://simpleanalytics.com" rel="noopener noreferrer">Simple Analytics</a>
class="text-indigo-400 hover:underline"
target="_blank"
rel="noopener noreferrer"
>Simple Analytics</a
>
solely to see anonymous visit counts. This means we can know how solely to see anonymous visit counts. This means we can know how
many users visit our site, but many users visit our site, but
<strong>we never know who you are</strong>. Simple Analytics is <strong>we never know who you are</strong>. Simple Analytics is
@@ -682,9 +464,7 @@
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12"> <h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
What Our <span class="marker-slanted">Users</span> Say What Our <span class="marker-slanted">Users</span> Say
</h2> </h2>
<div <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto"
>
<div class="testimonial-card"> <div class="testimonial-card">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<div> <div>
@@ -781,9 +561,7 @@
<div class="section-divider"></div> <div class="section-divider"></div>
<section id="support-section" class="py-20"> <section id="support-section" class="py-20">
<div <div class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl"
>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4"> <h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Like My Work? Like My Work?
</h2> </h2>
@@ -793,12 +571,8 @@
supporting its development. Every coffee helps! supporting its development. Every coffee helps!
</p> </p>
<a <a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer"
href="https://ko-fi.com/alio01" class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5"
>
<i data-lucide="coffee" class="w-7 h-7"></i> <i data-lucide="coffee" class="w-7 h-7"></i>
<span>Buy Me a Coffee</span> <span>Buy Me a Coffee</span>
</a> </a>
@@ -810,16 +584,10 @@
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
>
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4"> <div class="flex items-center justify-center md:justify-start mb-4">
<img <img src="public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
src="public/images/favicon.svg"
alt="Bento PDF Logo"
class="h-10 w-10 mr-3"
/>
<span class="text-xl font-bold text-white">BentoPDF</span> <span class="text-xl font-bold text-white">BentoPDF</span>
</div> </div>
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
@@ -834,17 +602,13 @@
<h3 class="font-bold text-white mb-4">Company</h3> <h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./about.html" class="hover:text-indigo-400" <a href="./about.html" class="hover:text-indigo-400">About Us</a>
>About Us</a
>
</li> </li>
<li> <li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a> <a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
</li> </li>
<li> <li>
<a href="./contact.html" class="hover:text-indigo-400" <a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
>Contact Us</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -853,14 +617,10 @@
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./terms.html" class="hover:text-indigo-400" <a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
>Terms and Conditions</a
>
</li> </li>
<li> <li>
<a href="./privacy.html" class="hover:text-indigo-400" <a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
>Privacy Policy</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -868,72 +628,33 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Follow Us</h3> <h3 class="font-bold text-white mb-4">Follow Us</h3>
<div class="flex justify-center md:justify-start space-x-4"> <div class="flex justify-center md:justify-start space-x-4">
<a <a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
href="https://github.com/alam00000/bentopdf" class="text-gray-400 hover:text-indigo-400" title="GitHub">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer" <path fill-rule="evenodd"
class="text-gray-400 hover:text-indigo-400"
title="GitHub"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
href="https://discord.gg/q42xWQmJ" class="text-gray-400 hover:text-indigo-400" title="Discord">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400"
title="Discord"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.instagram.com/thebentopdf/" title="Instagram">
class="text-gray-400 hover:text-indigo-400"
title="Instagram"
>
<i data-lucide="instagram"></i> <i data-lucide="instagram"></i>
</a> </a>
<a <a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.linkedin.com/company/bentopdf/" title="LinkedIn">
class="text-gray-400 hover:text-indigo-400"
title="LinkedIn"
>
<i data-lucide="linkedin"></i> <i data-lucide="linkedin"></i>
</a> </a>
<a <a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
href="https://x.com/BentoPDF" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
class="text-gray-400 hover:text-indigo-400"
title="X (Twitter)"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
/>
</svg> </svg>
</a> </a>
</div> </div>
@@ -945,5 +666,6 @@
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>

View File

@@ -510,3 +510,14 @@ details > summary .icon {
details[open] > summary .icon { details[open] > summary .icon {
transform: rotate(45deg); transform: rotate(45deg);
} }
button,
.btn,
.btn-gradient {
cursor: pointer;
}
button:disabled,
.btn:disabled {
cursor: not-allowed;
}

View File

@@ -3,6 +3,12 @@ export const categories = [
{ {
name: 'Popular Tools', name: 'Popular Tools',
tools: [ tools: [
{
href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool',
icon: 'pencil-ruler',
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
},
{ {
id: 'merge', id: 'merge',
name: 'Merge PDF', name: 'Merge PDF',
@@ -312,22 +318,23 @@ export const categories = [
icon: 'paperclip', icon: 'paperclip',
subtitle: 'Embed one or more files into your PDF.', subtitle: 'Embed one or more files into your PDF.',
}, },
{ // TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF
id: 'extract-attachments', // {
name: 'Extract Attachments', // id: 'extract-attachments',
icon: 'download', // name: 'Extract Attachments',
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', // icon: 'download',
}, // subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
{ // },
id: 'edit-attachments', // {
name: 'Edit Attachments', // id: 'edit-attachments',
icon: 'file-edit', // name: 'Edit Attachments',
subtitle: 'View, remove, or replace attachments in your PDF.', // icon: 'file-edit',
}, // subtitle: 'View, remove, or replace attachments in your PDF.',
// },
{ {
href: '/src/pages/pdf-multi-tool.html', href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool', name: 'PDF Multi Tool',
icon: 'layers', icon: 'pencil-ruler',
subtitle: 'Full-featured PDF editor with page management.', subtitle: 'Full-featured PDF editor with page management.',
}, },
{ {

View File

@@ -1,205 +1,207 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; // TODO@ALAM - USE CPDF HERE
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let currentAttachments: Array<{ name: string; index: number; size: number }> = []; // import { showLoader, hideLoader, showAlert } from '../ui.js';
let attachmentsToRemove: Set<number> = new Set(); // import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
let attachmentsToReplace: Map<number, File> = new Map(); // import { state } from '../state.js';
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function setupEditAttachmentsTool() { // let currentAttachments: Array<{ name: string; index: number; size: number }> = [];
const optionsDiv = document.getElementById('edit-attachments-options'); // let attachmentsToRemove: Set<number> = new Set();
if (!optionsDiv || !state.pdfDoc) return; // let attachmentsToReplace: Map<number, File> = new Map();
optionsDiv.classList.remove('hidden'); // export async function setupEditAttachmentsTool() {
await loadAttachmentsList(); // const optionsDiv = document.getElementById('edit-attachments-options');
} // if (!optionsDiv || !state.pdfDoc) return;
async function loadAttachmentsList() { // optionsDiv.classList.remove('hidden');
const attachmentsList = document.getElementById('attachments-list'); // await loadAttachmentsList();
if (!attachmentsList || !state.pdfDoc) return; // }
attachmentsList.innerHTML = ''; // async function loadAttachmentsList() {
currentAttachments = []; // const attachmentsList = document.getElementById('attachments-list');
attachmentsToRemove.clear(); // if (!attachmentsList || !state.pdfDoc) return;
attachmentsToReplace.clear();
try { // attachmentsList.innerHTML = '';
// Get embedded files from PDF // currentAttachments = [];
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() // attachmentsToRemove.clear();
.filter(([ref, obj]: any) => { // attachmentsToReplace.clear();
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
return dict && dict.get('Type')?.toString() === '/Filespec';
});
if (embeddedFiles.length === 0) { // try {
attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>'; // // Get embedded files from PDF
return; // const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
} // .filter(([ref, obj]: any) => {
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
// return dict && dict.get('Type')?.toString() === '/Filespec';
// });
let index = 0; // if (embeddedFiles.length === 0) {
for (const [ref, fileSpec] of embeddedFiles) { // attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
try { // return;
const fileSpecDict = fileSpec as any; // }
const fileName = fileSpecDict.get('UF')?.decodeText() ||
fileSpecDict.get('F')?.decodeText() ||
`attachment-${index + 1}`;
const ef = fileSpecDict.get('EF'); // let index = 0;
let fileSize = 0; // for (const [ref, fileSpec] of embeddedFiles) {
if (ef) { // try {
const fRef = ef.get('F') || ef.get('UF'); // const fileSpecDict = fileSpec as any;
if (fRef) { // const fileName = fileSpecDict.get('UF')?.decodeText() ||
const fileStream = state.pdfDoc.context.lookup(fRef); // fileSpecDict.get('F')?.decodeText() ||
if (fileStream) { // `attachment-${index + 1}`;
fileSize = (fileStream as any).getContents().length;
}
}
}
currentAttachments.push({ name: fileName, index, size: fileSize }); // const ef = fileSpecDict.get('EF');
// let fileSize = 0;
// if (ef) {
// const fRef = ef.get('F') || ef.get('UF');
// if (fRef) {
// const fileStream = state.pdfDoc.context.lookup(fRef);
// if (fileStream) {
// fileSize = (fileStream as any).getContents().length;
// }
// }
// }
const attachmentDiv = document.createElement('div'); // currentAttachments.push({ name: fileName, index, size: fileSize });
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = index.toString();
const infoDiv = document.createElement('div'); // const attachmentDiv = document.createElement('div');
infoDiv.className = 'flex-1'; // attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
const nameSpan = document.createElement('span'); // attachmentDiv.dataset.attachmentIndex = index.toString();
nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = fileName;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-gray-400 text-sm';
sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
infoDiv.append(nameSpan, sizeSpan);
const actionsDiv = document.createElement('div'); // const infoDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2'; // infoDiv.className = 'flex-1';
// const nameSpan = document.createElement('span');
// nameSpan.className = 'text-white font-medium block';
// nameSpan.textContent = fileName;
// const sizeSpan = document.createElement('span');
// sizeSpan.className = 'text-gray-400 text-sm';
// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
// infoDiv.append(nameSpan, sizeSpan);
// Remove button // const actionsDiv = document.createElement('div');
const removeBtn = document.createElement('button'); // actionsDiv.className = 'flex items-center gap-2';
removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = () => {
attachmentsToRemove.add(index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.disabled = true;
};
// Replace button // // Remove button
const replaceBtn = document.createElement('button'); // const removeBtn = document.createElement('button');
replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; // removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>'; // removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
replaceBtn.title = 'Replace attachment'; // removeBtn.title = 'Remove attachment';
replaceBtn.onclick = () => { // removeBtn.onclick = () => {
const input = document.createElement('input'); // attachmentsToRemove.add(index);
input.type = 'file'; // attachmentDiv.classList.add('opacity-50', 'line-through');
input.onchange = async (e) => { // removeBtn.disabled = true;
const file = (e.target as HTMLInputElement).files?.[0]; // };
if (file) {
attachmentsToReplace.set(index, file);
nameSpan.textContent = `${fileName}${file.name}`;
nameSpan.classList.add('text-yellow-400');
}
};
input.click();
};
actionsDiv.append(replaceBtn, removeBtn); // // Replace button
attachmentDiv.append(infoDiv, actionsDiv); // const replaceBtn = document.createElement('button');
attachmentsList.appendChild(attachmentDiv); // replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
index++; // replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
} catch (e) { // replaceBtn.title = 'Replace attachment';
console.warn(`Failed to process attachment ${index}:`, e); // replaceBtn.onclick = () => {
index++; // const input = document.createElement('input');
} // input.type = 'file';
} // input.onchange = async (e) => {
} catch (e) { // const file = (e.target as HTMLInputElement).files?.[0];
console.error('Error loading attachments:', e); // if (file) {
showAlert('Error', 'Failed to load attachments from PDF.'); // attachmentsToReplace.set(index, file);
} // nameSpan.textContent = `${fileName} → ${file.name}`;
} // nameSpan.classList.add('text-yellow-400');
// }
// };
// input.click();
// };
export async function editAttachments() { // actionsDiv.append(replaceBtn, removeBtn);
if (!state.pdfDoc) { // attachmentDiv.append(infoDiv, actionsDiv);
showAlert('Error', 'PDF is not loaded.'); // attachmentsList.appendChild(attachmentDiv);
return; // index++;
} // } catch (e) {
// console.warn(`Failed to process attachment ${index}:`, e);
// index++;
// }
// }
// } catch (e) {
// console.error('Error loading attachments:', e);
// showAlert('Error', 'Failed to load attachments from PDF.');
// }
// }
showLoader('Updating attachments...'); // export async function editAttachments() {
try { // if (!state.pdfDoc) {
// Create a new PDF document // showAlert('Error', 'PDF is not loaded.');
const newPdfDoc = await PDFLibDocument.create(); // return;
// }
// Copy all pages // showLoader('Updating attachments...');
const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); // try {
pages.forEach((page: any) => newPdfDoc.addPage(page)); // // Create a new PDF document
// const newPdfDoc = await PDFLibDocument.create();
// Handle attachments // // Copy all pages
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() // const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
.filter(([ref, obj]: any) => { // pages.forEach((page: any) => newPdfDoc.addPage(page));
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
return dict && dict.get('Type')?.toString() === '/Filespec';
});
let attachmentIndex = 0; // // Handle attachments
for (const [ref, fileSpec] of embeddedFiles) { // const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
if (attachmentsToRemove.has(attachmentIndex)) { // .filter(([ref, obj]: any) => {
attachmentIndex++; // const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
continue; // Skip removed attachments // return dict && dict.get('Type')?.toString() === '/Filespec';
} // });
if (attachmentsToReplace.has(attachmentIndex)) { // let attachmentIndex = 0;
// Replace attachment // for (const [ref, fileSpec] of embeddedFiles) {
const replacementFile = attachmentsToReplace.get(attachmentIndex)!; // if (attachmentsToRemove.has(attachmentIndex)) {
const fileBytes = await readFileAsArrayBuffer(replacementFile); // attachmentIndex++;
await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { // continue; // Skip removed attachments
mimeType: replacementFile.type || 'application/octet-stream', // }
description: `Attached file: ${replacementFile.name}`,
creationDate: new Date(),
modificationDate: new Date(replacementFile.lastModified),
});
} else {
// Keep existing attachment - copy it
try {
const fileSpecDict = fileSpec as any;
const fileName = fileSpecDict.get('UF')?.decodeText() ||
fileSpecDict.get('F')?.decodeText() ||
`attachment-${attachmentIndex + 1}`;
const ef = fileSpecDict.get('EF'); // if (attachmentsToReplace.has(attachmentIndex)) {
if (ef) { // // Replace attachment
const fRef = ef.get('F') || ef.get('UF'); // const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
if (fRef) { // const fileBytes = await readFileAsArrayBuffer(replacementFile);
const fileStream = state.pdfDoc.context.lookup(fRef); // await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
if (fileStream) { // mimeType: replacementFile.type || 'application/octet-stream',
const fileData = (fileStream as any).getContents(); // description: `Attached file: ${replacementFile.name}`,
await newPdfDoc.attach(fileData, fileName, { // creationDate: new Date(),
mimeType: 'application/octet-stream', // modificationDate: new Date(replacementFile.lastModified),
description: `Attached file: ${fileName}`, // });
}); // } else {
} // // Keep existing attachment - copy it
} // try {
} // const fileSpecDict = fileSpec as any;
} catch (e) { // const fileName = fileSpecDict.get('UF')?.decodeText() ||
console.warn(`Failed to copy attachment ${attachmentIndex}:`, e); // fileSpecDict.get('F')?.decodeText() ||
} // `attachment-${attachmentIndex + 1}`;
}
attachmentIndex++;
}
const pdfBytes = await newPdfDoc.save(); // const ef = fileSpecDict.get('EF');
downloadFile( // if (ef) {
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), // const fRef = ef.get('F') || ef.get('UF');
`edited-attachments-${state.files[0].name}` // if (fRef) {
); // const fileStream = state.pdfDoc.context.lookup(fRef);
showAlert('Success', 'Attachments updated successfully!'); // if (fileStream) {
} catch (e) { // const fileData = (fileStream as any).getContents();
console.error(e); // await newPdfDoc.attach(fileData, fileName, {
showAlert('Error', 'Failed to edit attachments.'); // mimeType: 'application/octet-stream',
} finally { // description: `Attached file: ${fileName}`,
hideLoader(); // });
} // }
} // }
// }
// } catch (e) {
// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
// }
// }
// attachmentIndex++;
// }
// const pdfBytes = await newPdfDoc.save();
// downloadFile(
// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
// `edited-attachments-${state.files[0].name}`
// );
// showAlert('Success', 'Attachments updated successfully!');
// } catch (e) {
// console.error(e);
// showAlert('Error', 'Failed to edit attachments.');
// } finally {
// hideLoader();
// }
// }

View File

@@ -1,86 +1,88 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; // TODO@ALAM - USE CPDF HERE
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import JSZip from 'jszip';
export async function extractAttachments() { // import { showLoader, hideLoader, showAlert } from '../ui.js';
if (state.files.length === 0) { // import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
showAlert('No Files', 'Please select at least one PDF file.'); // import { state } from '../state.js';
return; // import { PDFDocument as PDFLibDocument } from 'pdf-lib';
} // import JSZip from 'jszip';
showLoader('Extracting attachments...'); // export async function extractAttachments() {
try { // if (state.files.length === 0) {
const zip = new JSZip(); // showAlert('No Files', 'Please select at least one PDF file.');
let totalAttachments = 0; // return;
// }
for (const file of state.files) { // showLoader('Extracting attachments...');
const pdfBytes = await readFileAsArrayBuffer(file); // try {
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { // const zip = new JSZip();
ignoreEncryption: true, // let totalAttachments = 0;
});
const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() // for (const file of state.files) {
.filter(([ref, obj]: any) => { // const pdfBytes = await readFileAsArrayBuffer(file);
// obj must be a PDFDict // const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
if (obj && typeof obj.get === 'function') { // ignoreEncryption: true,
const type = obj.get('Type'); // });
return type && type.toString() === '/Filespec';
}
return false;
});
if (embeddedFiles.length === 0) { // const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
console.warn(`No attachments found in ${file.name}`); // .filter(([ref, obj]: any) => {
continue; // // obj must be a PDFDict
} // if (obj && typeof obj.get === 'function') {
// const type = obj.get('Type');
// return type && type.toString() === '/Filespec';
// }
// return false;
// });
// Extract attachments // if (embeddedFiles.length === 0) {
const baseName = file.name.replace(/\.pdf$/i, ''); // console.warn(`No attachments found in ${file.name}`);
for (let i = 0; i < embeddedFiles.length; i++) { // continue;
try { // }
const [ref, fileSpec] = embeddedFiles[i];
const fileSpecDict = fileSpec as any;
// Get attachment name // // Extract attachments
const fileName = fileSpecDict.get('UF')?.decodeText() || // const baseName = file.name.replace(/\.pdf$/i, '');
fileSpecDict.get('F')?.decodeText() || // for (let i = 0; i < embeddedFiles.length; i++) {
`attachment-${i + 1}`; // try {
// const [ref, fileSpec] = embeddedFiles[i];
// const fileSpecDict = fileSpec as any;
// Get embedded file stream // // Get attachment name
const ef = fileSpecDict.get('EF'); // const fileName = fileSpecDict.get('UF')?.decodeText() ||
if (ef) { // fileSpecDict.get('F')?.decodeText() ||
const fRef = ef.get('F') || ef.get('UF'); // `attachment-${i + 1}`;
if (fRef) {
const fileStream = pdfDoc.context.lookup(fRef);
if (fileStream) {
const fileData = (fileStream as any).getContents();
zip.file(`${baseName}_${fileName}`, fileData);
totalAttachments++;
}
}
}
} catch (e) {
console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
}
}
}
if (totalAttachments === 0) { // // Get embedded file stream
showAlert('No Attachments', 'No attachments were found in the selected PDF(s).'); // const ef = fileSpecDict.get('EF');
hideLoader(); // if (ef) {
return; // const fRef = ef.get('F') || ef.get('UF');
} // if (fRef) {
// const fileStream = pdfDoc.context.lookup(fRef);
// if (fileStream) {
// const fileData = (fileStream as any).getContents();
// zip.file(`${baseName}_${fileName}`, fileData);
// totalAttachments++;
// }
// }
// }
// } catch (e) {
// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
// }
// }
// }
const zipBlob = await zip.generateAsync({ type: 'blob' }); // if (totalAttachments === 0) {
downloadFile(zipBlob, 'extracted-attachments.zip'); // showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); // hideLoader();
} catch (e) { // return;
console.error(e); // }
showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
} finally { // const zipBlob = await zip.generateAsync({ type: 'blob' });
hideLoader(); // downloadFile(zipBlob, 'extracted-attachments.zip');
} // showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
} // } catch (e) {
// console.error(e);
// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
// } finally {
// hideLoader();
// }
// }

View File

@@ -63,8 +63,8 @@ import {
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
import { linearizePdf } from './linearize.js'; import { linearizePdf } from './linearize.js';
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
import { extractAttachments } from './extract-attachments.js'; // import { extractAttachments } from './extract-attachments.js';
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; // import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js'; import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js'; import { removeRestrictions } from './remove-restrictions.js';
@@ -140,10 +140,10 @@ export const toolLogic = {
process: addAttachments, process: addAttachments,
setup: setupAddAttachmentsTool, setup: setupAddAttachmentsTool,
}, },
'extract-attachments': extractAttachments, // 'extract-attachments': extractAttachments,
'edit-attachments': { // 'edit-attachments': {
process: editAttachments, // process: editAttachments,
setup: setupEditAttachmentsTool, // setup: setupEditAttachmentsTool,
}, // },
'sanitize-pdf': sanitizePdf, 'sanitize-pdf': sanitizePdf,
}; };

View File

@@ -1,8 +1,12 @@
// @TODO:@ALAM- sometimes I think... and then I forget...
//
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile } from '../utils/helpers';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -34,7 +38,7 @@ const redoStack: Snapshot[] = [];
function snapshot() { function snapshot() {
const snap: Snapshot = { const snap: Snapshot = {
allPages: allPages.map(p => ({ ...p })), allPages: allPages.map(p => ({ ...p, canvas: p.canvas })),
selectedPages: Array.from(selectedPages), selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers), splitMarkers: Array.from(splitMarkers),
}; };
@@ -43,7 +47,10 @@ function snapshot() {
} }
function restore(snap: Snapshot) { function restore(snap: Snapshot) {
allPages = snap.allPages.map(p => ({ ...p })); allPages = snap.allPages.map(p => ({
...p,
canvas: p.canvas
}));
selectedPages = new Set(snap.selectedPages); selectedPages = new Set(snap.selectedPages);
splitMarkers = new Set(snap.splitMarkers); splitMarkers = new Set(snap.splitMarkers);
updatePageDisplay(); updatePageDisplay();
@@ -202,7 +209,6 @@ function initializeTool() {
} }
}); });
// Modal close button
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal); document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
document.getElementById('modal')?.addEventListener('click', (e) => { document.getElementById('modal')?.addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) { if (e.target === document.getElementById('modal')) {
@@ -210,7 +216,6 @@ function initializeTool() {
} }
}); });
// Drag and drop
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
if (uploadArea) { if (uploadArea) {
uploadArea.addEventListener('dragover', (e) => { uploadArea.addEventListener('dragover', (e) => {
@@ -230,7 +235,6 @@ function initializeTool() {
}); });
} }
// Show upload area initially
document.getElementById('upload-area')?.classList.remove('hidden'); document.getElementById('upload-area')?.classList.remove('hidden');
} }
@@ -315,7 +319,6 @@ async function loadPdfs(files: File[]) {
} }
function getCacheKey(pdfIndex: number, pageIndex: number): string { function getCacheKey(pdfIndex: number, pageIndex: number): string {
// Removed rotation from cache key - canvas is always rendered at 0 degrees
return `${pdfIndex}-${pageIndex}`; return `${pdfIndex}-${pageIndex}`;
} }
@@ -330,12 +333,10 @@ async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: n
if (pageCanvasCache.has(cacheKey)) { if (pageCanvasCache.has(cacheKey)) {
canvas = pageCanvasCache.get(cacheKey)!; canvas = pageCanvasCache.get(cacheKey)!;
} else { } else {
// Render page preview at 0 degrees rotation using pdfjs
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const page = await pdf.getPage(pageIndex + 1); const page = await pdf.getPage(pageIndex + 1);
// Always render at 0 rotation - visual rotation is applied via CSS
const viewport = page.getViewport({ scale: 0.5, rotation: 0 }); const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@@ -384,20 +385,14 @@ function createPageCard(pageData: PageData, index: number) {
const preview = document.createElement('div'); const preview = document.createElement('div');
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
preview.style.minHeight = '160px'; preview.style.minHeight = '160px';
preview.style.maxHeight = '256px'; preview.style.height = '250px';
const previewCanvas = pageData.canvas; const previewCanvas = pageData.canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain'; previewCanvas.className = 'max-w-full max-h-full object-contain';
// Apply visual rotation using CSS transform // Apply visual rotation using CSS transform
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease';
// Adjust container dimensions based on rotation
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
preview.style.aspectRatio = `${previewCanvas.height} / ${previewCanvas.width}`;
} else {
preview.style.aspectRatio = `${previewCanvas.width} / ${previewCanvas.height}`;
}
preview.appendChild(previewCanvas); preview.appendChild(previewCanvas);
@@ -408,7 +403,11 @@ function createPageCard(pageData: PageData, index: number) {
// Actions toolbar // Actions toolbar
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity'; actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0';
const actionsInner = document.createElement('div');
actionsInner.className = 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1';
actions.appendChild(actionsInner);
// Select checkbox // Select checkbox
const selectBtn = document.createElement('button'); const selectBtn = document.createElement('button');
@@ -441,6 +440,7 @@ function createPageCard(pageData: PageData, index: number) {
const duplicateBtn = document.createElement('button'); const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700'; duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>'; duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
duplicateBtn.title = 'Duplicate this page';
duplicateBtn.onclick = (e) => { duplicateBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
snapshot(); snapshot();
@@ -451,6 +451,7 @@ function createPageCard(pageData: PageData, index: number) {
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'p-1 rounded hover:bg-gray-700'; deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>'; deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
deleteBtn.title = 'Delete this page';
deleteBtn.onclick = (e) => { deleteBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
snapshot(); snapshot();
@@ -480,7 +481,7 @@ function createPageCard(pageData: PageData, index: number) {
renderSplitMarkers(); renderSplitMarkers();
}; };
actions.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
card.append(preview, info, actions, selectBtn); card.append(preview, info, actions, selectBtn);
pagesContainer.appendChild(card); pagesContainer.appendChild(card);
@@ -506,7 +507,6 @@ function setupSortable() {
}); });
} }
// Optimized selection that only updates the specific card
function toggleSelectOptimized(index: number) { function toggleSelectOptimized(index: number) {
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
selectedPages.delete(index); selectedPages.delete(index);
@@ -546,7 +546,6 @@ function deselectAll() {
updatePageDisplay(); updatePageDisplay();
} }
// Instant rotation - just update visual rotation, no re-rendering
function rotatePage(index: number, delta: number) { function rotatePage(index: number, delta: number) {
snapshot(); snapshot();
@@ -554,7 +553,6 @@ function rotatePage(index: number, delta: number) {
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
// Just update the specific card's transform
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
@@ -566,13 +564,7 @@ function rotatePage(index: number, delta: number) {
if (canvas && preview) { if (canvas && preview) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
canvas.style.transition = 'transform 0.2s ease';
// Adjust container aspect ratio
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
(preview as HTMLElement).style.aspectRatio = `${canvas.height} / ${canvas.width}`;
} else {
(preview as HTMLElement).style.aspectRatio = `${canvas.width} / ${canvas.height}`;
}
} }
} }
@@ -580,7 +572,6 @@ function duplicatePage(index: number) {
const originalPageData = allPages[index]; const originalPageData = allPages[index];
const originalCanvas = originalPageData.canvas; const originalCanvas = originalPageData.canvas;
// Create a new canvas and copy content
const newCanvas = document.createElement('canvas'); const newCanvas = document.createElement('canvas');
newCanvas.width = originalCanvas.width; newCanvas.width = originalCanvas.width;
newCanvas.height = originalCanvas.height; newCanvas.height = originalCanvas.height;
@@ -603,13 +594,18 @@ function duplicatePage(index: number) {
function deletePage(index: number) { function deletePage(index: number) {
allPages.splice(index, 1); allPages.splice(index, 1);
selectedPages.delete(index); selectedPages.delete(index);
// Update selected indices
const newSelected = new Set<number>(); const newSelected = new Set<number>();
selectedPages.forEach(i => { selectedPages.forEach(i => {
if (i > index) newSelected.add(i - 1); if (i > index) newSelected.add(i - 1);
else if (i < index) newSelected.add(i); else if (i < index) newSelected.add(i);
}); });
selectedPages = newSelected; selectedPages = newSelected;
if (allPages.length === 0) {
resetAll();
return;
}
updatePageDisplay(); updatePageDisplay();
} }
@@ -640,7 +636,6 @@ async function handleInsertPdf(e: Event) {
newPages.push(allPages.pop()!); newPages.push(allPages.pop()!);
} }
// Insert pages after the specified index
allPages.splice(insertAfterIndex + 1, 0, ...newPages); allPages.splice(insertAfterIndex + 1, 0, ...newPages);
updatePageDisplay(); updatePageDisplay();
} catch (e) { } catch (e) {
@@ -660,10 +655,8 @@ function renderSplitMarkers() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
// Remove all existing split markers
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove()); pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
// Add split markers between cards
Array.from(pagesContainer.children).forEach((cardEl, i) => { Array.from(pagesContainer.children).forEach((cardEl, i) => {
if (splitMarkers.has(i)) { if (splitMarkers.has(i)) {
const marker = document.createElement('div'); const marker = document.createElement('div');
@@ -675,7 +668,6 @@ function renderSplitMarkers() {
} }
function addBlankPage() { function addBlankPage() {
// Create a blank page
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 595; canvas.width = 595;
canvas.height = 842; canvas.height = 842;
@@ -699,7 +691,6 @@ function addBlankPage() {
updatePageDisplay(); updatePageDisplay();
} }
// Instant bulk rotation - just update visual rotation
function bulkRotate(delta: number) { function bulkRotate(delta: number) {
if (selectedPages.size === 0) { if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to rotate.', 'info'); showModal('No Selection', 'Please select pages to rotate.', 'info');
@@ -712,7 +703,6 @@ function bulkRotate(delta: number) {
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
}); });
// Update display for all rotated pages
updatePageDisplay(); updatePageDisplay();
} }
@@ -724,6 +714,12 @@ function bulkDelete() {
const indices = Array.from(selectedPages).sort((a, b) => b - a); const indices = Array.from(selectedPages).sort((a, b) => b - a);
indices.forEach(index => allPages.splice(index, 1)); indices.forEach(index => allPages.splice(index, 1));
selectedPages.clear(); selectedPages.clear();
if (allPages.length === 0) {
resetAll();
return;
}
updatePageDisplay(); updatePageDisplay();
} }
@@ -742,11 +738,18 @@ function bulkDuplicate() {
function bulkSplit() { function bulkSplit() {
if (selectedPages.size === 0) { if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to split.', 'info'); showModal('No Selection', 'Please select pages to mark for splitting.', 'info');
return; return;
} }
const indices = Array.from(selectedPages); const indices = Array.from(selectedPages);
downloadPagesAsPdf(indices, 'selected-pages.pdf'); indices.forEach(index => {
if (!splitMarkers.has(index)) {
splitMarkers.add(index);
}
});
renderSplitMarkers();
selectedPages.clear();
updatePageDisplay();
} }
async function bulkDownload() { async function bulkDownload() {
@@ -759,8 +762,79 @@ async function bulkDownload() {
} }
async function downloadAll() { async function downloadAll() {
if (allPages.length === 0) {
showModal('No Pages', 'Please upload PDFs first.', 'info');
return;
}
// Check if there are split markers
if (splitMarkers.size > 0) {
// Split into multiple PDFs and download as ZIP
await downloadSplitPdfs();
} else {
// Download as single PDF
const indices = Array.from({ length: allPages.length }, (_, i) => i); const indices = Array.from({ length: allPages.length }, (_, i) => i);
await downloadPagesAsPdf(indices, 'all-pages.pdf'); await downloadPagesAsPdf(indices, 'all-pages.pdf');
}
}
async function downloadSplitPdfs() {
try {
const zip = new JSZip();
const sortedMarkers = Array.from(splitMarkers).sort((a, b) => a - b);
// Create segments based on split markers
const segments: number[][] = [];
let currentSegment: number[] = [];
for (let i = 0; i < allPages.length; i++) {
currentSegment.push(i);
// If this page has a split marker after it, start a new segment
if (splitMarkers.has(i)) {
segments.push(currentSegment);
currentSegment = [];
}
}
// Add the last segment if it has pages
if (currentSegment.length > 0) {
segments.push(currentSegment);
}
// Create PDFs for each segment
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
const segment = segments[segIndex];
const newPdf = await PDFLibDocument.create();
for (const index of segment) {
const pageData = allPages[index];
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
const page = newPdf.addPage(copiedPage);
if (pageData.rotation !== 0) {
const currentRotation = page.getRotation().angle;
page.setRotation(degrees(currentRotation + pageData.rotation));
}
} else {
newPdf.addPage([595, 842]);
}
}
const pdfBytes = await newPdf.save();
zip.file(`document-${segIndex + 1}.pdf`, pdfBytes);
}
// Generate and download ZIP
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-documents.zip');
showModal('Success', `Downloaded ${segments.length} PDF files in a ZIP archive.`, 'success');
} catch (e) {
console.error('Failed to create split PDFs:', e);
showModal('Error', 'Failed to create split PDFs.', 'error');
}
} }
async function downloadPagesAsPdf(indices: number[], filename: string) { async function downloadPagesAsPdf(indices: number[], filename: string) {
@@ -785,12 +859,8 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
const pdfBytes = await newPdf.save(); const pdfBytes = await newPdf.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); downloadFile(blob, filename);
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch (e) { } catch (e) {
console.error('Failed to create PDF:', e); console.error('Failed to create PDF:', e);
showModal('Error', 'Failed to create PDF.', 'error'); showModal('Error', 'Failed to create PDF.', 'error');

View File

@@ -4,6 +4,7 @@ import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css'; import '../css/styles.css';
import { formatStars } from './utils/helpers.js';
const init = () => { const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -274,7 +275,7 @@ const init = () => {
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.stargazers_count !== undefined) { if (data.stargazers_count !== undefined) {
githubStarsElement.textContent = data.stargazers_count.toLocaleString(); githubStarsElement.textContent = formatStars(data.stargazers_count);
} }
}) })
.catch(() => { .catch(() => {

View File

@@ -187,3 +187,10 @@ export function initializeIcons(): void {
}, },
}); });
} }
export function formatStars(num: number) {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toLocaleString();
};

View File

@@ -32,71 +32,89 @@
<!-- Main Container --> <!-- Main Container -->
<div class="flex flex-col h-[calc(100vh-4rem)]"> <div class="flex flex-col h-[calc(100vh-4rem)]">
<!-- Toolbar --> <!-- Toolbar -->
<div class="bg-gray-800 border-b border-gray-700 p-4 flex flex-wrap items-center gap-2 overflow-x-auto"> <div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-4 overflow-x-auto">
<div class="flex flex-wrap items-center gap-1 sm:gap-2 min-w-max">
<button id="upload-pdfs-btn" <button id="upload-pdfs-btn"
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2"> class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
<i data-lucide="upload" class="w-4 h-4"></i> Upload PDFs <i data-lucide="upload" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Upload PDFs</span>
<span class="sm:hidden">Upload</span>
</button> </button>
<div class="border-l border-gray-600 h-6 mx-2"></div> <div class="border-l border-gray-600 h-6 mx-1"></div>
<button id="add-blank-page-btn" <button id="add-blank-page-btn"
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm"> class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm">
<i data-lucide="file-plus" class="w-4 h-4"></i> Add Blank <i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Add Blank</span>
</button> </button>
<div class="border-l border-gray-600 h-6 mx-2"></div> <div class="border-l border-gray-600 h-6 mx-1"></div>
<button id="undo-btn" <button id="undo-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Undo <i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Undo</span>
</button> </button>
<button id="redo-btn" <button id="redo-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Redo <i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Redo</span>
</button> </button>
<button id="reset-btn" <button id="reset-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="refresh-ccw" class="w-4 h-4"></i> Reset <i data-lucide="refresh-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Reset</span>
</button> </button>
<div class="border-l border-gray-600 h-6 mx-2"></div> <div class="border-l border-gray-600 h-6 mx-1"></div>
<span class="text-gray-400 text-sm">Selection:</span> <span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span>
<button id="select-all-btn" <button id="select-all-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="check-square" class="w-4 h-4"></i> Select All <i data-lucide="check-square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Select All</span>
</button> </button>
<button id="deselect-all-btn" <button id="deselect-all-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="square" class="w-4 h-4"></i> Deselect All <i data-lucide="square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Deselect All</span>
</button> </button>
<div class="border-l border-gray-600 h-6 mx-2"></div> <div class="border-l border-gray-600 h-6 mx-1"></div>
<span class="text-gray-400 text-sm">Bulk Actions:</span> <span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Bulk:</span>
<button id="bulk-rotate-left-btn" <button id="bulk-rotate-left-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Rotate Left <i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Rotate Left</span>
</button> </button>
<button id="bulk-rotate-btn" <button id="bulk-rotate-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Rotate Right <i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Rotate Right</span>
</button> </button>
<button id="bulk-delete-btn" <button id="bulk-delete-btn"
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-red-600 hover:bg-red-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete <i data-lucide="trash-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Delete</span>
</button> </button>
<button id="bulk-duplicate-btn" <button id="bulk-duplicate-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="copy" class="w-4 h-4"></i> Duplicate <i data-lucide="copy" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Duplicate</span>
</button> </button>
<button id="bulk-split-btn" <button id="bulk-split-btn"
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="scissors" class="w-4 h-4"></i> Split <i data-lucide="scissors" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Split</span>
</button> </button>
<button id="bulk-download-btn" <button id="bulk-download-btn"
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2"> class="btn bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
<i data-lucide="download" class="w-4 h-4"></i> Download Selected <i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Download Selected</span>
</button> </button>
<div class="flex-1"></div> <div class="flex-1 min-w-[20px]"></div>
<button id="export-pdf-btn" <button id="export-pdf-btn"
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2"> class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
<i data-lucide="download" class="w-4 h-4"></i> Export PDF <i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Export PDF</span>
<span class="sm:hidden">Export</span>
</button> </button>
</div> </div>
</div>
<!-- Content Area --> <!-- Content Area -->
<div class="flex-1 overflow-auto p-4"> <div class="flex-1 overflow-auto p-4">