Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Upload Final PDF{% endblock %} | |
| {% block head %} | |
| <!-- Ensure mobile viewport settings are correct --> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1, viewport-fit=cover"> | |
| <style> | |
| :root { | |
| --app-bg: #181a1c; | |
| --card-bg: #212529; | |
| --input-bg: #2c3034; | |
| --border-color: #495057; | |
| --primary-color: #0d6efd; | |
| --touch-target: 48px; | |
| } | |
| body { | |
| background-color: var(--app-bg); | |
| color: #e9ecef; | |
| } | |
| /* Full height container with dynamic viewport support */ | |
| .upload-wrapper { | |
| min-height: calc(100vh - 56px); | |
| min-height: calc(100dvh - 56px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 1rem; | |
| padding-bottom: env(safe-area-inset-bottom); | |
| } | |
| .main-card { | |
| background-color: var(--card-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.4); | |
| width: 100%; | |
| max-width: 600px; | |
| overflow: hidden; | |
| } | |
| /* Custom segmented tabs */ | |
| .nav-pills { | |
| background: var(--input-bg); | |
| padding: 4px; | |
| border-radius: 12px; | |
| gap: 4px; | |
| } | |
| .nav-pills .nav-link { | |
| border-radius: 8px; | |
| color: #adb5bd ; | |
| font-size: 0.9rem; | |
| padding: 10px; | |
| transition: all 0.2s; | |
| } | |
| .nav-pills .nav-link.active { | |
| background-color: var(--primary-color) ; | |
| color: white ; | |
| font-weight: 600; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.2); | |
| } | |
| /* File Drop Zone Styling */ | |
| .file-drop-area { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| width: 100%; | |
| max-width: 100%; | |
| padding: 30px; | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| transition: 0.2s; | |
| background: rgba(255,255,255,0.02); | |
| cursor: pointer; | |
| } | |
| .file-drop-area:hover, .file-drop-area.drag-over { | |
| border-color: var(--primary-color); | |
| background: rgba(13, 110, 253, 0.05); | |
| } | |
| .fake-btn { | |
| background: var(--input-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 8px 16px; | |
| font-size: 0.9rem; | |
| margin-top: 10px; | |
| } | |
| .file-msg { | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| color: #dee2e6; | |
| text-align: center; | |
| } | |
| .file-input { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| width: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| /* Large touch-friendly inputs */ | |
| .form-control-lg { | |
| background-color: var(--input-bg); | |
| border-color: var(--border-color); | |
| color: white; | |
| font-size: 16px; /* Prevents iOS zoom */ | |
| min-height: var(--touch-target); | |
| } | |
| .form-control-lg:focus { | |
| background-color: var(--input-bg); | |
| border-color: var(--primary-color); | |
| color: white; | |
| box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25); | |
| } | |
| /* Main Action Button */ | |
| .btn-action { | |
| min-height: 54px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| transition: transform 0.1s; | |
| } | |
| .btn-action:active { | |
| transform: scale(0.98); | |
| } | |
| /* Animations */ | |
| .fade-in-up { | |
| animation: fadeInUp 0.4s ease-out forwards; | |
| } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="upload-wrapper"> | |
| <div class="main-card fade-in-up"> | |
| <!-- Header --> | |
| <div class="p-4 border-bottom border-secondary" style="border-color: #495057 !important;"> | |
| <div class="text-center"> | |
| <div class="bg-success bg-opacity-10 text-success d-inline-flex align-items-center justify-content-center rounded-circle mb-3" style="width: 56px; height: 56px;"> | |
| <i class="bi bi-file-earmark-check-fill fs-3"></i> | |
| </div> | |
| <h1 class="h4 mb-1">Upload Final PDF</h1> | |
| <p class="text-secondary small mb-0">Add a finished report to your collection</p> | |
| </div> | |
| </div> | |
| <div class="card-body p-4"> | |
| <form id="upload-form"> | |
| <!-- Subject Field (Common) --> | |
| <div class="mb-4"> | |
| <label for="subject" class="form-label small text-secondary">Subject (Required)</label> | |
| <input type="text" class="form-control form-control-lg" id="subject" name="subject" placeholder="e.g. Mathematics Test 1" required> | |
| </div> | |
| <!-- Navigation Tabs --> | |
| <ul class="nav nav-pills nav-fill mb-4" id="uploadMethodTab" role="tablist"> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link active" id="file-tab" data-bs-toggle="tab" data-bs-target="#file-upload-pane" type="button" role="tab" aria-controls="file-upload-pane" aria-selected="true"> | |
| <i class="bi bi-folder2-open me-1"></i> File | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url-upload-pane" type="button" role="tab" aria-controls="url-upload-pane" aria-selected="false"> | |
| <i class="bi bi-link-45deg me-1"></i> URL | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="curl-tab" data-bs-toggle="tab" data-bs-target="#curl-upload-pane" type="button" role="tab" aria-controls="curl-upload-pane" aria-selected="false"> | |
| <i class="bi bi-code-slash me-1"></i> cURL | |
| </button> | |
| </li> | |
| </ul> | |
| <div class="tab-content mb-4"> | |
| <!-- File Upload Pane --> | |
| <div class="tab-pane fade show active" id="file-upload-pane" role="tabpanel" aria-labelledby="file-tab"> | |
| <div class="file-drop-area" id="drop-zone"> | |
| <span class="file-msg" id="file-label-text">Tap to select or drag PDF here</span> | |
| <span class="fake-btn">Choose File</span> | |
| <input class="file-input" type="file" id="pdf-upload" name="pdf" accept=".pdf"> | |
| </div> | |
| </div> | |
| <!-- URL Pane --> | |
| <div class="tab-pane fade" id="url-upload-pane" role="tabpanel" aria-labelledby="url-tab"> | |
| <div class="mb-3"> | |
| <label for="pdf-url" class="form-label small text-secondary">PDF URL Address</label> | |
| <input type="url" class="form-control form-control-lg" id="pdf-url" name="pdf_url" placeholder="https://example.com/doc.pdf or Google Drive Link" autocomplete="off"> | |
| <div class="form-text text-muted small mt-1"><i class="bi bi-google"></i> Google Drive sharing links supported</div> | |
| </div> | |
| </div> | |
| <!-- cURL Pane --> | |
| <div class="tab-pane fade" id="curl-upload-pane" role="tabpanel" aria-labelledby="curl-tab"> | |
| <div class="mb-3"> | |
| <label for="curl-command" class="form-label small text-secondary">Paste cURL Command</label> | |
| <textarea class="form-control form-control-lg" id="curl-command" name="curl_command" placeholder="curl -o 'report.pdf' ..." rows="2" style="font-family: monospace; font-size: 14px;"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Action Button --> | |
| <button type="submit" class="btn btn-primary w-100 btn-action shadow"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <span class="btn-text">Upload PDF</span> | |
| <i class="bi bi-arrow-right-circle btn-icon"></i> | |
| </button> | |
| </form> | |
| <div id="status" class="mt-3"></div> | |
| <div class="mt-4 text-center"> | |
| <a href="/pdf_manager" class="text-secondary text-decoration-none small"> | |
| <i class="bi bi-arrow-left"></i> Back to PDF Manager | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| // --- File Input UX --- | |
| const fileInput = document.getElementById('pdf-upload'); | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileLabel = document.getElementById('file-label-text'); | |
| // Visual feedback for file selection | |
| fileInput.addEventListener('change', function() { | |
| if (this.files && this.files.length > 0) { | |
| fileLabel.innerHTML = `<i class="bi bi-check-circle-fill text-success"></i> ${this.files[0].name}`; | |
| dropZone.style.borderColor = '#198754'; | |
| dropZone.style.background = 'rgba(25, 135, 84, 0.1)'; | |
| } else { | |
| fileLabel.textContent = "Tap to select or drag PDF here"; | |
| dropZone.style.borderColor = ''; | |
| dropZone.style.background = ''; | |
| } | |
| }); | |
| // Drag and drop visual effects | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dropZone.classList.add('drag-over'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dropZone.classList.remove('drag-over'); | |
| }, false); | |
| }); | |
| // --- Paste Support --- | |
| document.addEventListener('paste', function(event) { | |
| const items = (event.clipboardData || event.originalEvent.clipboardData).items; | |
| for (let index in items) { | |
| const item = items[index]; | |
| if (item.kind === 'file') { | |
| const blob = item.getAsFile(); | |
| if (blob.type === 'application/pdf') { | |
| // It's a PDF! | |
| const dataTransfer = new DataTransfer(); | |
| dataTransfer.items.add(blob); | |
| fileInput.files = dataTransfer.files; | |
| // Trigger change event to update UI | |
| const changeEvent = new Event('change'); | |
| fileInput.dispatchEvent(changeEvent); | |
| // Switch to file tab if not active | |
| const fileTabBtn = document.querySelector('#file-tab'); | |
| if (!fileTabBtn.classList.contains('active')) { | |
| const tab = new bootstrap.Tab(fileTabBtn); | |
| tab.show(); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| }); | |
| // --- Tab Handling --- | |
| const tabs = document.querySelectorAll('#uploadMethodTab .nav-link'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('shown.bs.tab', function (event) { | |
| const currentTabId = event.target.id; | |
| document.getElementById('pdf-upload').required = (currentTabId === 'file-tab'); | |
| document.getElementById('pdf-url').required = (currentTabId === 'url-tab'); | |
| document.getElementById('curl-command').required = (currentTabId === 'curl-tab'); | |
| }); | |
| }); | |
| // Set initial required state | |
| document.getElementById('pdf-upload').required = true; | |
| // --- Form Submission --- | |
| document.getElementById('upload-form').addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const form = e.target; | |
| const button = form.querySelector('button[type="submit"]'); | |
| const spinner = button.querySelector('.spinner-border'); | |
| const btnText = button.querySelector('.btn-text'); | |
| const btnIcon = button.querySelector('.btn-icon'); | |
| const statusDiv = document.getElementById('status'); | |
| // Reset UI | |
| statusDiv.innerHTML = ''; | |
| button.disabled = true; | |
| spinner.classList.remove('d-none'); | |
| btnIcon.classList.add('d-none'); | |
| btnText.textContent = "Uploading..."; | |
| const activeTab = document.querySelector('#uploadMethodTab .nav-link.active').id; | |
| const formData = new FormData(form); | |
| // Validation & Data Prep (Subject is required and handled by 'required' attr) | |
| let isValid = false; | |
| // Subject Validation (Double check) | |
| const subject = formData.get('subject'); | |
| if (!subject || !subject.trim()) { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Subject is required.</div>`; | |
| button.disabled = false; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Upload PDF"; | |
| return; | |
| } | |
| if (activeTab === 'file-tab') { | |
| const fileInput = document.getElementById('pdf-upload'); | |
| if (fileInput.files.length > 0) { | |
| // formData already has 'pdf' from constructor | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please select a PDF file.</div>`; | |
| } | |
| } else if (activeTab === 'url-tab') { | |
| const urlInput = document.getElementById('pdf-url'); | |
| if (urlInput.value) { | |
| // formData has 'pdf_url' | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please enter a PDF URL.</div>`; | |
| } | |
| } else if (activeTab === 'curl-tab') { | |
| const curlInput = document.getElementById('curl-command'); | |
| if (curlInput.value) { | |
| // formData has 'curl_command' | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please enter a cURL command.</div>`; | |
| } | |
| } | |
| if (!isValid) { | |
| button.disabled = false; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Upload PDF"; | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/handle_final_pdf_upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (response.ok) { | |
| // Check if redirected to pdf_manager (success) | |
| if (response.redirected) { | |
| window.location.href = response.url; | |
| } else { | |
| // Fallback if not redirected but successful (e.g. 200 OK text) | |
| // But typically this route redirects. | |
| window.location.href = '/pdf_manager'; | |
| } | |
| } else { | |
| const errorText = await response.text(); | |
| throw new Error(errorText || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| statusDiv.innerHTML = ` | |
| <div class="alert alert-danger border-0 bg-danger bg-opacity-10 text-danger"> | |
| <i class="bi bi-x-circle-fill me-2"></i> ${error.message} | |
| </div> | |
| `; | |
| button.disabled = false; | |
| button.className = "btn btn-primary w-100 btn-action shadow"; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Try Again"; | |
| } | |
| }); | |
| </script> | |
| {% endblock %} |