mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc873d343 | ||
|
|
739dac55ee | ||
|
|
dbc1b8eb7f | ||
|
|
11c24c1ff1 | ||
|
|
9af9c33f74 | ||
|
|
835c3c9423 | ||
|
|
a8df98f20d | ||
|
|
11c9e14803 | ||
|
|
4e2d04fb9c | ||
|
|
edc0f27e0f | ||
|
|
bf57f36cb5 | ||
|
|
6ad6a7d730 | ||
|
|
0e7c53560a |
@@ -1,4 +1,4 @@
|
||||
name: build docker image and push to dockerhub
|
||||
name: Build and Push Docker Images (Default + Simple Mode)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker-build-and-push:
|
||||
@@ -13,6 +14,15 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
mode:
|
||||
- name: default
|
||||
simple_mode: false
|
||||
suffix: ''
|
||||
- name: simple
|
||||
simple_mode: true
|
||||
suffix: '-simple'
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -34,13 +44,17 @@ jobs:
|
||||
echo "version=latest" >> $GITHUB_OUTPUT
|
||||
echo "version_without_v=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push
|
||||
|
||||
- name: Build and push ${{ matrix.mode.name }} image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
|
||||
tags: |
|
||||
bentopdf/bentopdf:latest
|
||||
bentopdf/bentopdf:${{ steps.version.outputs.version_without_v }}
|
||||
bentopdf/bentopdf:${{ steps.version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
bentopdf/bentopdf${{ matrix.mode.suffix }}:latest
|
||||
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }}
|
||||
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -135,6 +135,7 @@ The application will be available at `http://localhost:3000`.
|
||||
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
|
||||
|
||||
**What Simple Mode does:**
|
||||
|
||||
- Hides navigation, hero section, features, FAQ, testimonials, and footer
|
||||
- Shows only the essential PDF tools
|
||||
- Updates page title to "PDF Tools"
|
||||
@@ -162,7 +163,8 @@ npm run release:minor
|
||||
# Release a major version (0.0.1 → 1.0.0)
|
||||
npm run release:major
|
||||
```
|
||||
```
|
||||
|
||||
````
|
||||
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||
### 🚀 Development Setup
|
||||
|
||||
@@ -173,7 +175,7 @@ For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
```
|
||||
````
|
||||
|
||||
2. **Install Dependencies**:
|
||||
|
||||
@@ -239,7 +241,7 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
|
||||
- **PDF to Office**: Converts PDF files into editable Word, Excel, and PowerPoint formats.
|
||||
- **Office to PDF**: Converts Word, Excel, and PowerPoint documents into optimized PDFs.
|
||||
|
||||
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/q42xWQmJ).
|
||||
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/AP2Y97juZT).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+36
-9
@@ -7,6 +7,7 @@
|
||||
**Situation:** You've completed a new feature, tested it locally, and want to release it.
|
||||
|
||||
**Current State:**
|
||||
|
||||
```bash
|
||||
$ git status
|
||||
On branch main
|
||||
@@ -22,6 +23,7 @@ Untracked files:
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# 1. Commit your feature changes
|
||||
git add .
|
||||
@@ -34,6 +36,7 @@ npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
||||
```
|
||||
|
||||
**What Happens:**
|
||||
|
||||
- ✅ Your feature commit stays as-is
|
||||
- ✅ Version gets bumped in `package.json`
|
||||
- ✅ New release commit is created
|
||||
@@ -48,6 +51,7 @@ npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
||||
**Situation:** You have local changes but haven't committed them yet.
|
||||
|
||||
**Current State:**
|
||||
|
||||
```bash
|
||||
$ git status
|
||||
Changes not staged for commit:
|
||||
@@ -57,6 +61,7 @@ Changes not staged for commit:
|
||||
```
|
||||
|
||||
**❌ This Will Fail:**
|
||||
|
||||
```bash
|
||||
npm run release
|
||||
# Error: Your local changes would be overwritten by merge
|
||||
@@ -65,6 +70,7 @@ npm run release
|
||||
**✅ Solution Options:**
|
||||
|
||||
**Option A: Commit Everything First (Recommended)**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add new features and improvements"
|
||||
@@ -72,6 +78,7 @@ npm run release
|
||||
```
|
||||
|
||||
**Option B: Stash Changes Temporarily**
|
||||
|
||||
```bash
|
||||
git stash
|
||||
npm run release
|
||||
@@ -79,6 +86,7 @@ git stash pop # Restore your changes after release
|
||||
```
|
||||
|
||||
**Option C: Commit Only What's Needed**
|
||||
|
||||
```bash
|
||||
git add package.json src/js/main.js
|
||||
git commit -m "Add core improvements"
|
||||
@@ -94,6 +102,7 @@ git commit -m "Update documentation"
|
||||
**Situation:** There's a critical bug in production that needs immediate fixing.
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# 1. Fix the bug
|
||||
git add src/js/bug-fix.js
|
||||
@@ -105,6 +114,7 @@ npm run release
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
- ✅ Bug fix gets released immediately
|
||||
- ✅ Docker image with fix is available
|
||||
- ✅ Users can pull the fixed version
|
||||
@@ -116,6 +126,7 @@ npm run release
|
||||
**Situation:** You've added significant new features that might break existing functionality.
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# 1. Commit all your changes
|
||||
git add .
|
||||
@@ -127,6 +138,7 @@ npm run release:major
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
- ✅ Major version bump indicates breaking changes
|
||||
- ✅ Users know to check compatibility
|
||||
- ✅ Both old and new versions available
|
||||
@@ -138,6 +150,7 @@ npm run release:major
|
||||
**Situation:** You've been working on multiple features and want to release them together.
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# 1. Commit all features
|
||||
git add .
|
||||
@@ -156,6 +169,7 @@ npm run release:major # For breaking changes (1.0.0 → 2.0.0)
|
||||
**Situation:** You want to test the release system without affecting production.
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# 1. Make a small test change
|
||||
echo "// Test comment" >> src/js/main.js
|
||||
@@ -179,23 +193,25 @@ git reset --hard HEAD~1
|
||||
|
||||
## 🎯 **Release Type Guidelines**
|
||||
|
||||
| Scenario | Command | Version Change | When to Use |
|
||||
|----------|---------|----------------|-------------|
|
||||
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
||||
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
||||
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
||||
| Scenario | Command | Version Change | When to Use |
|
||||
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
|
||||
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
||||
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
||||
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 **What Happens After You Run a Release Command**
|
||||
|
||||
### **Immediate Actions (Local):**
|
||||
|
||||
1. **Version Update**: `package.json` version gets bumped
|
||||
2. **Git Commit**: New commit created with "Release vX.X.X"
|
||||
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
|
||||
4. **Git Push**: Everything pushed to GitHub
|
||||
|
||||
### **Automatic Actions (GitHub):**
|
||||
|
||||
1. **GitHub Actions Triggered**: Workflow starts building Docker image
|
||||
2. **Docker Build**: Multi-architecture image created
|
||||
3. **Docker Push**: Images pushed to Docker Hub with tags:
|
||||
@@ -204,7 +220,9 @@ git reset --hard HEAD~1
|
||||
- `bentopdf/bentopdf:v1.0.1`
|
||||
|
||||
### **End Result:**
|
||||
|
||||
Users can immediately pull your new version:
|
||||
|
||||
```bash
|
||||
docker pull bentopdf/bentopdf:1.0.1
|
||||
```
|
||||
@@ -214,6 +232,7 @@ docker pull bentopdf/bentopdf:1.0.1
|
||||
## 🚨 **Before You Release - Prerequisites**
|
||||
|
||||
### **1. Docker Hub Credentials Setup**
|
||||
|
||||
You need to add these secrets to your GitHub repository:
|
||||
|
||||
1. Go to **Settings** → **Secrets and variables** → **Actions**
|
||||
@@ -222,6 +241,7 @@ You need to add these secrets to your GitHub repository:
|
||||
- `DOCKER_TOKEN`: Your Docker Hub access token
|
||||
|
||||
### **2. Get Docker Hub Token**
|
||||
|
||||
1. Go to [Docker Hub](https://hub.docker.com)
|
||||
2. Account Settings → Security → New Access Token
|
||||
3. Set permissions to "Read, Write, Delete"
|
||||
@@ -232,8 +252,10 @@ You need to add these secrets to your GitHub repository:
|
||||
## 🔧 **Troubleshooting Common Issues**
|
||||
|
||||
### **❌ "Your local changes would be overwritten by merge"**
|
||||
|
||||
**Problem:** You have uncommitted changes
|
||||
**Solution:**
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Your commit message"
|
||||
@@ -241,16 +263,20 @@ npm run release
|
||||
```
|
||||
|
||||
### **❌ "Permission denied" in GitHub Actions**
|
||||
|
||||
**Problem:** Missing Docker Hub credentials
|
||||
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
|
||||
|
||||
### **❌ "Tag already exists"**
|
||||
|
||||
**Problem:** You've run the same release before
|
||||
**Solution:** This is normal! The script will skip creating duplicate tags
|
||||
|
||||
### **❌ GitHub Actions fails**
|
||||
|
||||
**Problem:** Various build issues
|
||||
**Solution:**
|
||||
**Solution:**
|
||||
|
||||
1. Check Actions tab for detailed logs
|
||||
2. Verify Docker Hub credentials
|
||||
3. Check Dockerfile for syntax errors
|
||||
@@ -260,6 +286,7 @@ npm run release
|
||||
## 🧪 **Testing Your Release System**
|
||||
|
||||
### **Quick Test:**
|
||||
|
||||
```bash
|
||||
# Make a small change
|
||||
echo "// Test" >> src/js/main.js
|
||||
@@ -269,12 +296,14 @@ npm run release
|
||||
```
|
||||
|
||||
### **Verify Results:**
|
||||
|
||||
1. **GitHub Actions**: Check Actions tab for successful build
|
||||
2. **Docker Hub**: Verify images are published
|
||||
3. **Git Tags**: `git tag --list` should show new tag
|
||||
4. **Version**: `cat package.json | grep version` should show updated version
|
||||
|
||||
### **Undo Test Release:**
|
||||
|
||||
```bash
|
||||
git tag -d v1.0.1
|
||||
git push origin :refs/tags/v1.0.1
|
||||
@@ -286,5 +315,3 @@ git reset --hard HEAD~1
|
||||
## 🎉 **That's It!**
|
||||
|
||||
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
|
||||
|
||||
|
||||
|
||||
+105
-15
@@ -5,6 +5,7 @@ Simple Mode is designed for internal organizational use where you want to hide a
|
||||
## What Simple Mode Does
|
||||
|
||||
When enabled, Simple Mode will:
|
||||
|
||||
- Hide the navigation bar
|
||||
- Hide the hero section with marketing content
|
||||
- Hide the features section
|
||||
@@ -18,36 +19,89 @@ When enabled, Simple Mode will:
|
||||
|
||||
## How to Enable Simple Mode
|
||||
|
||||
### Using Docker Compose
|
||||
### Method 1: Using Pre-built Simple Mode Image (Recommended)
|
||||
|
||||
Use the pre-built Simple Mode image directly:
|
||||
|
||||
1. Create a `.env` file in your project root:
|
||||
```bash
|
||||
SIMPLE_MODE=true
|
||||
docker run -p 3000:80 bentopdf/bentopdf-simple:latest
|
||||
```
|
||||
|
||||
2. Run with docker-compose:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
Or with Docker Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bentopdf:
|
||||
image: bentopdf/bentopdf-simple:latest
|
||||
container_name: bentopdf
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:80'
|
||||
```
|
||||
|
||||
### Using Docker Build
|
||||
### Method 2: Using Docker Compose with Build
|
||||
|
||||
Build the image locally with Simple Mode enabled:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml build --build-arg SIMPLE_MODE=true
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### Method 3: Using Docker Build
|
||||
|
||||
Build the image with the SIMPLE_MODE build argument:
|
||||
|
||||
Build the image with the SIMPLE_MODE environment variable:
|
||||
```bash
|
||||
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||
docker run -p 3000:80 bentopdf-simple
|
||||
```
|
||||
|
||||
### Using Environment Variables
|
||||
### Method 4: Using npm Script (Easiest for Local Development)
|
||||
|
||||
Use the built-in npm script that handles everything:
|
||||
|
||||
```bash
|
||||
npm run serve:simple
|
||||
```
|
||||
|
||||
This command automatically:
|
||||
|
||||
- Sets `SIMPLE_MODE=true`
|
||||
- Builds the project with Simple Mode enabled
|
||||
- Serves the built files on `http://localhost:3000`
|
||||
|
||||
### Method 5: Using Environment Variables
|
||||
|
||||
Set the environment variable before building:
|
||||
|
||||
```bash
|
||||
export SIMPLE_MODE=true
|
||||
npm run build
|
||||
npx serve dist -p 3000
|
||||
```
|
||||
|
||||
## 🧪 Testing Simple Mode Locally
|
||||
|
||||
After building with Simple Mode enabled, you need to serve the built files locally.
|
||||
### Method 1: Using npm Script (Easiest for Development)
|
||||
|
||||
```bash
|
||||
npm run serve:simple
|
||||
```
|
||||
|
||||
This automatically builds and serves Simple Mode on `http://localhost:3000`.
|
||||
|
||||
### Method 2: Using Pre-built Image (Easiest for Production)
|
||||
|
||||
```bash
|
||||
# Pull and run the Simple Mode image
|
||||
docker pull bentopdf/bentopdf-simple:latest
|
||||
docker run -p 3000:80 bentopdf/bentopdf-simple:latest
|
||||
```
|
||||
|
||||
Open `http://localhost:3000` in your browser.
|
||||
|
||||
### Method 3: Build and Test Locally
|
||||
|
||||
```bash
|
||||
# Build with simple mode
|
||||
@@ -56,11 +110,26 @@ SIMPLE_MODE=true npm run build
|
||||
# Serve the built files
|
||||
npx serve dist -p 3000
|
||||
```
|
||||
Then open `http://localhost:3000` in your browser.
|
||||
|
||||
Open `http://localhost:3000` in your browser.
|
||||
|
||||
### Method 4: Compare Both Modes
|
||||
|
||||
```bash
|
||||
# Test Normal Mode
|
||||
docker run -p 3000:80 bentopdf/bentopdf:latest
|
||||
|
||||
# Test Simple Mode
|
||||
docker run -p 3001:80 bentopdf/bentopdf-simple:latest
|
||||
```
|
||||
|
||||
- Normal Mode: `http://localhost:3000`
|
||||
- Simple Mode: `http://localhost:3001`
|
||||
|
||||
## 🔍 What to Look For
|
||||
|
||||
When Simple Mode is working correctly, you should see:
|
||||
|
||||
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
||||
- ✅ "Select a tool to get started" subtitle
|
||||
- ✅ Search bar for tools
|
||||
@@ -69,17 +138,38 @@ When Simple Mode is working correctly, you should see:
|
||||
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
||||
- ❌ No features, FAQ, testimonials, or footer sections
|
||||
|
||||
## Example Docker Compose Configuration
|
||||
## 📦 Available Docker Images
|
||||
|
||||
### Normal Mode (Full Branding)
|
||||
|
||||
- `bentopdf/bentopdf:latest`
|
||||
- `bentopdf/bentopdf:v1.0.0` (versioned)
|
||||
|
||||
### Simple Mode (Clean Interface)
|
||||
|
||||
- `bentopdf/bentopdf-simple:latest`
|
||||
- `bentopdf/bentopdf-simple:v1.0.0` (versioned)
|
||||
|
||||
## 🚀 Production Deployment Examples
|
||||
|
||||
### Internal Company Tool
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bentopdf:
|
||||
image: bentopdf/bentopdf:latest
|
||||
image: bentopdf/bentopdf-simple:latest
|
||||
container_name: bentopdf
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:80'
|
||||
- '80:80'
|
||||
environment:
|
||||
- SIMPLE_MODE=true
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
```
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
- **Pre-built images**: Use `bentopdf/bentopdf-simple:latest` for Simple Mode
|
||||
- **Environment variables**: `SIMPLE_MODE=true` only works during build, not runtime
|
||||
- **Build-time optimization**: Simple Mode uses dead code elimination for smaller bundles
|
||||
- **Same functionality**: All PDF tools work identically in both modes
|
||||
|
||||
+83
-43
@@ -12,54 +12,94 @@
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900 text-gray-300">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
|
||||
+82
-42
@@ -11,53 +11,93 @@
|
||||
|
||||
<body class="antialiased bg-gray-900 text-gray-300">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
bentopdf:
|
||||
image: bentopdf/bentopdf:latest
|
||||
# simple mode - bentopdf/bentopdf-simple:latest
|
||||
# default mode - bentopdf/bentopdf:latest
|
||||
image: bentopdf/bentopdf-simple:latest
|
||||
container_name: bentopdf
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:80'
|
||||
environment:
|
||||
- SIMPLE_MODE=${SIMPLE_MODE:-false}
|
||||
|
||||
@@ -14,53 +14,93 @@
|
||||
|
||||
<body class="antialiased bg-gray-900 text-gray-300">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
|
||||
+82
-42
@@ -32,53 +32,93 @@
|
||||
|
||||
<body class="antialiased">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
<section id="hero-section" class="text-center py-20">
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,8 @@
|
||||
"format": "prettier --write .",
|
||||
"release": "node scripts/release.js patch",
|
||||
"release:minor": "node scripts/release.js minor",
|
||||
"release:major": "node scripts/release.js major"
|
||||
"release:major": "node scripts/release.js major",
|
||||
"serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
|
||||
+82
-42
@@ -13,53 +13,93 @@
|
||||
|
||||
<body class="antialiased bg-gray-900 text-gray-300">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
### Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
### [v1.0.0](https://github.com/alam00000/bentopdf/compare/v0.0.1...v1.0.0)
|
||||
|
||||
> 20 October 2025
|
||||
|
||||
- feat(ocr): add whitelist presets and improve UI for OCR tool [`0e7c535`](https://github.com/alam00000/bentopdf/commit/0e7c53560a23d3e26a18904aa47ca99d3f10d9a6)
|
||||
- Release v1.0.0 [`6ad6a7d`](https://github.com/alam00000/bentopdf/commit/6ad6a7d7305eb77fe9ebdb55d93059bf92304a98)
|
||||
|
||||
#### v0.0.1
|
||||
|
||||
> 20 October 2025
|
||||
|
||||
- feat: add Simple Mode for internal organizational use [`#64`](https://github.com/alam00000/bentopdf/pull/64)
|
||||
- Feature/reverse multi pdfs [`#58`](https://github.com/alam00000/bentopdf/pull/58)
|
||||
- added description to Unraid XML [`#65`](https://github.com/alam00000/bentopdf/pull/65)
|
||||
- Added hamburger Navbar [`#62`](https://github.com/alam00000/bentopdf/pull/62)
|
||||
- feat(versioning): Added version management sysytem [`#52`](https://github.com/alam00000/bentopdf/pull/52)
|
||||
- Fix#20 : fixed the tesseract issue [`#60`](https://github.com/alam00000/bentopdf/pull/60)
|
||||
- feat: add search bar keyboard shortcut [`#49`](https://github.com/alam00000/bentopdf/pull/49)
|
||||
- feat(pdf-tools): add attachments feature to embed files in PDFs [`#53`](https://github.com/alam00000/bentopdf/pull/53)
|
||||
- feat(pdf-tools): add linearize pdf tool for web optimization [`#51`](https://github.com/alam00000/bentopdf/pull/51)
|
||||
- feat: add support for multiple blank pages insertion [`#42`](https://github.com/alam00000/bentopdf/pull/42)
|
||||
- Multiupload handle encrypted pdf [`#43`](https://github.com/alam00000/bentopdf/pull/43)
|
||||
- feat:Setup Prettier for code formatting [`#30`](https://github.com/alam00000/bentopdf/pull/30)
|
||||
- created png as unraid does not supprot svg [`#24`](https://github.com/alam00000/bentopdf/pull/24)
|
||||
- adding unraid template [`#23`](https://github.com/alam00000/bentopdf/pull/23)
|
||||
- feat(pdf-tools): add alternate merge tool for mixing pdf pages [`#21`](https://github.com/alam00000/bentopdf/pull/21)
|
||||
- feat: add remove blank pages tool with sensitivity control [`#19`](https://github.com/alam00000/bentopdf/pull/19)
|
||||
- Posterize [`#17`](https://github.com/alam00000/bentopdf/pull/17)
|
||||
- feat: cicd for build and push to docker registry [`#15`](https://github.com/alam00000/bentopdf/pull/15)
|
||||
- Fix(posterize): Resolve incorrect vertical tiling/cropping [`#10`](https://github.com/alam00000/bentopdf/pull/10)
|
||||
- feat: add initial project setup with core PDF tools and utilities [`6712973`](https://github.com/alam00000/bentopdf/commit/671297320ec6ff93d05ad150836dd0a2b291c83c)
|
||||
- feat(test): add comprehensive test suite and testing infrastructure [`5e72d7c`](https://github.com/alam00000/bentopdf/commit/5e72d7ca2debe62fc3e74606b967cdb8334ce827)
|
||||
- refactor: fix type errors and improve type safety in PDF logic [`cc83d79`](https://github.com/alam00000/bentopdf/commit/cc83d7951f8748bb3b40223fc1ffd5d397faad72)
|
||||
+23
-16
@@ -18,7 +18,7 @@ function getCurrentVersion() {
|
||||
function updateVersion(type) {
|
||||
const currentVersion = getCurrentVersion();
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
|
||||
let newVersion;
|
||||
switch (type) {
|
||||
case 'major':
|
||||
@@ -32,23 +32,30 @@ function updateVersion(type) {
|
||||
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + '\n'
|
||||
);
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
function createGitTag(version) {
|
||||
const tagName = `v${version}`;
|
||||
|
||||
|
||||
try {
|
||||
// Check if tag already exists
|
||||
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, { stdio: 'ignore' });
|
||||
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
console.log(`✅ Tag v${version} already exists`);
|
||||
return tagName;
|
||||
} catch {
|
||||
// Tag doesn't exist, create it
|
||||
execSync(`git tag -a "v${version}" -m "Release v${version}"`, { stdio: 'inherit' });
|
||||
execSync(`git tag -a "v${version}" -m "Release v${version}"`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(`✅ Created tag v${version}`);
|
||||
return tagName;
|
||||
}
|
||||
@@ -56,36 +63,36 @@ function createGitTag(version) {
|
||||
|
||||
function main() {
|
||||
const type = process.argv[2] || 'patch';
|
||||
|
||||
|
||||
if (!['major', 'minor', 'patch'].includes(type)) {
|
||||
console.error('❌ Invalid version type. Use: major, minor, or patch');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log(`🚀 Releasing ${type} version...`);
|
||||
|
||||
|
||||
// 1. Update version in package.json
|
||||
const newVersion = updateVersion(type);
|
||||
console.log(`📦 Updated version to ${newVersion}`);
|
||||
|
||||
|
||||
// 2. Add and commit changes
|
||||
execSync('git add package.json', { stdio: 'inherit' });
|
||||
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
||||
console.log(`💾 Committed version change`);
|
||||
|
||||
|
||||
// 3. Create git tag
|
||||
const tagName = createGitTag(newVersion);
|
||||
|
||||
|
||||
// 4. Push everything to main
|
||||
console.log(`📤 Pushing to main...`);
|
||||
execSync('git push origin main', { stdio: 'inherit' });
|
||||
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
||||
|
||||
|
||||
console.log(`🎉 Release v${newVersion} complete!`);
|
||||
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
|
||||
console.log(`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`);
|
||||
console.log(
|
||||
`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
|
||||
+140
-20
@@ -7,7 +7,7 @@ import {
|
||||
renderFileDisplay,
|
||||
switchView,
|
||||
} from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||
@@ -25,8 +25,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
@@ -124,12 +124,13 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||
data: pdfBytes as ArrayBuffer,
|
||||
}).promise;
|
||||
const [metadata, fieldObjects] = await Promise.all([
|
||||
const [metadataResult, fieldObjects] = await Promise.all([
|
||||
pdfjsDoc.getMetadata(),
|
||||
pdfjsDoc.getFieldObjects(),
|
||||
]);
|
||||
|
||||
const { info, metadata: rawXmpString } = metadata;
|
||||
const { info, metadata } = metadataResult;
|
||||
const rawXmpString = metadata ? metadata.getRaw() : null;
|
||||
|
||||
resultsDiv.textContent = ''; // Clear safely
|
||||
|
||||
@@ -184,14 +185,29 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
const infoSection = createSection('Info Dictionary');
|
||||
if (info && Object.keys(info).length > 0) {
|
||||
for (const key in info) {
|
||||
let value = info[key] || '- Not Set -';
|
||||
if (
|
||||
let value = info[key];
|
||||
let displayValue;
|
||||
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
displayValue = '- Not Set -';
|
||||
} else if (typeof value === 'object' && value.name) {
|
||||
displayValue = value.name;
|
||||
} else if (typeof value === 'object') {
|
||||
try {
|
||||
displayValue = JSON.stringify(value);
|
||||
} catch {
|
||||
displayValue = '[object Object]';
|
||||
}
|
||||
} else if (
|
||||
(key === 'CreationDate' || key === 'ModDate') &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = parsePdfDate(value);
|
||||
displayValue = parsePdfDate(value);
|
||||
} else {
|
||||
displayValue = String(value);
|
||||
}
|
||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
||||
|
||||
infoSection.ul.appendChild(createListItem(key, displayValue));
|
||||
}
|
||||
} else {
|
||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||
@@ -212,19 +228,119 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
}
|
||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||
|
||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
||||
const xmpContainer = document.createElement('div');
|
||||
xmpContainer.className =
|
||||
'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||
const createXmpListItem = (key, value, indent = 0) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'flex flex-col sm:flex-row';
|
||||
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'w-56 flex-shrink-0 text-gray-400';
|
||||
strong.textContent = key;
|
||||
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex-grow text-white break-all';
|
||||
div.textContent = value;
|
||||
|
||||
li.append(strong, div);
|
||||
return li;
|
||||
};
|
||||
|
||||
const createXmpHeaderItem = (key, indent = 0) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'flex pt-2';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
|
||||
strong.textContent = key;
|
||||
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||
li.append(strong);
|
||||
return li;
|
||||
};
|
||||
|
||||
const appendXmpNodes = (xmlNode, ulElement, indentLevel) => {
|
||||
const xmpDateKeys = [
|
||||
'xap:CreateDate',
|
||||
'xap:ModifyDate',
|
||||
'xap:MetadataDate',
|
||||
];
|
||||
|
||||
const childNodes = Array.from(xmlNode.children);
|
||||
|
||||
for (const child of childNodes) {
|
||||
if ((child as Element).nodeType !== 1) continue;
|
||||
|
||||
let key = (child as Element).tagName;
|
||||
const elementChildren = Array.from((child as Element).children).filter(
|
||||
(c) => c.nodeType === 1
|
||||
);
|
||||
|
||||
if (key === 'rdf:li') {
|
||||
appendXmpNodes(child, ulElement, indentLevel);
|
||||
continue;
|
||||
}
|
||||
if (key === 'rdf:Alt') {
|
||||
key = '(alt container)';
|
||||
}
|
||||
|
||||
if (
|
||||
(child as Element).getAttribute('rdf:parseType') === 'Resource' &&
|
||||
elementChildren.length === 0
|
||||
) {
|
||||
ulElement.appendChild(
|
||||
createXmpListItem(key, '(Empty Resource)', indentLevel)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (elementChildren.length > 0) {
|
||||
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
|
||||
appendXmpNodes(child, ulElement, indentLevel + 1);
|
||||
} else {
|
||||
let value = (child as Element).textContent.trim();
|
||||
if (value) {
|
||||
if (xmpDateKeys.includes(key)) {
|
||||
value = formatIsoDate(value);
|
||||
}
|
||||
ulElement.appendChild(
|
||||
createXmpListItem(key, value, indentLevel)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const xmpSection = createSection('XMP Metadata');
|
||||
if (rawXmpString) {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||
pre.textContent = String(rawXmpString);
|
||||
xmpContainer.appendChild(pre);
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(
|
||||
rawXmpString,
|
||||
'application/xml'
|
||||
);
|
||||
|
||||
const descriptions = xmlDoc.getElementsByTagName('rdf:Description');
|
||||
if (descriptions.length > 0) {
|
||||
for (const desc of descriptions) {
|
||||
appendXmpNodes(desc, xmpSection.ul, 0);
|
||||
}
|
||||
} else {
|
||||
appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0);
|
||||
}
|
||||
|
||||
if (xmpSection.ul.children.length === 0) {
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
|
||||
}
|
||||
} catch (xmlError) {
|
||||
console.error('Failed to parse XMP XML:', xmlError);
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
|
||||
const pre = document.createElement('pre');
|
||||
pre.className =
|
||||
'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||
pre.textContent = rawXmpString;
|
||||
xmpSection.ul.appendChild(pre);
|
||||
}
|
||||
} else {
|
||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
|
||||
}
|
||||
xmpSection.wrapper.appendChild(xmpContainer);
|
||||
resultsDiv.appendChild(xmpSection.wrapper);
|
||||
|
||||
resultsDiv.classList.remove('hidden');
|
||||
@@ -324,7 +440,11 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
}
|
||||
|
||||
async function handleMultiFileUpload(toolId) {
|
||||
if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages') {
|
||||
if (
|
||||
toolId === 'merge' ||
|
||||
toolId === 'alternate-merge' ||
|
||||
toolId === 'reverse-pages'
|
||||
) {
|
||||
const pdfFilesUnloaded: File[] = [];
|
||||
|
||||
state.files.forEach((file) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function addAttachments() {
|
||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
clearAttachments();
|
||||
clearAttachments();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export function setupAddAttachmentsTool() {
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate text-sm';
|
||||
nameSpan.textContent = file.name;
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
|
||||
+54
-15
@@ -113,11 +113,9 @@ async function runOCR() {
|
||||
tessjs_create_hocr: '1',
|
||||
});
|
||||
|
||||
if (whitelist.trim()) {
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist.trim(),
|
||||
});
|
||||
}
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist,
|
||||
});
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(
|
||||
@@ -144,7 +142,11 @@ async function runOCR() {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
|
||||
const result = await worker.recognize(canvas, {}, { text: true, hocr: true });
|
||||
const result = await worker.recognize(
|
||||
canvas,
|
||||
{},
|
||||
{ text: true, hocr: true }
|
||||
);
|
||||
const data = result.data;
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
@@ -281,9 +283,51 @@ export function setupOcrTool() {
|
||||
);
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
// Whitelist presets
|
||||
const whitelistPresets: Record<string, string> = {
|
||||
alphanumeric:
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||
'numbers-currency': '0123456789$€£¥.,- ',
|
||||
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
|
||||
'numbers-only': '0123456789',
|
||||
invoice: '0123456789$.,/-#: ',
|
||||
forms:
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
|
||||
};
|
||||
|
||||
// Handle whitelist preset selection
|
||||
const presetSelect = document.getElementById(
|
||||
'whitelist-preset'
|
||||
) as HTMLSelectElement;
|
||||
const whitelistInput = document.getElementById(
|
||||
'ocr-whitelist'
|
||||
) as HTMLInputElement;
|
||||
|
||||
presetSelect?.addEventListener('change', (e) => {
|
||||
const preset = (e.target as HTMLSelectElement).value;
|
||||
if (preset && preset !== 'custom') {
|
||||
whitelistInput.value = whitelistPresets[preset];
|
||||
whitelistInput.disabled = true;
|
||||
} else {
|
||||
whitelistInput.disabled = false;
|
||||
if (preset === '') {
|
||||
whitelistInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle details toggle icon rotation
|
||||
document.querySelectorAll('details').forEach((details) => {
|
||||
details.addEventListener('toggle', () => {
|
||||
const icon = details.querySelector('.details-icon') as HTMLElement;
|
||||
if (icon) {
|
||||
icon.style.transform = details.open ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
langSearch.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach((label) => {
|
||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||
? ''
|
||||
@@ -291,19 +335,14 @@ export function setupOcrTool() {
|
||||
});
|
||||
});
|
||||
|
||||
// Update the display of selected languages
|
||||
langList.addEventListener('change', () => {
|
||||
const selected = Array.from(
|
||||
langList.querySelectorAll('.lang-checkbox:checked')
|
||||
)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
.map((cb) => tesseractLanguages[cb.value]);
|
||||
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||
selectedLangsDisplay.textContent =
|
||||
selected.length > 0 ? selected.join(', ') : 'None';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = selected.length === 0;
|
||||
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||
});
|
||||
|
||||
// Attach the main OCR function to the process button
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function reversePages() {
|
||||
const pdfDocs = state.files.filter((file: File) => file.type === 'application/pdf');
|
||||
const pdfDocs = state.files.filter(
|
||||
(file: File) => file.type === 'application/pdf'
|
||||
);
|
||||
if (!pdfDocs.length) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
@@ -16,7 +18,7 @@ export async function reversePages() {
|
||||
const zip = new JSZip();
|
||||
for (let j = 0; j < pdfDocs.length; j++) {
|
||||
const file = pdfDocs[j];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
|
||||
+107
-69
@@ -5,82 +5,120 @@ import { createIcons, icons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import '../css/styles.css';
|
||||
|
||||
const hideBrandingSections = () => {
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav) {
|
||||
nav.style.display = 'none';
|
||||
}
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const featuresSection = document.getElementById('features-section');
|
||||
if (featuresSection) {
|
||||
featuresSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const securitySection = document.getElementById('security-compliance-section');
|
||||
if (securitySection) {
|
||||
securitySection.style.display = 'none';
|
||||
}
|
||||
|
||||
const faqSection = document.getElementById('faq-accordion');
|
||||
if (faqSection) {
|
||||
faqSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const testimonialsSection = document.getElementById('testimonials-section');
|
||||
if (testimonialsSection) {
|
||||
testimonialsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const supportSection = document.getElementById('support-section');
|
||||
if (supportSection) {
|
||||
supportSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer) {
|
||||
footer.style.display = 'none';
|
||||
}
|
||||
|
||||
const sectionDividers = document.querySelectorAll('.section-divider');
|
||||
sectionDividers.forEach(divider => {
|
||||
(divider as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
document.title = 'PDF Tools';
|
||||
|
||||
const toolsHeader = document.getElementById('tools-header');
|
||||
if (toolsHeader) {
|
||||
const title = toolsHeader.querySelector('h2');
|
||||
const subtitle = toolsHeader.querySelector('p');
|
||||
if (title) {
|
||||
title.textContent = 'PDF Tools';
|
||||
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
|
||||
}
|
||||
if (subtitle) {
|
||||
subtitle.textContent = 'Select a tool to get started';
|
||||
subtitle.className = 'text-lg text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (app) {
|
||||
app.style.paddingTop = '2rem';
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
// Handle simple mode - hide branding sections
|
||||
// Handle simple mode - hide branding sections but keep logo and copyright
|
||||
if (__SIMPLE_MODE__) {
|
||||
const hideBrandingSections = () => {
|
||||
// Hide navigation but keep logo
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav) {
|
||||
// Hide the entire nav but we'll create a minimal one with just logo
|
||||
nav.style.display = 'none';
|
||||
|
||||
// Create a simple nav with just logo on the right
|
||||
const simpleNav = document.createElement('nav');
|
||||
simpleNav.className =
|
||||
'bg-gray-800 border-b border-gray-700 sticky top-0 z-30';
|
||||
simpleNav.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-start items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||
<span class="text-white font-bold text-xl ml-2">BentoPDF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertBefore(simpleNav, document.body.firstChild);
|
||||
}
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const featuresSection = document.getElementById('features-section');
|
||||
if (featuresSection) {
|
||||
featuresSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const securitySection = document.getElementById(
|
||||
'security-compliance-section'
|
||||
);
|
||||
if (securitySection) {
|
||||
securitySection.style.display = 'none';
|
||||
}
|
||||
|
||||
const faqSection = document.getElementById('faq-accordion');
|
||||
if (faqSection) {
|
||||
faqSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const testimonialsSection = document.getElementById(
|
||||
'testimonials-section'
|
||||
);
|
||||
if (testimonialsSection) {
|
||||
testimonialsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const supportSection = document.getElementById('support-section');
|
||||
if (supportSection) {
|
||||
supportSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide footer but keep copyright
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer) {
|
||||
footer.style.display = 'none';
|
||||
|
||||
const simpleFooter = document.createElement('footer');
|
||||
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
|
||||
simpleFooter.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-lg">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2025 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(simpleFooter);
|
||||
}
|
||||
|
||||
const sectionDividers = document.querySelectorAll('.section-divider');
|
||||
sectionDividers.forEach((divider) => {
|
||||
(divider as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
document.title = 'BentoPDF - PDF Tools';
|
||||
|
||||
const toolsHeader = document.getElementById('tools-header');
|
||||
if (toolsHeader) {
|
||||
const title = toolsHeader.querySelector('h2');
|
||||
const subtitle = toolsHeader.querySelector('p');
|
||||
if (title) {
|
||||
title.textContent = 'PDF Tools';
|
||||
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
|
||||
}
|
||||
if (subtitle) {
|
||||
subtitle.textContent = 'Select a tool to get started';
|
||||
subtitle.className = 'text-lg text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (app) {
|
||||
app.style.paddingTop = '1rem';
|
||||
}
|
||||
};
|
||||
|
||||
hideBrandingSections();
|
||||
}
|
||||
|
||||
|
||||
+49
-44
@@ -1,48 +1,53 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const menuIcon = document.getElementById('menu-icon');
|
||||
const closeIcon = document.getElementById('close-icon');
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const menuIcon = document.getElementById('menu-icon');
|
||||
const closeIcon = document.getElementById('close-icon');
|
||||
|
||||
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
||||
// Toggle menu on button click
|
||||
mobileMenuButton.addEventListener('click', () => {
|
||||
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Toggle menu visibility
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
|
||||
// Toggle icons
|
||||
menuIcon.classList.toggle('hidden');
|
||||
closeIcon.classList.toggle('hidden');
|
||||
|
||||
// Update aria-expanded for accessibility
|
||||
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
});
|
||||
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
||||
// Toggle menu on button click
|
||||
mobileMenuButton.addEventListener('click', () => {
|
||||
const isExpanded =
|
||||
mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Close menu when clicking on a link
|
||||
const mobileLinks = mobileMenu.querySelectorAll('a');
|
||||
mobileLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenu.classList.add('hidden');
|
||||
menuIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
// Toggle menu visibility
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target as Node;
|
||||
const isClickInsideMenu = mobileMenu.contains(target);
|
||||
const isClickOnButton = mobileMenuButton.contains(target);
|
||||
|
||||
if (!isClickInsideMenu && !isClickOnButton && !mobileMenu.classList.contains('hidden')) {
|
||||
mobileMenu.classList.add('hidden');
|
||||
menuIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Toggle icons
|
||||
menuIcon.classList.toggle('hidden');
|
||||
closeIcon.classList.toggle('hidden');
|
||||
|
||||
// Update aria-expanded for accessibility
|
||||
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
});
|
||||
|
||||
// Close menu when clicking on a link
|
||||
const mobileLinks = mobileMenu.querySelectorAll('a');
|
||||
mobileLinks.forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenu.classList.add('hidden');
|
||||
menuIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target as Node;
|
||||
const isClickInsideMenu = mobileMenu.contains(target);
|
||||
const isClickOnButton = mobileMenuButton.contains(target);
|
||||
|
||||
if (
|
||||
!isClickInsideMenu &&
|
||||
!isClickOnButton &&
|
||||
!mobileMenu.classList.contains('hidden')
|
||||
) {
|
||||
mobileMenu.classList.add('hidden');
|
||||
menuIcon.classList.remove('hidden');
|
||||
closeIcon.classList.add('hidden');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+34
-2
@@ -1367,6 +1367,17 @@ export const toolTemplates = {
|
||||
'ocr-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
|
||||
|
||||
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-6">
|
||||
<p class="text-sm text-gray-300"><strong class="text-white">How it works:</strong></p>
|
||||
<ul class="list-disc list-inside text-xs text-gray-400 mt-1 space-y-1">
|
||||
<li><strong class="text-white">Extract Text:</strong> Uses Tesseract OCR to recognize text from scanned images or PDFs.</li>
|
||||
<li><strong class="text-white">Searchable Output:</strong> Creates a new PDF with an invisible text layer, making your document fully searchable while preserving the original appearance.</li>
|
||||
<li><strong class="text-white">Character Filtering:</strong> Use whitelists to filter out unwanted characters and improve accuracy for specific document types (invoices, forms, etc.).</li>
|
||||
<li><strong class="text-white">Multi-language Support:</strong> Select multiple languages for documents containing mixed language content.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
@@ -1393,7 +1404,10 @@ export const toolTemplates = {
|
||||
|
||||
<!-- Advanced settings section -->
|
||||
<details class="bg-gray-900 border border-gray-700 rounded-lg p-3">
|
||||
<summary class="text-sm font-medium text-gray-300 cursor-pointer">Advanced Settings</summary>
|
||||
<summary class="text-sm font-medium text-gray-300 cursor-pointer flex items-center justify-between">
|
||||
<span>Advanced Settings (Recommended to improve accuracy)</span>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform details-icon"></i>
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<!-- Resolution Setting -->
|
||||
<div>
|
||||
@@ -1409,10 +1423,28 @@ export const toolTemplates = {
|
||||
<input type="checkbox" id="ocr-binarize" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600">
|
||||
Binarize Image (Enhance Contrast for Clean Scans)
|
||||
</label>
|
||||
<!-- Character Whitelist -->
|
||||
|
||||
<!-- Character Whitelist Presets -->
|
||||
<div>
|
||||
<label for="whitelist-preset" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist Preset</label>
|
||||
<select id="whitelist-preset" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm mb-2">
|
||||
<option value="">None (All characters)</option>
|
||||
<option value="alphanumeric">Alphanumeric + Basic Punctuation</option>
|
||||
<option value="numbers-currency">Numbers + Currency Symbols</option>
|
||||
<option value="letters-only">Letters Only (A-Z, a-z)</option>
|
||||
<option value="numbers-only">Numbers Only (0-9)</option>
|
||||
<option value="invoice">Invoice/Receipt (Numbers, $, ., -, /)</option>
|
||||
<option value="forms">Forms (Alphanumeric + Common Symbols)</option>
|
||||
<option value="custom">Custom...</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
|
||||
</div>
|
||||
|
||||
<!-- Character Whitelist Input -->
|
||||
<div>
|
||||
<label for="ocr-whitelist" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist (Optional)</label>
|
||||
<input type="text" id="ocr-whitelist" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm" placeholder="e.g., abcdefghijklmnopqrstuvwxyz0123456789$.,">
|
||||
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -123,3 +123,26 @@ export function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00")
|
||||
* into a localized, human-readable string.
|
||||
* @param {string} isoDateString - The ISO 8601 date string.
|
||||
* @returns {string} A localized date and time string, or the original string if parsing fails.
|
||||
*/
|
||||
export function formatIsoDate(isoDateString) {
|
||||
if (!isoDateString || typeof isoDateString !== 'string') {
|
||||
return isoDateString; // Return original value if it's not a valid string
|
||||
}
|
||||
try {
|
||||
const date = new Date(isoDateString);
|
||||
// Check if the date object is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return isoDateString; // Return original string if the date is invalid
|
||||
}
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
console.error('Could not parse ISO date:', e);
|
||||
return isoDateString; // Return original string on any error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ describe('reversePages - multi PDF support', () => {
|
||||
|
||||
await reversePages();
|
||||
|
||||
expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'reversed_pdfs.zip');
|
||||
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||
expect.any(Blob),
|
||||
'reversed_pdfs.zip'
|
||||
);
|
||||
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2);
|
||||
expect(mockNewDoc.addPage).toHaveBeenCalled();
|
||||
expect(mockNewDoc.save).toHaveBeenCalledTimes(2);
|
||||
@@ -77,38 +80,61 @@ describe('reversePages - multi PDF support', () => {
|
||||
});
|
||||
|
||||
it('should handle PDF creation errors', async () => {
|
||||
vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('Create failed'));
|
||||
vi.mocked(PDFLibDocument.create).mockRejectedValue(
|
||||
new Error('Create failed')
|
||||
);
|
||||
state.files = [
|
||||
{ name: 'x.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
||||
{
|
||||
name: 'x.pdf',
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||
},
|
||||
];
|
||||
|
||||
await reversePages();
|
||||
|
||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'Could not reverse the PDF pages.'
|
||||
);
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle PDF processing errors', async () => {
|
||||
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
|
||||
state.files = [
|
||||
{ name: 'y.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
||||
{
|
||||
name: 'y.pdf',
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||
},
|
||||
];
|
||||
|
||||
await reversePages();
|
||||
|
||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'Could not reverse the PDF pages.'
|
||||
);
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle save errors', async () => {
|
||||
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
|
||||
state.files = [
|
||||
{ name: 'z.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
||||
{
|
||||
name: 'z.pdf',
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||
},
|
||||
];
|
||||
|
||||
await reversePages();
|
||||
|
||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
||||
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'Could not reverse the PDF pages.'
|
||||
);
|
||||
expect(ui.hideLoader).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+83
-43
@@ -11,54 +11,94 @@
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="antialiased bg-gray-900 text-gray-300">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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"> <a href="index.html">BentoPDF</a> </span>
|
||||
</div>
|
||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="./about.html" class="nav-link">About</a>
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg id="menu-icon" class="block h-6 w-6" 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>
|
||||
<!-- Close Icon -->
|
||||
<svg id="close-icon" class="hidden h-6 w-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
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>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
<!-- Mobile Menu Dropdown -->
|
||||
<div
|
||||
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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||
<section class="max-w-4xl mx-auto py-12">
|
||||
|
||||
Reference in New Issue
Block a user