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/upload.js') }}"></script>
 | 
			
		||||
  </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>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -41,23 +41,30 @@ function renderGallery() {
 | 
			
		||||
      <div class="flex-item">
 | 
			
		||||
        <img src="/photos/${img.src}">
 | 
			
		||||
      </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-end">
 | 
			
		||||
        <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
			
		||||
          <button onclick="showDeleteModal('gallery', ${i})">🗑 Delete</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="tag-input" data-index="${i}"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
    container.appendChild(div);
 | 
			
		||||
 | 
			
		||||
    renderTags(div.querySelector('.tag-input'), img.tags || [], i);
 | 
			
		||||
    renderTags(i, img.tags || []);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Render tags for a single image ---
 | 
			
		||||
function renderTags(container, tags, imgIndex) {
 | 
			
		||||
  container.innerHTML = '';
 | 
			
		||||
function renderTags(imgIndex, tags) {
 | 
			
		||||
  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 => {
 | 
			
		||||
    const span = document.createElement('span');
 | 
			
		||||
    span.className = 'tag';
 | 
			
		||||
@@ -69,23 +76,23 @@ function renderTags(container, tags, imgIndex) {
 | 
			
		||||
    remove.onclick = () => {
 | 
			
		||||
      tags.splice(tags.indexOf(tag), 1);
 | 
			
		||||
      updateTags(imgIndex, tags);
 | 
			
		||||
      renderTags(container, tags, imgIndex);
 | 
			
		||||
      renderTags(imgIndex, tags);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    span.appendChild(remove);
 | 
			
		||||
    container.appendChild(span);
 | 
			
		||||
    tagsDisplay.appendChild(span);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Input for new tags
 | 
			
		||||
  // --- input (en bas) ---
 | 
			
		||||
  const input = document.createElement('input');
 | 
			
		||||
  input.type = 'text';
 | 
			
		||||
  input.placeholder = 'Add tag...';
 | 
			
		||||
  container.appendChild(input);
 | 
			
		||||
  inputContainer.appendChild(input);
 | 
			
		||||
 | 
			
		||||
  // Suggestion dropdown
 | 
			
		||||
  // suggestion box
 | 
			
		||||
  const suggestionBox = document.createElement('ul');
 | 
			
		||||
  suggestionBox.className = 'suggestions';
 | 
			
		||||
  container.appendChild(suggestionBox);
 | 
			
		||||
  inputContainer.appendChild(suggestionBox);
 | 
			
		||||
 | 
			
		||||
  let selectedIndex = -1;
 | 
			
		||||
 | 
			
		||||
@@ -93,8 +100,8 @@ function renderTags(container, tags, imgIndex) {
 | 
			
		||||
    tag = tag.trim();
 | 
			
		||||
    if (!tag) return;
 | 
			
		||||
    if (!tags.includes(tag)) tags.push(tag);
 | 
			
		||||
    updateTags(imgIndex, tags); // save to galleryImages and server
 | 
			
		||||
    renderTags(container, tags, imgIndex);
 | 
			
		||||
    updateTags(imgIndex, tags);
 | 
			
		||||
    renderTags(imgIndex, tags);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const updateSuggestions = () => {
 | 
			
		||||
@@ -107,7 +114,6 @@ function renderTags(container, tags, imgIndex) {
 | 
			
		||||
    const allTagsSorted = Object.keys(tagCount)
 | 
			
		||||
      .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));
 | 
			
		||||
 | 
			
		||||
    suggestionBox.innerHTML = '';
 | 
			
		||||
@@ -141,8 +147,7 @@ function renderTags(container, tags, imgIndex) {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  input.addEventListener('input', updateSuggestions);
 | 
			
		||||
 | 
			
		||||
  input.addEventListener('focus', updateSuggestions); // Show suggestions on focus
 | 
			
		||||
  input.addEventListener('focus', updateSuggestions);
 | 
			
		||||
 | 
			
		||||
  input.addEventListener('keydown', (e) => {
 | 
			
		||||
    const items = suggestionBox.querySelectorAll('li');
 | 
			
		||||
@@ -176,15 +181,14 @@ function renderTags(container, tags, imgIndex) {
 | 
			
		||||
  input.addEventListener('blur', () => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      suggestionBox.style.display = 'none';
 | 
			
		||||
      input.value = ''; // Clear input without saving
 | 
			
		||||
      input.value = '';
 | 
			
		||||
    }, 150);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  input.focus();
 | 
			
		||||
  updateSuggestions(); // show suggestions on render
 | 
			
		||||
  updateSuggestions();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// --- Update tags in galleryImages array ---
 | 
			
		||||
function updateTags(index, tags) {
 | 
			
		||||
  galleryImages[index].tags = tags;
 | 
			
		||||
@@ -204,56 +208,13 @@ function renderHero() {
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="flex-item flex-full">
 | 
			
		||||
        <div class="flex-item flex-end">
 | 
			
		||||
        <button onclick="deleteHeroImage(${i})">🗑 Delete</button>
 | 
			
		||||
        <button onclick="showDeleteModal('hero', ${i})">🗑 Delete</button>
 | 
			
		||||
      </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 ---
 | 
			
		||||
async function saveGallery() {
 | 
			
		||||
@@ -312,5 +273,90 @@ function showToast(message, type = "success", duration = 3000) {
 | 
			
		||||
  }, 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 ---
 | 
			
		||||
loadData();
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ h1, h2 {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo {
 | 
			
		||||
  background-color: rgba(58, 62, 65, 0.26);
 | 
			
		||||
  background-color: rgb(67 67 67 / 26%);
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
@@ -80,7 +80,7 @@ h1, h2 {
 | 
			
		||||
    border-radius: 30px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: background-color 0.2s;
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
    margin: 5px 4px 0 4px;
 | 
			
		||||
    width:100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -139,20 +139,22 @@ h1, h2 {
 | 
			
		||||
/* Tags */
 | 
			
		||||
.tag-input {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    flex-wrap: wrap-reverse;
 | 
			
		||||
    align-content: flex-start;
 | 
			
		||||
    gap: 4px;
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag-input input {
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  min-width: 60px;
 | 
			
		||||
  background-color: #1f2223;
 | 
			
		||||
  border: 1px solid #585858;
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag {
 | 
			
		||||
@@ -161,6 +163,7 @@ h1, h2 {
 | 
			
		||||
  border-radius: 15px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag .remove-tag {
 | 
			
		||||
@@ -195,6 +198,13 @@ h1, h2 {
 | 
			
		||||
  background-color: #007782;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags-display {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 4px;
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.suggestions li.selected {
 | 
			
		||||
  background-color: #007782;
 | 
			
		||||
  color: white;
 | 
			
		||||
@@ -214,6 +224,7 @@ h1, h2 {
 | 
			
		||||
 | 
			
		||||
.flex-full {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  flex-direction: column-reverse;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-end {
 | 
			
		||||
@@ -263,7 +274,7 @@ h1, h2 {
 | 
			
		||||
.nav > .nav-links {
 | 
			
		||||
  display: inline;
 | 
			
		||||
  float: right;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  line-height: 70px;
 | 
			
		||||
}
 | 
			
		||||
@@ -357,3 +368,61 @@ h1, h2 {
 | 
			
		||||
.custom-upload-btn:hover {
 | 
			
		||||
  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