2.0 - WebUI builder ("Cielight" merge) #9

Merged
Djeex merged 43 commits from beta into main 2025-08-26 10:52:13 +02:00
22 changed files with 1393 additions and 38 deletions
Showing only changes of commit b56d03303e - Show all commits

View File

@ -96,6 +96,38 @@ def delete_hero_photo():
return {"status": "ok"} return {"status": "ok"}
return {"error": "File not found"}, 404 return {"error": "File not found"}, 404
@app.route("/api/gallery/delete_all", methods=["POST"])
def delete_all_gallery_photos():
"""Delete all gallery photos from disk and YAML."""
gallery_dir = PHOTOS_DIR / "gallery"
deleted = 0
# Remove all files in gallery folder
for file in gallery_dir.glob("*"):
if file.is_file():
file.unlink()
deleted += 1
# Clear YAML gallery images
data = load_yaml(GALLERY_YAML)
data["gallery"]["images"] = []
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok", "deleted": deleted})
@app.route("/api/hero/delete_all", methods=["POST"])
def delete_all_hero_photos():
"""Delete all hero photos from disk and YAML."""
hero_dir = PHOTOS_DIR / "hero"
deleted = 0
# Remove all files in hero folder
for file in hero_dir.glob("*"):
if file.is_file():
file.unlink()
deleted += 1
# Clear YAML hero images
data = load_yaml(GALLERY_YAML)
data["hero"]["images"] = []
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok", "deleted": deleted})
@app.route("/photos/<section>/<path:filename>") @app.route("/photos/<section>/<path:filename>")
def photos(section, filename): def photos(section, filename):
"""Serve uploaded photos from disk.""" """Serve uploaded photos from disk."""

View File

