Compare commits
2 Commits
080eb2593d
...
d3484a4b50
Author | SHA1 | Date | |
---|---|---|---|
d3484a4b50 | |||
9d37b0a60f |
@ -75,5 +75,17 @@
|
|||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Delete confirmation modal -->
|
||||||
|
<div id="delete-modal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="delete-modal-close" class="modal-close">×</span>
|
||||||
|
<h3>Confirm Deletion</h3>
|
||||||
|
<p id="delete-modal-text">Are you sure you want to delete this image?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="delete-modal-confirm" class="modal-btn danger">Delete</button>
|
||||||
|
<button id="delete-modal-cancel" class="modal-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -41,23 +41,30 @@ function renderGallery() {
|
|||||||
<div class="flex-item">
|
<div class="flex-item">
|
||||||
<img src="/photos/${img.src}">
|
<img src="/photos/${img.src}">
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-input" data-index="${i}"></div>
|
<div class="tags-display" data-index="${i}"></div>
|
||||||
<div class="flex-item flex-full">
|
<div class="flex-item flex-full">
|
||||||
<div class="flex-item flex-end">
|
<div class="flex-item flex-end">
|
||||||
<button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
|
<button onclick="showDeleteModal('gallery', ${i})">🗑 Delete</button>
|
||||||
|
</div>
|
||||||
|
<div class="tag-input" data-index="${i}"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
|
|
||||||
renderTags(div.querySelector('.tag-input'), img.tags || [], i);
|
renderTags(i, img.tags || []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Render tags for a single image ---
|
// --- Render tags for a single image ---
|
||||||
function renderTags(container, tags, imgIndex) {
|
function renderTags(imgIndex, tags) {
|
||||||
container.innerHTML = '';
|
const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
|
||||||
|
const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
|
||||||
|
|
||||||
// Render existing tags
|
// vider
|
||||||
|
tagsDisplay.innerHTML = '';
|
||||||
|
inputContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// --- rendre les tags (en haut) ---
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = 'tag';
|
span.className = 'tag';
|
||||||
@ -69,23 +76,23 @@ function renderTags(container, tags, imgIndex) {
|
|||||||
remove.onclick = () => {
|
remove.onclick = () => {
|
||||||
tags.splice(tags.indexOf(tag), 1);
|
tags.splice(tags.indexOf(tag), 1);
|
||||||
updateTags(imgIndex, tags);
|
updateTags(imgIndex, tags);
|
||||||
renderTags(container, tags, imgIndex);
|
renderTags(imgIndex, tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
span.appendChild(remove);
|
span.appendChild(remove);
|
||||||
container.appendChild(span);
|
tagsDisplay.appendChild(span);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Input for new tags
|
// --- input (en bas) ---
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.placeholder = 'Add tag...';
|
input.placeholder = 'Add tag...';
|
||||||
container.appendChild(input);
|
inputContainer.appendChild(input);
|
||||||
|
|
||||||
// Suggestion dropdown
|
// suggestion box
|
||||||
const suggestionBox = document.createElement('ul');
|
const suggestionBox = document.createElement('ul');
|
||||||
suggestionBox.className = 'suggestions';
|
suggestionBox.className = 'suggestions';
|
||||||
container.appendChild(suggestionBox);
|
inputContainer.appendChild(suggestionBox);
|
||||||
|
|
||||||
let selectedIndex = -1;
|
let selectedIndex = -1;
|
||||||
|
|
||||||
@ -93,8 +100,8 @@ function renderTags(container, tags, imgIndex) {
|
|||||||
tag = tag.trim();
|
tag = tag.trim();
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
if (!tags.includes(tag)) tags.push(tag);
|
if (!tags.includes(tag)) tags.push(tag);
|
||||||
updateTags(imgIndex, tags); // save to galleryImages and server
|
updateTags(imgIndex, tags);
|
||||||
renderTags(container, tags, imgIndex);
|
renderTags(imgIndex, tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSuggestions = () => {
|
const updateSuggestions = () => {
|
||||||
@ -107,7 +114,6 @@ function renderTags(container, tags, imgIndex) {
|
|||||||
const allTagsSorted = Object.keys(tagCount)
|
const allTagsSorted = Object.keys(tagCount)
|
||||||
.sort((a, b) => tagCount[b] - tagCount[a]);
|
.sort((a, b) => tagCount[b] - tagCount[a]);
|
||||||
|
|
||||||
// Show suggestions that start with input (or all if empty)
|
|
||||||
const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
|
const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
|
||||||
|
|
||||||
suggestionBox.innerHTML = '';
|
suggestionBox.innerHTML = '';
|
||||||
@ -141,8 +147,7 @@ function renderTags(container, tags, imgIndex) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
input.addEventListener('input', updateSuggestions);
|
input.addEventListener('input', updateSuggestions);
|
||||||
|
input.addEventListener('focus', updateSuggestions);
|
||||||
input.addEventListener('focus', updateSuggestions); // Show suggestions on focus
|
|
||||||
|
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
const items = suggestionBox.querySelectorAll('li');
|
const items = suggestionBox.querySelectorAll('li');
|
||||||
@ -176,15 +181,14 @@ function renderTags(container, tags, imgIndex) {
|
|||||||
input.addEventListener('blur', () => {
|
input.addEventListener('blur', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
suggestionBox.style.display = 'none';
|
suggestionBox.style.display = 'none';
|
||||||
input.value = ''; // Clear input without saving
|
input.value = '';
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
input.focus();
|
input.focus();
|
||||||
updateSuggestions(); // show suggestions on render
|
updateSuggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Update tags in galleryImages array ---
|
// --- Update tags in galleryImages array ---
|
||||||
function updateTags(index, tags) {
|
function updateTags(index, tags) {
|
||||||
galleryImages[index].tags = tags;
|
galleryImages[index].tags = tags;
|
||||||
@ -204,56 +208,13 @@ function renderHero() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-item flex-full">
|
<div class="flex-item flex-full">
|
||||||
<div class="flex-item flex-end">
|
<div class="flex-item flex-end">
|
||||||
<button onclick="deleteHeroImage(${i})">🗑 Delete</button>
|
<button onclick="showDeleteModal('hero', ${i})">🗑 Delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Delete gallery image ---
|
|
||||||
async function deleteGalleryImage(index) {
|
|
||||||
const img = galleryImages[index];
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/gallery/delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ src: img.src.split('/').pop() })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
galleryImages.splice(index, 1);
|
|
||||||
renderGallery();
|
|
||||||
await saveGallery();
|
|
||||||
showToast("✅ Gallery image deleted!", "success");
|
|
||||||
} else showToast("Error: " + data.error, "error");
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
showToast("Server error!", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Delete hero image ---
|
|
||||||
async function deleteHeroImage(index) {
|
|
||||||
const img = heroImages[index];
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/hero/delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ src: img.src.split('/').pop() })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
heroImages.splice(index, 1);
|
|
||||||
renderHero();
|
|
||||||
await saveHero();
|
|
||||||
showToast("✅ Hero image deleted!", "success");
|
|
||||||
} else showToast("Error: " + data.error, "error");
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
showToast("Server error!", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Save gallery to server ---
|
// --- Save gallery to server ---
|
||||||
async function saveGallery() {
|
async function saveGallery() {
|
||||||
@ -312,5 +273,90 @@ function showToast(message, type = "success", duration = 3000) {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pendingDelete = null; // { type: 'gallery'|'hero', index: number }
|
||||||
|
|
||||||
|
// --- Show delete confirmation modal ---
|
||||||
|
function showDeleteModal(type, index) {
|
||||||
|
pendingDelete = { type, index };
|
||||||
|
document.getElementById('delete-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hide modal ---
|
||||||
|
function hideDeleteModal() {
|
||||||
|
document.getElementById('delete-modal').style.display = 'none';
|
||||||
|
pendingDelete = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Confirm deletion ---
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!pendingDelete) return;
|
||||||
|
if (pendingDelete.type === 'gallery') {
|
||||||
|
await actuallyDeleteGalleryImage(pendingDelete.index);
|
||||||
|
} else if (pendingDelete.type === 'hero') {
|
||||||
|
await actuallyDeleteHeroImage(pendingDelete.index);
|
||||||
|
}
|
||||||
|
hideDeleteModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Modal event listeners ---
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('delete-modal-close').onclick = hideDeleteModal;
|
||||||
|
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
|
||||||
|
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Actual delete functions ---
|
||||||
|
async function actuallyDeleteGalleryImage(index) {
|
||||||
|
const img = galleryImages[index];
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/gallery/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ src: img.src.split('/').pop() })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
galleryImages.splice(index, 1);
|
||||||
|
renderGallery();
|
||||||
|
await saveGallery();
|
||||||
|
showToast("✅ Gallery image deleted!", "success");
|
||||||
|
} else showToast("Error: " + data.error, "error");
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
showToast("Server error!", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function actuallyDeleteHeroImage(index) {
|
||||||
|
const img = heroImages[index];
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/hero/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ src: img.src.split('/').pop() })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
heroImages.splice(index, 1);
|
||||||
|
renderHero();
|
||||||
|
await saveHero();
|
||||||
|
showToast("✅ Hero image deleted!", "success");
|
||||||
|
} else showToast("Error: " + data.error, "error");
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
showToast("Server error!", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Modal event listeners ---
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('delete-modal-close').onclick = hideDeleteModal;
|
||||||
|
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
|
||||||
|
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Initialize ---
|
// --- Initialize ---
|
||||||
loadData();
|
loadData();
|
||||||
|
@ -51,7 +51,7 @@ h1, h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.photo {
|
.photo {
|
||||||
background-color: rgba(58, 62, 65, 0.26);
|
background-color: rgb(67 67 67 / 26%);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -80,7 +80,7 @@ h1, h2 {
|
|||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
margin-top: 10px;
|
margin: 5px 4px 0 4px;
|
||||||
width:100%;
|
width:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,20 +139,22 @@ h1, h2 {
|
|||||||
/* Tags */
|
/* Tags */
|
||||||
.tag-input {
|
.tag-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap-reverse;
|
||||||
|
align-content: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-input input {
|
.tag-input input {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
flex: 1;
|
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
background-color: #1f2223;
|
background-color: #1f2223;
|
||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@ -161,6 +163,7 @@ h1, h2 {
|
|||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag .remove-tag {
|
.tag .remove-tag {
|
||||||
@ -195,6 +198,13 @@ h1, h2 {
|
|||||||
background-color: #007782;
|
background-color: #007782;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-display {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.suggestions li.selected {
|
.suggestions li.selected {
|
||||||
background-color: #007782;
|
background-color: #007782;
|
||||||
color: white;
|
color: white;
|
||||||
@ -214,6 +224,7 @@ h1, h2 {
|
|||||||
|
|
||||||
.flex-full {
|
.flex-full {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-end {
|
.flex-end {
|
||||||
@ -263,7 +274,7 @@ h1, h2 {
|
|||||||
.nav > .nav-links {
|
.nav > .nav-links {
|
||||||
display: inline;
|
display: inline;
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 70px;
|
line-height: 70px;
|
||||||
}
|
}
|
||||||
@ -357,3 +368,61 @@ h1, h2 {
|
|||||||
.custom-upload-btn:hover {
|
.custom-upload-btn:hover {
|
||||||
background: #55c3ec;
|
background: #55c3ec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: #ffffff29;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.25);
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 90vw;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px; right: 18px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.modal-close:hover { opacity: 1; }
|
||||||
|
.modal-actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-btn {
|
||||||
|
padding: 0.5em 1.5em;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: none;
|
||||||
|
background: #09A0C1;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.modal-btn.danger {
|
||||||
|
background: #c62828;
|
||||||
|
}
|
||||||
|
.modal-btn:hover {
|
||||||
|
background: #55c3ec;
|
||||||
|
}
|
||||||
|
.modal-btn.danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
Reference in New Issue
Block a user