Compare commits
15
Commits
b7fdcacf77
...
v2.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5b1c4234 | ||
|
|
2968e875a7 | ||
|
|
a7ce49b989 | ||
|
|
26e3ca684b | ||
|
|
a938234495 | ||
|
|
193b1704f8 | ||
|
|
9d56d729c1 | ||
|
|
40a01c4701 | ||
|
|
28e3b087b0 | ||
|
|
116a9574ce | ||
|
|
6c7216c0e1 | ||
|
|
7c98a6ceaf | ||
|
|
69d303c7a1 | ||
|
|
1d1d7d8304 | ||
|
|
205dcae2bc |
+11
-6
@@ -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"]
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
Lumeex is a static site generator designed to create minimalist photo galleries that highlight your artworks over the author. It empowers users to organize and explore images using tags, with each page load presenting photos in a random order to encourage discovery of new content.
|
Lumeex is a static site generator designed to create minimalist photo galleries that highlight your artworks over the author. It empowers users to organize and explore images using tags, with each page load presenting photos in a random order to encourage discovery of new content.
|
||||||
|
|
||||||
|
👉 __Check the online version : [Lumeex-web](https://lumeex.djeex.fr)__
|
||||||
|
|
||||||
The project includes two thoughtfully designed themes—one modern, one minimalistic—both crafted to keep the spotlight on your photos:
|
The project includes two thoughtfully designed themes—one modern, one minimalistic—both crafted to keep the spotlight on your photos:
|
||||||
|
|
||||||
- **Modern** — [View Demo](https://modern.djeex.fr)
|
- **Modern** — [View Demo](https://modern.djeex.fr)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CYAN="\033[1;36m"
|
CYAN="\033[1;36m"
|
||||||
@@ -6,27 +6,26 @@ 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/folders need to be copied..."
|
echo "[~] Checking if default config files/folders need to be copied..."
|
||||||
files_copied=false
|
files_copied=false
|
||||||
|
|
||||||
# Recursively check all files and folders in /app/default
|
for src in $(find /app/default -mindepth 1); do
|
||||||
while IFS= read -r src; do
|
|
||||||
relpath="${src#/app/default/}"
|
relpath="${src#/app/default/}"
|
||||||
target="/app/config/$relpath"
|
target="/app/config/$relpath"
|
||||||
if [ ! -e "$target" ]; then
|
if [ ! -e "$target" ]; then
|
||||||
echo "[→] Copying: $relpath"
|
echo "[→] Copying: $relpath"
|
||||||
if [ -d "$src" ]; then
|
if [ -d "$src" ]; then
|
||||||
cp -r "$src" "$target"
|
mkdir -p "$target"
|
||||||
|
cp -r "$src/" "$target/"
|
||||||
else
|
else
|
||||||
|
mkdir -p "$(dirname "$target")"
|
||||||
cp "$src" "$target"
|
cp "$src" "$target"
|
||||||
fi
|
fi
|
||||||
files_copied=true
|
files_copied=true
|
||||||
fi
|
fi
|
||||||
done < <(find /app/default -mindepth 1)
|
done
|
||||||
|
|
||||||
if [ "$files_copied" = true ]; then
|
if [ "$files_copied" = true ]; then
|
||||||
echo "[✓] Default configuration files/folders copied successfully."
|
echo "[✓] Default configuration files/folders copied successfully."
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||||||
img.classList.add("loaded");
|
img.classList.add("loaded");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Chrome HDR artefacting fix
|
||||||
|
const clearWillChange = (e) => {
|
||||||
|
if (e.propertyName !== "transform") return;
|
||||||
|
img.style.willChange = "auto";
|
||||||
|
img.removeEventListener("transitionend", clearWillChange);
|
||||||
|
};
|
||||||
|
img.addEventListener("transitionend", clearWillChange);
|
||||||
|
|
||||||
if (img.complete && img.naturalHeight !== 0) {
|
if (img.complete && img.naturalHeight !== 0) {
|
||||||
onLoad(); // already loaded
|
onLoad(); // already loaded
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def convert_and_resize_image(input_path, output_path, resize=True, max_width=114
|
|||||||
return
|
return
|
||||||
|
|
||||||
img = Image.open(input_path)
|
img = Image.open(input_path)
|
||||||
|
icc_profile = img.info.get("icc_profile")
|
||||||
if img.mode != "RGB":
|
if img.mode != "RGB":
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
@@ -27,7 +28,10 @@ def convert_and_resize_image(input_path, output_path, resize=True, max_width=114
|
|||||||
if fmt == "JPEG":
|
if fmt == "JPEG":
|
||||||
output_path = output_path.with_suffix(".jpg")
|
output_path = output_path.with_suffix(".jpg")
|
||||||
|
|
||||||
img.save(output_path, fmt, quality=90 if fmt == "JPEG" else 100)
|
save_kwargs = {"quality": 90 if fmt == "JPEG" else 100}
|
||||||
|
if icc_profile:
|
||||||
|
save_kwargs["icc_profile"] = icc_profile
|
||||||
|
img.save(output_path, fmt, **save_kwargs)
|
||||||
logging.info(f"[✓] Processed image: {input_path} → {output_path}")
|
logging.info(f"[✓] Processed image: {input_path} → {output_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+64
-157
@@ -1,19 +1,14 @@
|
|||||||
// --- 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 ---
|
// --- Fade-in helper ---
|
||||||
function applyFadeInImages(container) {
|
function applyFadeInImages(container) {
|
||||||
const fadeImages = container.querySelectorAll("img.fade-in-img");
|
container.querySelectorAll("img.fade-in-img").forEach(img => {
|
||||||
fadeImages.forEach(img => {
|
const onLoad = () => img.classList.add("loaded");
|
||||||
const onLoad = () => {
|
if (img.complete && img.naturalHeight !== 0) onLoad();
|
||||||
img.classList.add("loaded");
|
else {
|
||||||
};
|
|
||||||
if (img.complete && img.naturalHeight !== 0) {
|
|
||||||
onLoad();
|
|
||||||
} else {
|
|
||||||
img.addEventListener("load", onLoad, { once: true });
|
img.addEventListener("load", onLoad, { once: true });
|
||||||
img.addEventListener("error", () => {
|
img.addEventListener("error", () => {
|
||||||
console.warn("Image failed to load:", img.dataset?.src || img.src);
|
console.warn("Image failed to load:", img.dataset?.src || img.src);
|
||||||
@@ -25,13 +20,10 @@ function applyFadeInImages(container) {
|
|||||||
// --- 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);
|
||||||
@@ -43,25 +35,38 @@ 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 = `
|
||||||
@@ -77,41 +82,10 @@ 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');
|
|
||||||
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
|
|
||||||
applyFadeInImages(container);
|
applyFadeInImages(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +93,6 @@ function renderGallery() {
|
|||||||
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 = '';
|
||||||
|
|
||||||
@@ -127,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 = '×';
|
||||||
@@ -136,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);
|
||||||
});
|
});
|
||||||
@@ -146,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);
|
||||||
|
|
||||||
@@ -170,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);
|
||||||
@@ -201,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);
|
||||||
});
|
});
|
||||||
@@ -218,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') {
|
||||||
@@ -249,26 +207,22 @@ function renderTags(imgIndex, tags) {
|
|||||||
validateBtn.style.display = 'none';
|
validateBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
input.addEventListener('blur', () => {
|
||||||
suggestionBox.style.display = 'none';
|
suggestionBox.style.display = 'none';
|
||||||
input.value = '';
|
input.value = '';
|
||||||
validateBtn.style.display = 'none';
|
validateBtn.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
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';
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
updateSuggestions();
|
updateSuggestions();
|
||||||
if (!input.value.trim()) {
|
if (!input.value.trim()) suggestionBox.style.display = 'none';
|
||||||
suggestionBox.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Update tags in galleryImages array ---
|
// --- Update tags in galleryImages array ---
|
||||||
@@ -296,38 +250,7 @@ function renderHero() {
|
|||||||
`;
|
`;
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
|
updateCountAndButtons('hero', heroImages.length);
|
||||||
// Update hero count (top)
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade-in effect for loaded images
|
|
||||||
applyFadeInImages(container);
|
applyFadeInImages(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,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';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,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) {
|
|
||||||
showAllRadio.addEventListener('change', () => {
|
|
||||||
if (showAllRadio.checked) {
|
|
||||||
showOnlyUntagged = false;
|
showOnlyUntagged = false;
|
||||||
renderGallery();
|
renderGallery();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
showUntaggedRadio.addEventListener('change', () => {
|
if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
|
||||||
if (showUntaggedRadio.checked) {
|
|
||||||
showOnlyUntagged = true;
|
showOnlyUntagged = true;
|
||||||
renderGallery();
|
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();
|
||||||
+110
-222
@@ -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";
|
|
||||||
div.style.marginBottom = "6px";
|
|
||||||
div.innerHTML = `
|
|
||||||
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
|
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
|
||||||
<input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
|
<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>
|
<button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
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";
|
|
||||||
div.style.marginBottom = "6px";
|
|
||||||
div.innerHTML = `
|
|
||||||
<textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
|
<textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
|
||||||
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
|
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
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,7 +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();
|
||||||
|
} else {
|
||||||
|
showToast(result.error || "❌ Error removing theme", "error");
|
||||||
|
}
|
||||||
|
document.getElementById("delete-theme-modal").style.display = "none";
|
||||||
|
themeToDelete = null;
|
||||||
|
updateSectionStatus("build");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Theme select refresh ---
|
||||||
|
function refreshThemes() {
|
||||||
fetch("/api/themes")
|
fetch("/api/themes")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(themes => {
|
.then(themes => {
|
||||||
@@ -282,17 +242,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
option.textContent = theme;
|
option.textContent = theme;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
loadConfigAndUpdateBuildStatus();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
showToast(result.error || "❌ Error removing theme", "error");
|
|
||||||
}
|
|
||||||
deleteThemeModal.style.display = "none";
|
|
||||||
themeToDelete = null;
|
|
||||||
updateSectionStatus("build");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fetch theme list and populate select, then load config and update build status ---
|
// --- Config loading ---
|
||||||
let loadedConfig = {};
|
let loadedConfig = {};
|
||||||
function loadConfigAndUpdateBuildStatus() {
|
function loadConfigAndUpdateBuildStatus() {
|
||||||
fetch("/api/site-info")
|
fetch("/api/site-info")
|
||||||
@@ -300,17 +254,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
loadedConfig = data;
|
loadedConfig = data;
|
||||||
// Info
|
// Info
|
||||||
if (infoForm) {
|
if (forms.info) {
|
||||||
infoForm.elements["info.title"].value = data.info?.title || "";
|
forms.info.elements["info.title"].value = data.info?.title || "";
|
||||||
infoForm.elements["info.subtitle"].value = data.info?.subtitle || "";
|
forms.info.elements["info.subtitle"].value = data.info?.subtitle || "";
|
||||||
infoForm.elements["info.description"].value = data.info?.description || "";
|
forms.info.elements["info.description"].value = data.info?.description || "";
|
||||||
infoForm.elements["info.canonical"].value = data.info?.canonical || "";
|
forms.info.elements["info.canonical"].value = data.info?.canonical || "";
|
||||||
infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
|
forms.info.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
|
||||||
infoForm.elements["info.author"].value = data.info?.author || "";
|
forms.info.elements["info.author"].value = data.info?.author || "";
|
||||||
}
|
}
|
||||||
// Social
|
// Social
|
||||||
if (socialForm) {
|
if (forms.social) {
|
||||||
socialForm.elements["social.instagram_url"].value = data.social?.instagram_url || "";
|
forms.social.elements["social.instagram_url"].value = data.social?.instagram_url || "";
|
||||||
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
|
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
|
||||||
updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
|
updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
|
||||||
}
|
}
|
||||||
@@ -318,66 +272,37 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
||||||
renderMenuItems();
|
renderMenuItems();
|
||||||
// Footer
|
// Footer
|
||||||
if (footerForm) {
|
if (forms.footer) {
|
||||||
footerForm.elements["footer.copyright"].value = data.footer?.copyright || "";
|
forms.footer.elements["footer.copyright"].value = data.footer?.copyright || "";
|
||||||
footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || "";
|
forms.footer.elements["footer.legal_label"].value = data.footer?.legal_label || "";
|
||||||
}
|
}
|
||||||
// Legals
|
// Legals
|
||||||
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
||||||
? data.legals.intellectual_property
|
? data.legals.intellectual_property
|
||||||
: [];
|
: [];
|
||||||
renderIpParagraphs();
|
renderIpParagraphs();
|
||||||
if (legalsForm) {
|
if (forms.legals) {
|
||||||
legalsForm.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
||||||
legalsForm.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
|
forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
|
||||||
legalsForm.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
||||||
}
|
}
|
||||||
// Build
|
// Build
|
||||||
if (themeSelect) {
|
if (themeSelect) themeSelect.value = data.build?.theme || "";
|
||||||
themeSelect.value = data.build?.theme || "";
|
if (convertImagesCheckbox) convertImagesCheckbox.checked = !!data.build?.convert_images;
|
||||||
}
|
if (resizeImagesCheckbox) resizeImagesCheckbox.checked = !!data.build?.resize_images;
|
||||||
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);
|
["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
|
||||||
// For build, update status after theme select is set
|
|
||||||
updateSectionStatus("build");
|
updateSectionStatus("build");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (themeSelect) refreshThemes();
|
||||||
|
else loadConfigAndUpdateBuildStatus();
|
||||||
|
|
||||||
if (themeSelect) {
|
// --- Add/remove menu items ---
|
||||||
fetch("/api/themes")
|
if (addMenuBtn) addMenuBtn.addEventListener("click", () => {
|
||||||
.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);
|
|
||||||
});
|
|
||||||
// Now load config and update build status after theme select is ready
|
|
||||||
loadConfigAndUpdateBuildStatus();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If no theme select, just load config
|
|
||||||
loadConfigAndUpdateBuildStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Add menu item ---
|
|
||||||
if (addMenuBtn) {
|
|
||||||
addMenuBtn.addEventListener("click", () => {
|
|
||||||
menuItems.push({ label: "", href: "" });
|
menuItems.push({ label: "", href: "" });
|
||||||
renderMenuItems();
|
renderMenuItems();
|
||||||
updateSectionStatus("menu");
|
updateSectionStatus("menu");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// --- Remove menu item ---
|
|
||||||
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") {
|
|
||||||
if (!thumbnailInput || !thumbnailInput.value) {
|
|
||||||
showToast("❌ Thumbnail is required.", "error");
|
showToast("❌ Thumbnail is required.", "error");
|
||||||
updateSectionStatus(section);
|
updateSectionStatus(section);
|
||||||
return;
|
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", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
+17
-87
@@ -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