aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/css/main.css87
-rw-r--r--static/scripts/upload.js236
2 files changed, 318 insertions, 5 deletions
diff --git a/static/css/main.css b/static/css/main.css
index ca70e63..969b156 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -605,4 +605,91 @@ footer::before {
.ib-loader-seg:nth-child(12) {
top: 3px;
left: 5px;
+}
+
+/* Upload All Button Styles */
+.upload-all-btn {
+ padding: 12px 24px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ width: auto;
+ min-width: 120px;
+}
+
+.upload-all-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.upload-all-btn.uploading {
+ background-color: #444400;
+ border-color: #ffff00;
+ color: #ffffcc;
+}
+
+.upload-all-btn.success {
+ background-color: #004400;
+ border-color: #00ff00;
+ color: #ccffcc;
+ animation: successPulse 1s ease-in-out;
+}
+
+.upload-all-btn.warning {
+ background-color: #664400;
+ border-color: #ffaa00;
+ color: #ffeecc;
+}
+
+.upload-all-btn.error {
+ background-color: #440000;
+ border-color: #ff0000;
+ color: #ffcccc;
+}
+
+@keyframes successPulse {
+ 0% {
+ box-shadow: 0 0 0 rgba(0, 255, 0, 0.4);
+ }
+
+ 50% {
+ box-shadow: 0 0 16px rgba(0, 255, 0, 0.6);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 rgba(0, 255, 0, 0.4);
+ }
+}
+
+.preview-area.uploaded {
+ opacity: 0.7;
+ background-color: rgba(0, 68, 0, 0.1);
+ border-left: 3px solid #00ff00;
+ padding-left: 8px;
+}
+
+.preview-area.upload-error {
+ background-color: rgba(68, 0, 0, 0.1);
+ border-left: 3px solid #ff0000;
+ padding-left: 8px;
+}
+
+.preview-remove-btn.uploaded {
+ background-color: #004400;
+ border-color: #00ff00;
+ color: #ccffcc;
+ cursor: default;
+}
+
+.preview-remove-btn.uploading {
+ background-color: #333333;
+ border-color: #666666;
+ color: #999999;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.preview-remove-btn.error {
+ background-color: #440000;
+ border-color: #ff0000;
+ color: #ffcccc;
} \ No newline at end of file
diff --git a/static/scripts/upload.js b/static/scripts/upload.js
index 309b0da..d190014 100644
--- a/static/scripts/upload.js
+++ b/static/scripts/upload.js
@@ -6,6 +6,12 @@
const imageBlobMapping = new Map();
/**
+ * Tracks whether an upload is currently in progress
+ * @type {boolean}
+ */
+let isUploading = false;
+
+/**
* Shows or hides the Upload All button in the .upload-area container based on imageBlobMapping size.
* The button is created once and appended/removed as needed.
* @function
@@ -19,11 +25,8 @@ function updateUploadAllBtn() {
uploadAllBtn.type = 'button';
uploadAllBtn.textContent = 'Upload All';
uploadAllBtn.className = 'upload-all-btn';
- /**
- * TODO: Implement actual upload logic here
- */
- uploadAllBtn.onclick = function () {
- alert('Upload All clicked!');
+ uploadAllBtn.onclick = async function () {
+ await uploadAllImages();
};
}
if (imageBlobMapping.size > 0) {
@@ -95,6 +98,7 @@ function createPreviewElement(key, blob, type, nameOrUrl) {
removeBtn.textContent = 'Remove';
removeBtn.className = 'preview-remove-btn';
removeBtn.onclick = () => {
+ if (isUploading) return; // Prevent removal during upload
previewElement.remove();
imageBlobMapping.delete(key);
updateUploadAllBtn();
@@ -369,4 +373,226 @@ function handleFiles(files) {
updateUploadAllBtn();
}
+/**
+ * Uploads all images in the imageBlobMapping to the server.
+ * Shows progress and handles success/error states.
+ */
+async function uploadAllImages() {
+ if (imageBlobMapping.size === 0) return;
+
+ const totalImages = imageBlobMapping.size;
+ let uploadedCount = 0;
+ let hasErrors = false;
+
+ // Set upload state and disable remove buttons
+ isUploading = true;
+ disableAllRemoveButtons();
+
+ // Disable the upload button and show progress
+ uploadAllBtn.disabled = true;
+ updateUploadButtonText(uploadedCount, totalImages, false);
+
+ try {
+ for (const [key, imageData] of imageBlobMapping) {
+ try {
+ // Get the selected rating for this image
+ const selectedRating = getSelectedRating(key);
+ imageData.rating = selectedRating;
+
+ // Create and submit form for this image
+ await uploadSingleImage(imageData, selectedRating);
+
+ // Mark this image as successfully uploaded
+ markImageAsUploaded(imageData.previewElement);
+ uploadedCount++;
+ updateUploadButtonText(uploadedCount, totalImages, false);
+
+ } catch (error) {
+ console.error(`Failed to upload image ${key}:`, error);
+ markImageAsError(imageData.previewElement, error.message);
+ hasErrors = true;
+ }
+ }
+
+ // Update button text based on results
+ if (hasErrors) {
+ updateUploadButtonText(uploadedCount, totalImages, true);
+ } else {
+ uploadAllBtn.textContent = `✓ All ${totalImages} images uploaded successfully!`;
+ uploadAllBtn.className = 'upload-all-btn success';
+
+ // Clear all uploaded images after a delay
+ setTimeout(() => {
+ clearAllUploadedImages();
+ }, 2000);
+ }
+
+ } catch (error) {
+ console.error('Upload process failed:', error);
+ uploadAllBtn.textContent = 'Upload failed - Try again';
+ uploadAllBtn.className = 'upload-all-btn error';
+ uploadAllBtn.disabled = false;
+ } finally {
+ // Reset upload state
+ isUploading = false;
+ enableAllRemoveButtons();
+ }
+}
+
+/**
+ * Disables all remove buttons to prevent removal during upload.
+ */
+function disableAllRemoveButtons() {
+ const removeButtons = document.querySelectorAll('.preview-remove-btn:not(.uploaded):not(.error)');
+ removeButtons.forEach(btn => {
+ btn.disabled = true;
+ btn.classList.add('uploading');
+ });
+}
+
+/**
+ * Re-enables all remove buttons after upload is complete.
+ */
+function enableAllRemoveButtons() {
+ const removeButtons = document.querySelectorAll('.preview-remove-btn:not(.uploaded):not(.error)');
+ removeButtons.forEach(btn => {
+ btn.disabled = false;
+ btn.classList.remove('uploading');
+ });
+}
+
+/**
+ * Gets the selected rating for an image from its radio buttons.
+ * @param {string} key - The key for the image
+ * @returns {string} The selected rating
+ */
+function getSelectedRating(key) {
+ const checkedInput = document.querySelector(`input[name="rating-${key}"]:checked`);
+ return checkedInput ? checkedInput.value : 'safe';
+}
+
+/**
+ * Uploads a single image to the server by creating a FormData and submitting it.
+ * @param {Object} imageData - The image data object
+ * @param {string} rating - The selected rating
+ * @returns {Promise<void>}
+ */
+async function uploadSingleImage(imageData, rating) {
+ const formData = new FormData();
+
+ // Add the image blob to the form
+ if (imageData.type === 'local') {
+ formData.append('image', imageData.blob, imageData.nameOrUrl);
+ } else {
+ // For link images, create a File object from the blob
+ const file = new File([imageData.blob], 'image.jpg', { type: imageData.blob.type });
+ formData.append('image', file);
+ formData.append('source_url', imageData.nameOrUrl);
+ }
+
+ // Add the rating
+ formData.append('rating', rating);
+
+ // Submit to the backend
+ const response = await fetch('/posts/new', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => 'Unknown error');
+ throw new Error(errorText || `Upload failed with status ${response.status}`);
+ }
+
+ return response;
+}
+
+/**
+ * Updates the upload button text to show progress.
+ * @param {number} current - Current number of uploaded images
+ * @param {number} total - Total number of images to upload
+ * @param {boolean} hasErrors - Whether there were errors during upload
+ */
+function updateUploadButtonText(current, total, hasErrors) {
+ if (hasErrors) {
+ uploadAllBtn.textContent = `⚠ Uploaded ${current}/${total} (some failed)`;
+ uploadAllBtn.className = 'upload-all-btn warning';
+ uploadAllBtn.disabled = false;
+ } else if (current === total) {
+ uploadAllBtn.textContent = `✓ All ${total} images uploaded!`;
+ uploadAllBtn.className = 'upload-all-btn success';
+ } else {
+ uploadAllBtn.textContent = `⏳ Uploading (${current}/${total})`;
+ uploadAllBtn.className = 'upload-all-btn uploading';
+ }
+}
+
+/**
+ * Marks an image preview as successfully uploaded.
+ * @param {HTMLElement} previewElement - The preview element to mark
+ */
+function markImageAsUploaded(previewElement) {
+ previewElement.classList.add('uploaded');
+ const removeBtn = previewElement.querySelector('.preview-remove-btn');
+ if (removeBtn) {
+ removeBtn.textContent = '✓ Uploaded';
+ removeBtn.disabled = true;
+ removeBtn.className = 'preview-remove-btn uploaded';
+ }
+}
+
+/**
+ * Marks an image preview as failed to upload.
+ * @param {HTMLElement} previewElement - The preview element to mark
+ * @param {string} errorMessage - The error message to display
+ */
+function markImageAsError(previewElement, errorMessage) {
+ previewElement.classList.add('upload-error');
+ const removeBtn = previewElement.querySelector('.preview-remove-btn');
+ if (removeBtn) {
+ removeBtn.textContent = '✗ Failed';
+ removeBtn.className = 'preview-remove-btn error';
+ removeBtn.title = errorMessage;
+ }
+}
+
+/**
+ * Clears all successfully uploaded images from the preview area.
+ */
+function clearAllUploadedImages() {
+ const uploadedElements = document.querySelectorAll('.preview-area.uploaded');
+ uploadedElements.forEach(element => {
+ const key = getImageKeyFromElement(element);
+ if (key) {
+ imageBlobMapping.delete(key);
+ }
+ element.remove();
+ });
+ updateUploadAllBtn();
+
+ // Reset button state if all images are cleared
+ if (imageBlobMapping.size === 0) {
+ if (uploadAllBtn) {
+ uploadAllBtn.textContent = 'Upload All';
+ uploadAllBtn.className = 'upload-all-btn';
+ uploadAllBtn.disabled = false;
+ }
+ }
+}
+
+/**
+ * Gets the image key from a preview element by looking at its radio button names.
+ * @param {HTMLElement} previewElement - The preview element
+ * @returns {string|null} The image key or null if not found
+ */
+function getImageKeyFromElement(previewElement) {
+ const radioInput = previewElement.querySelector('input[type="radio"]');
+ if (radioInput && radioInput.name) {
+ // Extract key from "rating-{key}" format
+ const match = radioInput.name.match(/^rating-(.+)$/);
+ return match ? match[1] : null;
+ }
+ return null;
+}
+
setupLocalImageUpload();