@ -3,46 +3,40 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Photo WebUI</title> <title>Photo WebUI</title>
<!-- Link to your CSS in the package -->
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head> </head>
<body> <body>
<!-- Top bar --> <!-- Top bar -->
<div class="nav-bar"> <div class="nav-bar">
<div class="content-inner nav"> <div class="content-inner nav">
<div class="nav-cta"> <div class="nav-cta">
<div class="arrow"> <div class="arrow"></div>
<a class="button" href="#" target="_blank">
</div> <span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
<a class="button" href="#" target="_blank"><span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span></a> </a>
</div> </div>
<input type="checkbox" id="nav-check"> <input type="checkbox" id="nav-check">
<div class="nav-header"> <div class="nav-header">
<div class="nav-title"> <div class="nav-title">
<img src="{{ url_for('static', filename='img/logo.svg') }}"> <img src="{{ url_for('static', filename='img/logo.svg') }}">
</div> </div>
</div> </div>
<div class="nav-btn">
<div class="nav-btn"> <label for="nav-check">
<label for="nav-check"> <span></span>
<span></span> <span></span>
<span></span> <span></span>
<span></span> </label>
</label> </div>
</div> <div class="nav-links">
<ul class="nav-list">
<div class="nav-links"> <li class="nav-item appear2"><a href="#">Site info</a>
<ul class="nav-list"> <li class="nav-item appear2"><a href="#qui-suis-je">Theme info</a>
<li class="nav-item appear2"><a href="#">Site info</a> <li class="nav-item appear2"><a href="#mariages">Gallery</a>
<li class="nav-item appear2"><a href="#qui-suis-je">Theme info</a> </ul>
<li class="nav-item appear2"><a href="#mariages">Gallery</a> </div>
</ul> </div>
</div>
</div>
</div>
</div>
<!-- Toast container for notifications --> <!-- Toast container for notifications -->
<div class="content-inner"> <div class="content-inner">
<div id="toast-container"></div> <div id="toast-container"></div>
@ -53,9 +47,12 @@
<div class="upload-section"> <div class="upload-section">
<h2>Title Carrousel</h2> <h2>Title Carrousel</h2>
<p> Select photos to display in the Title Carrousel</p> <p> Select photos to display in the Title Carrousel</p>
<label for="upload-hero" class="custom-upload-btn"> <div class="upload-actions-row">
📸 Upload photos <label for="upload-hero" class="up-btn">
</label> 📸 Upload photos
</label>
<button id="remove-all-hero" class="up-btn">🗑 Remove All</button>
</div>
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden> <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
<div id="hero"></div> <div id="hero"></div>
</div> </div>
@ -64,14 +61,16 @@
<div class="upload-section"> <div class="upload-section">
<h2>Gallery</h2> <h2>Gallery</h2>
<p> Select and tags photos to display in the Gallery</p> <p> Select and tags photos to display in the Gallery</p>
<label for="upload-gallery" class="custom-upload-btn"> <div class="upload-actions-row">
📸 Upload photos <label for="upload-gallery" class="up-btn">
</label> 📸 Upload photos
</label>
<button id="remove-all-gallery" class="up-btn">🗑 Remove All</button>
</div>
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden> <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
<div id="gallery"></div> <div id="gallery"></div>
</div> </div>
<!-- JS files for rendering, uploading, and actions -->
<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>
@ -88,4 +87,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -43,7 +43,7 @@ function renderGallery() {
</div> </div>
<div class="tags-display" 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="showDeleteModal('gallery', ${i})">🗑 Delete</button> <button onclick="showDeleteModal('gallery', ${i})">🗑 Delete</button>
</div> </div>
<div class="tag-input" data-index="${i}"></div> <div class="tag-input" data-index="${i}"></div>
@ -53,6 +53,12 @@ function renderGallery() {
renderTags(i, img.tags || []); renderTags(i, img.tags || []);
}); });
// Show/hide Remove All button
const removeAllBtn = document.getElementById('remove-all-gallery');
if (removeAllBtn) {
removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
}
} }
// --- Render tags for a single image --- // --- Render tags for a single image ---
@ -60,11 +66,9 @@ function renderTags(imgIndex, tags) {
const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`); const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`); const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
// vider
tagsDisplay.innerHTML = ''; tagsDisplay.innerHTML = '';
inputContainer.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';
@ -83,13 +87,11 @@ function renderTags(imgIndex, tags) {
tagsDisplay.appendChild(span); tagsDisplay.appendChild(span);
}); });
// --- 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...';
inputContainer.appendChild(input); inputContainer.appendChild(input);
// suggestion box
const suggestionBox = document.createElement('ul'); const suggestionBox = document.createElement('ul');
suggestionBox.className = 'suggestions'; suggestionBox.className = 'suggestions';
inputContainer.appendChild(suggestionBox); inputContainer.appendChild(suggestionBox);
@ -208,13 +210,19 @@ 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="showDeleteModal('hero', ${i})">🗑 Delete</button> <button onclick="showDeleteModal('hero', ${i})">🗑 Delete</button>
</div>
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);
}); });
}
// Show/hide Remove All button
const removeAllBtn = document.getElementById('remove-all-hero');
if (removeAllBtn) {
removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
}
}
// --- Save gallery to server --- // --- Save gallery to server ---
async function saveGallery() { async function saveGallery() {
@ -273,11 +281,19 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, duration);
} }
let pendingDelete = null; // { type: 'gallery'|'hero', index: number } let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
// --- Show delete confirmation modal --- // --- Show delete confirmation modal ---
function showDeleteModal(type, index) { function showDeleteModal(type, index = null) {
pendingDelete = { type, index }; pendingDelete = { type, index };
const modalText = document.getElementById('delete-modal-text');
if (type === 'gallery-all') {
modalText.textContent = "Are you sure you want to delete ALL gallery images?";
} else if (type === 'hero-all') {
modalText.textContent = "Are you sure you want to delete ALL hero images?";
} else {
modalText.textContent = "Are you sure you want to delete this image?";
}
document.getElementById('delete-modal').style.display = 'flex'; document.getElementById('delete-modal').style.display = 'flex';
} }
@ -294,18 +310,14 @@ async function confirmDelete() {
await actuallyDeleteGalleryImage(pendingDelete.index); await actuallyDeleteGalleryImage(pendingDelete.index);
} else if (pendingDelete.type === 'hero') { } else if (pendingDelete.type === 'hero') {
await actuallyDeleteHeroImage(pendingDelete.index); await actuallyDeleteHeroImage(pendingDelete.index);
} else if (pendingDelete.type === 'gallery-all') {
await actuallyDeleteAllGalleryImages();
} else if (pendingDelete.type === 'hero-all') {
await actuallyDeleteAllHeroImages();
} }
hideDeleteModal(); 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 --- // --- Actual delete functions ---
async function actuallyDeleteGalleryImage(index) { async function actuallyDeleteGalleryImage(index) {
const img = galleryImages[index]; const img = galleryImages[index];
@ -349,14 +361,51 @@ async function actuallyDeleteHeroImage(index) {
} }
} }
// --- Modal event listeners --- // --- Bulk delete functions ---
async function actuallyDeleteAllGalleryImages() {
try {
const res = await fetch('/api/gallery/delete_all', { method: 'POST' });
const data = await res.json();
if (res.ok) {
galleryImages = [];
renderGallery();
await saveGallery();
showToast("✅ All gallery images removed!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
async function actuallyDeleteAllHeroImages() {
try {
const res = await fetch('/api/hero/delete_all', { method: 'POST' });
const data = await res.json();
if (res.ok) {
heroImages = [];
renderHero();
await saveHero();
showToast("✅ All hero images removed!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
// --- Modal event listeners and bulk delete buttons ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.getElementById('delete-modal-close').onclick = hideDeleteModal; document.getElementById('delete-modal-close').onclick = hideDeleteModal;
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal; document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
document.getElementById('delete-modal-confirm').onclick = confirmDelete; document.getElementById('delete-modal-confirm').onclick = confirmDelete;
// Bulk delete buttons
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
const removeAllHeroBtn = document.getElementById('remove-all-hero');
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
}); });
// --- Initialize --- // --- Initialize ---
loadData(); loadData();

View File

@ -39,7 +39,6 @@ h1, h2 {
} }
.upload-section label { .upload-section label {
margin-right: 20px;
cursor: pointer; cursor: pointer;
} }
@ -108,7 +107,7 @@ h1, h2 {
/* Toast notifications */ /* Toast notifications */
#toast-container { #toast-container {
position: fixed; position: fixed;
top: 1rem; bottom: 1rem;
right: 1rem; right: 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -120,12 +119,13 @@ h1, h2 {
background: rgba(0,0,0,0.85); background: rgba(0,0,0,0.85);
color: white; color: white;
padding: 0.75rem 1.25rem; padding: 0.75rem 1.25rem;
border-radius: 0.5rem; border-radius: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3); box-shadow: 0 2px 8px rgba(0,0,0,0.3);
opacity: 0; opacity: 0;
transform: translateY(-20px); transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease;
pointer-events: none; pointer-events: none;
backdrop-filter: blur(20px);
} }
.toast.show { .toast.show {
@ -133,7 +133,7 @@ h1, h2 {
transform: translateY(0); transform: translateY(0);
} }
.toast.success { background-color: #28a745; } .toast.success { background-color: #28a7468c; }
.toast.error { background-color: #dc3545; } .toast.error { background-color: #dc3545; }
/* Tags */ /* Tags */
@ -351,7 +351,7 @@ h1, h2 {
} }
/* Custom upload buttons */ /* Custom upload buttons */
.custom-upload-btn { .up-btn {
display: inline-block; display: inline-block;
background: #09A0C1; background: #09A0C1;
color: #fff; color: #fff;
@ -363,9 +363,11 @@ h1, h2 {
transition: all 0.1s ease; transition: all 0.1s ease;
user-select: none; user-select: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.25); box-shadow: 0 4px 10px rgba(0,0,0,0.25);
font-size: 14px;
border: none;
} }
.custom-upload-btn:hover { .up-btn:hover {
background: #55c3ec; background: #55c3ec;
} }
@ -425,4 +427,32 @@ h1, h2 {
} }
.modal-btn.danger:hover { .modal-btn.danger:hover {
background: #d32f2f; background: #d32f2f;
}
/* Add this for horizontal alignment of upload and remove-all buttons */
.upload-actions-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 10px;
}
/* Style for Remove All buttons to match upload button look */
#remove-all-hero, #remove-all-gallery {
background: rgb(121, 26, 19);
}
#remove-all-gallery:hover,
#remove-all-hero:hover {
background: #d32f2f;
}
/* Responsive: stack buttons vertically on small screens */
@media (max-width: 500px) {
.upload-actions-row {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
} }