Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Document Converter</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/@tailwindcss/typography/dist/typography.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .drop-zone { | |
| border: 2px dashed #cbd5e0; | |
| transition: all 0.3s ease; | |
| } | |
| .drop-zone:hover, .drop-zone.dragging { | |
| border-color: #4a5568; | |
| background-color: #f7fafc; | |
| } | |
| .loading-spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #3498db; | |
| border-radius: 50%; | |
| width: 24px; | |
| height: 24px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="app" class="min-h-screen py-12 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-3xl mx-auto"> | |
| <!-- Header --> | |
| <div class="text-center mb-12"> | |
| <h1 class="text-3xl font-bold text-gray-900 mb-2">Document Converter</h1> | |
| <p class="text-gray-600">Convert your documents to various formats quickly and easily</p> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <!-- File Upload Zone --> | |
| <div | |
| class="drop-zone rounded-lg p-8 text-center cursor-pointer mb-6" | |
| :class="{ 'dragging': isDragging }" | |
| @dragenter.prevent="isDragging = true" | |
| @dragleave.prevent="isDragging = false" | |
| @dragover.prevent | |
| @drop.prevent="handleDrop" | |
| @click="triggerFileInput"> | |
| <input | |
| type="file" | |
| ref="fileInput" | |
| @change="handleFileSelect" | |
| class="hidden"> | |
| <div v-if="!selectedFile"> | |
| <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/> | |
| </svg> | |
| <p class="mt-2 text-gray-600">Drag and drop your file here or click to browse</p> | |
| </div> | |
| <div v-else class="text-gray-700"> | |
| <p class="font-medium">Selected file:</p> | |
| <p class="text-sm">{{ selectedFile.name }}</p> | |
| </div> | |
| </div> | |
| <!-- Conversion Options --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Output Format</label> | |
| <select | |
| v-model="outputFormat" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| <option value="pdf">PDF</option> | |
| <option value="docx">DOCX</option> | |
| <option value="xlsx">XLSX</option> | |
| <option value="png">PNG</option> | |
| <option value="jpg">JPG</option> | |
| <option value="csv">CSV</option> | |
| </select> | |
| </div> | |
| <!-- Filter Options --> | |
| <div class="mb-6" v-if="showFilterOptions"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Filter Options</label> | |
| <input | |
| type="text" | |
| v-model="filterOptions" | |
| placeholder="e.g., PixelWidth=640,PixelHeight=480" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| <p class="mt-1 text-sm text-gray-500">{{ filterOptionsHint }}</p> | |
| </div> | |
| <!-- Convert Button --> | |
| <button | |
| @click="convertFile" | |
| :disabled="!selectedFile || converting" | |
| class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <span v-if="!converting">Convert Document</span> | |
| <div v-else class="loading-spinner"></div> | |
| </button> | |
| <!-- Status Messages --> | |
| <div v-if="status" :class="statusClass" class="mt-4 p-4 rounded-md"> | |
| {{ status }} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp } = Vue | |
| createApp({ | |
| data() { | |
| return { | |
| selectedFile: null, | |
| outputFormat: 'pdf', | |
| filterOptions: '', | |
| isDragging: false, | |
| converting: false, | |
| status: '', | |
| statusType: 'info' | |
| } | |
| }, | |
| computed: { | |
| showFilterOptions() { | |
| return ['png', 'jpg', 'csv'].includes(this.outputFormat) | |
| }, | |
| filterOptionsHint() { | |
| if (this.outputFormat === 'png' || this.outputFormat === 'jpg') { | |
| return 'Example: PixelWidth=640,PixelHeight=480' | |
| } else if (this.outputFormat === 'csv') { | |
| return 'Example: 59,34,76,1 for CSV specific options' | |
| } | |
| return '' | |
| }, | |
| statusClass() { | |
| const baseClasses = 'text-sm p-4 rounded-md' | |
| switch (this.statusType) { | |
| case 'success': | |
| return `${baseClasses} bg-green-50 text-green-700` | |
| case 'error': | |
| return `${baseClasses} bg-red-50 text-red-700` | |
| default: | |
| return `${baseClasses} bg-blue-50 text-blue-700` | |
| } | |
| } | |
| }, | |
| methods: { | |
| triggerFileInput() { | |
| this.$refs.fileInput.click() | |
| }, | |
| handleFileSelect(event) { | |
| const file = event.target.files[0] | |
| if (file) { | |
| this.selectedFile = file | |
| this.status = '' | |
| } | |
| }, | |
| handleDrop(event) { | |
| this.isDragging = false | |
| const file = event.dataTransfer.files[0] | |
| if (file) { | |
| this.selectedFile = file | |
| this.status = '' | |
| } | |
| }, | |
| async convertFile() { | |
| if (!this.selectedFile) return | |
| this.converting = true | |
| this.status = 'Converting document...' | |
| this.statusType = 'info' | |
| const formData = new FormData() | |
| formData.append('file', this.selectedFile) | |
| try { | |
| const queryParams = new URLSearchParams({ | |
| output_format: this.outputFormat | |
| }) | |
| if (this.filterOptions) { | |
| queryParams.append('filter_options', this.filterOptions) | |
| } | |
| const response = await fetch(`/convert/?${queryParams}`, { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| if (!response.ok) { | |
| throw new Error(`Conversion failed: ${response.statusText}`) | |
| } | |
| // Handle successful conversion | |
| const blob = await response.blob() | |
| const url = window.URL.createObjectURL(blob) | |
| const a = document.createElement('a') | |
| a.href = url | |
| a.download = `converted.${this.outputFormat}` | |
| document.body.appendChild(a) | |
| a.click() | |
| window.URL.revokeObjectURL(url) | |
| document.body.removeChild(a) | |
| this.status = 'Document converted successfully!' | |
| this.statusType = 'success' | |
| } catch (error) { | |
| this.status = `Error: ${error.message}` | |
| this.statusType = 'error' | |
| } finally { | |
| this.converting = false | |
| } | |
| } | |
| } | |
| }).mount('#app') | |
| </script> | |
| </body> | |
| </html> |