Compare commits
	
		
			13 Commits
		
	
	
		
			147ad1545c
			...
			alpine
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					28e3b087b0 | ||
| 116a9574ce | |||
| 
						 | 
					6c7216c0e1 | ||
| 7c98a6ceaf | |||
| 
						 | 
					69d303c7a1 | ||
| 
						 | 
					1d1d7d8304 | ||
| 
						 | 
					205dcae2bc | ||
| 
						 | 
					b7fdcacf77 | ||
| 
						 | 
					9cdf6bbd32 | ||
| 
						 | 
					f92060603d | ||
| 
						 | 
					54e9281793 | ||
| 
						 | 
					5fb6ef18d1 | ||
| 
						 | 
					1119647884 | 
							
								
								
									
										17
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,10 +1,18 @@
 | 
				
			|||||||
FROM python:3.11-slim
 | 
					FROM python:3.13-alpine AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY requirements.txt .
 | 
					RUN apk add --no-cache gcc musl-dev jpeg-dev zlib-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN pip install --no-cache-dir -r requirements.txt
 | 
					COPY requirements.txt .
 | 
				
			||||||
 | 
					RUN pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM python:3.13-alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY --from=builder /wheels /wheels
 | 
				
			||||||
 | 
					RUN pip install --no-cache-dir --find-links=/wheels /wheels/* && rm -rf /wheels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY build.py gallery.py VERSION /app/
 | 
					COPY build.py gallery.py VERSION /app/
 | 
				
			||||||
COPY ./src/ ./src/
 | 
					COPY ./src/ ./src/
 | 
				
			||||||
@@ -12,7 +20,4 @@ COPY ./config /app/default
 | 
				
			|||||||
COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
 | 
					COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
 | 
				
			||||||
RUN chmod +x /app/entrypoint.sh
 | 
					RUN chmod +x /app/entrypoint.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN printf '#!/bin/sh\n/app/entrypoint.sh build\n' > /usr/local/bin/build && chmod +x /usr/local/bin/build && \
 | 
					 | 
				
			||||||
    printf '#!/bin/sh\n/app/entrypoint.sh gallery\n' > /usr/local/bin/gallery && chmod +x /usr/local/bin/gallery
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ENTRYPOINT ["/app/entrypoint.sh"]
 | 
					ENTRYPOINT ["/app/entrypoint.sh"]
 | 
				
			||||||
@@ -50,7 +50,7 @@ img, tag {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    .hero-background {
 | 
					    .hero-background {
 | 
				
			||||||
        max-width: 90%;
 | 
					        max-width: 90%;
 | 
				
			||||||
        margin: auto;
 | 
					        margin: 0 auto;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .back-button {
 | 
					    .back-button {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,12 @@ colors:
 | 
				
			|||||||
  primary_dark: '#005384'
 | 
					  primary_dark: '#005384'
 | 
				
			||||||
  secondary: '#00B0F0'
 | 
					  secondary: '#00B0F0'
 | 
				
			||||||
  text_dark: '#616161'
 | 
					  text_dark: '#616161'
 | 
				
			||||||
 | 
					 | 
				
			||||||
favicon:
 | 
					favicon:
 | 
				
			||||||
  path: favicon.png
 | 
					  path: favicon.png
 | 
				
			||||||
  
 | 
					 | 
				
			||||||
fonts:
 | 
					fonts:
 | 
				
			||||||
  primary:
 | 
					  primary:
 | 
				
			||||||
    name: trixie.woff
 | 
					    name: trixie
 | 
				
			||||||
    fallback: sans-serif
 | 
					    fallback: sans-serif
 | 
				
			||||||
  secondary:
 | 
					  secondary:
 | 
				
			||||||
    name: trixie.woff
 | 
					    name: trixie
 | 
				
			||||||
    fallback: serif
 | 
					    fallback: serif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					#!/bin/sh
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CYAN="\033[1;36m"
 | 
					CYAN="\033[1;36m"
 | 
				
			||||||
@@ -6,28 +6,31 @@ NC="\033[0m"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
copy_default_config() {
 | 
					copy_default_config() {
 | 
				
			||||||
  echo "[~] Checking configuration directory..."
 | 
					  echo "[~] Checking configuration directory..."
 | 
				
			||||||
  if [ ! -d "/app/config" ]; then
 | 
					  mkdir -p /app/config
 | 
				
			||||||
    mkdir -p /app/config
 | 
					 | 
				
			||||||
  fi
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  echo "[~] Checking if default config files need to be copied..."
 | 
					  echo "[~] Checking if default config files/folders need to be copied..."
 | 
				
			||||||
  files_copied=false
 | 
					  files_copied=false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for file in /app/default/*; do
 | 
					  for src in $(find /app/default -mindepth 1); do
 | 
				
			||||||
    filename=$(basename "$file")
 | 
					    relpath="${src#/app/default/}"
 | 
				
			||||||
    target="/app/config/$filename"
 | 
					    target="/app/config/$relpath"
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if [ ! -e "$target" ]; then
 | 
					    if [ ! -e "$target" ]; then
 | 
				
			||||||
      echo "[→] Copying default config file: $filename"
 | 
					      echo "[→] Copying: $relpath"
 | 
				
			||||||
      cp -r "$file" "$target"
 | 
					      if [ -d "$src" ]; then
 | 
				
			||||||
 | 
					        mkdir -p "$target"
 | 
				
			||||||
 | 
					        cp -r "$src/" "$target/"
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        mkdir -p "$(dirname "$target")"
 | 
				
			||||||
 | 
					        cp "$src" "$target"
 | 
				
			||||||
 | 
					      fi
 | 
				
			||||||
      files_copied=true
 | 
					      files_copied=true
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
  done
 | 
					  done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if [ "$files_copied" = true ]; then
 | 
					  if [ "$files_copied" = true ]; then
 | 
				
			||||||
    echo "[✓] Default configuration files copied successfully."
 | 
					    echo "[✓] Default configuration files/folders copied successfully."
 | 
				
			||||||
  else
 | 
					  else
 | 
				
			||||||
    echo "[✓] No default files needed to be copied."
 | 
					    echo "[✓] No default files/folders needed to be copied."
 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -415,7 +415,8 @@ def upload_font():
 | 
				
			|||||||
    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
					    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
				
			||||||
    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
					    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
    file.save(fonts_dir / file.filename)
 | 
					    file.save(fonts_dir / file.filename)
 | 
				
			||||||
    return jsonify({"status": "ok", "filename": file.filename})
 | 
					    font_basename = Path(file.filename).stem
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok", "filename": font_basename})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/font/remove", methods=["POST"])
 | 
					@app.route("/api/font/remove", methods=["POST"])
 | 
				
			||||||
def remove_font():
 | 
					def remove_font():
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,29 @@
 | 
				
			|||||||
// --- Arrays to store gallery and hero images ---
 | 
					 | 
				
			||||||
let galleryImages = [];
 | 
					let galleryImages = [];
 | 
				
			||||||
let heroImages = [];
 | 
					let heroImages = [];
 | 
				
			||||||
let allTags = []; // global tag list
 | 
					let allTags = [];
 | 
				
			||||||
let showOnlyUntagged = false;
 | 
					let showOnlyUntagged = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Fade-in helper ---
 | 
				
			||||||
 | 
					function applyFadeInImages(container) {
 | 
				
			||||||
 | 
					  container.querySelectorAll("img.fade-in-img").forEach(img => {
 | 
				
			||||||
 | 
					    const onLoad = () => img.classList.add("loaded");
 | 
				
			||||||
 | 
					    if (img.complete && img.naturalHeight !== 0) onLoad();
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      img.addEventListener("load", onLoad, { once: true });
 | 
				
			||||||
 | 
					      img.addEventListener("error", () => {
 | 
				
			||||||
 | 
					        console.warn("Image failed to load:", img.dataset?.src || img.src);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Load images from server on page load ---
 | 
					// --- Load images from server on page load ---
 | 
				
			||||||
async function loadData() {
 | 
					async function loadData() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const galleryRes = await fetch('/api/gallery');
 | 
					    galleryImages = await (await fetch('/api/gallery')).json();
 | 
				
			||||||
    galleryImages = await galleryRes.json();
 | 
					 | 
				
			||||||
    updateAllTags();
 | 
					    updateAllTags();
 | 
				
			||||||
    renderGallery();
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					    heroImages = await (await fetch('/api/hero')).json();
 | 
				
			||||||
    const heroRes = await fetch('/api/hero');
 | 
					 | 
				
			||||||
    heroImages = await heroRes.json();
 | 
					 | 
				
			||||||
    renderHero();
 | 
					    renderHero();
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err);
 | 
				
			||||||
@@ -25,31 +35,44 @@ async function loadData() {
 | 
				
			|||||||
function updateAllTags() {
 | 
					function updateAllTags() {
 | 
				
			||||||
  allTags = [];
 | 
					  allTags = [];
 | 
				
			||||||
  galleryImages.forEach(img => {
 | 
					  galleryImages.forEach(img => {
 | 
				
			||||||
    if (img.tags) img.tags.forEach(t => {
 | 
					    (img.tags || []).forEach(t => {
 | 
				
			||||||
      if (!allTags.includes(t)) allTags.push(t);
 | 
					      if (!allTags.includes(t)) allTags.push(t);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Helper: update count and button visibility ---
 | 
				
			||||||
 | 
					function updateCountAndButtons(prefix, count) {
 | 
				
			||||||
 | 
					  const countTop = document.getElementById(`${prefix}-count`);
 | 
				
			||||||
 | 
					  const countBottom = document.getElementById(`${prefix}-count-bottom`);
 | 
				
			||||||
 | 
					  if (countTop) countTop.innerHTML = `<p>${count} photos</p>`;
 | 
				
			||||||
 | 
					  if (countBottom) countBottom.innerHTML = `<p>${count} photos</p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const removeAllBtn = document.getElementById(`remove-all-${prefix}`);
 | 
				
			||||||
 | 
					  const removeAllBtnBottom = document.getElementById(`remove-all-${prefix}-bottom`);
 | 
				
			||||||
 | 
					  if (removeAllBtn) removeAllBtn.style.display = count > 0 ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					  if (removeAllBtnBottom) removeAllBtnBottom.style.display = count > 0 ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bottomUpload = document.getElementById(`bottom-${prefix}-upload`);
 | 
				
			||||||
 | 
					  if (bottomUpload) bottomUpload.style.display = count > 0 ? 'flex' : 'none';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render gallery images with tags and delete buttons ---
 | 
					// --- Render gallery images with tags and delete buttons ---
 | 
				
			||||||
function renderGallery() {
 | 
					function renderGallery() {
 | 
				
			||||||
  const container = document.getElementById('gallery');
 | 
					  const container = document.getElementById('gallery');
 | 
				
			||||||
  container.innerHTML = '';
 | 
					  container.innerHTML = '';
 | 
				
			||||||
 | 
					  let imagesToShow = showOnlyUntagged
 | 
				
			||||||
  // Filter images if toggle is on
 | 
					    ? galleryImages.filter(img => !img.tags || img.tags.length === 0)
 | 
				
			||||||
  let imagesToShow = galleryImages;
 | 
					    : galleryImages;
 | 
				
			||||||
  if (showOnlyUntagged) {
 | 
					 | 
				
			||||||
    imagesToShow = galleryImages.filter(img => !img.tags || img.tags.length === 0);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  imagesToShow.forEach((img) => {
 | 
					  imagesToShow.forEach((img) => {
 | 
				
			||||||
    const i = galleryImages.indexOf(img); // Use real index for tag/delete actions
 | 
					    const i = galleryImages.indexOf(img);
 | 
				
			||||||
    const div = document.createElement('div');
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
    div.className = 'photo flex-item flex-column';
 | 
					    div.className = 'photo flex-item flex-column';
 | 
				
			||||||
    div.innerHTML = `
 | 
					    div.innerHTML = `
 | 
				
			||||||
        <div class="flex-item">
 | 
					      <div class="flex-item">
 | 
				
			||||||
          <img class="fade-in-img" src="/photos/${img.src}">
 | 
					        <img class="fade-in-img" src="/photos/${img.src}">
 | 
				
			||||||
        </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">
 | 
				
			||||||
@@ -59,62 +82,17 @@ function renderGallery() {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div);
 | 
					    container.appendChild(div);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderTags(i, img.tags || []);
 | 
					    renderTags(i, img.tags || []);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Update gallery count (top)
 | 
					  updateCountAndButtons('gallery', imagesToShow.length);
 | 
				
			||||||
  const galleryCount = document.getElementById('gallery-count');
 | 
					  applyFadeInImages(container);
 | 
				
			||||||
  if (galleryCount) {
 | 
					 | 
				
			||||||
    galleryCount.innerHTML = `<p>${imagesToShow.length} photos</p>`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update gallery count (bottom)
 | 
					 | 
				
			||||||
  const galleryCountBottom = document.getElementById('gallery-count-bottom');
 | 
					 | 
				
			||||||
  if (galleryCountBottom) {
 | 
					 | 
				
			||||||
    galleryCountBottom.innerHTML = `<p>${imagesToShow.length} photos</p>`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide Remove All button (top)
 | 
					 | 
				
			||||||
  const removeAllBtn = document.getElementById('remove-all-gallery');
 | 
					 | 
				
			||||||
  if (removeAllBtn) {
 | 
					 | 
				
			||||||
    removeAllBtn.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide bottom upload row
 | 
					 | 
				
			||||||
  const bottomGalleryUpload = document.getElementById('bottom-gallery-upload');
 | 
					 | 
				
			||||||
  if (bottomGalleryUpload) {
 | 
					 | 
				
			||||||
    bottomGalleryUpload.style.display = imagesToShow.length > 0 ? 'flex' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide Remove All button (bottom)
 | 
					 | 
				
			||||||
  const removeAllBtnBottom = document.getElementById('remove-all-gallery-bottom');
 | 
					 | 
				
			||||||
  if (removeAllBtnBottom) {
 | 
					 | 
				
			||||||
    removeAllBtnBottom.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Fade-in effect for loaded images
 | 
					 | 
				
			||||||
  const fadeImages = document.querySelectorAll("img.fade-in-img");
 | 
					 | 
				
			||||||
  fadeImages.forEach(img => {
 | 
					 | 
				
			||||||
    const onLoad = () => {
 | 
					 | 
				
			||||||
      img.classList.add("loaded");
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    if (img.complete && img.naturalHeight !== 0) {
 | 
					 | 
				
			||||||
      onLoad();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      img.addEventListener("load", onLoad, { once: true });
 | 
					 | 
				
			||||||
      img.addEventListener("error", () => {
 | 
					 | 
				
			||||||
        console.warn("Image failed to load:", img.dataset.src || img.src);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render tags for a single image ---
 | 
					// --- Render tags for a single image ---
 | 
				
			||||||
function renderTags(imgIndex, tags) {
 | 
					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}"]`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  tagsDisplay.innerHTML = '';
 | 
					  tagsDisplay.innerHTML = '';
 | 
				
			||||||
  inputContainer.innerHTML = '';
 | 
					  inputContainer.innerHTML = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,7 +100,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
    const span = document.createElement('span');
 | 
					    const span = document.createElement('span');
 | 
				
			||||||
    span.className = 'tag';
 | 
					    span.className = 'tag';
 | 
				
			||||||
    span.textContent = tag;
 | 
					    span.textContent = tag;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const remove = document.createElement('span');
 | 
					    const remove = document.createElement('span');
 | 
				
			||||||
    remove.className = 'remove-tag';
 | 
					    remove.className = 'remove-tag';
 | 
				
			||||||
    remove.textContent = '×';
 | 
					    remove.textContent = '×';
 | 
				
			||||||
@@ -131,7 +108,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
      updateTags(imgIndex, tags);
 | 
					      updateTags(imgIndex, tags);
 | 
				
			||||||
      renderTags(imgIndex, tags);
 | 
					      renderTags(imgIndex, tags);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					 | 
				
			||||||
    span.appendChild(remove);
 | 
					    span.appendChild(remove);
 | 
				
			||||||
    tagsDisplay.appendChild(span);
 | 
					    tagsDisplay.appendChild(span);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -141,11 +117,10 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
  input.placeholder = 'Add tag...';
 | 
					  input.placeholder = 'Add tag...';
 | 
				
			||||||
  inputContainer.appendChild(input);
 | 
					  inputContainer.appendChild(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Validate button ---
 | 
					 | 
				
			||||||
  const validateBtn = document.createElement('button');
 | 
					  const validateBtn = document.createElement('button');
 | 
				
			||||||
  validateBtn.textContent = '✔️';
 | 
					  validateBtn.textContent = '✔️';
 | 
				
			||||||
  validateBtn.className = 'validate-tag-btn';
 | 
					  validateBtn.className = 'validate-tag-btn';
 | 
				
			||||||
  validateBtn.style.display = 'none'; // hidden by default
 | 
					  validateBtn.style.display = 'none';
 | 
				
			||||||
  validateBtn.style.marginLeft = '4px';
 | 
					  validateBtn.style.marginLeft = '4px';
 | 
				
			||||||
  inputContainer.appendChild(validateBtn);
 | 
					  inputContainer.appendChild(validateBtn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -165,30 +140,20 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const updateSuggestions = () => {
 | 
					  const updateSuggestions = () => {
 | 
				
			||||||
    const value = input.value.toLowerCase();
 | 
					    const value = input.value.toLowerCase();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
 | 
					    const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
 | 
				
			||||||
    const tagCount = {};
 | 
					    const tagCount = {};
 | 
				
			||||||
    allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
 | 
					    allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
 | 
				
			||||||
 | 
					    const allTagsSorted = Object.keys(tagCount).sort((a, b) => tagCount[b] - tagCount[a]);
 | 
				
			||||||
    const allTagsSorted = Object.keys(tagCount)
 | 
					 | 
				
			||||||
      .sort((a, b) => tagCount[b] - tagCount[a]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 = '';
 | 
				
			||||||
    selectedIndex = -1;
 | 
					    selectedIndex = -1;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (suggestions.length) {
 | 
					    if (suggestions.length) {
 | 
				
			||||||
      suggestionBox.style.display = 'block';
 | 
					      suggestionBox.style.display = 'block';
 | 
				
			||||||
      suggestions.forEach((s, idx) => {
 | 
					      suggestions.forEach((s, idx) => {
 | 
				
			||||||
        const li = document.createElement('li');
 | 
					        const li = document.createElement('li');
 | 
				
			||||||
        li.style.fontStyle = 'italic';
 | 
					        li.style.fontStyle = 'italic';
 | 
				
			||||||
        li.style.textAlign = 'left';
 | 
					        li.style.textAlign = 'left';
 | 
				
			||||||
 | 
					        li.innerHTML = `<b>${s.substring(0, input.value.length)}</b>${s.substring(input.value.length)}`;
 | 
				
			||||||
        const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
 | 
					 | 
				
			||||||
        const rest = s.substring(input.value.length);
 | 
					 | 
				
			||||||
        li.innerHTML = boldPart + rest;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        li.addEventListener('mousedown', (e) => {
 | 
					        li.addEventListener('mousedown', (e) => {
 | 
				
			||||||
          e.preventDefault();
 | 
					          e.preventDefault();
 | 
				
			||||||
          addTag(s);
 | 
					          addTag(s);
 | 
				
			||||||
@@ -196,7 +161,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
          input.focus();
 | 
					          input.focus();
 | 
				
			||||||
          updateSuggestions();
 | 
					          updateSuggestions();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
        li.onmouseover = () => selectedIndex = idx;
 | 
					        li.onmouseover = () => selectedIndex = idx;
 | 
				
			||||||
        suggestionBox.appendChild(li);
 | 
					        suggestionBox.appendChild(li);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@@ -213,7 +177,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
    updateSuggestions();
 | 
					    updateSuggestions();
 | 
				
			||||||
    validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
 | 
					    validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  input.addEventListener('keydown', (e) => {
 | 
					  input.addEventListener('keydown', (e) => {
 | 
				
			||||||
    const items = suggestionBox.querySelectorAll('li');
 | 
					    const items = suggestionBox.querySelectorAll('li');
 | 
				
			||||||
    if (e.key === 'ArrowDown') {
 | 
					    if (e.key === 'ArrowDown') {
 | 
				
			||||||
@@ -244,27 +207,22 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
      validateBtn.style.display = 'none';
 | 
					      validateBtn.style.display = 'none';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  input.addEventListener('blur', () => {
 | 
					  input.addEventListener('blur', () => {
 | 
				
			||||||
    setTimeout(() => {
 | 
					    suggestionBox.style.display = 'none';
 | 
				
			||||||
      suggestionBox.style.display = 'none';
 | 
					    input.value = '';
 | 
				
			||||||
      input.value = '';
 | 
					    validateBtn.style.display = 'none';
 | 
				
			||||||
      validateBtn.style.display = 'none';
 | 
					 | 
				
			||||||
    }, 150);
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  validateBtn.addEventListener('mousedown', (e) => {
 | 
				
			||||||
  // --- Validate button action ---
 | 
					    e.preventDefault();
 | 
				
			||||||
  validateBtn.onclick = () => {
 | 
					 | 
				
			||||||
    if (input.value.trim()) {
 | 
					    if (input.value.trim()) {
 | 
				
			||||||
      addTag(input.value.trim());
 | 
					      addTag(input.value.trim());
 | 
				
			||||||
      input.value = '';
 | 
					      input.value = '';
 | 
				
			||||||
      updateSuggestions();
 | 
					      updateSuggestions();
 | 
				
			||||||
      validateBtn.style.display = 'none';
 | 
					      validateBtn.style.display = 'none';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  input.focus();
 | 
					 | 
				
			||||||
  updateSuggestions();
 | 
					  updateSuggestions();
 | 
				
			||||||
 | 
					  if (!input.value.trim()) suggestionBox.style.display = 'none';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Update tags in galleryImages array ---
 | 
					// --- Update tags in galleryImages array ---
 | 
				
			||||||
@@ -292,36 +250,8 @@ function renderHero() {
 | 
				
			|||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div);
 | 
					    container.appendChild(div);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  updateCountAndButtons('hero', heroImages.length);
 | 
				
			||||||
  // Update hero count (top)
 | 
					  applyFadeInImages(container);
 | 
				
			||||||
  const heroCount = document.getElementById('hero-count');
 | 
					 | 
				
			||||||
  if (heroCount) {
 | 
					 | 
				
			||||||
    heroCount.innerHTML = `<p>${heroImages.length} photos</p>`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update hero count (bottom)
 | 
					 | 
				
			||||||
  const heroCountBottom = document.getElementById('hero-count-bottom');
 | 
					 | 
				
			||||||
  if (heroCountBottom) {
 | 
					 | 
				
			||||||
    heroCountBottom.innerHTML = `<p>${heroImages.length} photos</p>`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide Remove All button (top)
 | 
					 | 
				
			||||||
  const removeAllBtn = document.getElementById('remove-all-hero');
 | 
					 | 
				
			||||||
  if (removeAllBtn) {
 | 
					 | 
				
			||||||
    removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide bottom upload row
 | 
					 | 
				
			||||||
  const bottomHeroUpload = document.getElementById('bottom-hero-upload');
 | 
					 | 
				
			||||||
  if (bottomHeroUpload) {
 | 
					 | 
				
			||||||
    bottomHeroUpload.style.display = heroImages.length > 0 ? 'flex' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide Remove All button (bottom)
 | 
					 | 
				
			||||||
  const removeAllBtnBottom = document.getElementById('remove-all-hero-bottom');
 | 
					 | 
				
			||||||
  if (removeAllBtnBottom) {
 | 
					 | 
				
			||||||
    removeAllBtnBottom.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Save gallery to server ---
 | 
					// --- Save gallery to server ---
 | 
				
			||||||
@@ -367,33 +297,27 @@ async function refreshHero() {
 | 
				
			|||||||
function showToast(message, type = "success", duration = 3000) {
 | 
					function showToast(message, type = "success", duration = 3000) {
 | 
				
			||||||
  const container = document.getElementById("toast-container");
 | 
					  const container = document.getElementById("toast-container");
 | 
				
			||||||
  if (!container) return;
 | 
					  if (!container) return;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toast = document.createElement("div");
 | 
					  const toast = document.createElement("div");
 | 
				
			||||||
  toast.className = `toast ${type}`;
 | 
					  toast.className = `toast ${type}`;
 | 
				
			||||||
  toast.textContent = message;
 | 
					  toast.textContent = message;
 | 
				
			||||||
  container.appendChild(toast);
 | 
					  container.appendChild(toast);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  requestAnimationFrame(() => toast.classList.add("show"));
 | 
					  requestAnimationFrame(() => toast.classList.add("show"));
 | 
				
			||||||
 | 
					 | 
				
			||||||
  setTimeout(() => {
 | 
					  setTimeout(() => {
 | 
				
			||||||
    toast.classList.remove("show");
 | 
					    toast.classList.remove("show");
 | 
				
			||||||
    toast.addEventListener("transitionend", () => toast.remove());
 | 
					    toast.addEventListener("transitionend", () => toast.remove());
 | 
				
			||||||
  }, duration);
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
 | 
					let pendingDelete = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Show delete confirmation modal ---
 | 
					// --- Show delete confirmation modal ---
 | 
				
			||||||
function showDeleteModal(type, index = null) {
 | 
					function showDeleteModal(type, index = null) {
 | 
				
			||||||
  pendingDelete = { type, index };
 | 
					  pendingDelete = { type, index };
 | 
				
			||||||
  const modalText = document.getElementById('delete-modal-text');
 | 
					  const modalText = document.getElementById('delete-modal-text');
 | 
				
			||||||
  if (type === 'gallery-all') {
 | 
					  modalText.textContent =
 | 
				
			||||||
    modalText.textContent = "Are you sure you want to delete ALL gallery images?";
 | 
					    type === 'gallery-all' ? "Are you sure you want to delete ALL gallery images?"
 | 
				
			||||||
  } else if (type === 'hero-all') {
 | 
					    : type === 'hero-all' ? "Are you sure you want to delete ALL hero images?"
 | 
				
			||||||
    modalText.textContent = "Are you sure you want to delete ALL hero images?";
 | 
					    : "Are you sure you want to delete this image?";
 | 
				
			||||||
  } 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';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -406,15 +330,10 @@ function hideDeleteModal() {
 | 
				
			|||||||
// --- Confirm deletion ---
 | 
					// --- Confirm deletion ---
 | 
				
			||||||
async function confirmDelete() {
 | 
					async function confirmDelete() {
 | 
				
			||||||
  if (!pendingDelete) return;
 | 
					  if (!pendingDelete) return;
 | 
				
			||||||
  if (pendingDelete.type === 'gallery') {
 | 
					  if (pendingDelete.type === 'gallery') await actuallyDeleteGalleryImage(pendingDelete.index);
 | 
				
			||||||
    await actuallyDeleteGalleryImage(pendingDelete.index);
 | 
					  else if (pendingDelete.type === 'hero') await actuallyDeleteHeroImage(pendingDelete.index);
 | 
				
			||||||
  } else if (pendingDelete.type === 'hero') {
 | 
					  else if (pendingDelete.type === 'gallery-all') await actuallyDeleteAllGalleryImages();
 | 
				
			||||||
    await actuallyDeleteHeroImage(pendingDelete.index);
 | 
					  else if (pendingDelete.type === 'hero-all') await actuallyDeleteAllHeroImages();
 | 
				
			||||||
  } else if (pendingDelete.type === 'gallery-all') {
 | 
					 | 
				
			||||||
    await actuallyDeleteAllGalleryImages();
 | 
					 | 
				
			||||||
  } else if (pendingDelete.type === 'hero-all') {
 | 
					 | 
				
			||||||
    await actuallyDeleteAllHeroImages();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  hideDeleteModal();
 | 
					  hideDeleteModal();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -496,41 +415,36 @@ async function actuallyDeleteAllHeroImages() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// --- Modal event listeners and bulk delete buttons ---
 | 
					// --- Modal event listeners and bulk delete buttons ---
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', () => {
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  document.getElementById('delete-modal-close').onclick = hideDeleteModal;
 | 
					  ['delete-modal-close', 'delete-modal-cancel'].forEach(id => {
 | 
				
			||||||
  document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
 | 
					    const el = document.getElementById(id);
 | 
				
			||||||
  document.getElementById('delete-modal-confirm').onclick = confirmDelete;
 | 
					    if (el) el.onclick = hideDeleteModal;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const confirmBtn = document.getElementById('delete-modal-confirm');
 | 
				
			||||||
 | 
					  if (confirmBtn) confirmBtn.onclick = confirmDelete;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Radio toggle for gallery filter ---
 | 
					  // Gallery filter radios
 | 
				
			||||||
  const showAllRadio = document.getElementById('show-all-radio');
 | 
					  const showAllRadio = document.getElementById('show-all-radio');
 | 
				
			||||||
  const showUntaggedRadio = document.getElementById('show-untagged-radio');
 | 
					  const showUntaggedRadio = document.getElementById('show-untagged-radio');
 | 
				
			||||||
 | 
					  if (showAllRadio) showAllRadio.addEventListener('change', () => {
 | 
				
			||||||
  if (showAllRadio && showUntaggedRadio) {
 | 
					    showOnlyUntagged = false;
 | 
				
			||||||
    showAllRadio.addEventListener('change', () => {
 | 
					    renderGallery();
 | 
				
			||||||
      if (showAllRadio.checked) {
 | 
					  });
 | 
				
			||||||
        showOnlyUntagged = false;
 | 
					  if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
 | 
				
			||||||
        renderGallery();
 | 
					    showOnlyUntagged = true;
 | 
				
			||||||
      }
 | 
					    renderGallery();
 | 
				
			||||||
    });
 | 
					  });
 | 
				
			||||||
    showUntaggedRadio.addEventListener('change', () => {
 | 
					 | 
				
			||||||
      if (showUntaggedRadio.checked) {
 | 
					 | 
				
			||||||
        showOnlyUntagged = true;
 | 
					 | 
				
			||||||
        renderGallery();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Bulk delete buttons
 | 
					  // Bulk delete buttons
 | 
				
			||||||
  const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
 | 
					  [
 | 
				
			||||||
  const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom');
 | 
					    ['remove-all-gallery', 'gallery-all'],
 | 
				
			||||||
  const removeAllHeroBtn = document.getElementById('remove-all-hero');
 | 
					    ['remove-all-gallery-bottom', 'gallery-all'],
 | 
				
			||||||
  const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom');
 | 
					    ['remove-all-hero', 'hero-all'],
 | 
				
			||||||
  if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
 | 
					    ['remove-all-hero-bottom', 'hero-all']
 | 
				
			||||||
  if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all');
 | 
					  ].forEach(([btnId, type]) => {
 | 
				
			||||||
  if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
 | 
					    const btn = document.getElementById(btnId);
 | 
				
			||||||
  if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all');
 | 
					    if (btn) btn.onclick = () => showDeleteModal(type);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Initialize ---
 | 
					// --- Initialize ---
 | 
				
			||||||
loadData();
 | 
					loadData();
 | 
				
			||||||
@@ -12,7 +12,6 @@ function showToast(message, type = "success", duration = 3000) {
 | 
				
			|||||||
  }, duration);
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Loader helpers ---
 | 
					 | 
				
			||||||
function showLoader(text = "Uploading...") {
 | 
					function showLoader(text = "Uploading...") {
 | 
				
			||||||
  const loader = document.getElementById("global-loader");
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
  if (loader) {
 | 
					  if (loader) {
 | 
				
			||||||
@@ -27,77 +26,65 @@ function hideLoader() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", () => {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
  // --- Section Forms ---
 | 
					  // --- Section Forms ---
 | 
				
			||||||
  const infoForm = document.getElementById("info-form");
 | 
					  const forms = {
 | 
				
			||||||
  const socialForm = document.getElementById("social-form");
 | 
					    info: document.getElementById("info-form"),
 | 
				
			||||||
  const menuForm = document.getElementById("menu-form");
 | 
					    social: document.getElementById("social-form"),
 | 
				
			||||||
  const footerForm = document.getElementById("footer-form");
 | 
					    menu: document.getElementById("menu-form"),
 | 
				
			||||||
  const legalsForm = document.getElementById("legals-form");
 | 
					    footer: document.getElementById("footer-form"),
 | 
				
			||||||
  const buildForm = document.getElementById("build-form");
 | 
					    legals: document.getElementById("legals-form"),
 | 
				
			||||||
 | 
					    build: document.getElementById("build-form")
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Menu logic ---
 | 
					  // --- Menu logic ---
 | 
				
			||||||
  const menuList = document.getElementById("menu-items-list");
 | 
					  const menuList = document.getElementById("menu-items-list");
 | 
				
			||||||
  const addMenuBtn = document.getElementById("add-menu-item");
 | 
					  const addMenuBtn = document.getElementById("add-menu-item");
 | 
				
			||||||
  let menuItems = [];
 | 
					  let menuItems = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
  function renderMenuItems() {
 | 
					  function renderMenuItems() {
 | 
				
			||||||
    menuList.innerHTML = "";
 | 
					    menuList.innerHTML = "";
 | 
				
			||||||
    menuItems.forEach((item, idx) => {
 | 
					    menuItems.forEach((item, idx) => {
 | 
				
			||||||
      const div = document.createElement("div");
 | 
					      menuList.innerHTML += `
 | 
				
			||||||
      div.style.display = "flex";
 | 
					        <div style="display:flex;gap:8px;margin-bottom:6px;">
 | 
				
			||||||
      div.style.gap = "8px";
 | 
					          <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					          <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
				
			||||||
      div.innerHTML = `
 | 
					          <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
				
			||||||
        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
					        </div>
 | 
				
			||||||
        <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
					 | 
				
			||||||
        <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
					 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      menuList.appendChild(div);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  function updateMenuItemsFromInputs() {
 | 
					  function updateMenuItemsFromInputs() {
 | 
				
			||||||
    const inputs = menuList.querySelectorAll("input");
 | 
					    const inputs = menuList.querySelectorAll("input");
 | 
				
			||||||
    const items = [];
 | 
					    menuItems = [];
 | 
				
			||||||
    for (let i = 0; i < inputs.length; i += 2) {
 | 
					    for (let i = 0; i < inputs.length; i += 2) {
 | 
				
			||||||
      const label = inputs[i].value.trim();
 | 
					      const label = inputs[i].value.trim();
 | 
				
			||||||
      const href = inputs[i + 1].value.trim();
 | 
					      const href = inputs[i + 1].value.trim();
 | 
				
			||||||
      if (label || href) items.push({ label, href });
 | 
					      if (label || href) menuItems.push({ label, href });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    menuItems = items;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Intellectual property paragraphs logic ---
 | 
					  // --- Intellectual property paragraphs logic ---
 | 
				
			||||||
  const ipList = document.getElementById("ip-list");
 | 
					  const ipList = document.getElementById("ip-list");
 | 
				
			||||||
  const addIpBtn = document.getElementById("add-ip-paragraph");
 | 
					  const addIpBtn = document.getElementById("add-ip-paragraph");
 | 
				
			||||||
  let ipParagraphs = [];
 | 
					  let ipParagraphs = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
  function renderIpParagraphs() {
 | 
					  function renderIpParagraphs() {
 | 
				
			||||||
    ipList.innerHTML = "";
 | 
					    ipList.innerHTML = "";
 | 
				
			||||||
    ipParagraphs.forEach((item, idx) => {
 | 
					    ipParagraphs.forEach((item, idx) => {
 | 
				
			||||||
      const div = document.createElement("div");
 | 
					      ipList.innerHTML += `
 | 
				
			||||||
      div.style.display = "flex";
 | 
					        <div style="display:flex;gap:8px;margin-bottom:6px;">
 | 
				
			||||||
      div.style.gap = "8px";
 | 
					          <textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					          <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
				
			||||||
      div.innerHTML = `
 | 
					        </div>
 | 
				
			||||||
        <textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
 | 
					 | 
				
			||||||
        <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
					 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      ipList.appendChild(div);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  function updateIpParagraphsFromInputs() {
 | 
					  function updateIpParagraphsFromInputs() {
 | 
				
			||||||
    const textareas = ipList.querySelectorAll("textarea");
 | 
					    ipParagraphs = Array.from(ipList.querySelectorAll("textarea"))
 | 
				
			||||||
    ipParagraphs = Array.from(textareas).map(textarea => ({
 | 
					      .map(textarea => ({ paragraph: textarea.value.trim() }))
 | 
				
			||||||
      paragraph: textarea.value.trim()
 | 
					      .filter(item => item.paragraph !== "");
 | 
				
			||||||
    })).filter(item => item.paragraph !== "");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Build options ---
 | 
					  // --- Build options & Theme select ---
 | 
				
			||||||
  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
					  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
				
			||||||
  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
					  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Theme select ---
 | 
					 | 
				
			||||||
  const themeSelect = document.getElementById("theme-select");
 | 
					  const themeSelect = document.getElementById("theme-select");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Thumbnail upload and modal logic ---
 | 
					  // --- Thumbnail upload and modal logic ---
 | 
				
			||||||
@@ -107,40 +94,30 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
  const thumbnailPreview = document.getElementById("thumbnail-preview");
 | 
					  const thumbnailPreview = document.getElementById("thumbnail-preview");
 | 
				
			||||||
  const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
 | 
					  const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Modal elements for delete confirmation ---
 | 
					  // --- Modal helpers ---
 | 
				
			||||||
  const deleteModal = document.getElementById("delete-modal");
 | 
					  function setupModal(modal, closeBtn, confirmBtn, cancelBtn, onConfirm) {
 | 
				
			||||||
  const deleteModalClose = document.getElementById("delete-modal-close");
 | 
					    if (!modal) return;
 | 
				
			||||||
  const deleteModalConfirm = document.getElementById("delete-modal-confirm");
 | 
					    if (closeBtn) closeBtn.onclick = () => modal.style.display = "none";
 | 
				
			||||||
  const deleteModalCancel = document.getElementById("delete-modal-cancel");
 | 
					    if (cancelBtn) cancelBtn.onclick = () => modal.style.display = "none";
 | 
				
			||||||
 | 
					    window.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					      if (e.target === modal) modal.style.display = "none";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (confirmBtn && onConfirm) confirmBtn.onclick = onConfirm;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Modal elements for theme deletion ---
 | 
					  // --- Thumbnail preview logic ---
 | 
				
			||||||
  const deleteThemeModal = document.getElementById("delete-theme-modal");
 | 
					 | 
				
			||||||
  const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
 | 
					 | 
				
			||||||
  const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
 | 
					 | 
				
			||||||
  const deleteThemeModalCancel = document.getElementById("delete-theme-modal-cancel");
 | 
					 | 
				
			||||||
  const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
 | 
					 | 
				
			||||||
  let themeToDelete = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Show/hide thumbnail preview, remove button, and choose button ---
 | 
					 | 
				
			||||||
  function updateThumbnailPreview(src) {
 | 
					  function updateThumbnailPreview(src) {
 | 
				
			||||||
    if (thumbnailPreview) {
 | 
					    if (thumbnailPreview) {
 | 
				
			||||||
      thumbnailPreview.src = src || "";
 | 
					      thumbnailPreview.src = src || "";
 | 
				
			||||||
      thumbnailPreview.style.display = src ? "block" : "none";
 | 
					      thumbnailPreview.style.display = src ? "block" : "none";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (removeThumbnailBtn) {
 | 
					    if (removeThumbnailBtn) removeThumbnailBtn.style.display = src ? "inline-block" : "none";
 | 
				
			||||||
      removeThumbnailBtn.style.display = src ? "inline-block" : "none";
 | 
					    if (chooseThumbnailBtn) chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (chooseThumbnailBtn) {
 | 
					 | 
				
			||||||
      chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Choose thumbnail button triggers file input ---
 | 
					 | 
				
			||||||
  if (chooseThumbnailBtn && thumbnailUpload) {
 | 
					  if (chooseThumbnailBtn && thumbnailUpload) {
 | 
				
			||||||
    chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
 | 
					    chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Handle thumbnail upload and refresh preview (with cache busting) ---
 | 
					 | 
				
			||||||
  if (thumbnailUpload) {
 | 
					  if (thumbnailUpload) {
 | 
				
			||||||
    thumbnailUpload.addEventListener("change", async (e) => {
 | 
					    thumbnailUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const file = e.target.files[0];
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
@@ -161,25 +138,17 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      updateSectionStatus("social");
 | 
					      updateSectionStatus("social");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Remove thumbnail button triggers modal ---
 | 
					 | 
				
			||||||
  if (removeThumbnailBtn) {
 | 
					  if (removeThumbnailBtn) {
 | 
				
			||||||
    removeThumbnailBtn.addEventListener("click", () => {
 | 
					    removeThumbnailBtn.addEventListener("click", () => {
 | 
				
			||||||
      deleteModal.style.display = "flex";
 | 
					      document.getElementById("delete-modal").style.display = "flex";
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  setupModal(
 | 
				
			||||||
  // --- Modal logic for thumbnail deletion ---
 | 
					    document.getElementById("delete-modal"),
 | 
				
			||||||
  if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
 | 
					    document.getElementById("delete-modal-close"),
 | 
				
			||||||
    deleteModalClose.onclick = deleteModalCancel.onclick = () => {
 | 
					    document.getElementById("delete-modal-confirm"),
 | 
				
			||||||
      deleteModal.style.display = "none";
 | 
					    document.getElementById("delete-modal-cancel"),
 | 
				
			||||||
    };
 | 
					    async () => {
 | 
				
			||||||
    window.onclick = function(event) {
 | 
					 | 
				
			||||||
      if (event.target === deleteModal) {
 | 
					 | 
				
			||||||
        deleteModal.style.display = "none";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    deleteModalConfirm.onclick = async () => {
 | 
					 | 
				
			||||||
      const res = await fetch("/api/thumbnail/remove", { method: "POST" });
 | 
					      const res = await fetch("/api/thumbnail/remove", { method: "POST" });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
@@ -189,41 +158,28 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error removing thumbnail", "error");
 | 
					        showToast("❌ Error removing thumbnail", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deleteModal.style.display = "none";
 | 
					      document.getElementById("delete-modal").style.display = "none";
 | 
				
			||||||
      updateSectionStatus("social");
 | 
					      updateSectionStatus("social");
 | 
				
			||||||
    };
 | 
					    }
 | 
				
			||||||
  }
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Theme upload logic (custom theme folder) ---
 | 
					  // --- Theme upload logic ---
 | 
				
			||||||
  const themeUpload = document.getElementById("theme-upload");
 | 
					  const themeUpload = document.getElementById("theme-upload");
 | 
				
			||||||
  const chooseThemeBtn = document.getElementById("choose-theme-btn");
 | 
					  const chooseThemeBtn = document.getElementById("choose-theme-btn");
 | 
				
			||||||
  if (chooseThemeBtn && themeUpload) {
 | 
					  if (chooseThemeBtn && themeUpload) {
 | 
				
			||||||
    chooseThemeBtn.addEventListener("click", () => themeUpload.click());
 | 
					    chooseThemeBtn.addEventListener("click", () => themeUpload.click());
 | 
				
			||||||
    themeUpload.addEventListener("change", async (e) => {
 | 
					    themeUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const files = Array.from(e.target.files);
 | 
					      const files = Array.from(e.target.files);
 | 
				
			||||||
      if (files.length === 0) return;
 | 
					      if (!files.length) return;
 | 
				
			||||||
      showLoader("Uploading theme...");
 | 
					      showLoader("Uploading theme...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      files.forEach(file => {
 | 
					      files.forEach(file => formData.append("files", file, file.webkitRelativePath || file.name));
 | 
				
			||||||
        formData.append("files", file, file.webkitRelativePath || file.name);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      hideLoader();
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme uploaded!", "success");
 | 
					        showToast("✅ Theme uploaded!", "success");
 | 
				
			||||||
        // Refresh theme select after upload
 | 
					        refreshThemes();
 | 
				
			||||||
        fetch("/api/themes")
 | 
					 | 
				
			||||||
          .then(res => res.json())
 | 
					 | 
				
			||||||
          .then(themes => {
 | 
					 | 
				
			||||||
            themeSelect.innerHTML = "";
 | 
					 | 
				
			||||||
            themes.forEach(theme => {
 | 
					 | 
				
			||||||
              const option = document.createElement("option");
 | 
					 | 
				
			||||||
              option.value = theme;
 | 
					 | 
				
			||||||
              option.textContent = theme;
 | 
					 | 
				
			||||||
              themeSelect.appendChild(option);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error uploading theme", "error");
 | 
					        showToast("❌ Error uploading theme", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -231,8 +187,9 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Remove theme button triggers modal ---
 | 
					  // --- Remove theme logic ---
 | 
				
			||||||
  const removeThemeBtn = document.getElementById("remove-theme-btn");
 | 
					  const removeThemeBtn = document.getElementById("remove-theme-btn");
 | 
				
			||||||
 | 
					  let themeToDelete = null;
 | 
				
			||||||
  if (removeThemeBtn && themeSelect) {
 | 
					  if (removeThemeBtn && themeSelect) {
 | 
				
			||||||
    removeThemeBtn.addEventListener("click", () => {
 | 
					    removeThemeBtn.addEventListener("click", () => {
 | 
				
			||||||
      const theme = themeSelect.value;
 | 
					      const theme = themeSelect.value;
 | 
				
			||||||
@@ -242,24 +199,16 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      themeToDelete = theme;
 | 
					      themeToDelete = theme;
 | 
				
			||||||
      deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`;
 | 
					      document.getElementById("delete-theme-modal-text").textContent = `Are you sure you want to remove theme "${theme}"?`;
 | 
				
			||||||
      deleteThemeModal.style.display = "flex";
 | 
					      document.getElementById("delete-theme-modal").style.display = "flex";
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  setupModal(
 | 
				
			||||||
  // --- Modal logic for theme deletion ---
 | 
					    document.getElementById("delete-theme-modal"),
 | 
				
			||||||
  if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
 | 
					    document.getElementById("delete-theme-modal-close"),
 | 
				
			||||||
    deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
 | 
					    document.getElementById("delete-theme-modal-confirm"),
 | 
				
			||||||
      deleteThemeModal.style.display = "none";
 | 
					    document.getElementById("delete-theme-modal-cancel"),
 | 
				
			||||||
      themeToDelete = null;
 | 
					    async () => {
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    window.onclick = function(event) {
 | 
					 | 
				
			||||||
      if (event.target === deleteThemeModal) {
 | 
					 | 
				
			||||||
        deleteThemeModal.style.display = "none";
 | 
					 | 
				
			||||||
        themeToDelete = null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    deleteThemeModalConfirm.onclick = async () => {
 | 
					 | 
				
			||||||
      if (!themeToDelete) return;
 | 
					      if (!themeToDelete) return;
 | 
				
			||||||
      showLoader("Removing theme...");
 | 
					      showLoader("Removing theme...");
 | 
				
			||||||
      const res = await fetch("/api/theme/remove", {
 | 
					      const res = await fetch("/api/theme/remove", {
 | 
				
			||||||
@@ -271,85 +220,18 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      hideLoader();
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme removed!", "success");
 | 
					        showToast("✅ Theme removed!", "success");
 | 
				
			||||||
        // Refresh theme select
 | 
					        refreshThemes();
 | 
				
			||||||
        fetch("/api/themes")
 | 
					 | 
				
			||||||
          .then(res => res.json())
 | 
					 | 
				
			||||||
          .then(themes => {
 | 
					 | 
				
			||||||
            themeSelect.innerHTML = "";
 | 
					 | 
				
			||||||
            themes.forEach(theme => {
 | 
					 | 
				
			||||||
              const option = document.createElement("option");
 | 
					 | 
				
			||||||
              option.value = theme;
 | 
					 | 
				
			||||||
              option.textContent = theme;
 | 
					 | 
				
			||||||
              themeSelect.appendChild(option);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast(result.error || "❌ Error removing theme", "error");
 | 
					        showToast(result.error || "❌ Error removing theme", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deleteThemeModal.style.display = "none";
 | 
					      document.getElementById("delete-theme-modal").style.display = "none";
 | 
				
			||||||
      themeToDelete = null;
 | 
					      themeToDelete = null;
 | 
				
			||||||
      updateSectionStatus("build");
 | 
					      updateSectionStatus("build");
 | 
				
			||||||
    };
 | 
					    }
 | 
				
			||||||
  }
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Fetch theme list and populate select, then load config and update build status ---
 | 
					  // --- Theme select refresh ---
 | 
				
			||||||
  let loadedConfig = {};
 | 
					  function refreshThemes() {
 | 
				
			||||||
  function loadConfigAndUpdateBuildStatus() {
 | 
					 | 
				
			||||||
    fetch("/api/site-info")
 | 
					 | 
				
			||||||
      .then(res => res.json())
 | 
					 | 
				
			||||||
      .then(data => {
 | 
					 | 
				
			||||||
        loadedConfig = data;
 | 
					 | 
				
			||||||
        // Info
 | 
					 | 
				
			||||||
        if (infoForm) {
 | 
					 | 
				
			||||||
          infoForm.elements["info.title"].value = data.info?.title || "";
 | 
					 | 
				
			||||||
          infoForm.elements["info.subtitle"].value = data.info?.subtitle || "";
 | 
					 | 
				
			||||||
          infoForm.elements["info.description"].value = data.info?.description || "";
 | 
					 | 
				
			||||||
          infoForm.elements["info.canonical"].value = data.info?.canonical || "";
 | 
					 | 
				
			||||||
          infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
					 | 
				
			||||||
          infoForm.elements["info.author"].value = data.info?.author || "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Social
 | 
					 | 
				
			||||||
        if (socialForm) {
 | 
					 | 
				
			||||||
          socialForm.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
					 | 
				
			||||||
          if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
 | 
					 | 
				
			||||||
          updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Menu
 | 
					 | 
				
			||||||
        menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
 | 
					 | 
				
			||||||
        renderMenuItems();
 | 
					 | 
				
			||||||
        // Footer
 | 
					 | 
				
			||||||
        if (footerForm) {
 | 
					 | 
				
			||||||
          footerForm.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
					 | 
				
			||||||
          footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Legals
 | 
					 | 
				
			||||||
        ipParagraphs = Array.isArray(data.legals?.intellectual_property)
 | 
					 | 
				
			||||||
          ? data.legals.intellectual_property
 | 
					 | 
				
			||||||
          : [];
 | 
					 | 
				
			||||||
        renderIpParagraphs();
 | 
					 | 
				
			||||||
        if (legalsForm) {
 | 
					 | 
				
			||||||
          legalsForm.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
					 | 
				
			||||||
          legalsForm.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
 | 
					 | 
				
			||||||
          legalsForm.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Build
 | 
					 | 
				
			||||||
        if (themeSelect) {
 | 
					 | 
				
			||||||
          themeSelect.value = data.build?.theme || "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (convertImagesCheckbox) {
 | 
					 | 
				
			||||||
          convertImagesCheckbox.checked = !!data.build?.convert_images;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (resizeImagesCheckbox) {
 | 
					 | 
				
			||||||
          resizeImagesCheckbox.checked = !!data.build?.resize_images;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Initial status update for all sections except build
 | 
					 | 
				
			||||||
        ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
 | 
					 | 
				
			||||||
        // For build, update status after theme select is set
 | 
					 | 
				
			||||||
        updateSectionStatus("build");
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (themeSelect) {
 | 
					 | 
				
			||||||
    fetch("/api/themes")
 | 
					    fetch("/api/themes")
 | 
				
			||||||
      .then(res => res.json())
 | 
					      .then(res => res.json())
 | 
				
			||||||
      .then(themes => {
 | 
					      .then(themes => {
 | 
				
			||||||
@@ -360,24 +242,67 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          option.textContent = theme;
 | 
					          option.textContent = theme;
 | 
				
			||||||
          themeSelect.appendChild(option);
 | 
					          themeSelect.appendChild(option);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Now load config and update build status after theme select is ready
 | 
					 | 
				
			||||||
        loadConfigAndUpdateBuildStatus();
 | 
					        loadConfigAndUpdateBuildStatus();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    // If no theme select, just load config
 | 
					 | 
				
			||||||
    loadConfigAndUpdateBuildStatus();
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Add menu item ---
 | 
					  // --- Config loading ---
 | 
				
			||||||
  if (addMenuBtn) {
 | 
					  let loadedConfig = {};
 | 
				
			||||||
    addMenuBtn.addEventListener("click", () => {
 | 
					  function loadConfigAndUpdateBuildStatus() {
 | 
				
			||||||
      menuItems.push({ label: "", href: "" });
 | 
					    fetch("/api/site-info")
 | 
				
			||||||
      renderMenuItems();
 | 
					      .then(res => res.json())
 | 
				
			||||||
      updateSectionStatus("menu");
 | 
					      .then(data => {
 | 
				
			||||||
    });
 | 
					        loadedConfig = data;
 | 
				
			||||||
 | 
					        // Info
 | 
				
			||||||
 | 
					        if (forms.info) {
 | 
				
			||||||
 | 
					          forms.info.elements["info.title"].value = data.info?.title || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.subtitle"].value = data.info?.subtitle || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.description"].value = data.info?.description || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.canonical"].value = data.info?.canonical || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
				
			||||||
 | 
					          forms.info.elements["info.author"].value = data.info?.author || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Social
 | 
				
			||||||
 | 
					        if (forms.social) {
 | 
				
			||||||
 | 
					          forms.social.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
				
			||||||
 | 
					          if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
 | 
				
			||||||
 | 
					          updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Menu
 | 
				
			||||||
 | 
					        menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
 | 
				
			||||||
 | 
					        renderMenuItems();
 | 
				
			||||||
 | 
					        // Footer
 | 
				
			||||||
 | 
					        if (forms.footer) {
 | 
				
			||||||
 | 
					          forms.footer.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
				
			||||||
 | 
					          forms.footer.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Legals
 | 
				
			||||||
 | 
					        ipParagraphs = Array.isArray(data.legals?.intellectual_property)
 | 
				
			||||||
 | 
					          ? data.legals.intellectual_property
 | 
				
			||||||
 | 
					          : [];
 | 
				
			||||||
 | 
					        renderIpParagraphs();
 | 
				
			||||||
 | 
					        if (forms.legals) {
 | 
				
			||||||
 | 
					          forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
				
			||||||
 | 
					          forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
 | 
				
			||||||
 | 
					          forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Build
 | 
				
			||||||
 | 
					        if (themeSelect) themeSelect.value = data.build?.theme || "";
 | 
				
			||||||
 | 
					        if (convertImagesCheckbox) convertImagesCheckbox.checked = !!data.build?.convert_images;
 | 
				
			||||||
 | 
					        if (resizeImagesCheckbox) resizeImagesCheckbox.checked = !!data.build?.resize_images;
 | 
				
			||||||
 | 
					        ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
 | 
				
			||||||
 | 
					        updateSectionStatus("build");
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (themeSelect) refreshThemes();
 | 
				
			||||||
 | 
					  else loadConfigAndUpdateBuildStatus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Remove menu item ---
 | 
					  // --- Add/remove menu items ---
 | 
				
			||||||
 | 
					  if (addMenuBtn) addMenuBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
					    menuItems.push({ label: "", href: "" });
 | 
				
			||||||
 | 
					    renderMenuItems();
 | 
				
			||||||
 | 
					    updateSectionStatus("menu");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
  menuList.addEventListener("click", (e) => {
 | 
					  menuList.addEventListener("click", (e) => {
 | 
				
			||||||
    if (e.target.classList.contains("remove-menu-item")) {
 | 
					    if (e.target.classList.contains("remove-menu-item")) {
 | 
				
			||||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
					      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
				
			||||||
@@ -386,23 +311,17 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      updateSectionStatus("menu");
 | 
					      updateSectionStatus("menu");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Update menuItems on input change ---
 | 
					 | 
				
			||||||
  menuList.addEventListener("input", () => {
 | 
					  menuList.addEventListener("input", () => {
 | 
				
			||||||
    updateMenuItemsFromInputs();
 | 
					    updateMenuItemsFromInputs();
 | 
				
			||||||
    updateSectionStatus("menu");
 | 
					    updateSectionStatus("menu");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Add paragraph ---
 | 
					  // --- Add/remove IP paragraphs ---
 | 
				
			||||||
  if (addIpBtn) {
 | 
					  if (addIpBtn) addIpBtn.addEventListener("click", () => {
 | 
				
			||||||
    addIpBtn.addEventListener("click", () => {
 | 
					    ipParagraphs.push({ paragraph: "" });
 | 
				
			||||||
      ipParagraphs.push({ paragraph: "" });
 | 
					    renderIpParagraphs();
 | 
				
			||||||
      renderIpParagraphs();
 | 
					    updateSectionStatus("legals");
 | 
				
			||||||
      updateSectionStatus("legals");
 | 
					  });
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Remove paragraph ---
 | 
					 | 
				
			||||||
  ipList.addEventListener("click", (e) => {
 | 
					  ipList.addEventListener("click", (e) => {
 | 
				
			||||||
    if (e.target.classList.contains("remove-ip-paragraph")) {
 | 
					    if (e.target.classList.contains("remove-ip-paragraph")) {
 | 
				
			||||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
					      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
				
			||||||
@@ -411,8 +330,6 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      updateSectionStatus("legals");
 | 
					      updateSectionStatus("legals");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Update ipParagraphs on input change ---
 | 
					 | 
				
			||||||
  ipList.addEventListener("input", () => {
 | 
					  ipList.addEventListener("input", () => {
 | 
				
			||||||
    updateIpParagraphsFromInputs();
 | 
					    updateIpParagraphsFromInputs();
 | 
				
			||||||
    updateSectionStatus("legals");
 | 
					    updateSectionStatus("legals");
 | 
				
			||||||
@@ -423,16 +340,16 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    switch (section) {
 | 
					    switch (section) {
 | 
				
			||||||
      case "info":
 | 
					      case "info":
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          title: infoForm.elements["info.title"].value,
 | 
					          title: forms.info.elements["info.title"].value,
 | 
				
			||||||
          subtitle: infoForm.elements["info.subtitle"].value,
 | 
					          subtitle: forms.info.elements["info.subtitle"].value,
 | 
				
			||||||
          description: infoForm.elements["info.description"].value,
 | 
					          description: forms.info.elements["info.description"].value,
 | 
				
			||||||
          canonical: infoForm.elements["info.canonical"].value,
 | 
					          canonical: forms.info.elements["info.canonical"].value,
 | 
				
			||||||
          keywords: infoForm.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
 | 
					          keywords: forms.info.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
 | 
				
			||||||
          author: infoForm.elements["info.author"].value
 | 
					          author: forms.info.elements["info.author"].value
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      case "social":
 | 
					      case "social":
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          instagram_url: socialForm.elements["social.instagram_url"].value,
 | 
					          instagram_url: forms.social.elements["social.instagram_url"].value,
 | 
				
			||||||
          thumbnail: thumbnailInput ? thumbnailInput.value : ""
 | 
					          thumbnail: thumbnailInput ? thumbnailInput.value : ""
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      case "menu":
 | 
					      case "menu":
 | 
				
			||||||
@@ -440,15 +357,15 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        return { items: menuItems };
 | 
					        return { items: menuItems };
 | 
				
			||||||
      case "footer":
 | 
					      case "footer":
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          copyright: footerForm.elements["footer.copyright"].value,
 | 
					          copyright: forms.footer.elements["footer.copyright"].value,
 | 
				
			||||||
          legal_label: footerForm.elements["footer.legal_label"].value
 | 
					          legal_label: forms.footer.elements["footer.legal_label"].value
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      case "legals":
 | 
					      case "legals":
 | 
				
			||||||
        updateIpParagraphsFromInputs();
 | 
					        updateIpParagraphsFromInputs();
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          hoster_name: legalsForm.elements["legals.hoster_name"].value,
 | 
					          hoster_name: forms.legals.elements["legals.hoster_name"].value,
 | 
				
			||||||
          hoster_address: legalsForm.elements["legals.hoster_address"].value,
 | 
					          hoster_address: forms.legals.elements["legals.hoster_address"].value,
 | 
				
			||||||
          hoster_contact: legalsForm.elements["legals.hoster_contact"].value,
 | 
					          hoster_contact: forms.legals.elements["legals.hoster_contact"].value,
 | 
				
			||||||
          intellectual_property: ipParagraphs
 | 
					          intellectual_property: ipParagraphs
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      case "build":
 | 
					      case "build":
 | 
				
			||||||
@@ -533,7 +450,7 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          values.intellectual_property.every(ip => ip.paragraph)
 | 
					          values.intellectual_property.every(ip => ip.paragraph)
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      case "build":
 | 
					      case "build":
 | 
				
			||||||
        return !!values.theme; // Only check theme is present
 | 
					        return !!values.theme;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -561,46 +478,22 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // --- Listen for changes in each section ---
 | 
					  // --- Listen for changes in each section ---
 | 
				
			||||||
  [
 | 
					  Object.entries(forms).forEach(([section, form]) => {
 | 
				
			||||||
    { form: infoForm, section: "info" },
 | 
					 | 
				
			||||||
    { form: socialForm, section: "social" },
 | 
					 | 
				
			||||||
    { form: menuForm, section: "menu" },
 | 
					 | 
				
			||||||
    { form: footerForm, section: "footer" },
 | 
					 | 
				
			||||||
    { form: legalsForm, section: "legals" },
 | 
					 | 
				
			||||||
    { form: buildForm, section: "build" }
 | 
					 | 
				
			||||||
  ].forEach(({ form, section }) => {
 | 
					 | 
				
			||||||
    if (!form) return;
 | 
					    if (!form) return;
 | 
				
			||||||
    form.addEventListener("input", () => updateSectionStatus(section));
 | 
					    form.addEventListener("input", () => updateSectionStatus(section));
 | 
				
			||||||
    form.addEventListener("change", () => updateSectionStatus(section));
 | 
					    form.addEventListener("change", () => updateSectionStatus(section));
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Save section handler (form submit) ---
 | 
					 | 
				
			||||||
  [
 | 
					 | 
				
			||||||
    { form: infoForm, section: "info" },
 | 
					 | 
				
			||||||
    { form: socialForm, section: "social" },
 | 
					 | 
				
			||||||
    { form: menuForm, section: "menu" },
 | 
					 | 
				
			||||||
    { form: footerForm, section: "footer" },
 | 
					 | 
				
			||||||
    { form: legalsForm, section: "legals" },
 | 
					 | 
				
			||||||
    { form: buildForm, section: "build" }
 | 
					 | 
				
			||||||
  ].forEach(({ form, section }) => {
 | 
					 | 
				
			||||||
    if (!form) return;
 | 
					 | 
				
			||||||
    form.addEventListener("submit", async (e) => {
 | 
					    form.addEventListener("submit", async (e) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      // Native browser validation
 | 
					 | 
				
			||||||
      if (!form.reportValidity()) {
 | 
					      if (!form.reportValidity()) {
 | 
				
			||||||
        showToast("❌ Please fill all required fields before saving.", "error");
 | 
					        showToast("❌ Please fill all required fields before saving.", "error");
 | 
				
			||||||
        updateSectionStatus(section);
 | 
					        updateSectionStatus(section);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Social section: check thumbnail
 | 
					      if (section === "social" && (!thumbnailInput || !thumbnailInput.value)) {
 | 
				
			||||||
      if (section === "social") {
 | 
					        showToast("❌ Thumbnail is required.", "error");
 | 
				
			||||||
        if (!thumbnailInput || !thumbnailInput.value) {
 | 
					        updateSectionStatus(section);
 | 
				
			||||||
          showToast("❌ Thumbnail is required.", "error");
 | 
					        return;
 | 
				
			||||||
          updateSectionStatus(section);
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Menu section: check all menu items
 | 
					 | 
				
			||||||
      if (section === "menu") {
 | 
					      if (section === "menu") {
 | 
				
			||||||
        updateMenuItemsFromInputs();
 | 
					        updateMenuItemsFromInputs();
 | 
				
			||||||
        if (!menuItems.length || !menuItems.every(item => item.label && item.href)) {
 | 
					        if (!menuItems.length || !menuItems.every(item => item.label && item.href)) {
 | 
				
			||||||
@@ -609,7 +502,6 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Legals section: check all paragraphs
 | 
					 | 
				
			||||||
      if (section === "legals") {
 | 
					      if (section === "legals") {
 | 
				
			||||||
        updateIpParagraphsFromInputs();
 | 
					        updateIpParagraphsFromInputs();
 | 
				
			||||||
        if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
 | 
					        if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
 | 
				
			||||||
@@ -618,10 +510,8 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Build payload for this section only
 | 
					 | 
				
			||||||
      let payload = {};
 | 
					      let payload = {};
 | 
				
			||||||
      payload[section] = getSectionValues(section);
 | 
					      payload[section] = getSectionValues(section);
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const res = await fetch("/api/site-info", {
 | 
					      const res = await fetch("/api/site-info", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
@@ -630,7 +520,6 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Section saved!", "success");
 | 
					        showToast("✅ Section saved!", "success");
 | 
				
			||||||
        // Reload config for this section
 | 
					 | 
				
			||||||
        fetch("/api/site-info")
 | 
					        fetch("/api/site-info")
 | 
				
			||||||
          .then(res => res.json())
 | 
					          .then(res => res.json())
 | 
				
			||||||
          .then(data => {
 | 
					          .then(data => {
 | 
				
			||||||
@@ -642,5 +531,4 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -70,9 +70,11 @@ function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			|||||||
function setFontDropdown(selectId, value, options) {
 | 
					function setFontDropdown(selectId, value, options) {
 | 
				
			||||||
  const select = document.getElementById(selectId);
 | 
					  const select = document.getElementById(selectId);
 | 
				
			||||||
  if (!select) return;
 | 
					  if (!select) return;
 | 
				
			||||||
  select.innerHTML = options.map(opt =>
 | 
					  select.innerHTML = options.map(opt => {
 | 
				
			||||||
    `<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
 | 
					    // Remove extension if present
 | 
				
			||||||
  ).join("");
 | 
					    const base = opt.replace(/\.(woff2?|ttf|otf)$/, "");
 | 
				
			||||||
 | 
					    return `<option value="${base}"${base === value ? " selected" : ""}>${base}</option>`;
 | 
				
			||||||
 | 
					  }).join("");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setFallbackDropdown(selectId, value) {
 | 
					function setFallbackDropdown(selectId, value) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,107 +11,37 @@ function hideLoader() {
 | 
				
			|||||||
  if (loader) loader.classList.remove("active");
 | 
					  if (loader) loader.classList.remove("active");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Generic upload handler ---
 | 
				
			||||||
// --- Upload gallery images ---
 | 
					function setupUpload(inputId, apiUrl, loaderText, successMsg, refreshFn) {
 | 
				
			||||||
const galleryInput = document.getElementById('upload-gallery');
 | 
					  const input = document.getElementById(inputId);
 | 
				
			||||||
if (galleryInput) {
 | 
					  if (!input) return;
 | 
				
			||||||
  galleryInput.addEventListener('change', async (e) => {
 | 
					  input.addEventListener('change', async (e) => {
 | 
				
			||||||
    const files = e.target.files;
 | 
					    const files = e.target.files;
 | 
				
			||||||
    if (!files.length) return;
 | 
					    if (!files.length) return;
 | 
				
			||||||
    showLoader("Uploading photos...");
 | 
					    showLoader(loaderText);
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					    for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
					      const res = await fetch(apiUrl, { method: 'POST', body: formData });
 | 
				
			||||||
      const data = await res.json();
 | 
					      const data = await res.json();
 | 
				
			||||||
      hideLoader();
 | 
					      hideLoader();
 | 
				
			||||||
      if (res.ok) {
 | 
					      if (res.ok) {
 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
					        showToast(`✅ ${data.uploaded.length} ${successMsg}`, "success");
 | 
				
			||||||
        if (typeof refreshGallery === "function") refreshGallery();
 | 
					        if (typeof refreshFn === "function") refreshFn();
 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					      } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
    } catch(err) {
 | 
					    } catch(err) {
 | 
				
			||||||
      hideLoader();
 | 
					      hideLoader();
 | 
				
			||||||
      console.error(err);
 | 
					      console.error(err);
 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					      showToast('Server error!', "error");
 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					    } finally {
 | 
				
			||||||
 | 
					      e.target.value = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Upload hero images ---
 | 
					// --- Setup all upload inputs ---
 | 
				
			||||||
const heroInput = document.getElementById('upload-hero');
 | 
					setupUpload('upload-gallery', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
 | 
				
			||||||
if (heroInput) {
 | 
					setupUpload('upload-hero', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
 | 
				
			||||||
  heroInput.addEventListener('change', async (e) => {
 | 
					setupUpload('upload-gallery-bottom', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
 | 
				
			||||||
    const files = e.target.files;
 | 
					setupUpload('upload-hero-bottom', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
 | 
				
			||||||
    if (!files.length) return;
 | 
					 | 
				
			||||||
    showLoader("Uploading hero photos...");
 | 
					 | 
				
			||||||
    const formData = new FormData();
 | 
					 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
					 | 
				
			||||||
      const data = await res.json();
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      if (res.ok) {
 | 
					 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
					 | 
				
			||||||
        if (typeof refreshHero === "function") refreshHero();
 | 
					 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					 | 
				
			||||||
    } catch(err) {
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      console.error(err);
 | 
					 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Upload gallery images (bottom button) ---
 | 
					 | 
				
			||||||
const galleryInputBottom = document.getElementById('upload-gallery-bottom');
 | 
					 | 
				
			||||||
if (galleryInputBottom) {
 | 
					 | 
				
			||||||
  galleryInputBottom.addEventListener('change', async (e) => {
 | 
					 | 
				
			||||||
    const files = e.target.files;
 | 
					 | 
				
			||||||
    if (!files.length) return;
 | 
					 | 
				
			||||||
    showLoader("Uploading photos...");
 | 
					 | 
				
			||||||
    const formData = new FormData();
 | 
					 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
					 | 
				
			||||||
      const data = await res.json();
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      if (res.ok) {
 | 
					 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
					 | 
				
			||||||
        if (typeof refreshGallery === "function") refreshGallery();
 | 
					 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					 | 
				
			||||||
    } catch(err) {
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      console.error(err);
 | 
					 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Upload hero images (bottom button) ---
 | 
					 | 
				
			||||||
const heroInputBottom = document.getElementById('upload-hero-bottom');
 | 
					 | 
				
			||||||
if (heroInputBottom) {
 | 
					 | 
				
			||||||
  heroInputBottom.addEventListener('change', async (e) => {
 | 
					 | 
				
			||||||
    const files = e.target.files;
 | 
					 | 
				
			||||||
    if (!files.length) return;
 | 
					 | 
				
			||||||
    showLoader("Uploading hero photos...");
 | 
					 | 
				
			||||||
    const formData = new FormData();
 | 
					 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
					 | 
				
			||||||
      const data = await res.json();
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      if (res.ok) {
 | 
					 | 
				
			||||||
        showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
					 | 
				
			||||||
        if (typeof refreshHero === "function") refreshHero();
 | 
					 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					 | 
				
			||||||
    } catch(err) {
 | 
					 | 
				
			||||||
      hideLoader();
 | 
					 | 
				
			||||||
      console.error(err);
 | 
					 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					 | 
				
			||||||
    } finally { e.target.value = ''; }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user