diff --git a/frontend/index.html b/frontend/index.html index 75a3a7d..0111ab9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ Аналог DropMeFiles - +
@@ -34,6 +34,12 @@
+ @@ -54,6 +60,6 @@ - + diff --git a/frontend/script.js b/frontend/script.js index eec6898..4408ffa 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -366,25 +366,57 @@ function initUploadPage() { uploadData.fields["key"] = fileUuid; delete uploadData.fields["Content-Type"]; - setStatus('Загрузка файла на сервер...', ''); + // Hide status, show progress bar + statusDiv.style.display = 'none'; + const uploadProgress = document.getElementById('uploadProgress'); + const uploadProgressBar = document.getElementById('uploadProgressBar'); + const uploadProgressText = document.getElementById('uploadProgressText'); + uploadProgress.style.display = 'block'; + uploadProgressBar.style.width = '0%'; + uploadProgressText.textContent = '0%'; - // Step 2: Upload to S3 using form-data - // Add all fields from the API response (order matters for S3, file should be last) - const formData = new FormData(); - Object.keys(uploadData.fields).forEach(key => { - formData.append(key, uploadData.fields[key]); - }); - // File must be appended last - formData.append('file', file); - - const uploadResponse = await fetch(uploadData.url, { - method: 'POST', - body: formData + // Step 2: Upload to S3 using XMLHttpRequest for progress tracking + await new Promise((resolve, reject) => { + const formData = new FormData(); + Object.keys(uploadData.fields).forEach(key => { + formData.append(key, uploadData.fields[key]); + }); + // File must be appended last + formData.append('file', file); + + const xhr = new XMLHttpRequest(); + + // Track upload progress + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + const percent = Math.round((event.loaded / event.total) * 100); + uploadProgressBar.style.width = percent + '%'; + uploadProgressText.textContent = `${percent}% (${formatBytes(event.loaded)} / ${formatBytes(event.total)})`; + } + }); + + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`Ошибка загрузки: ${xhr.status}`)); + } + }); + + xhr.addEventListener('error', () => { + reject(new Error('Ошибка при загрузке файла на сервер')); + }); + + xhr.addEventListener('abort', () => { + reject(new Error('Загрузка прервана')); + }); + + xhr.open('POST', uploadData.url); + xhr.send(formData); }); - if (!uploadResponse.ok) { - throw new Error('Ошибка при загрузке файла на сервер'); - } + // Hide progress bar after successful upload + uploadProgress.style.display = 'none'; // Success! const downloadUrl = `${window.location.origin}/get/${fileUuid}`; @@ -405,6 +437,11 @@ function initUploadPage() { } showErrorPopup(errorMessage); setStatus('', ''); + // Hide progress bar on error + const uploadProgress = document.getElementById('uploadProgress'); + if (uploadProgress) { + uploadProgress.style.display = 'none'; + } } finally { uploadButton.disabled = false; } diff --git a/frontend/style.css b/frontend/style.css index ac77033..4ba8173 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -15,6 +15,13 @@ body { padding: 24px; } +@media (max-width: 768px) { + body { + padding: 16px; + justify-content: center; + } +} + .page-content { display: flex; flex-direction: column; @@ -30,6 +37,13 @@ h1 { width: 100%; } +@media (max-width: 768px) { + h1 { + font-size: clamp(24px, 6vw, 32px); + margin: 0 0 24px 0; + } +} + .upload-container { width: min(640px, 100%); background: #ffffffdd; @@ -42,6 +56,14 @@ h1 { backdrop-filter: blur(8px); } +@media (max-width: 768px) { + .upload-container { + padding: 20px; + gap: 16px; + border-radius: 16px; + } +} + .dropzone { border: 2px dashed #6366f1; border-radius: 16px; @@ -55,6 +77,13 @@ h1 { overflow: hidden; } +@media (max-width: 768px) { + .dropzone { + padding: 32px 16px; + border-radius: 12px; + } +} + .dropzone:focus, .dropzone:focus-within { outline: none; @@ -130,6 +159,17 @@ button { font-weight: 600; cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +@media (max-width: 768px) { + button { + padding: 16px 24px; + font-size: 16px; + min-height: 48px; + width: 100%; + } } button:hover:not(:disabled) { @@ -204,6 +244,16 @@ button:disabled { cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; font-family: inherit; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +@media (max-width: 768px) { + .download-button { + padding: 16px 24px; + width: 100%; + min-height: 48px; + } } .download-button:hover { @@ -246,6 +296,53 @@ button:disabled { } } +@media (max-width: 768px) { + .error-popup { + top: 10px; + left: 10px; + right: 10px; + transform: none; + max-width: none; + padding: 12px 16px; + font-size: 14px; + } + + @keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .status { + font-size: 14px; + } + + .status.error { + font-size: 16px; + } + + .file-info h2 { + font-size: 20px; + } + + .file-size { + font-size: 14px; + } + + .dropzone p { + font-size: 14px; + } + + .dropzone p.strong { + font-size: 16px; + } +} + /* Success content */ .success-content { animation: fadeIn 0.3s ease-in; @@ -282,6 +379,15 @@ button:disabled { cursor: text; } +@media (max-width: 768px) { + .download-link-input { + width: 100%; + min-width: 0; + font-size: 12px; + padding: 14px; + } +} + .download-link-input:focus { outline: none; border-color: #6366f1; @@ -299,6 +405,16 @@ button:disabled { cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; white-space: nowrap; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +@media (max-width: 768px) { + .copy-link-button { + width: 100%; + padding: 14px 24px; + min-height: 48px; + } } .copy-link-button:hover { @@ -325,6 +441,16 @@ button:disabled { font-weight: 600; cursor: pointer; transition: all 0.2s ease; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +@media (max-width: 768px) { + .upload-another-button { + width: 100%; + padding: 14px 24px; + min-height: 48px; + } } .upload-another-button:hover { @@ -363,3 +489,82 @@ button:disabled { margin: 0; font-weight: 500; } + +@media (max-width: 768px) { + .status { + font-size: 14px; + } + + .status.error { + font-size: 16px; + } + + .file-info h2 { + font-size: 20px; + } + + .file-size { + font-size: 14px; + } + + .dropzone p { + font-size: 14px; + } + + .dropzone p.strong { + font-size: 16px; + } + + .error-popup { + top: 10px; + left: 10px; + right: 10px; + transform: none; + max-width: none; + padding: 12px 16px; + font-size: 14px; + } + + @keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .progress-text { + font-size: 12px; + } + + .button-container { + margin-top: 16px; + } +} + +@media (max-width: 480px) { + body { + padding: 12px; + } + + h1 { + font-size: 22px; + margin: 0 0 20px 0; + } + + .upload-container { + padding: 16px; + gap: 12px; + } + + .dropzone { + padding: 24px 12px; + } + + .file-info { + padding: 16px; + } +}