feat: improve modal sidebar hierarchy, collapse add-to-series, in-context feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:51:03 -04:00
parent 9d7d76953e
commit 7cae463fe8
3 changed files with 177 additions and 68 deletions
+64 -37
View File
@@ -28,8 +28,22 @@ document.addEventListener('DOMContentLoaded', () => {
const addSeriesPageNumber = document.getElementById('addSeriesPageNumber');
const addToSeriesBtn = document.getElementById('addToSeriesBtn');
const addToSeriesStatus = document.getElementById('addToSeriesStatus');
const addToSeriesToggle = document.getElementById('addToSeriesToggle');
const addToSeriesBody = document.getElementById('addToSeriesBody');
let seriesTagsLoaded = false;
// In-context feedback: show a brief message next to an element, auto-fades
function showFeedback(el, message, isError = false) {
if (!el) return;
el.textContent = message;
el.style.color = isError ? 'var(--flash-danger)' : 'var(--flash-success)';
el.classList.add('visible');
clearTimeout(el._feedbackTimeout);
el._feedbackTimeout = setTimeout(() => {
el.classList.remove('visible');
}, 1500);
}
// Autocomplete state
let autocompleteItems = [];
let autocompleteSelectedIndex = -1;
@@ -133,12 +147,15 @@ document.addEventListener('DOMContentLoaded', () => {
function hideSeriesInfo() {
if (seriesInfoEl) seriesInfoEl.style.display = 'none';
currentSeriesPageId = null;
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); }
}
function hideAddToSeries() {
if (addToSeriesEl) addToSeriesEl.style.display = 'none';
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); }
// Reset toggle state
if (addToSeriesToggle) addToSeriesToggle.classList.remove('open');
if (addToSeriesBody) addToSeriesBody.style.display = 'none';
}
async function loadSeriesTags() {
@@ -169,25 +186,10 @@ document.addEventListener('DOMContentLoaded', () => {
function showAddToSeries(seriesPage = null) {
if (!addToSeriesEl) return;
loadSeriesTags().then(() => {
// Pre-fill if image is already in a series
if (seriesPage && seriesSelect) {
seriesSelect.value = seriesPage.series_tag_id;
if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number;
if (addToSeriesBtn) {
addToSeriesBtn.disabled = false;
addToSeriesBtn.textContent = 'Update';
}
} else {
if (seriesSelect) seriesSelect.value = '';
if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
if (addToSeriesBtn) {
addToSeriesBtn.disabled = true;
addToSeriesBtn.textContent = 'Add';
}
}
});
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
// Store pending pre-fill data for when the toggle is opened
addToSeriesEl._pendingSeriesPage = seriesPage || null;
if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); }
// Show the outer container; inner body stays hidden until toggle click
addToSeriesEl.style.display = 'block';
}
@@ -200,7 +202,7 @@ document.addEventListener('DOMContentLoaded', () => {
const isUpdate = addToSeriesBtn?.textContent === 'Update';
if (addToSeriesBtn) addToSeriesBtn.disabled = true;
if (addToSeriesStatus) addToSeriesStatus.textContent = isUpdate ? 'Updating...' : 'Adding...';
showFeedback(addToSeriesStatus, isUpdate ? 'Updating' : 'Adding…');
try {
const r = await fetch(`/api/image/${imageId}/add-to-series`, {
@@ -211,18 +213,15 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
if (addToSeriesStatus) {
addToSeriesStatus.textContent = '✓ Saved';
setTimeout(() => { addToSeriesStatus.textContent = ''; }, 2000);
}
showFeedback(addToSeriesStatus, 'Saved');
// Fully reload series info to show updated management interface
await loadSeriesInfo(imageId);
} else {
if (addToSeriesStatus) addToSeriesStatus.textContent = j.error || 'Error';
showFeedback(addToSeriesStatus, j.error || 'Error', true);
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
}
} catch (e) {
if (addToSeriesStatus) addToSeriesStatus.textContent = 'Error';
showFeedback(addToSeriesStatus, 'Error', true);
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
}
}
@@ -245,7 +244,7 @@ document.addEventListener('DOMContentLoaded', () => {
pageNumberInput.value = seriesPage.page_number;
}
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); }
seriesInfoEl.style.display = 'block';
}
@@ -279,12 +278,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (!currentSeriesPageId || !pageNumberInput) return;
const newPageNumber = parseInt(pageNumberInput.value);
if (isNaN(newPageNumber) || newPageNumber < 1) {
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Invalid';
showFeedback(pageUpdateStatus, 'Invalid', true);
return;
}
if (updatePageBtn) updatePageBtn.disabled = true;
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Saving...';
showFeedback(pageUpdateStatus, 'Saving…');
try {
const fd = new FormData();
@@ -295,15 +294,12 @@ document.addEventListener('DOMContentLoaded', () => {
});
const j = await r.json();
if (j.ok) {
if (pageUpdateStatus) {
pageUpdateStatus.textContent = '✓';
setTimeout(() => { pageUpdateStatus.textContent = ''; }, 2000);
}
showFeedback(pageUpdateStatus, 'Saved');
} else {
if (pageUpdateStatus) pageUpdateStatus.textContent = j.error || 'Error';
showFeedback(pageUpdateStatus, j.error || 'Error', true);
}
} catch (e) {
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Error';
showFeedback(pageUpdateStatus, 'Error', true);
} finally {
if (updatePageBtn) updatePageBtn.disabled = false;
}
@@ -354,6 +350,35 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Toggle "Add to Series" collapse
if (addToSeriesToggle) {
addToSeriesToggle.addEventListener('click', () => {
const isOpen = addToSeriesToggle.classList.toggle('open');
addToSeriesBody.style.display = isOpen ? 'block' : 'none';
// Load series options on first open, then apply any pending pre-fill
if (isOpen) {
const seriesPage = addToSeriesEl._pendingSeriesPage;
loadSeriesTags().then(() => {
if (seriesPage && seriesSelect) {
seriesSelect.value = seriesPage.series_tag_id;
if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number;
if (addToSeriesBtn) {
addToSeriesBtn.disabled = false;
addToSeriesBtn.textContent = 'Update';
}
} else {
if (seriesSelect) seriesSelect.value = '';
if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
if (addToSeriesBtn) {
addToSeriesBtn.disabled = true;
addToSeriesBtn.textContent = 'Add';
}
}
});
}
});
}
async function addTag(imageId, name) {
const fd = new FormData();
fd.append('name', name);
@@ -361,6 +386,7 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
await loadTags(imageId);
showFeedback(document.getElementById('tagActionFeedback'), 'Tag added');
return true;
}
return false;
@@ -372,6 +398,7 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
await loadTags(imageId);
showFeedback(document.getElementById('tagActionFeedback'), 'Tag removed');
return true;
}
return false;