717 lines
28 KiB
JavaScript
717 lines
28 KiB
JavaScript
import { showToast, addRippleEffect } from './utils.js';
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const selectionGrid = document.getElementById('selection-grid');
|
|
const filterButtons = document.querySelectorAll('.filter-buttons .filter-btn');
|
|
const orientationButtons = document.querySelectorAll('.orientation-filters .filter-btn');
|
|
const resolutionFilter = document.getElementById('resolution-filter');
|
|
const selectAllBtn = document.getElementById('select-all');
|
|
const deselectAllBtn = document.getElementById('deselect-all');
|
|
const downloadSelectedBtn = document.getElementById('download-selected');
|
|
const filteredCountEl = document.getElementById('filtered-count');
|
|
|
|
// Add ripple effect to all action buttons
|
|
document.querySelectorAll('.action-btn').forEach(button => {
|
|
addRippleEffect(button);
|
|
});
|
|
|
|
const actionModal = document.getElementById('action-modal');
|
|
const closeActionModal = document.getElementById('close-action-modal');
|
|
const actionButtons = actionModal.querySelectorAll('.action-btn');
|
|
const modalPreviewImg = document.getElementById('modal-preview-img');
|
|
const modalMessage = document.getElementById('modal-message');
|
|
|
|
const resetBtn = document.getElementById('reset-db');
|
|
const resetModal = document.getElementById('reset-modal');
|
|
const confirmResetBtn = document.getElementById('confirm-reset');
|
|
const cancelResetBtn = document.getElementById('cancel-reset');
|
|
const resetMessage = document.getElementById('reset-message');
|
|
|
|
let currentFilter = 'all';
|
|
let currentOrientation = 'all';
|
|
let currentResolution = 'all';
|
|
let selectedItems = [];
|
|
let currentSelectionId = null;
|
|
let allSelections = [];
|
|
|
|
// Enhanced loading animation
|
|
function showLoading() {
|
|
selectionGrid.classList.add('loading');
|
|
selectionGrid.innerHTML = `
|
|
<div class="loading-container">
|
|
<div class="loading-spinner"></div>
|
|
<div class="loading-text">Loading selections...</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function hideLoading() {
|
|
selectionGrid.classList.remove('loading');
|
|
}
|
|
|
|
const loadSelections = () => {
|
|
showLoading();
|
|
|
|
fetch('/selections')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
hideLoading();
|
|
|
|
if (data.selections && data.selections.length > 0) {
|
|
allSelections = data.selections;
|
|
populateResolutionFilter(data.selections);
|
|
renderSelections(data.selections);
|
|
|
|
// Show stats
|
|
const stats = calculateStats(data.selections);
|
|
updateStats(stats);
|
|
|
|
showToast(`Loaded ${data.selections.length} images`);
|
|
} else {
|
|
selectionGrid.innerHTML = `
|
|
<div class="no-selections">
|
|
<i class="fa-solid fa-image-slash fa-3x"></i>
|
|
<p>No selections found</p>
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
hideLoading();
|
|
console.error('Error loading selections:', error);
|
|
selectionGrid.innerHTML = `
|
|
<div class="error">
|
|
<i class="fa-solid fa-triangle-exclamation fa-3x"></i>
|
|
<p>Error loading selections: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
showToast('Error loading selections', 'error');
|
|
});
|
|
};
|
|
|
|
const calculateStats = (selections) => {
|
|
const stats = {
|
|
total: selections.length,
|
|
byAction: {
|
|
left: selections.filter(s => s.action === 'left').length,
|
|
right: selections.filter(s => s.action === 'right').length,
|
|
up: selections.filter(s => s.action === 'up').length,
|
|
down: selections.filter(s => s.action === 'down').length
|
|
},
|
|
byOrientation: {
|
|
portrait: selections.filter(s => s.orientation === 'portrait').length,
|
|
landscape: selections.filter(s => s.orientation === 'landscape').length,
|
|
square: selections.filter(s => s.orientation === 'square').length
|
|
}
|
|
};
|
|
|
|
return stats;
|
|
};
|
|
|
|
const updateStats = (stats) => {
|
|
const statsContainer = document.getElementById('stats-container');
|
|
if (!statsContainer) return;
|
|
|
|
statsContainer.innerHTML = `
|
|
<div class="stat-item">
|
|
<span class="stat-value">${stats.total}</span>
|
|
<span class="stat-label">Total Images</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${stats.byAction.left}</span>
|
|
<span class="stat-label">Discarded</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${stats.byAction.right}</span>
|
|
<span class="stat-label">Kept</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${stats.byAction.up}</span>
|
|
<span class="stat-label">Favorited</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${stats.byAction.down}</span>
|
|
<span class="stat-label">For Review</span>
|
|
</div>
|
|
`;
|
|
|
|
// Add animation to stats
|
|
const statItems = statsContainer.querySelectorAll('.stat-item');
|
|
statItems.forEach((item, index) => {
|
|
item.style.opacity = 0;
|
|
item.style.transform = 'translateY(20px)';
|
|
|
|
setTimeout(() => {
|
|
item.style.transition = 'all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
|
|
item.style.opacity = 1;
|
|
item.style.transform = 'translateY(0)';
|
|
}, 100 * index);
|
|
});
|
|
};
|
|
|
|
const populateResolutionFilter = (selections) => {
|
|
const resolutions = [...new Set(selections.map(s => s.resolution))].sort();
|
|
resolutionFilter.innerHTML = '<option value="all">All Resolutions</option>';
|
|
resolutions.forEach(resolution => {
|
|
const option = document.createElement('option');
|
|
option.value = resolution;
|
|
option.textContent = resolution;
|
|
resolutionFilter.appendChild(option);
|
|
});
|
|
};
|
|
|
|
const renderSelections = (selections) => {
|
|
selectionGrid.innerHTML = '';
|
|
|
|
const filteredSelections = selections.filter(s =>
|
|
(currentFilter === 'all' || s.action === currentFilter) &&
|
|
(currentOrientation === 'all' || s.orientation === currentOrientation) &&
|
|
(currentResolution === 'all' || s.resolution === currentResolution)
|
|
);
|
|
|
|
if (filteredSelections.length === 0) {
|
|
selectionGrid.innerHTML = `
|
|
<div class="no-selections">
|
|
<i class="fa-solid fa-filter-circle-xmark fa-3x"></i>
|
|
<p>No selections match the current filters</p>
|
|
</div>
|
|
`;
|
|
filteredCountEl.textContent = `0 images match your filters (out of ${selections.length} total)`;
|
|
return;
|
|
}
|
|
|
|
// Update the filtered count
|
|
if (filteredCountEl) {
|
|
filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${selections.length} images`;
|
|
}
|
|
|
|
// Create a document fragment for better performance
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
filteredSelections.forEach((selection, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'selection-item';
|
|
item.dataset.id = selection.id;
|
|
|
|
// Add animation delay for staggered appearance
|
|
const delay = Math.min(0.05 * index, 1);
|
|
item.style.animationDelay = `${delay}s`;
|
|
|
|
const actionName = getActionName(selection.action);
|
|
const actionIcon = getActionIcon(selection.action);
|
|
|
|
item.innerHTML = `
|
|
<div class="selection-checkbox-container">
|
|
<input type="checkbox" class="selection-checkbox" id="checkbox-${selection.id}">
|
|
<label for="checkbox-${selection.id}" class="checkbox-label"></label>
|
|
</div>
|
|
<div class="image-container">
|
|
<img src="${selection.image_path}" alt="${actionName} image" loading="lazy">
|
|
</div>
|
|
<div class="selection-action action-${selection.action}">
|
|
<i class="fa-solid ${actionIcon}"></i> ${actionName}
|
|
</div>
|
|
<div class="selection-info">
|
|
<p class="filename">${selection.image_path.split('/').pop()}</p>
|
|
<p class="resolution">Resolution: ${selection.resolution}</p>
|
|
<p class="timestamp">Date: ${formatDate(selection.timestamp)}</p>
|
|
</div>
|
|
<div class="selection-controls">
|
|
<button class="control-btn edit-btn" title="Change action">
|
|
<i class="fa-solid fa-pen-to-square"></i>
|
|
</button>
|
|
<button class="control-btn view-btn" title="View full size">
|
|
<i class="fa-solid fa-expand"></i>
|
|
</button>
|
|
<button class="control-btn delete-btn" title="Remove">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add the item to the fragment
|
|
fragment.appendChild(item);
|
|
});
|
|
|
|
// Append all items at once
|
|
selectionGrid.appendChild(fragment);
|
|
|
|
// Add fade-in animation class
|
|
setTimeout(() => {
|
|
selectionGrid.classList.add('loaded');
|
|
}, 10);
|
|
};
|
|
|
|
const formatDate = (timestamp) => {
|
|
if (!timestamp) return 'Unknown';
|
|
const date = new Date(timestamp * 1000);
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
const getActionName = (action) => {
|
|
const names = { left: 'Discard', right: 'Keep', up: 'Favorite', down: 'Review' };
|
|
return names[action] || action;
|
|
};
|
|
|
|
const getActionIcon = (action) => {
|
|
const icons = { left: 'fa-trash', right: 'fa-folder-plus', up: 'fa-star', down: 'fa-clock' };
|
|
return icons[action] || 'fa-question';
|
|
};
|
|
|
|
const updateDownloadButton = () => {
|
|
downloadSelectedBtn.disabled = selectedItems.length === 0;
|
|
downloadSelectedBtn.querySelector('.label').textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download';
|
|
};
|
|
|
|
// Enhanced selection item click handler
|
|
selectionGrid.addEventListener('click', (e) => {
|
|
const target = e.target;
|
|
const selectionItem = target.closest('.selection-item');
|
|
if (!selectionItem) return;
|
|
|
|
const selectionId = selectionItem.dataset.id;
|
|
const selection = {
|
|
id: selectionId,
|
|
image_path: selectionItem.querySelector('img').src,
|
|
action: selectionItem.querySelector('.selection-action').classList[1].replace('action-', '')
|
|
};
|
|
|
|
// Handle checkbox click
|
|
if (target.classList.contains('selection-checkbox') || target.classList.contains('checkbox-label')) {
|
|
const checkbox = selectionItem.querySelector('.selection-checkbox');
|
|
const isChecked = checkbox.checked;
|
|
|
|
if (isChecked) {
|
|
selectionItem.classList.add('selected');
|
|
selectedItems.push(selection);
|
|
showToast(`Selected image (${selectedItems.length} total)`);
|
|
} else {
|
|
selectionItem.classList.remove('selected');
|
|
selectedItems = selectedItems.filter(item => item.id !== selectionId);
|
|
showToast(`Deselected image (${selectedItems.length} total)`);
|
|
}
|
|
updateDownloadButton();
|
|
}
|
|
// Handle edit button click
|
|
else if (target.classList.contains('edit-btn') || target.closest('.edit-btn')) {
|
|
currentSelectionId = selectionId;
|
|
modalPreviewImg.src = selection.image_path;
|
|
|
|
// Highlight the current action in the modal
|
|
actionButtons.forEach(btn => {
|
|
btn.classList.remove('active');
|
|
if (btn.dataset.action === selection.action) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
actionModal.style.display = 'flex';
|
|
setTimeout(() => {
|
|
actionModal.classList.add('show');
|
|
}, 10);
|
|
}
|
|
// Handle view button click
|
|
else if (target.classList.contains('view-btn') || target.closest('.view-btn')) {
|
|
// Open image in fullscreen modal
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal fullscreen-modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<span class="close-modal">×</span>
|
|
<img src="${selection.image_path}" alt="Full size image">
|
|
<div class="modal-info">
|
|
<p>Action: <span class="action-${selection.action}">${getActionName(selection.action)}</span></p>
|
|
<p>Filename: ${selection.image_path.split('/').pop()}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
// Show the modal with animation
|
|
setTimeout(() => {
|
|
modal.style.display = 'flex';
|
|
setTimeout(() => modal.classList.add('show'), 10);
|
|
}, 10);
|
|
|
|
// Add close functionality
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal || e.target.classList.contains('close-modal')) {
|
|
modal.classList.remove('show');
|
|
setTimeout(() => {
|
|
modal.style.display = 'none';
|
|
modal.remove();
|
|
}, 400);
|
|
}
|
|
});
|
|
}
|
|
// Handle delete button click
|
|
else if (target.classList.contains('delete-btn') || target.closest('.delete-btn')) {
|
|
// Create a confirmation dialog
|
|
const confirmDialog = document.createElement('div');
|
|
confirmDialog.className = 'modal confirmation-modal';
|
|
confirmDialog.innerHTML = `
|
|
<div class="modal-content">
|
|
<h3>Confirm Deletion</h3>
|
|
<p>Are you sure you want to delete this selection?</p>
|
|
<div class="confirmation-buttons">
|
|
<button class="action-btn confirm-delete-btn">Yes, Delete</button>
|
|
<button class="action-btn cancel-delete-btn">Cancel</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(confirmDialog);
|
|
|
|
// Show the dialog with animation
|
|
setTimeout(() => {
|
|
confirmDialog.style.display = 'flex';
|
|
setTimeout(() => confirmDialog.classList.add('show'), 10);
|
|
}, 10);
|
|
|
|
// Add button functionality
|
|
confirmDialog.querySelector('.confirm-delete-btn').addEventListener('click', () => {
|
|
// Add animation before removing
|
|
selectionItem.classList.add('removing');
|
|
|
|
// Close the dialog
|
|
confirmDialog.classList.remove('show');
|
|
setTimeout(() => {
|
|
confirmDialog.style.display = 'none';
|
|
confirmDialog.remove();
|
|
}, 400);
|
|
|
|
// Simulate delete functionality (replace with actual API call)
|
|
setTimeout(() => {
|
|
selectionItem.remove();
|
|
showToast('Selection removed');
|
|
|
|
// Update selected items if this was selected
|
|
if (selectedItems.some(item => item.id === selectionId)) {
|
|
selectedItems = selectedItems.filter(item => item.id !== selectionId);
|
|
updateDownloadButton();
|
|
}
|
|
|
|
// Update allSelections array
|
|
allSelections = allSelections.filter(s => s.id !== selectionId);
|
|
|
|
// Update stats
|
|
const stats = calculateStats(allSelections);
|
|
updateStats(stats);
|
|
|
|
// Update filtered count
|
|
if (filteredCountEl) {
|
|
const filteredSelections = allSelections.filter(s =>
|
|
(currentFilter === 'all' || s.action === currentFilter) &&
|
|
(currentOrientation === 'all' || s.orientation === currentOrientation) &&
|
|
(currentResolution === 'all' || s.resolution === currentResolution)
|
|
);
|
|
filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${allSelections.length} images`;
|
|
}
|
|
}, 300);
|
|
});
|
|
|
|
confirmDialog.querySelector('.cancel-delete-btn').addEventListener('click', () => {
|
|
confirmDialog.classList.remove('show');
|
|
setTimeout(() => {
|
|
confirmDialog.style.display = 'none';
|
|
confirmDialog.remove();
|
|
}, 400);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Enhanced filter button click handlers
|
|
filterButtons.forEach(button => button.addEventListener('click', function() {
|
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentFilter = this.dataset.filter;
|
|
|
|
// Apply filter animation
|
|
selectionGrid.classList.add('filtering');
|
|
setTimeout(() => {
|
|
renderSelections(allSelections);
|
|
selectionGrid.classList.remove('filtering');
|
|
}, 300);
|
|
}));
|
|
|
|
orientationButtons.forEach(button => button.addEventListener('click', function() {
|
|
orientationButtons.forEach(btn => btn.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentOrientation = this.dataset.orientation;
|
|
|
|
// Apply filter animation
|
|
selectionGrid.classList.add('filtering');
|
|
setTimeout(() => {
|
|
renderSelections(allSelections);
|
|
selectionGrid.classList.remove('filtering');
|
|
}, 300);
|
|
}));
|
|
|
|
resolutionFilter.addEventListener('change', function() {
|
|
currentResolution = this.value;
|
|
|
|
// Apply filter animation
|
|
selectionGrid.classList.add('filtering');
|
|
setTimeout(() => {
|
|
renderSelections(allSelections);
|
|
selectionGrid.classList.remove('filtering');
|
|
}, 300);
|
|
});
|
|
|
|
// Enhanced select/deselect all functionality
|
|
selectAllBtn.addEventListener('click', () => {
|
|
const checkboxes = document.querySelectorAll('.selection-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = true);
|
|
|
|
selectedItems = Array.from(document.querySelectorAll('.selection-item')).map(item => ({
|
|
id: item.dataset.id,
|
|
image_path: item.querySelector('img').src,
|
|
action: item.querySelector('.selection-action').classList[1].replace('action-', '')
|
|
}));
|
|
|
|
document.querySelectorAll('.selection-item').forEach(item => item.classList.add('selected'));
|
|
updateDownloadButton();
|
|
|
|
showToast(`Selected all ${checkboxes.length} visible images`);
|
|
});
|
|
|
|
deselectAllBtn.addEventListener('click', () => {
|
|
document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = false);
|
|
selectedItems = [];
|
|
document.querySelectorAll('.selection-item').forEach(item => item.classList.remove('selected'));
|
|
updateDownloadButton();
|
|
|
|
showToast('Deselected all images');
|
|
});
|
|
|
|
// Enhanced download functionality
|
|
downloadSelectedBtn.addEventListener('click', () => {
|
|
if (selectedItems.length === 0) return;
|
|
|
|
// Show loading toast
|
|
showToast(`Preparing ${selectedItems.length} images for download...`);
|
|
|
|
const paths = selectedItems.map(item => item.image_path);
|
|
const query = paths.map(p => `paths=${encodeURIComponent(p)}`).join('&');
|
|
|
|
// Add a small delay to show the loading toast
|
|
setTimeout(() => {
|
|
window.location.href = `/download-selected?${query}`;
|
|
}, 800);
|
|
});
|
|
|
|
// Enhanced modal functionality
|
|
closeActionModal.addEventListener('click', () => {
|
|
actionModal.classList.remove('show');
|
|
setTimeout(() => {
|
|
actionModal.style.display = 'none';
|
|
}, 400);
|
|
});
|
|
|
|
actionButtons.forEach(button => {
|
|
// Add ripple effect
|
|
addRippleEffect(button);
|
|
|
|
button.addEventListener('click', function() {
|
|
// Remove active class from all buttons
|
|
actionButtons.forEach(btn => btn.classList.remove('active'));
|
|
// Add active class to clicked button
|
|
this.classList.add('active');
|
|
|
|
const action = this.dataset.action;
|
|
|
|
// Show feedback
|
|
modalMessage.textContent = `Updating action to ${getActionName(action)}...`;
|
|
modalMessage.style.color = '#3498db';
|
|
|
|
// Simulate API call (replace with actual implementation)
|
|
setTimeout(() => {
|
|
modalMessage.textContent = `Action updated successfully!`;
|
|
modalMessage.style.color = '#2ecc71';
|
|
|
|
// Close modal after success
|
|
setTimeout(() => {
|
|
actionModal.classList.remove('show');
|
|
setTimeout(() => {
|
|
actionModal.style.display = 'none';
|
|
modalMessage.textContent = '';
|
|
}, 400);
|
|
|
|
// Update the UI to reflect the change
|
|
const selectionItem = document.querySelector(`.selection-item[data-id="${currentSelectionId}"]`);
|
|
if (selectionItem) {
|
|
const actionEl = selectionItem.querySelector('.selection-action');
|
|
const oldAction = actionEl.classList[1].replace('action-', '');
|
|
|
|
// Update the action element
|
|
actionEl.className = `selection-action action-${action}`;
|
|
actionEl.innerHTML = `<i class="fa-solid ${getActionIcon(action)}"></i> ${getActionName(action)}`;
|
|
|
|
// Update the selection in allSelections
|
|
const selectionIndex = allSelections.findIndex(s => s.id === currentSelectionId);
|
|
if (selectionIndex !== -1) {
|
|
allSelections[selectionIndex].action = action;
|
|
}
|
|
|
|
// Update stats
|
|
const stats = calculateStats(allSelections);
|
|
updateStats(stats);
|
|
|
|
// Show toast notification
|
|
showToast(`Updated to ${getActionName(action)}`);
|
|
}
|
|
}, 1000);
|
|
}, 800);
|
|
});
|
|
});
|
|
|
|
// Enhanced reset functionality
|
|
resetBtn.addEventListener('click', () => {
|
|
resetModal.style.display = 'flex';
|
|
setTimeout(() => {
|
|
resetModal.classList.add('show');
|
|
}, 10);
|
|
});
|
|
|
|
confirmResetBtn.addEventListener('click', () => {
|
|
// Show loading state
|
|
confirmResetBtn.disabled = true;
|
|
confirmResetBtn.textContent = 'Deleting...';
|
|
resetMessage.textContent = 'Deleting all selections...';
|
|
resetMessage.style.color = '#3498db';
|
|
|
|
// Simulate API call (replace with actual implementation)
|
|
setTimeout(() => {
|
|
resetMessage.textContent = 'All selections have been deleted successfully!';
|
|
resetMessage.style.color = '#2ecc71';
|
|
|
|
// Close modal after success
|
|
setTimeout(() => {
|
|
resetModal.classList.remove('show');
|
|
setTimeout(() => {
|
|
resetModal.style.display = 'none';
|
|
confirmResetBtn.disabled = false;
|
|
confirmResetBtn.textContent = 'Yes, Delete All';
|
|
resetMessage.textContent = '';
|
|
|
|
// Clear the grid and update state
|
|
selectionGrid.innerHTML = `
|
|
<div class="no-selections">
|
|
<i class="fa-solid fa-image-slash fa-3x"></i>
|
|
<p>No selections found</p>
|
|
</div>
|
|
`;
|
|
selectedItems = [];
|
|
allSelections = [];
|
|
updateDownloadButton();
|
|
|
|
// Update stats
|
|
const stats = calculateStats([]);
|
|
updateStats(stats);
|
|
|
|
// Show toast notification
|
|
showToast('All selections have been deleted');
|
|
}, 400);
|
|
}, 1000);
|
|
}, 1500);
|
|
});
|
|
|
|
cancelResetBtn.addEventListener('click', () => {
|
|
resetModal.classList.remove('show');
|
|
setTimeout(() => {
|
|
resetModal.style.display = 'none';
|
|
}, 400);
|
|
});
|
|
|
|
// Add filtering animation styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.selection-grid.filtering {
|
|
opacity: 0.6;
|
|
transform: scale(0.98);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.no-selections {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 50px;
|
|
color: #999;
|
|
text-align: center;
|
|
gap: 20px;
|
|
}
|
|
|
|
.loading-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 300px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.loading-text {
|
|
color: #3498db;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.checkbox-label {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
background-color: white;
|
|
border: 2px solid #ddd;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.checkbox-label:hover {
|
|
border-color: #3498db;
|
|
}
|
|
|
|
.selection-checkbox {
|
|
position: absolute;
|
|
opacity: 0;
|
|
}
|
|
|
|
.selection-checkbox:checked + .checkbox-label {
|
|
background-color: #3498db;
|
|
border-color: #3498db;
|
|
}
|
|
|
|
.selection-checkbox:checked + .checkbox-label::after {
|
|
content: '\\f00c';
|
|
font-family: 'Font Awesome 6 Free';
|
|
font-weight: 900;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.confirmation-buttons {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.confirm-delete-btn {
|
|
background: var(--gradient-danger);
|
|
}
|
|
|
|
.cancel-delete-btn {
|
|
background: var(--gradient-primary);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// Initialize by loading selections
|
|
loadSelections();
|
|
});
|