Spaces:
Running
Running
<!-- templates/index.html --> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Image Uploader</title> | |
<link rel="icon" type="image/svg+xml" href="/static/favicon/logo-svg.png"> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
:root { | |
--primary-color: #4361ee; | |
--secondary-color: #3f37c9; | |
--accent-color: #4cc9f0; | |
--success-color: #22cc88; | |
--light-bg: #f8f9fa; | |
--dark-text: #212529; | |
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); | |
--hover-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); | |
} | |
body { | |
background-color: #f5f7fa; | |
color: var(--dark-text); | |
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
padding-bottom: 40px; | |
} | |
.navbar { | |
background-color: white; | |
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05); | |
padding: 15px 0; | |
margin-bottom: 30px; | |
} | |
.navbar-brand { | |
font-weight: 600; | |
color: var(--primary-color); | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.navbar-brand i { | |
font-size: 1.5em; | |
} | |
.container { | |
max-width: 1200px; | |
} | |
.card { | |
border: none; | |
border-radius: 12px; | |
box-shadow: var(--card-shadow); | |
transition: all 0.3s ease; | |
} | |
.section-title { | |
margin-bottom: 20px; | |
font-weight: 600; | |
color: var(--dark-text); | |
border-left: 4px solid var(--primary-color); | |
padding-left: 12px; | |
} | |
.upload-container { | |
background-color: white; | |
padding: 25px; | |
border-radius: 12px; | |
margin-bottom: 30px; | |
box-shadow: var(--card-shadow); | |
} | |
.gallery-container { | |
background-color: white; | |
padding: 25px; | |
border-radius: 12px; | |
margin-bottom: 30px; | |
box-shadow: var(--card-shadow); | |
} | |
.search-container { | |
background-color: #f5f7fa; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
} | |
.image-card { | |
margin-bottom: 25px; | |
border-radius: 12px; | |
overflow: hidden; | |
position: relative; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.image-card:hover { | |
transform: translateY(-5px); | |
box-shadow: var(--hover-shadow); | |
} | |
.image-preview { | |
max-height: 200px; | |
object-fit: cover; | |
width: 100%; | |
height: 200px; | |
} | |
.card-body { | |
padding: 20px; | |
} | |
.card-title { | |
font-weight: 600; | |
margin-bottom: 15px; | |
} | |
.hidden { | |
display: none; | |
} | |
#uploadProgress { | |
margin-top: 15px; | |
height: 10px; | |
border-radius: 5px; | |
} | |
.progress-bar { | |
background-color: var(--primary-color); | |
} | |
.form-control, .form-select { | |
border-radius: 8px; | |
padding: 10px 15px; | |
border: 1px solid #e0e0e0; | |
} | |
.form-control:focus, .form-select:focus { | |
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15); | |
border-color: var(--primary-color); | |
} | |
.btn { | |
border-radius: 8px; | |
padding: 10px 20px; | |
font-weight: 500; | |
} | |
.btn-primary { | |
background-color: var(--primary-color); | |
border-color: var(--primary-color); | |
} | |
.btn-primary:hover, .btn-primary:focus { | |
background-color: var(--secondary-color); | |
border-color: var(--secondary-color); | |
} | |
.btn-outline-primary { | |
color: var(--primary-color); | |
border-color: var(--primary-color); | |
} | |
.btn-outline-primary:hover, .btn-outline-primary:focus, | |
.btn-check:checked + .btn-outline-primary { | |
background-color: var(--primary-color); | |
border-color: var(--primary-color); | |
color: white; | |
} | |
.btn-danger { | |
background-color: #e63946; | |
border-color: #e63946; | |
} | |
.btn-danger:hover, .btn-danger:focus { | |
background-color: #d00000; | |
border-color: #d00000; | |
} | |
.hashtag { | |
display: inline-block; | |
background-color: rgba(67, 97, 238, 0.1); | |
padding: 5px 12px; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
margin-right: 5px; | |
margin-bottom: 5px; | |
color: var(--primary-color); | |
transition: all 0.2s ease; | |
font-weight: 500; | |
text-decoration: none; | |
} | |
.hashtag:hover { | |
background-color: rgba(67, 97, 238, 0.2); | |
color: var(--primary-color); | |
text-decoration: none; | |
} | |
.new-badge { | |
position: absolute; | |
top: 15px; | |
left: 15px; | |
background-color: var(--success-color); | |
color: white; | |
padding: 5px 10px; | |
border-radius: 20px; | |
font-size: 0.7rem; | |
font-weight: 600; | |
z-index: 2; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
} | |
.delete-icon { | |
position: absolute; | |
top: 15px; | |
right: 15px; | |
background-color: rgba(255, 255, 255, 0.9); | |
color: var(--danger-color); | |
border-radius: 50%; | |
width: 32px; | |
height: 32px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
z-index: 3; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
cursor: pointer; | |
transition: all 0.2s ease; | |
} | |
.delete-icon:hover { | |
background-color: var(--danger-color); | |
color: white; | |
transform: scale(1.1); | |
} | |
.card-link { | |
color: inherit; | |
text-decoration: none; | |
} | |
.card-link:hover { | |
color: inherit; | |
text-decoration: none; | |
} | |
.layout-controls { | |
margin-bottom: 15px; | |
} | |
/* Custom 5-column layout */ | |
.col-5-layout { | |
position: relative; | |
width: 100%; | |
padding-right: 15px; | |
padding-left: 15px; | |
} | |
@media (min-width: 992px) { | |
.col-5-layout { | |
flex: 0 0 20%; | |
max-width: 20%; | |
} | |
} | |
@media (min-width: 768px) and (max-width: 991.98px) { | |
.col-5-layout { | |
flex: 0 0 25%; | |
max-width: 25%; | |
} | |
} | |
@media (max-width: 767.98px) { | |
.col-5-layout { | |
flex: 0 0 50%; | |
max-width: 50%; | |
} | |
} | |
.card-img-overlay { | |
background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0) 50%); | |
transition: all 0.3s ease; | |
} | |
.toast-container { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
z-index: 9999; | |
} | |
.toast { | |
min-width: 250px; | |
background-color: white; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); | |
border: none; | |
border-radius: 8px; | |
} | |
.toast-header { | |
border-radius: 8px 8px 0 0; | |
} | |
.action-buttons { | |
display: flex; | |
gap: 8px; | |
} | |
.btn-sm { | |
padding: 5px 10px; | |
font-size: 0.8rem; | |
} | |
.empty-state { | |
text-align: center; | |
padding: 60px 0; | |
} | |
.empty-state i { | |
font-size: 3rem; | |
color: #dee2e6; | |
margin-bottom: 20px; | |
} | |
.empty-state p { | |
color: #6c757d; | |
font-size: 1.1rem; | |
} | |
.gallery-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 20px; | |
} | |
.site-footer { | |
background-color: white; | |
padding: 20px 0; | |
text-align: center; | |
margin-top: 40px; | |
border-top: 1px solid #eaeaea; | |
color: #6c757d; | |
font-size: 0.9rem; | |
} | |
</style> | |
</head> | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-light"> | |
<div class="container"> | |
<a class="navbar-brand" href="/"> | |
<i class="fas fa-photo-film"></i> | |
Image Uploader | |
</a> | |
<div class="ms-auto"> | |
<a href="/logout" class="btn btn-outline-danger"> | |
<i class="fas fa-sign-out-alt me-2"></i>Logout | |
</a> | |
</div> | |
</div> | |
</nav> | |
<div class="container"> | |
<!-- Upload Section (Top) --> | |
<div class="upload-container"> | |
<h3 class="section-title">Upload Images</h3> | |
<form id="uploadForm" enctype="multipart/form-data"> | |
<div class="row"> | |
<div class="col-md-8"> | |
<div class="mb-3"> | |
<label for="files" class="form-label"> | |
<i class="fas fa-images me-2"></i>Select images | |
</label> | |
<input class="form-control" type="file" id="files" name="files" accept="image/*" multiple> | |
<small class="text-muted">You can select multiple images</small> | |
</div> | |
</div> | |
<div class="col-md-4"> | |
<div class="mb-3"> | |
<label for="hashtags" class="form-label"> | |
<i class="fas fa-hashtag me-2"></i>Add hashtags | |
</label> | |
<input type="text" class="form-control" id="hashtags" name="hashtags" placeholder="nature travel photography"> | |
<small class="text-muted">Separate with spaces or commas</small> | |
</div> | |
</div> | |
</div> | |
<button type="submit" class="btn btn-primary"> | |
<i class="fas fa-cloud-upload-alt me-2"></i>Upload Images | |
</button> | |
</form> | |
<div id="uploadProgress" class="progress hidden"> | |
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div> | |
</div> | |
</div> | |
<!-- Gallery Section (Bottom with integrated search) --> | |
<div class="gallery-container"> | |
<div class="gallery-header"> | |
<h3 class="section-title mb-0">Your Gallery</h3> | |
<div class="layout-controls btn-group" role="group"> | |
<input type="radio" class="btn-check" name="layout" id="layout3" autocomplete="off" checked> | |
<label class="btn btn-outline-primary" for="layout3"><i class="fas fa-th-large"></i></label> | |
<input type="radio" class="btn-check" name="layout" id="layout4" autocomplete="off"> | |
<label class="btn btn-outline-primary" for="layout4"><i class="fas fa-th"></i></label> | |
<input type="radio" class="btn-check" name="layout" id="layout5" autocomplete="off"> | |
<label class="btn btn-outline-primary" for="layout5"><i class="fas fa-grip-horizontal"></i></label> | |
</div> | |
</div> | |
<!-- Search and Filter (Inside Gallery) --> | |
<div class="search-container"> | |
<form id="searchForm" method="get" action="/" class="row g-3"> | |
<div class="col-md-6"> | |
<label for="search" class="form-label"> | |
<i class="fas fa-search me-2"></i>Search by name or hashtag | |
</label> | |
<input type="text" class="form-control" id="search" name="search" value="{{ current_search or '' }}" placeholder="Search..."> | |
</div> | |
<div class="col-md-6"> | |
<label for="tag" class="form-label"> | |
<i class="fas fa-filter me-2"></i>Filter by hashtag | |
</label> | |
<select class="form-select" id="tag" name="tag"> | |
<option value="">All hashtags</option> | |
{% for tag in all_hashtags %} | |
<option value="{{ tag }}" {% if current_tag == tag %}selected{% endif %}>{{ tag }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
</form> | |
</div> | |
<!-- Image Gallery --> | |
<div id="gallery"> | |
{% if not uploaded_images %} | |
<div class="empty-state"> | |
<i class="fas fa-images"></i> | |
<h4>No images yet</h4> | |
<p>Upload your first image to get started!</p> | |
</div> | |
{% else %} | |
<div class="row" id="imageGrid"> | |
{# First, render new images #} | |
{% for image in uploaded_images %} | |
{% if image.is_new %} | |
<div class="image-item col-md-4"> | |
<div class="card image-card"> | |
<a href="/view/{{ image.name }}" class="card-link"> | |
<span class="new-badge">NEW</span> | |
<button class="delete-icon delete-btn" data-filename="{{ image.name }}" title="Delete image"> | |
<i class="fas fa-trash-alt"></i> | |
</button> | |
<img src="{{ image.url }}" class="card-img-top image-preview" alt="{{ image.original_filename }}"> | |
<div class="card-body"> | |
<h5 class="card-title text-truncate" title="{{ image.original_filename }}">{{ image.original_filename }}</h5> | |
<div class="hashtags mb-3"> | |
{% for tag in image.hashtags %} | |
<a href="/?tag={{ tag }}" class="hashtag">#{{ tag }}</a> | |
{% endfor %} | |
</div> | |
</div> | |
</a> | |
</div> | |
</div> | |
{% endif %} | |
{% endfor %} | |
{# Then, render viewed images #} | |
{% for image in uploaded_images %} | |
{% if not image.is_new %} | |
<div class="image-item col-md-4"> | |
<div class="card image-card"> | |
<a href="/view/{{ image.name }}" class="card-link"> | |
<button class="delete-icon delete-btn" data-filename="{{ image.name }}" title="Delete image"> | |
<i class="fas fa-trash-alt"></i> | |
</button> | |
<img src="{{ image.url }}" class="card-img-top image-preview" alt="{{ image.original_filename }}"> | |
<div class="card-body"> | |
<h5 class="card-title text-truncate" title="{{ image.original_filename }}">{{ image.original_filename }}</h5> | |
<div class="hashtags mb-3"> | |
{% for tag in image.hashtags %} | |
<a href="/?tag={{ tag }}" class="hashtag">#{{ tag }}</a> | |
{% endfor %} | |
</div> | |
</div> | |
</a> | |
</div> | |
</div> | |
{% endif %} | |
{% endfor %} | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
</div> | |
<!-- Footer --> | |
<footer class="site-footer"> | |
<div class="container"> | |
<p class="mb-0">©2025 Detomo. All rights reserved</p> | |
</div> | |
</footer> | |
<!-- Toast container for notifications --> | |
<div class="toast-container"> | |
<div id="uploadSuccessToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> | |
<div class="toast-header bg-success text-white"> | |
<i class="fas fa-check-circle me-2"></i> | |
<strong class="me-auto">Success</strong> | |
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> | |
</div> | |
<div class="toast-body" id="toastMessage"> | |
Images uploaded successfully! | |
</div> | |
</div> | |
</div> | |
<!-- Modal for Delete Confirmation --> | |
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div class="modal-header border-0"> | |
<h5 class="modal-title" id="deleteConfirmModalLabel">Confirm Deletion</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body text-center py-4"> | |
<div class="mb-4"> | |
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 3.5rem;"></i> | |
</div> | |
<h5 class="mb-3">Are you sure you want to delete this image?</h5> | |
<p class="text-muted mb-0">This action cannot be undone.</p> | |
</div> | |
<div class="modal-footer border-0 justify-content-center"> | |
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal"> | |
<i class="fas fa-times me-2"></i>Cancel | |
</button> | |
<button type="button" class="btn btn-danger px-4" id="confirmDeleteBtn"> | |
<i class="fas fa-trash-alt me-2"></i>Delete | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modal for No Files Selected --> | |
<div class="modal fade" id="noFilesModal" tabindex="-1" aria-labelledby="noFilesModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div class="modal-header border-0"> | |
<h5 class="modal-title" id="noFilesModalLabel">No Files Selected</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body text-center py-4"> | |
<div class="mb-4"> | |
<i class="fas fa-images text-warning" style="font-size: 3.5rem;"></i> | |
</div> | |
<h5 class="mb-2">Please select at least one image file</h5> | |
<p class="text-muted">You need to browse and select images before uploading.</p> | |
</div> | |
<div class="modal-footer border-0 justify-content-center"> | |
<button type="button" class="btn btn-primary px-4" data-bs-dismiss="modal"> | |
<i class="fas fa-check me-2"></i>OK | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modal for Duplicate Filename Confirmation --> | |
<div class="modal fade" id="duplicateFilesModal" tabindex="-1" aria-labelledby="duplicateFilesModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-dialog-centered modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header border-0"> | |
<h5 class="modal-title" id="duplicateFilesModalLabel">Duplicate Filenames Detected</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body py-4"> | |
<div class="text-center mb-4"> | |
<i class="fas fa-exclamation-circle text-warning" style="font-size: 3.5rem;"></i> | |
<h5 class="mt-3 mb-4">Some files have the same names as existing images</h5> | |
<p class="text-muted mb-4">Please confirm if you want to replace the existing images with the new ones</p> | |
</div> | |
<div class="table-responsive"> | |
<table class="table table-borderless align-middle" id="duplicateFilesTable"> | |
<thead class="table-light"> | |
<tr> | |
<th>Replace</th> | |
<th>New Image</th> | |
<th>Will Replace</th> | |
</tr> | |
</thead> | |
<tbody> | |
<!-- Populated dynamically --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div class="modal-footer border-0 justify-content-center"> | |
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal"> | |
<i class="fas fa-times me-2"></i>Cancel Upload | |
</button> | |
<button type="button" class="btn btn-primary px-4" id="confirmReplaceBtn"> | |
<i class="fas fa-check me-2"></i>Continue With Selected Replacements | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const uploadForm = document.getElementById('uploadForm'); | |
const uploadProgress = document.getElementById('uploadProgress'); | |
const progressBar = uploadProgress.querySelector('.progress-bar'); | |
const toast = new bootstrap.Toast(document.getElementById('uploadSuccessToast'), { | |
delay: 3000 | |
}); | |
// Store files and hashtags for potential reuse if duplicates are found | |
let currentFiles = null; | |
let currentHashtags = ''; | |
let duplicateFiles = []; | |
// Instant search functionality | |
const searchInput = document.getElementById('search'); | |
const tagSelect = document.getElementById('tag'); | |
const searchForm = document.getElementById('searchForm'); | |
// Handle search input changes | |
searchInput.addEventListener('input', function() { | |
// Add small delay to prevent too many requests while typing | |
clearTimeout(searchInput.timer); | |
searchInput.timer = setTimeout(() => { | |
searchForm.submit(); | |
}, 500); // Wait 500ms after typing stops | |
}); | |
// Handle tag selection changes | |
tagSelect.addEventListener('change', function() { | |
searchForm.submit(); | |
}); | |
// Handle layout controls | |
const layoutControls = document.querySelectorAll('input[name="layout"]'); | |
const imageGrid = document.getElementById('imageGrid'); | |
const imageItems = document.querySelectorAll('.image-item'); | |
function updateLayout(columns) { | |
// First, remove all column classes from all items | |
imageItems.forEach(item => { | |
item.classList.remove('col-md-3', 'col-md-4', 'col-md-6', 'col-5-layout'); | |
}); | |
// Then apply the new column class | |
imageItems.forEach(item => { | |
if (columns === 3) { | |
item.classList.add('col-md-4'); // 3 per row (4/12 width) | |
} else if (columns === 4) { | |
item.classList.add('col-md-3'); // 4 per row (3/12 width) | |
} else if (columns === 5) { | |
item.classList.add('col-5-layout'); // Custom 5 per row | |
} | |
}); | |
// Save the preference to localStorage | |
localStorage.setItem('preferredLayout', columns); | |
} | |
// Initialize layout based on localStorage or default to 3 columns | |
const savedLayout = localStorage.getItem('preferredLayout') || '3'; | |
const initialLayout = parseInt(savedLayout); | |
// Make sure the correct radio button is checked | |
const layoutButton = document.getElementById(`layout${initialLayout}`); | |
if (layoutButton) { | |
layoutButton.checked = true; | |
updateLayout(initialLayout); | |
} else { | |
// Fallback to layout3 if saved layout is invalid | |
document.getElementById('layout3').checked = true; | |
updateLayout(3); | |
} | |
// Add event listeners to layout controls | |
layoutControls.forEach(control => { | |
control.addEventListener('change', function() { | |
let columns = 3; // Default | |
if (this.id === 'layout3') columns = 3; | |
else if (this.id === 'layout4') columns = 4; | |
else if (this.id === 'layout5') columns = 5; | |
updateLayout(columns); | |
}); | |
}); | |
// Handle form submission | |
uploadForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const filesInput = document.getElementById('files'); | |
if (!filesInput.files.length) { | |
// Show the no files modal instead of alert | |
const noFilesModal = new bootstrap.Modal(document.getElementById('noFilesModal')); | |
noFilesModal.show(); | |
return; | |
} | |
// Store current files and hashtags in case we need to handle duplicates | |
currentFiles = filesInput.files; | |
currentHashtags = document.getElementById('hashtags').value; | |
const formData = new FormData(); | |
// Add all files | |
for (let i = 0; i < filesInput.files.length; i++) { | |
formData.append('files', filesInput.files[i]); | |
} | |
// Add hashtags | |
formData.append('hashtags', currentHashtags); | |
// Show progress | |
uploadProgress.classList.remove('hidden'); | |
progressBar.style.width = '0%'; | |
const xhr = new XMLHttpRequest(); | |
xhr.upload.addEventListener('progress', function(e) { | |
if (e.lengthComputable) { | |
const percentComplete = (e.loaded / e.total) * 100; | |
progressBar.style.width = percentComplete + '%'; | |
} | |
}); | |
xhr.addEventListener('load', function() { | |
// Hide progress bar | |
uploadProgress.classList.add('hidden'); | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
// Check if we need to handle duplicates | |
if (response.success === false && response.action_required === 'confirm_replace') { | |
// Store the duplicates | |
duplicateFiles = response.duplicates; | |
// Populate the duplicate files table | |
const tableBody = document.querySelector('#duplicateFilesTable tbody'); | |
tableBody.innerHTML = ''; | |
duplicateFiles.forEach(function(file, index) { | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td> | |
<div class="form-check"> | |
<input class="form-check-input replace-checkbox" type="checkbox" | |
value="${file.existing_file}" | |
id="replace-check-${index}" | |
data-original="${file.original_name}" checked> | |
</div> | |
</td> | |
<td><strong>${file.new_file}</strong></td> | |
<td>${file.existing_file}</td> | |
`; | |
tableBody.appendChild(row); | |
}); | |
// Show the duplicate files modal | |
const duplicateModal = new bootstrap.Modal(document.getElementById('duplicateFilesModal')); | |
duplicateModal.show(); | |
return; | |
} | |
// Normal successful upload | |
handleSuccessfulUpload(response); | |
} else { | |
alert('Upload failed. Please try again.'); | |
} | |
}); | |
xhr.addEventListener('error', function() { | |
alert('Upload failed. Please try again.'); | |
uploadProgress.classList.add('hidden'); | |
}); | |
xhr.open('POST', '/upload/'); | |
xhr.send(formData); | |
}); | |
// Handle confirmation of file replacements | |
document.getElementById('confirmReplaceBtn').addEventListener('click', function() { | |
// Get selected replacements | |
const checkboxes = document.querySelectorAll('.replace-checkbox:checked'); | |
const filesToReplace = Array.from(checkboxes).map(function(checkbox) { | |
return { | |
existing_file: checkbox.value, | |
original_name: checkbox.dataset.original | |
}; | |
}); | |
// Hide the modal | |
const duplicateModal = bootstrap.Modal.getInstance(document.getElementById('duplicateFilesModal')); | |
duplicateModal.hide(); | |
// Create a new FormData with the current files | |
const formData = new FormData(); | |
// Add current files (those that we stored earlier) | |
for (let i = 0; i < currentFiles.length; i++) { | |
formData.append('files', currentFiles[i]); | |
} | |
// Add hashtags | |
formData.append('hashtags', currentHashtags); | |
// Add replacement information | |
formData.append('replace_files', JSON.stringify(filesToReplace)); | |
// Show progress again | |
uploadProgress.classList.remove('hidden'); | |
progressBar.style.width = '0%'; | |
// Send the request to the replacement endpoint | |
const xhr = new XMLHttpRequest(); | |
xhr.upload.addEventListener('progress', function(e) { | |
if (e.lengthComputable) { | |
const percentComplete = (e.loaded / e.total) * 100; | |
progressBar.style.width = percentComplete + '%'; | |
} | |
}); | |
xhr.addEventListener('load', function() { | |
// Hide progress bar | |
uploadProgress.classList.add('hidden'); | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
handleSuccessfulUpload(response); | |
} else { | |
alert('Upload failed. Please try again.'); | |
} | |
}); | |
xhr.addEventListener('error', function() { | |
alert('Upload failed. Please try again.'); | |
uploadProgress.classList.add('hidden'); | |
}); | |
xhr.open('POST', '/upload-with-replace/'); | |
xhr.send(formData); | |
}); | |
// Function to handle successful upload | |
function handleSuccessfulUpload(response) { | |
// Check if multiple files or single file | |
let uploadCount = 1; | |
if (response.files) { | |
uploadCount = response.uploaded_count; | |
} | |
// Show success toast | |
document.getElementById('toastMessage').textContent = | |
`Successfully uploaded ${uploadCount} image${uploadCount > 1 ? 's' : ''}!`; | |
toast.show(); | |
// Reset form for next upload | |
uploadForm.reset(); | |
currentFiles = null; | |
currentHashtags = ''; | |
// Scroll to gallery section | |
document.getElementById('gallery').scrollIntoView({ behavior: 'smooth' }); | |
// Refresh the page after a short delay to show the new images | |
setTimeout(() => { | |
window.location.reload(); | |
}, 1000); | |
} | |
// Handle delete buttons to stop event propagation | |
document.querySelectorAll('.delete-btn').forEach(function(btn) { | |
btn.addEventListener('click', function (e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
const filename = this.dataset.filename; | |
// Store the filename for later use | |
document.getElementById('confirmDeleteBtn').dataset.filename = filename; | |
// Show the delete confirmation modal | |
const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); | |
deleteModal.show(); | |
}); | |
}); | |
// Handle delete confirmation | |
document.getElementById('confirmDeleteBtn').addEventListener('click', function() { | |
const filename = this.dataset.filename; | |
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')); | |
fetch(`/delete/${filename}`, { | |
method: 'DELETE' | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
deleteModal.hide(); | |
window.location.reload(); | |
} else { | |
alert('Failed to delete the image.'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('An error occurred while deleting the image.'); | |
}); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |