Compare commits
58 Commits
cb91b92555
...
Beta-2.1
Author | SHA1 | Date | |
---|---|---|---|
6c7216c0e1 | |||
69d303c7a1 | |||
1d1d7d8304 | |||
205dcae2bc | |||
b7fdcacf77 | |||
9cdf6bbd32 | |||
f92060603d | |||
54e9281793 | |||
5fb6ef18d1 | |||
1119647884 | |||
2cc0a213c9 | |||
24113a4aa8 | |||
be8ce4f62c | |||
edca473e29 | |||
eac90820d2 | |||
191fa82711 | |||
b17652e471 | |||
65fd62e342 | |||
6b03ee30fa | |||
fd45ebbd53 | |||
795f5fbd13 | |||
f8bebb9c95 | |||
3198755576 | |||
f98f2d598f | |||
e8718e71ab | |||
5b65e5efe3 | |||
021e0c7974 | |||
debbf07280 | |||
df96782500 | |||
febf4d2be5 | |||
617545e1bb | |||
922ce99679 | |||
cd0428990a | |||
d3af86be8c | |||
c825798b13 | |||
b5375343a8 | |||
757e676d2d | |||
0079c166e8 | |||
ee6d4a1fa2 | |||
c6c3162b83 | |||
04c1214cd1 | |||
b03779b487 | |||
b5f8ceeb31 | |||
1591886505 | |||
a6b63c2d2b | |||
8a04fe5aa6 | |||
2cb171806c | |||
ded97700d9 | |||
8533ce72e9 | |||
b2ba1d7c7f | |||
5d238fcf33 | |||
7675b90909 | |||
a916c80c2a | |||
7a95ef0255 | |||
906699f023 | |||
643a729f94 | |||
a02da47e73 | |||
f7f2356510 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.*
|
.*
|
||||||
|
!.env
|
||||||
!.sh
|
!.sh
|
||||||
!.gitignore
|
!.gitignore
|
||||||
output/
|
output/
|
||||||
|
@ -6,9 +6,8 @@ COPY requirements.txt .
|
|||||||
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY build.py gallery.py VERSION /app/
|
||||||
COPY ./src/ ./src/
|
COPY ./src/ ./src/
|
||||||
COPY ./build.py ./build.py
|
|
||||||
COPY ./gallery.py ./gallery.py
|
|
||||||
COPY ./config /app/default
|
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
|
||||||
|
22
README.MD
22
README.MD
@ -18,7 +18,7 @@ The project includes two thoughtfully designed themes—one modern, one minimali
|
|||||||
- **Typewriter** — [View Demo](https://typewriter.djeex.fr)
|
- **Typewriter** — [View Demo](https://typewriter.djeex.fr)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This GitHub repository is a mirror of the primary source at [git.djeex.fr/Djeex/lumeex](https://git.djeex.fr/Djeex/lumeex). The main repository includes the full history, releases, and bug-checking assisted by an LLM.
|
> _This GitHub repository is a mirror of the primary source at [git.djeex.fr/Djeex/lumeex](https://git.djeex.fr/Djeex/lumeex). The main repository includes the full history and releases_.
|
||||||
|
|
||||||
|
|
||||||
## 📌 Table of Contents
|
## 📌 Table of Contents
|
||||||
@ -41,20 +41,26 @@ The project includes two thoughtfully designed themes—one modern, one minimali
|
|||||||
- Typewriter — [Demo](https://typewriter.djeex.fr)
|
- Typewriter — [Demo](https://typewriter.djeex.fr)
|
||||||
- Supports Google Fonts and locally hosted fonts
|
- Supports Google Fonts and locally hosted fonts
|
||||||
|
|
||||||
### No-Code Builder (YAML Based)
|
### No-Code Builder (WebUI Manager)
|
||||||
|
|
||||||
- Configure site info, SEO, colors, fonts, and more through simple YAML files
|
|
||||||
- Reference and tag photos without any coding required
|
<div align="center">
|
||||||
- *(Optional)* Automatically update photo references via script
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
### Simple Build Process
|
|
||||||
|
|
||||||
- Compiles static site from YAML configuration files (themes, templates, fonts, colors)
|
- Configure site info, SEO, colors, fonts, and more through a simple convenient WebUI
|
||||||
|
- Add and tag your photo photos without any coding required
|
||||||
- Converts favicon automatically to all required formats
|
- Converts favicon automatically to all required formats
|
||||||
- Resizes social sharing thumbnails
|
- Resizes social sharing thumbnails
|
||||||
- *(Optional)* Automatically resizes photos to a maximum width of 1140px
|
- *(Optional)* Automatically resizes photos to a maximum width of 1140px
|
||||||
- *(Optional)* Converts images to WebP format for optimized performance
|
- *(Optional)* Converts images to WebP format for optimized performance
|
||||||
- Outputs a complete static website ready to deploy on any web server
|
- Build your static site in one click and get a zip archive or an output folder, ready to deploy to your preferred webserver
|
||||||
|
|
||||||
|
### Don't want a WebUI ?
|
||||||
|
|
||||||
|
- CLI process is documented
|
||||||
|
|
||||||
## 🐳 Docker or 🐍 Python Installation
|
## 🐳 Docker or 🐍 Python Installation
|
||||||
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
|
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# Please change this by your settings.
|
|
||||||
info:
|
info:
|
||||||
title:
|
title:
|
||||||
subtitle:
|
subtitle:
|
||||||
@ -9,7 +8,7 @@ info:
|
|||||||
|
|
||||||
social:
|
social:
|
||||||
instagram_url:
|
instagram_url:
|
||||||
thumbnail:
|
thumbnail: ''
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
items:
|
items:
|
||||||
@ -18,19 +17,17 @@ menu:
|
|||||||
|
|
||||||
footer:
|
footer:
|
||||||
copyright: Copyright © 2025
|
copyright: Copyright © 2025
|
||||||
legal_link: '/legals/'
|
legal_link: /legals/
|
||||||
legal_label: Legal notice
|
legal_label: Legal notice
|
||||||
|
|
||||||
# Build parameters
|
|
||||||
build:
|
build:
|
||||||
theme: modern # choose a theme in config/theme folder
|
theme: modern
|
||||||
convert_images: true # true to enable image conversion
|
convert_images: true
|
||||||
resize_images: true # true to enable image resizing
|
resize_images: true
|
||||||
|
|
||||||
# Change this by your legals
|
|
||||||
legals:
|
legals:
|
||||||
hoster_name:
|
hoster_name:
|
||||||
hoster_adress:
|
hoster_address:
|
||||||
hoster_contact:
|
hoster_contact:
|
||||||
intellectual_property:
|
intellectual_property:
|
||||||
- paragraph: ""
|
- paragraph: ''
|
||||||
|
@ -35,16 +35,13 @@ img, tag {
|
|||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 15px 15px 0 0;
|
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@ -53,7 +50,7 @@ img, tag {
|
|||||||
|
|
||||||
.hero-background {
|
.hero-background {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
#-----------------------------------#
|
|
||||||
# Modern theme for Lumeex #
|
|
||||||
# https://git.djeex.fr/Djeex/lumeex #
|
|
||||||
#-----------------------------------#
|
|
||||||
colors:
|
colors:
|
||||||
primary: '#0065a1'
|
primary: '#0065A1'
|
||||||
primary_dark: '#005384'
|
primary_dark: '#005384'
|
||||||
secondary: '#00b0f0'
|
secondary: '#00B0F0'
|
||||||
accent: '#ffc700'
|
accent: '#FFC700'
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#ffffff'
|
background: '#FFFFFF'
|
||||||
browser_color: '#ffffff'
|
browser_color: '#FFFFFF'
|
||||||
|
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
primary:
|
||||||
|
fallback: sans-serif
|
||||||
|
name: Lato
|
||||||
|
secondary:
|
||||||
|
fallback: serif
|
||||||
|
name: Montserrat
|
||||||
|
|
||||||
google_fonts:
|
google_fonts:
|
||||||
- family: Lato
|
- family: 'Lato'
|
||||||
weights:
|
weights:
|
||||||
- '200'
|
- '200'
|
||||||
- '400'
|
- '400'
|
||||||
@ -23,10 +29,3 @@ google_fonts:
|
|||||||
- '200'
|
- '200'
|
||||||
- '400'
|
- '400'
|
||||||
- '700'
|
- '700'
|
||||||
fonts:
|
|
||||||
primary:
|
|
||||||
name: Lato
|
|
||||||
fallback: sans-serif
|
|
||||||
secondary:
|
|
||||||
name: Montserrat
|
|
||||||
fallback: serif
|
|
@ -1,21 +1,17 @@
|
|||||||
#-----------------------------------#
|
|
||||||
# Typewriter theme for Lumeex #
|
|
||||||
# https://git.djeex.fr/Djeex/lumeex #
|
|
||||||
#-----------------------------------#
|
|
||||||
colors:
|
colors:
|
||||||
primary: '#0065a1'
|
accent: '#FFC700'
|
||||||
|
background: '#FFFFFF'
|
||||||
|
browser_color: '#FFFFFF'
|
||||||
|
primary: '#0065A1'
|
||||||
primary_dark: '#005384'
|
primary_dark: '#005384'
|
||||||
secondary: '#00b0f0'
|
secondary: '#00B0F0'
|
||||||
accent: '#ffc700'
|
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#ffffff'
|
|
||||||
browser_color: '#ffffff'
|
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
fonts:
|
fonts:
|
||||||
primary:
|
primary:
|
||||||
name: Trixie
|
name: trixie
|
||||||
fallback: sans-serif
|
fallback: sans-serif
|
||||||
secondary:
|
secondary:
|
||||||
name: Trixie
|
name: trixie
|
||||||
fallback: serif
|
fallback: serif
|
@ -1,19 +1,25 @@
|
|||||||
#-----------------------------------#
|
|
||||||
# Modern theme for Lumeex #
|
|
||||||
# https://git.djeex.fr/Djeex/lumeex #
|
|
||||||
#-----------------------------------#
|
|
||||||
colors:
|
colors:
|
||||||
primary: '#0065a1'
|
primary: '#0065A1'
|
||||||
primary_dark: '#005384'
|
primary_dark: '#005384'
|
||||||
secondary: '#00b0f0'
|
secondary: '#00B0F0'
|
||||||
accent: '#ffc700'
|
accent: '#FFC700'
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#fff'
|
background: '#FFFFFF'
|
||||||
browser_color: '#fff'
|
browser_color: '#FFFFFF'
|
||||||
|
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
primary:
|
||||||
|
fallback: sans-serif
|
||||||
|
name: Lato
|
||||||
|
secondary:
|
||||||
|
fallback: serif
|
||||||
|
name: Montserrat
|
||||||
|
|
||||||
google_fonts:
|
google_fonts:
|
||||||
- family: Lato
|
- family: ''
|
||||||
weights:
|
weights:
|
||||||
- '200'
|
- '200'
|
||||||
- '400'
|
- '400'
|
||||||
@ -23,10 +29,3 @@ google_fonts:
|
|||||||
- '200'
|
- '200'
|
||||||
- '400'
|
- '400'
|
||||||
- '700'
|
- '700'
|
||||||
fonts:
|
|
||||||
primary:
|
|
||||||
name: Lato
|
|
||||||
fallback: sans-serif
|
|
||||||
secondary:
|
|
||||||
name: Montserrat
|
|
||||||
fallback: serif
|
|
@ -1,21 +1,19 @@
|
|||||||
#-----------------------------------#
|
|
||||||
# Typewriter theme for Lumeex #
|
|
||||||
# https://git.djeex.fr/Djeex/lumeex #
|
|
||||||
#-----------------------------------#
|
|
||||||
colors:
|
colors:
|
||||||
primary: '#0065a1'
|
accent: '#FFC700'
|
||||||
|
background: '#FFFFFF'
|
||||||
|
browser_color: '#FFFFFF'
|
||||||
|
primary: '#0065A1'
|
||||||
primary_dark: '#005384'
|
primary_dark: '#005384'
|
||||||
secondary: '#00b0f0'
|
secondary: '#00B0F0'
|
||||||
accent: '#ffc700'
|
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#fff'
|
|
||||||
browser_color: '#fff'
|
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
primary:
|
primary:
|
||||||
name: Trixie
|
name: trixie.woff
|
||||||
fallback: sans-serif
|
fallback: sans-serif
|
||||||
secondary:
|
secondary:
|
||||||
name: Trixie
|
name: trixie.woff
|
||||||
fallback: serif
|
fallback: serif
|
2
docker/.env
Normal file
2
docker/.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PREVIEW_PORT=3000
|
||||||
|
WEBUI_PORT=5000
|
@ -5,29 +5,32 @@ CYAN="\033[1;36m"
|
|||||||
NC="\033[0m"
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,16 +46,26 @@ start_server() {
|
|||||||
cat /tmp/build_logs_fifo >&2 &
|
cat /tmp/build_logs_fifo >&2 &
|
||||||
cat /tmp/build_logs_fifo2 >&2 &
|
cat /tmp/build_logs_fifo2 >&2 &
|
||||||
|
|
||||||
echo "Starting HTTP server on port 3000..."
|
PREVIEW_PORT="${PREVIEW_PORT:-3000}"
|
||||||
|
echo "[~]Starting preview HTTP server on port 3000..."
|
||||||
|
echo "[i] Preview host port is set to: ${PREVIEW_PORT}"
|
||||||
python3 -u -m http.server 3000 -d /app/output &
|
python3 -u -m http.server 3000 -d /app/output &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
trap "echo 'Stopping server...'; kill -TERM $SERVER_PID 2>/dev/null; wait $SERVER_PID; exit 0" SIGINT SIGTERM
|
|
||||||
|
echo "[~] Starting Lumeex Flask webui..."
|
||||||
|
python3 -u -m src.py.webui.webui &
|
||||||
|
WEBUI_PID=$!
|
||||||
|
|
||||||
|
trap "echo 'Stopping servers...'; kill -TERM $SERVER_PID $WEBUI_PID 2>/dev/null; wait $SERVER_PID $WEBUI_PID; exit 0" SIGINT SIGTERM
|
||||||
|
|
||||||
wait $SERVER_PID
|
wait $SERVER_PID
|
||||||
|
wait $WEBUI_PID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VERSION=$(cat VERSION)
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
|
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
|
||||||
echo -e "${CYAN}│${NC} Lum${CYAN}eex${NC} - Version 1.3.1${NC} ${CYAN}│${NC}"
|
echo -e "${CYAN}│${NC} Lum${CYAN}eex${NC} - Version ${VERSION}${NC} ${CYAN}│${NC}"
|
||||||
echo -e "${CYAN}├───────────────────────────────────────────┤${NC}"
|
echo -e "${CYAN}├───────────────────────────────────────────┤${NC}"
|
||||||
echo -e "${CYAN}│${NC} Source: https://git.djeex.fr/Djeex/lumeex ${CYAN}│${NC}"
|
echo -e "${CYAN}│${NC} Source: https://git.djeex.fr/Djeex/lumeex ${CYAN}│${NC}"
|
||||||
echo -e "${CYAN}│${NC} Mirror: https://github.com/Djeex/lumeex ${CYAN}│${NC}"
|
echo -e "${CYAN}│${NC} Mirror: https://github.com/Djeex/lumeex ${CYAN}│${NC}"
|
||||||
@ -64,15 +77,15 @@ fi
|
|||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
build)
|
build)
|
||||||
echo "Running build.py..."
|
echo "[~] Running build.py..."
|
||||||
python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
|
python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
|
||||||
;;
|
;;
|
||||||
gallery)
|
gallery)
|
||||||
echo "Running gallery.py..."
|
echo "[~] Running gallery.py..."
|
||||||
python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
|
python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown command: $1"
|
echo "[!] Unknown command: $1"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
@ -2,9 +2,15 @@ services:
|
|||||||
lumeex:
|
lumeex:
|
||||||
container_name: lmx
|
container_name: lmx
|
||||||
build: ..
|
build: ..
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- PREVIEW_PORT=${PREVIEW_PORT:-3000} # port for preview server - set it in .env file
|
||||||
|
- WEBUI_PORT=${WEBUI_PORT:-5000} # port for webui server - set it in .env file
|
||||||
volumes:
|
volumes:
|
||||||
- ../config:/app/config # mount config directory
|
- ../config:/app/config # mount config directory
|
||||||
- ../output:/app/output # mount output directory
|
- ../output:/app/output # mount output directory
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "${PREVIEW_PORT:-3000}:3000"
|
||||||
|
- "${WEBUI_PORT:-5000}:5000"
|
||||||
|
|
BIN
illustration/lumeex-webui.png
Normal file
BIN
illustration/lumeex-webui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 620 KiB |
@ -9,7 +9,6 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const img = entry.target;
|
const img = entry.target;
|
||||||
console.log("Lazy-loading image:", img.dataset.src);
|
|
||||||
img.src = img.dataset.src;
|
img.src = img.dataset.src;
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
img.classList.add("loaded");
|
img.classList.add("loaded");
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
|
|
||||||
// Fade in effect for elements with class 'appear'
|
// Fade in effect for elements with class 'appear'
|
||||||
const setupIntersectionObserver = () => {
|
const setupIntersectionObserver = () => {
|
||||||
|
document.querySelectorAll('.appear').forEach(parent => {
|
||||||
|
const children = parent.querySelectorAll('.appear');
|
||||||
|
children.forEach((child, i) => {
|
||||||
|
child.style.transitionDelay = `${i * 0.2}s`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const items = document.querySelectorAll('.appear');
|
const items = document.querySelectorAll('.appear');
|
||||||
const io = new IntersectionObserver((entries) => {
|
const io = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
@ -32,6 +39,7 @@ const randomizeHeroBackground = () => {
|
|||||||
if (images.length === 0) return;
|
if (images.length === 0) return;
|
||||||
let currentIndex = Math.floor(Math.random() * images.length);
|
let currentIndex = Math.floor(Math.random() * images.length);
|
||||||
heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
|
heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
|
||||||
|
if (images.length < 2) return; // <-- Prevent interval if only one image
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
let nextIndex;
|
let nextIndex;
|
||||||
do {
|
do {
|
||||||
|
@ -78,6 +78,8 @@ html,body {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height:1.5;
|
line-height:1.5;
|
||||||
color:var(--color-primary-dark);
|
color:var(--color-primary-dark);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@ -179,8 +181,8 @@ h2 {
|
|||||||
/* animation */
|
/* animation */
|
||||||
|
|
||||||
.appear {
|
.appear {
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 1s;
|
||||||
transition: all 0.3s;
|
transition: all 1s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
-webkit-transform: translateY(20px);
|
-webkit-transform: translateY(20px);
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
@ -192,36 +194,6 @@ h2 {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appear.inview:nth-child(1) {
|
|
||||||
-webkit-transition-delay: 0s;
|
|
||||||
transition-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appear.inview:nth-child(2) {
|
|
||||||
-webkit-transition-delay: 0.2s;
|
|
||||||
transition-delay: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appear.inview:nth-child(3) {
|
|
||||||
-webkit-transition-delay: 0.4s;
|
|
||||||
transition-delay: 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appear.inview:nth-child(4) {
|
|
||||||
-webkit-transition-delay: 0.6s;
|
|
||||||
transition-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appear.inview:nth-child(5) {
|
|
||||||
-webkit-transition-delay: 0.8s;
|
|
||||||
transition-delay: 0.8s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appear.inview:nth-child(6) {
|
|
||||||
-webkit-transition-delay: 1s;
|
|
||||||
transition-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* img fade in */
|
/* img fade in */
|
||||||
|
|
||||||
.fade-in-img {
|
.fade-in-img {
|
||||||
@ -267,12 +239,23 @@ h2 {
|
|||||||
/* Hero */
|
/* Hero */
|
||||||
|
|
||||||
#hero {
|
#hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero .section {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#hero .content-wrapper, #hero .section {
|
#hero .content-wrapper, #hero .section {
|
||||||
height:100%;
|
height:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-background {
|
.hero-background {
|
||||||
height: 66%;
|
height: 66%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -333,7 +316,6 @@ h2 {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
@ -371,6 +353,11 @@ h2 {
|
|||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
margin: auto 0 0 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
@ -491,7 +478,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section img {
|
.section img {
|
||||||
margin: 0px 0 60px 0;
|
margin: 0px 0 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@ -500,8 +487,7 @@ h2 {
|
|||||||
|
|
||||||
#legals.content-wrapper {
|
#legals.content-wrapper {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin: auto;
|
margin: 50px auto;
|
||||||
margin-top: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legals-content {
|
.legals-content {
|
||||||
|
@ -21,12 +21,14 @@ STYLE_DIR = SRC_DIR / "src/public/style"
|
|||||||
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
|
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
|
||||||
SITE_FILE = SRC_DIR / "config/site.yaml"
|
SITE_FILE = SRC_DIR / "config/site.yaml"
|
||||||
THEMES_DIR = SRC_DIR / "config/themes"
|
THEMES_DIR = SRC_DIR / "config/themes"
|
||||||
|
VERSION_FILE = SRC_DIR / "VERSION"
|
||||||
|
with open(VERSION_FILE, "r") as vf:
|
||||||
|
build_version = vf.read().strip()
|
||||||
|
|
||||||
def build():
|
def build():
|
||||||
build_version = "v1.3.1"
|
|
||||||
logging.info("\n")
|
logging.info("\n")
|
||||||
logging.info("=" * 24)
|
logging.info("=" * 24)
|
||||||
logging.info(f"🚀 Lumeex builder {build_version}")
|
logging.info(f"🚀 Lumeex builder v{build_version}")
|
||||||
logging.info("=" * 24)
|
logging.info("=" * 24)
|
||||||
logging.info("\n === Starting build === ")
|
logging.info("\n === Starting build === ")
|
||||||
ensure_dir(BUILD_DIR)
|
ensure_dir(BUILD_DIR)
|
||||||
|
@ -18,6 +18,10 @@ from src.py.webui.upload import upload_bp
|
|||||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
# --- Flask app setup ---
|
# --- Flask app setup ---
|
||||||
|
VERSION_FILE = Path(__file__).resolve().parents[3] / "VERSION"
|
||||||
|
with open(VERSION_FILE, "r") as vf:
|
||||||
|
lumeex_version = vf.read().strip()
|
||||||
|
|
||||||
WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates
|
WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates
|
||||||
app = Flask(
|
app = Flask(
|
||||||
__name__,
|
__name__,
|
||||||
@ -26,6 +30,8 @@ app = Flask(
|
|||||||
static_url_path=""
|
static_url_path=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
WEBUI_PORT = int(os.getenv("WEBUI_PORT", 5000))
|
||||||
|
|
||||||
# --- Config paths ---
|
# --- Config paths ---
|
||||||
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||||
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
|
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
|
||||||
@ -68,6 +74,15 @@ def get_local_fonts(theme_name):
|
|||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
|
PREVIEW_PORT = int(os.getenv("PREVIEW_PORT", 3000))
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_version():
|
||||||
|
return dict(
|
||||||
|
lumeex_version=lumeex_version,
|
||||||
|
preview_port=PREVIEW_PORT
|
||||||
|
)
|
||||||
|
|
||||||
# --- Gallery & Hero API ---
|
# --- Gallery & Hero API ---
|
||||||
@app.route("/gallery-editor")
|
@app.route("/gallery-editor")
|
||||||
def gallery_editor():
|
def gallery_editor():
|
||||||
@ -195,9 +210,21 @@ def get_site_info():
|
|||||||
@app.route("/api/site-info", methods=["POST"])
|
@app.route("/api/site-info", methods=["POST"])
|
||||||
def update_site_info():
|
def update_site_info():
|
||||||
"""Update site info YAML."""
|
"""Update site info YAML."""
|
||||||
data = request.json
|
new_data = request.json
|
||||||
|
with open(SITE_YAML, "r") as f:
|
||||||
|
old_data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
def deep_merge(old, new):
|
||||||
|
for k, v in new.items():
|
||||||
|
if isinstance(v, dict) and isinstance(old.get(k), dict):
|
||||||
|
old[k] = deep_merge(old[k], v)
|
||||||
|
else:
|
||||||
|
old[k] = v
|
||||||
|
return old
|
||||||
|
|
||||||
|
merged = deep_merge(old_data, new_data)
|
||||||
with open(SITE_YAML, "w") as f:
|
with open(SITE_YAML, "w") as f:
|
||||||
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
yaml.safe_dump(merged, f, sort_keys=False, allow_unicode=True)
|
||||||
return jsonify({"status": "ok"})
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
# --- Theme management ---
|
# --- Theme management ---
|
||||||
@ -388,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():
|
||||||
@ -479,5 +507,6 @@ def download_output_zip():
|
|||||||
|
|
||||||
# --- Run server ---
|
# --- Run server ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.info("Starting WebUI at http://127.0.0.1:5000")
|
logging.info("[~] Starting WebUI at http://0.0.0.0:5000")
|
||||||
app.run(debug=True)
|
logging.info(f"[i] WebUI host port is set to {WEBUI_PORT}")
|
||||||
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="inner bottom-link appear">
|
<div class="inner bottom-link appear">
|
||||||
<p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"> • </span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p>
|
<p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"> • </span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p>
|
||||||
<p class="navigation-subtitle appear"> Built with <a href="https://git.djeex.fr/Djeex/lumeex">Lumeex</a></p>
|
<p class="navigation-subtitle appear"> Built with <a href="https://lumeex.djeex.fr">Lumeex</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,48 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
{% extends "template/base.html" %}
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
{% block title %}Lumeex - Gallery Editor{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Lumeex</title>
|
{% block content %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Top bar -->
|
|
||||||
<div class="nav-bar">
|
|
||||||
<div class="content-inner nav">
|
|
||||||
<input type="checkbox" id="nav-check">
|
|
||||||
<div class="nav-header">
|
|
||||||
<div class="nav-title">
|
|
||||||
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="nav-btn">
|
|
||||||
<label for="nav-check">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="nav-links">
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
|
||||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
|
||||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<button id="build-btn" class="button">Build !</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Toast container for notifications -->
|
|
||||||
<div class="content-inner">
|
|
||||||
<div id="toast-container"></div>
|
|
||||||
|
|
||||||
<h1>Gallery editor</h1>
|
<h1>Gallery editor</h1>
|
||||||
|
|
||||||
<!-- Hero Upload Section -->
|
<!-- Hero Upload Section -->
|
||||||
<div class="upload-section">
|
<div class="section">
|
||||||
<h2>Title Carrousel</h2>
|
<h2>Title Carrousel</h2>
|
||||||
<p> Select photos to display in the Title Carrousel</p>
|
<p> Select photos to display in the Title Carrousel</p>
|
||||||
<div class="upload-actions-row">
|
<div class="upload-actions-row">
|
||||||
@ -52,11 +18,20 @@
|
|||||||
<button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
|
<button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
||||||
|
<span id="hero-count" class="photo-count"></span>
|
||||||
<div id="hero"></div>
|
<div id="hero"></div>
|
||||||
|
<input type="file" id="upload-hero-bottom" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
||||||
|
<div id="bottom-hero-upload" class="upload-actions-row bottom-action-row">
|
||||||
|
<span id="hero-count-bottom" class="photo-count"></span>
|
||||||
|
<label for="upload-hero-bottom" class="up-btn">
|
||||||
|
📸 Upload photos
|
||||||
|
</label>
|
||||||
|
<button id="remove-all-hero-bottom" class="up-btn bottom-remove-btn">🗑 Delete all</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gallery Upload Section -->
|
<!-- Gallery Upload Section -->
|
||||||
<div class="upload-section">
|
<div class="section">
|
||||||
<h2>Gallery</h2>
|
<h2>Gallery</h2>
|
||||||
<p> Select and tags photos to display in the Gallery</p>
|
<p> Select and tags photos to display in the Gallery</p>
|
||||||
<div class="upload-actions-row">
|
<div class="upload-actions-row">
|
||||||
@ -65,12 +40,41 @@
|
|||||||
</label>
|
</label>
|
||||||
<button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
|
<button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="gallery-filter" id="show-all-radio" checked>
|
||||||
|
All
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="gallery-filter" id="show-untagged-radio">
|
||||||
|
Untagged
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span id="gallery-count" class="photo-count"></span>
|
||||||
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
||||||
<div id="gallery"></div>
|
<div id="gallery"></div>
|
||||||
|
<input type="file" id="upload-gallery-bottom" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
|
||||||
|
<div id="bottom-gallery-upload" class="upload-actions-row bottom-action-row">
|
||||||
|
<span id="gallery-count-bottom" class="photo-count"></span>
|
||||||
|
<label for="upload-gallery-bottom" class="up-btn">
|
||||||
|
📸 Upload photos
|
||||||
|
</label>
|
||||||
|
<button id="remove-all-gallery-bottom" class="up-btn bottom-remove-btn">🗑 Delete all</button>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
|
<div class="section">
|
||||||
|
<h2>Steps</h2>
|
||||||
|
<p> Follow the steps to generate your static gallery</p>
|
||||||
|
<ul id="stepper">
|
||||||
|
<li><a class="step-active" href="/gallery-editor">Upload your photos</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/site-info">Configure site info</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/theme-editor">Customize your theme</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Delete confirmation modal -->
|
<!-- Delete confirmation modal -->
|
||||||
<div id="delete-modal" class="modal" style="display:none;">
|
<div id="delete-modal" class="modal" style="display:none;">
|
||||||
@ -84,15 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Build success modal -->
|
|
||||||
<div id="build-success-modal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
{% endblock %}
|
||||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
|
||||||
<h3>✅ Build completed!</h3>
|
{% block scripts %}
|
||||||
<p>Your files are available in the output folder.</p>
|
<script src="{{ url_for('static', filename='js/gallery-editor.js') }}"></script>
|
||||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
||||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
{% endblock %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
154
src/webui/img/favicon.svg
Normal file
154
src/webui/img/favicon.svg
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1000">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: url(#Dégradé_sans_nom_265);
|
||||||
|
stroke: url(#Dégradé_sans_nom_33);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st0, .st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11 {
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: url(#Dégradé_sans_nom_269);
|
||||||
|
stroke: url(#Dégradé_sans_nom_334);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st2 {
|
||||||
|
fill: url(#Dégradé_sans_nom_268);
|
||||||
|
stroke: url(#Dégradé_sans_nom_333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st3 {
|
||||||
|
fill: url(#Dégradé_sans_nom_266);
|
||||||
|
stroke: url(#Dégradé_sans_nom_331);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st4 {
|
||||||
|
fill: url(#Dégradé_sans_nom_267);
|
||||||
|
stroke: url(#Dégradé_sans_nom_332);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st12 {
|
||||||
|
fill: url(#Dégradé_sans_nom_261);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st13 {
|
||||||
|
fill: url(#Dégradé_sans_nom_262);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st14 {
|
||||||
|
fill: url(#Dégradé_sans_nom_264);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st15 {
|
||||||
|
fill: url(#Dégradé_sans_nom_263);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st5 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2616);
|
||||||
|
stroke: url(#Dégradé_sans_nom_3311);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st6 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2615);
|
||||||
|
stroke: url(#Dégradé_sans_nom_3310);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st16 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st17 {
|
||||||
|
fill: url(#Dégradé_sans_nom_26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st7 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2610);
|
||||||
|
stroke: url(#Dégradé_sans_nom_335);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st8 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2613);
|
||||||
|
stroke: url(#Dégradé_sans_nom_338);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st9 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2614);
|
||||||
|
stroke: url(#Dégradé_sans_nom_339);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st10 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2611);
|
||||||
|
stroke: url(#Dégradé_sans_nom_336);
|
||||||
|
}
|
||||||
|
|
||||||
|
.st11 {
|
||||||
|
fill: url(#Dégradé_sans_nom_2612);
|
||||||
|
stroke: url(#Dégradé_sans_nom_337);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#55c3ec"/>
|
||||||
|
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#55c3ec"/>
|
||||||
|
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
|
||||||
|
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
|
||||||
|
</defs>
|
||||||
|
<g id="Calque_1">
|
||||||
|
<circle class="st16" cx="499.5" cy="499.5" r="499.5"/>
|
||||||
|
</g>
|
||||||
|
<g id="Calque_2">
|
||||||
|
<g id="Calque_3">
|
||||||
|
<ellipse class="st17" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
|
||||||
|
<ellipse class="st12" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
|
||||||
|
<ellipse class="st13" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
|
||||||
|
<ellipse class="st15" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
|
||||||
|
<ellipse class="st14" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
|
||||||
|
<circle class="st0" cx="90.4" cy="576" r="22.4"/>
|
||||||
|
<circle class="st3" cx="175.6" cy="607.9" r="13.1"/>
|
||||||
|
<circle class="st4" cx="140.8" cy="691.6" r="28"/>
|
||||||
|
<circle class="st2" cx="829.7" cy="602.6" r="28"/>
|
||||||
|
<circle class="st1" cx="908.9" cy="562.1" r="13.1"/>
|
||||||
|
<circle class="st7" cx="840.9" cy="698.1" r="22.4"/>
|
||||||
|
<circle class="st10" cx="466.1" cy="876.5" r="22.5"/>
|
||||||
|
<circle class="st11" cx="538.6" cy="839.8" r="13.1"/>
|
||||||
|
<circle class="st8" cx="686.1" cy="170.1" r="28"/>
|
||||||
|
<circle class="st9" cx="733.7" cy="247.7" r="13.1"/>
|
||||||
|
<circle class="st6" cx="236.9" cy="206.5" r="21.1"/>
|
||||||
|
<circle class="st5" cx="315.4" cy="164.9" r="13.1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.6 KiB |
2
src/webui/img/gitea.svg
Normal file
2
src/webui/img/gitea.svg
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#48cf51ff" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gitea icon</title><path d="M4.186 5.421C2.341 5.417-.13 6.59.006 9.531c.213 4.594 4.92 5.02 6.801 5.057.206.862 2.42 3.834 4.059 3.99h7.18c4.306-.286 7.53-13.022 5.14-13.07-3.953.186-6.296.28-8.305.296v3.975l-.626-.277-.004-3.696c-2.306-.001-4.336-.108-8.189-.298-.482-.003-1.154-.085-1.876-.087zm.261 1.625h.22c.262 2.355.688 3.732 1.55 5.836-2.2-.26-4.072-.899-4.416-3.285-.178-1.235.422-2.524 2.646-2.552zm8.557 2.315c.15.002.303.03.447.096l.749.323-.537.979a.672.597 0 0 0-.241.038.672.597 0 0 0-.405.764.672.597 0 0 0 .112.174l-.926 1.686a.672.597 0 0 0-.222.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.765.672.597 0 0 0-.158-.22l.902-1.642a.672.597 0 0 0 .293-.03.672.597 0 0 0 .213-.112c.348.146.633.265.838.366.308.152.417.253.45.365.033.11-.003.322-.177.694-.13.277-.345.67-.599 1.133a.672.597 0 0 0-.251.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.764.672.597 0 0 0-.137-.202c.251-.458.467-.852.606-1.148.188-.402.286-.701.2-.99-.086-.289-.35-.477-.7-.65-.23-.113-.517-.233-.86-.377a.672.597 0 0 0-.038-.239.672.597 0 0 0-.145-.209l.528-.963 2.924 1.263c.528.229.746.79.49 1.26l-2.01 3.68c-.257.469-.888.663-1.416.435l-4.137-1.788c-.528-.228-.747-.79-.49-1.26l2.01-3.679c.176-.323.53-.515.905-.53h.064z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
19
src/webui/img/github.svg
Normal file
19
src/webui/img/github.svg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
||||||
|
<title>github [#142]</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#ffffffff">
|
||||||
|
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||||
|
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
31
src/webui/index.html
Normal file
31
src/webui/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "template/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Lumeex{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Static Gallery Generator</h1>
|
||||||
|
<p>Use this generator to create a static gallery from your photos. Then, upload the static files to your preferred web server.</p>
|
||||||
|
|
||||||
|
<!-- Hero Upload Section -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Steps</h2>
|
||||||
|
<p> Follow the steps to generate your static gallery</p>
|
||||||
|
<div class="stepper">
|
||||||
|
<ul id="stepper">
|
||||||
|
<li><a href="/gallery-editor">Upload your photos</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/site-info">Configure site info</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/theme-editor">Customize your theme</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -4,6 +4,21 @@
|
|||||||
* @param {string} type - "success" or "error".
|
* @param {string} type - "success" or "error".
|
||||||
* @param {number} duration - Duration in ms.
|
* @param {number} duration - Duration in ms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// --- Loader helpers ---
|
||||||
|
function showLoader(text = "Uploading...") {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) {
|
||||||
|
loader.classList.add("active");
|
||||||
|
document.getElementById("loader-text").textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideLoader() {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) loader.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toast helpers ---
|
||||||
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;
|
||||||
@ -21,24 +36,34 @@ function showToast(message, type = "success", duration = 3000) {
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Get build button and modal elements
|
// Get build button and modal elements
|
||||||
const buildBtn = document.getElementById("build-btn");
|
const buildBtn = document.getElementById("build-btn");
|
||||||
|
const stepperBuildBtn = document.getElementById("stepper-build"); // Added for stepper build button
|
||||||
const buildModal = document.getElementById("build-success-modal");
|
const buildModal = document.getElementById("build-success-modal");
|
||||||
const buildModalClose = document.getElementById("build-success-modal-close");
|
const buildModalClose = document.getElementById("build-success-modal-close");
|
||||||
const downloadZipBtn = document.getElementById("download-zip-btn");
|
const downloadZipBtn = document.getElementById("download-zip-btn");
|
||||||
const zipLoader = document.getElementById("zip-loader");
|
const zipLoader = document.getElementById("zip-loader");
|
||||||
|
|
||||||
// Handle build button click
|
// Build action handler
|
||||||
if (buildBtn) {
|
async function handleBuildClick() {
|
||||||
buildBtn.addEventListener("click", async () => {
|
showLoader("Building static site...");
|
||||||
// Trigger build on backend
|
// Trigger build on backend
|
||||||
const res = await fetch("/api/build", { method: "POST" });
|
const res = await fetch("/api/build", { method: "POST" });
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
// Show build success modal
|
// Show build success modal
|
||||||
if (buildModal) buildModal.style.display = "flex";
|
if (buildModal) buildModal.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
showToast(result.message || "❌ Build failed!", "error");
|
showToast(result.message || "❌ Build failed!", "error");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Handle build button click
|
||||||
|
if (buildBtn) {
|
||||||
|
buildBtn.addEventListener("click", handleBuildClick);
|
||||||
|
}
|
||||||
|
// Handle stepper-build button click
|
||||||
|
if (stepperBuildBtn) {
|
||||||
|
stepperBuildBtn.addEventListener("click", handleBuildClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle download zip button click
|
// Handle download zip button click
|
||||||
@ -67,6 +92,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preview Site button
|
||||||
|
const previewBtn = document.getElementById("preview-site-btn");
|
||||||
|
if (previewBtn) {
|
||||||
|
const previewPort = previewBtn.getAttribute("data-preview-port") || "3000";
|
||||||
|
previewBtn.onclick = () => {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const url = `${protocol}//${host}:${previewPort}/`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Modal close logic
|
// Modal close logic
|
||||||
if (buildModal && buildModalClose) {
|
if (buildModal && buildModalClose) {
|
||||||
buildModalClose.onclick = () => {
|
buildModalClose.onclick = () => {
|
||||||
|
@ -1,18 +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;
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
@ -24,22 +35,43 @@ 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 = '';
|
||||||
galleryImages.forEach((img, i) => {
|
let imagesToShow = showOnlyUntagged
|
||||||
|
? galleryImages.filter(img => !img.tags || img.tags.length === 0)
|
||||||
|
: galleryImages;
|
||||||
|
|
||||||
|
imagesToShow.forEach((img) => {
|
||||||
|
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 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">
|
||||||
@ -50,22 +82,17 @@ function renderGallery() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
|
|
||||||
renderTags(i, img.tags || []);
|
renderTags(i, img.tags || []);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show/hide Remove All button
|
updateCountAndButtons('gallery', imagesToShow.length);
|
||||||
const removeAllBtn = document.getElementById('remove-all-gallery');
|
applyFadeInImages(container);
|
||||||
if (removeAllBtn) {
|
|
||||||
removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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 = '';
|
||||||
|
|
||||||
@ -73,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 = '×';
|
||||||
@ -82,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);
|
||||||
});
|
});
|
||||||
@ -92,6 +117,13 @@ function renderTags(imgIndex, tags) {
|
|||||||
input.placeholder = 'Add tag...';
|
input.placeholder = 'Add tag...';
|
||||||
inputContainer.appendChild(input);
|
inputContainer.appendChild(input);
|
||||||
|
|
||||||
|
const validateBtn = document.createElement('button');
|
||||||
|
validateBtn.textContent = '✔️';
|
||||||
|
validateBtn.className = 'validate-tag-btn';
|
||||||
|
validateBtn.style.display = 'none';
|
||||||
|
validateBtn.style.marginLeft = '4px';
|
||||||
|
inputContainer.appendChild(validateBtn);
|
||||||
|
|
||||||
const suggestionBox = document.createElement('ul');
|
const suggestionBox = document.createElement('ul');
|
||||||
suggestionBox.className = 'suggestions';
|
suggestionBox.className = 'suggestions';
|
||||||
inputContainer.appendChild(suggestionBox);
|
inputContainer.appendChild(suggestionBox);
|
||||||
@ -108,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);
|
||||||
@ -139,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);
|
||||||
});
|
});
|
||||||
@ -148,9 +169,14 @@ function renderTags(imgIndex, tags) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
input.addEventListener('input', updateSuggestions);
|
input.addEventListener('input', () => {
|
||||||
input.addEventListener('focus', updateSuggestions);
|
updateSuggestions();
|
||||||
|
validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
|
||||||
|
});
|
||||||
|
input.addEventListener('focus', () => {
|
||||||
|
updateSuggestions();
|
||||||
|
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') {
|
||||||
@ -172,23 +198,31 @@ function renderTags(imgIndex, tags) {
|
|||||||
}
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
updateSuggestions();
|
updateSuggestions();
|
||||||
|
validateBtn.style.display = 'none';
|
||||||
} else if ([' ', ','].includes(e.key)) {
|
} else if ([' ', ','].includes(e.key)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addTag(input.value);
|
addTag(input.value);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
updateSuggestions();
|
updateSuggestions();
|
||||||
|
validateBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
input.addEventListener('blur', () => {
|
||||||
setTimeout(() => {
|
|
||||||
suggestionBox.style.display = 'none';
|
suggestionBox.style.display = 'none';
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}, 150);
|
validateBtn.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
validateBtn.addEventListener('mousedown', (e) => {
|
||||||
input.focus();
|
e.preventDefault();
|
||||||
|
if (input.value.trim()) {
|
||||||
|
addTag(input.value.trim());
|
||||||
|
input.value = '';
|
||||||
updateSuggestions();
|
updateSuggestions();
|
||||||
|
validateBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSuggestions();
|
||||||
|
if (!input.value.trim()) suggestionBox.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Update tags in galleryImages array ---
|
// --- Update tags in galleryImages array ---
|
||||||
@ -206,7 +240,7 @@ function renderHero() {
|
|||||||
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 src="/photos/${img.src}">
|
<img class="fade-in-img" src="/photos/${img.src}">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item flex-full">
|
<div class="flex-item flex-full">
|
||||||
<div class="flex-item flex-end">
|
<div class="flex-item flex-end">
|
||||||
@ -216,12 +250,8 @@ function renderHero() {
|
|||||||
`;
|
`;
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
|
updateCountAndButtons('hero', heroImages.length);
|
||||||
// Show/hide Remove All button
|
applyFadeInImages(container);
|
||||||
const removeAllBtn = document.getElementById('remove-all-hero');
|
|
||||||
if (removeAllBtn) {
|
|
||||||
removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Save gallery to server ---
|
// --- Save gallery to server ---
|
||||||
@ -267,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';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,15 +415,35 @@ 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;
|
||||||
|
|
||||||
|
// Gallery filter radios
|
||||||
|
const showAllRadio = document.getElementById('show-all-radio');
|
||||||
|
const showUntaggedRadio = document.getElementById('show-untagged-radio');
|
||||||
|
if (showAllRadio) showAllRadio.addEventListener('change', () => {
|
||||||
|
showOnlyUntagged = false;
|
||||||
|
renderGallery();
|
||||||
|
});
|
||||||
|
if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
|
||||||
|
showOnlyUntagged = true;
|
||||||
|
renderGallery();
|
||||||
|
});
|
||||||
|
|
||||||
// Bulk delete buttons
|
// Bulk delete buttons
|
||||||
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
|
[
|
||||||
const removeAllHeroBtn = document.getElementById('remove-all-hero');
|
['remove-all-gallery', 'gallery-all'],
|
||||||
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
|
['remove-all-gallery-bottom', 'gallery-all'],
|
||||||
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
|
['remove-all-hero', 'hero-all'],
|
||||||
|
['remove-all-hero-bottom', 'hero-all']
|
||||||
|
].forEach(([btnId, type]) => {
|
||||||
|
const btn = document.getElementById(btnId);
|
||||||
|
if (btn) btn.onclick = () => showDeleteModal(type);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initialize ---
|
// --- Initialize ---
|
@ -12,128 +12,122 @@ function showToast(message, type = "success", duration = 3000) {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showLoader(text = "Uploading...") {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) {
|
||||||
|
loader.classList.add("active");
|
||||||
|
document.getElementById("loader-text").textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideLoader() {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) loader.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Form and menu logic
|
// --- Section Forms ---
|
||||||
const form = document.getElementById("site-info-form");
|
const forms = {
|
||||||
|
info: document.getElementById("info-form"),
|
||||||
|
social: document.getElementById("social-form"),
|
||||||
|
menu: document.getElementById("menu-form"),
|
||||||
|
footer: document.getElementById("footer-form"),
|
||||||
|
legals: document.getElementById("legals-form"),
|
||||||
|
build: document.getElementById("build-form")
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 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 = [];
|
||||||
|
|
||||||
// Render menu items
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update menu items from inputs
|
|
||||||
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 = [];
|
||||||
|
|
||||||
// Render IP paragraphs
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update IP paragraphs from textareas
|
|
||||||
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 ---
|
||||||
const thumbnailInput = form?.elements["social.thumbnail"];
|
const thumbnailInput = document.getElementById("social-thumbnail");
|
||||||
const thumbnailUpload = document.getElementById("thumbnail-upload");
|
const thumbnailUpload = document.getElementById("thumbnail-upload");
|
||||||
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
|
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
|
||||||
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];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
showLoader("Uploading thumbnail...");
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
|
const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
if (thumbnailInput) thumbnailInput.value = result.filename;
|
if (thumbnailInput) thumbnailInput.value = result.filename;
|
||||||
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
|
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
|
||||||
@ -141,27 +135,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
} else {
|
} else {
|
||||||
showToast("❌ Error uploading thumbnail", "error");
|
showToast("❌ Error uploading thumbnail", "error");
|
||||||
}
|
}
|
||||||
|
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") {
|
||||||
@ -171,46 +158,38 @@ 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");
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 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...");
|
||||||
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();
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
updateSectionStatus("build");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@ -220,55 +199,39 @@ 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...");
|
||||||
const res = await fetch("/api/theme/remove", {
|
const res = await fetch("/api/theme/remove", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme: themeToDelete })
|
body: JSON.stringify({ theme: themeToDelete })
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch theme list and populate select
|
// --- Theme select refresh ---
|
||||||
if (themeSelect) {
|
function refreshThemes() {
|
||||||
fetch("/api/themes")
|
fetch("/api/themes")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(themes => {
|
.then(themes => {
|
||||||
@ -279,144 +242,276 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
option.textContent = theme;
|
option.textContent = theme;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
// Set selected value after loading config
|
loadConfigAndUpdateBuildStatus();
|
||||||
fetch("/api/site-info")
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
themeSelect.value = data.build?.theme || "";
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config from server and populate form
|
// --- Config loading ---
|
||||||
if (form) {
|
let loadedConfig = {};
|
||||||
|
function loadConfigAndUpdateBuildStatus() {
|
||||||
fetch("/api/site-info")
|
fetch("/api/site-info")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.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)
|
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
||||||
? data.legals.intellectual_property
|
? data.legals.intellectual_property
|
||||||
: [];
|
: [];
|
||||||
renderIpParagraphs();
|
renderIpParagraphs();
|
||||||
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
if (forms.legals) {
|
||||||
renderMenuItems();
|
forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
||||||
form.elements["info.title"].value = data.info?.title || "";
|
forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
|
||||||
form.elements["info.subtitle"].value = data.info?.subtitle || "";
|
forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
||||||
form.elements["info.description"].value = data.info?.description || "";
|
|
||||||
form.elements["info.canonical"].value = data.info?.canonical || "";
|
|
||||||
form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
|
|
||||||
form.elements["info.author"].value = data.info?.author || "";
|
|
||||||
form.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()}` : "");
|
|
||||||
form.elements["footer.copyright"].value = data.footer?.copyright || "";
|
|
||||||
form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
|
|
||||||
if (themeSelect) {
|
|
||||||
themeSelect.value = data.build?.theme || "";
|
|
||||||
}
|
|
||||||
form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
|
||||||
form.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
|
|
||||||
form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
|
||||||
// Build checkboxes
|
|
||||||
if (convertImagesCheckbox) {
|
|
||||||
convertImagesCheckbox.checked = !!data.build?.convert_images;
|
|
||||||
}
|
|
||||||
if (resizeImagesCheckbox) {
|
|
||||||
resizeImagesCheckbox.checked = !!data.build?.resize_images;
|
|
||||||
}
|
}
|
||||||
|
// 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();
|
||||||
|
|
||||||
// Add menu item
|
// --- Add/remove menu items ---
|
||||||
if (addMenuBtn) {
|
if (addMenuBtn) addMenuBtn.addEventListener("click", () => {
|
||||||
addMenuBtn.addEventListener("click", () => {
|
|
||||||
menuItems.push({ label: "", href: "" });
|
menuItems.push({ label: "", href: "" });
|
||||||
renderMenuItems();
|
renderMenuItems();
|
||||||
|
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"));
|
||||||
menuItems.splice(idx, 1);
|
menuItems.splice(idx, 1);
|
||||||
renderMenuItems();
|
renderMenuItems();
|
||||||
|
updateSectionStatus("menu");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update menuItems on input change
|
|
||||||
menuList.addEventListener("input", () => {
|
menuList.addEventListener("input", () => {
|
||||||
updateMenuItemsFromInputs();
|
updateMenuItemsFromInputs();
|
||||||
|
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");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 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"));
|
||||||
ipParagraphs.splice(idx, 1);
|
ipParagraphs.splice(idx, 1);
|
||||||
renderIpParagraphs();
|
renderIpParagraphs();
|
||||||
|
updateSectionStatus("legals");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update ipParagraphs on input change
|
|
||||||
ipList.addEventListener("input", () => {
|
ipList.addEventListener("input", () => {
|
||||||
updateIpParagraphsFromInputs();
|
updateIpParagraphsFromInputs();
|
||||||
|
updateSectionStatus("legals");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save config to server
|
// --- Section value helpers ---
|
||||||
if (form) {
|
function getSectionValues(section) {
|
||||||
form.addEventListener("submit", async (e) => {
|
switch (section) {
|
||||||
e.preventDefault();
|
case "info":
|
||||||
|
return {
|
||||||
|
title: forms.info.elements["info.title"].value,
|
||||||
|
subtitle: forms.info.elements["info.subtitle"].value,
|
||||||
|
description: forms.info.elements["info.description"].value,
|
||||||
|
canonical: forms.info.elements["info.canonical"].value,
|
||||||
|
keywords: forms.info.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
|
||||||
|
author: forms.info.elements["info.author"].value
|
||||||
|
};
|
||||||
|
case "social":
|
||||||
|
return {
|
||||||
|
instagram_url: forms.social.elements["social.instagram_url"].value,
|
||||||
|
thumbnail: thumbnailInput ? thumbnailInput.value : ""
|
||||||
|
};
|
||||||
|
case "menu":
|
||||||
updateMenuItemsFromInputs();
|
updateMenuItemsFromInputs();
|
||||||
|
return { items: menuItems };
|
||||||
|
case "footer":
|
||||||
|
return {
|
||||||
|
copyright: forms.footer.elements["footer.copyright"].value,
|
||||||
|
legal_label: forms.footer.elements["footer.legal_label"].value
|
||||||
|
};
|
||||||
|
case "legals":
|
||||||
updateIpParagraphsFromInputs();
|
updateIpParagraphsFromInputs();
|
||||||
|
return {
|
||||||
// Check if thumbnail is set before saving (uploaded or present in input)
|
hoster_name: forms.legals.elements["legals.hoster_name"].value,
|
||||||
if (!thumbnailInput || !thumbnailInput.value) {
|
hoster_address: forms.legals.elements["legals.hoster_address"].value,
|
||||||
showToast("❌ Thumbnail is required.", "error");
|
hoster_contact: forms.legals.elements["legals.hoster_contact"].value,
|
||||||
return;
|
intellectual_property: ipParagraphs
|
||||||
}
|
};
|
||||||
|
case "build":
|
||||||
const build = {
|
return {
|
||||||
theme: themeSelect ? themeSelect.value : "",
|
theme: themeSelect ? themeSelect.value : "",
|
||||||
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
|
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
|
||||||
resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
|
resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
|
||||||
};
|
};
|
||||||
|
default:
|
||||||
const payload = {
|
return {};
|
||||||
info: {
|
|
||||||
title: form.elements["info.title"].value,
|
|
||||||
subtitle: form.elements["info.subtitle"].value,
|
|
||||||
description: form.elements["info.description"].value,
|
|
||||||
canonical: form.elements["info.canonical"].value,
|
|
||||||
keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
|
|
||||||
author: form.elements["info.author"].value
|
|
||||||
},
|
|
||||||
social: {
|
|
||||||
instagram_url: form.elements["social.instagram_url"].value,
|
|
||||||
thumbnail: thumbnailInput ? thumbnailInput.value : ""
|
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
items: menuItems
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
copyright: form.elements["footer.copyright"].value,
|
|
||||||
legal_label: form.elements["footer.legal_label"].value
|
|
||||||
},
|
|
||||||
build,
|
|
||||||
legals: {
|
|
||||||
hoster_name: form.elements["legals.hoster_name"].value,
|
|
||||||
hoster_address: form.elements["legals.hoster_address"].value,
|
|
||||||
hoster_contact: form.elements["legals.hoster_contact"].value,
|
|
||||||
intellectual_property: ipParagraphs
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function isSectionSaved(section) {
|
||||||
|
const values = getSectionValues(section);
|
||||||
|
const config = loadedConfig[section] || {};
|
||||||
|
function normalizeMenuItems(items) {
|
||||||
|
return (items || []).map(item => ({
|
||||||
|
label: item.label || "",
|
||||||
|
href: item.href || ""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
switch (section) {
|
||||||
|
case "info":
|
||||||
|
return Object.keys(values).every(
|
||||||
|
key => values[key] && (
|
||||||
|
key === "keywords"
|
||||||
|
? Array.isArray(config.keywords) && values.keywords.join(",") === config.keywords.join(",")
|
||||||
|
: values[key] === (config[key] || "")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case "social":
|
||||||
|
return values.instagram_url && values.thumbnail &&
|
||||||
|
values.instagram_url === (config.instagram_url || "") &&
|
||||||
|
values.thumbnail === (config.thumbnail || "");
|
||||||
|
case "menu":
|
||||||
|
return JSON.stringify(normalizeMenuItems(values.items)) === JSON.stringify(normalizeMenuItems(config.items));
|
||||||
|
case "footer":
|
||||||
|
return values.copyright && values.legal_label &&
|
||||||
|
values.copyright === (config.copyright || "") &&
|
||||||
|
values.legal_label === (config.legal_label || "");
|
||||||
|
case "legals":
|
||||||
|
return values.hoster_name && values.hoster_address && values.hoster_contact &&
|
||||||
|
values.hoster_name === (config.hoster_name || "") &&
|
||||||
|
values.hoster_address === (config.hoster_address || "") &&
|
||||||
|
values.hoster_contact === (config.hoster_contact || "") &&
|
||||||
|
JSON.stringify(values.intellectual_property) === JSON.stringify(config.intellectual_property || []);
|
||||||
|
case "build":
|
||||||
|
return values.theme === (config.theme || "") &&
|
||||||
|
!!values.convert_images === !!config.convert_images &&
|
||||||
|
!!values.resize_images === !!config.resize_images;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSectionComplete(section) {
|
||||||
|
const values = getSectionValues(section);
|
||||||
|
switch (section) {
|
||||||
|
case "info":
|
||||||
|
return (
|
||||||
|
values.title &&
|
||||||
|
values.subtitle &&
|
||||||
|
values.description &&
|
||||||
|
values.canonical &&
|
||||||
|
values.keywords.length > 0 &&
|
||||||
|
values.author
|
||||||
|
);
|
||||||
|
case "social":
|
||||||
|
return values.instagram_url && values.thumbnail;
|
||||||
|
case "menu":
|
||||||
|
return Array.isArray(values.items) && values.items.every(item => item.label && item.href);
|
||||||
|
case "footer":
|
||||||
|
return values.copyright && values.legal_label;
|
||||||
|
case "legals":
|
||||||
|
return (
|
||||||
|
values.hoster_name &&
|
||||||
|
values.hoster_address &&
|
||||||
|
values.hoster_contact &&
|
||||||
|
Array.isArray(values.intellectual_property) &&
|
||||||
|
values.intellectual_property.length > 0 &&
|
||||||
|
values.intellectual_property.every(ip => ip.paragraph)
|
||||||
|
);
|
||||||
|
case "build":
|
||||||
|
return !!values.theme;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSectionStatus(section) {
|
||||||
|
const statusEl = document.querySelector(`#${section}-section .section-status`);
|
||||||
|
if (!statusEl) return;
|
||||||
|
if (!isSectionComplete(section)) {
|
||||||
|
statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
|
||||||
|
statusEl.style.color = "#ffc700";
|
||||||
|
statusEl.style.display = "";
|
||||||
|
statusEl.style.fontStyle = "normal";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSectionSaved(section)) {
|
||||||
|
statusEl.innerHTML = "";
|
||||||
|
statusEl.style.display = "none";
|
||||||
|
} else {
|
||||||
|
statusEl.innerHTML = "⚠️ Section not yet saved";
|
||||||
|
statusEl.style.color = "#ffc700";
|
||||||
|
statusEl.style.display = "";
|
||||||
|
statusEl.style.fontStyle = "normal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Listen for changes in each section ---
|
||||||
|
Object.entries(forms).forEach(([section, form]) => {
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener("input", () => updateSectionStatus(section));
|
||||||
|
form.addEventListener("change", () => updateSectionStatus(section));
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!form.reportValidity()) {
|
||||||
|
showToast("❌ Please fill all required fields before saving.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (section === "social" && (!thumbnailInput || !thumbnailInput.value)) {
|
||||||
|
showToast("❌ Thumbnail is required.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (section === "menu") {
|
||||||
|
updateMenuItemsFromInputs();
|
||||||
|
if (!menuItems.length || !menuItems.every(item => item.label && item.href)) {
|
||||||
|
showToast("❌ Please fill all menu item fields.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (section === "legals") {
|
||||||
|
updateIpParagraphsFromInputs();
|
||||||
|
if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
|
||||||
|
showToast("❌ Please fill all intellectual property paragraphs.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let payload = {};
|
||||||
|
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" },
|
||||||
@ -424,10 +519,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
showToast("✅ Site info saved!", "success");
|
showToast("✅ Section saved!", "success");
|
||||||
|
fetch("/api/site-info")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
loadedConfig = data;
|
||||||
|
updateSectionStatus(section);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast("❌ Error saving site info", "error");
|
showToast("❌ Error saving section", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
@ -31,6 +31,20 @@ function showToast(message, type = "success", duration = 3000) {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Loader helpers ---
|
||||||
|
function showLoader(text = "Uploading...") {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) {
|
||||||
|
loader.classList.add("active");
|
||||||
|
document.getElementById("loader-text").textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideLoader() {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) loader.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Color Picker
|
||||||
function setupColorPicker(colorId, btnId, textId, initial) {
|
function setupColorPicker(colorId, btnId, textId, initial) {
|
||||||
const colorInput = document.getElementById(colorId);
|
const colorInput = document.getElementById(colorId);
|
||||||
const colorBtn = document.getElementById(btnId);
|
const colorBtn = document.getElementById(btnId);
|
||||||
@ -40,7 +54,6 @@ function setupColorPicker(colorId, btnId, textId, initial) {
|
|||||||
colorBtn.style.background = initial;
|
colorBtn.style.background = initial;
|
||||||
textInput.value = initial.toUpperCase();
|
textInput.value = initial.toUpperCase();
|
||||||
|
|
||||||
// Color input is positioned over the button and is clickable
|
|
||||||
colorInput.addEventListener("input", () => {
|
colorInput.addEventListener("input", () => {
|
||||||
colorBtn.style.background = colorInput.value;
|
colorBtn.style.background = colorInput.value;
|
||||||
textInput.value = colorInput.value.toUpperCase();
|
textInput.value = colorInput.value.toUpperCase();
|
||||||
@ -57,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) {
|
||||||
@ -103,54 +118,173 @@ function renderLocalFonts(fonts) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Section helpers ---
|
||||||
|
function getSectionValues(section) {
|
||||||
|
switch (section) {
|
||||||
|
case "colors":
|
||||||
|
return {
|
||||||
|
primary: document.getElementById("color-primary-text").value,
|
||||||
|
primary_dark: document.getElementById("color-primary-dark-text").value,
|
||||||
|
secondary: document.getElementById("color-secondary-text").value,
|
||||||
|
accent: document.getElementById("color-accent-text").value,
|
||||||
|
text_dark: document.getElementById("color-text-dark-text").value,
|
||||||
|
background: document.getElementById("color-background-text").value,
|
||||||
|
browser_color: document.getElementById("color-browser-color-text").value
|
||||||
|
};
|
||||||
|
case "google-fonts":
|
||||||
|
const googleFontsFields = document.getElementById("google-fonts-fields");
|
||||||
|
const fonts = [];
|
||||||
|
if (googleFontsFields) {
|
||||||
|
googleFontsFields.querySelectorAll(".input-field").forEach(field => {
|
||||||
|
const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value.trim();
|
||||||
|
const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
|
||||||
|
.split(",").map(w => w.trim()).filter(Boolean);
|
||||||
|
if (family) fonts.push({ family, weights });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return fonts;
|
||||||
|
case "fonts":
|
||||||
|
return {
|
||||||
|
primary: {
|
||||||
|
name: document.getElementById("font-primary").value,
|
||||||
|
fallback: document.getElementById("font-primary-fallback").value
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
name: document.getElementById("font-secondary").value,
|
||||||
|
fallback: document.getElementById("font-secondary-fallback").value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "favicon":
|
||||||
|
return {
|
||||||
|
path: document.getElementById("favicon-path").value
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSectionComplete(section) {
|
||||||
|
switch (section) {
|
||||||
|
case "colors":
|
||||||
|
const v = getSectionValues("colors");
|
||||||
|
return (
|
||||||
|
v.primary &&
|
||||||
|
v.primary_dark &&
|
||||||
|
v.secondary &&
|
||||||
|
v.accent &&
|
||||||
|
v.text_dark &&
|
||||||
|
v.background &&
|
||||||
|
v.browser_color
|
||||||
|
);
|
||||||
|
case "google-fonts":
|
||||||
|
const fonts = getSectionValues("google-fonts");
|
||||||
|
return fonts.every(f => f.family);
|
||||||
|
case "fonts":
|
||||||
|
const f = getSectionValues("fonts");
|
||||||
|
return f.primary.name && f.primary.fallback && f.secondary.name && f.secondary.fallback;
|
||||||
|
case "favicon":
|
||||||
|
const fav = getSectionValues("favicon");
|
||||||
|
return !!fav.path;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSectionSaved(section, loadedConfig) {
|
||||||
|
switch (section) {
|
||||||
|
case "colors":
|
||||||
|
const v = getSectionValues("colors");
|
||||||
|
const c = loadedConfig.colors || {};
|
||||||
|
return (
|
||||||
|
v.primary === c.primary &&
|
||||||
|
v.primary_dark === c.primary_dark &&
|
||||||
|
v.secondary === c.secondary &&
|
||||||
|
v.accent === c.accent &&
|
||||||
|
v.text_dark === c.text_dark &&
|
||||||
|
v.background === c.background &&
|
||||||
|
v.browser_color === c.browser_color
|
||||||
|
);
|
||||||
|
case "google-fonts":
|
||||||
|
const fonts = getSectionValues("google-fonts");
|
||||||
|
const cf = loadedConfig.google_fonts || [];
|
||||||
|
return JSON.stringify(fonts) === JSON.stringify(cf);
|
||||||
|
case "fonts":
|
||||||
|
const f = getSectionValues("fonts");
|
||||||
|
const cfnt = loadedConfig.fonts || {};
|
||||||
|
return (
|
||||||
|
f.primary.name === (cfnt.primary?.name || "") &&
|
||||||
|
f.primary.fallback === (cfnt.primary?.fallback || "") &&
|
||||||
|
f.secondary.name === (cfnt.secondary?.name || "") &&
|
||||||
|
f.secondary.fallback === (cfnt.secondary?.fallback || "")
|
||||||
|
);
|
||||||
|
case "favicon":
|
||||||
|
const fav = getSectionValues("favicon");
|
||||||
|
const cfav = loadedConfig.favicon || {};
|
||||||
|
return fav.path === (cfav.path || "");
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSectionStatus(section, loadedConfig) {
|
||||||
|
const statusEl = document.querySelector(`#${section}-form .section-status`);
|
||||||
|
if (!statusEl) return;
|
||||||
|
if (!isSectionComplete(section)) {
|
||||||
|
statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
|
||||||
|
statusEl.style.color = "#ffc700";
|
||||||
|
statusEl.style.display = "";
|
||||||
|
statusEl.style.fontStyle = "normal";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSectionSaved(section, loadedConfig)) {
|
||||||
|
statusEl.innerHTML = "";
|
||||||
|
statusEl.style.display = "none";
|
||||||
|
} else {
|
||||||
|
statusEl.innerHTML = "⚠️ Section not yet saved";
|
||||||
|
statusEl.style.color = "#ffc700";
|
||||||
|
statusEl.style.display = "";
|
||||||
|
statusEl.style.fontStyle = "normal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
const themeInfo = await fetchThemeInfo();
|
const themeInfo = await fetchThemeInfo();
|
||||||
const themeNameSpan = document.getElementById("current-theme");
|
const themeNameSpan = document.getElementById("current-theme");
|
||||||
if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
|
if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
|
||||||
|
|
||||||
const themeYaml = themeInfo.theme_yaml;
|
let loadedConfig = themeInfo.theme_yaml;
|
||||||
const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
|
let googleFonts = loadedConfig.google_fonts ? JSON.parse(JSON.stringify(loadedConfig.google_fonts)) : [];
|
||||||
let localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
let localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
if (themeYaml.colors) {
|
if (loadedConfig.colors) {
|
||||||
setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1");
|
setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", loadedConfig.colors.primary || "#0065a1");
|
||||||
setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
|
setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", loadedConfig.colors.primary_dark || "#005384");
|
||||||
setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
|
setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", loadedConfig.colors.secondary || "#00b0f0");
|
||||||
setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700");
|
setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", loadedConfig.colors.accent || "#ffc700");
|
||||||
setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
|
setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", loadedConfig.colors.text_dark || "#616161");
|
||||||
setupColorPicker("color-background", "color-background-btn", "color-background-text", themeYaml.colors.background || "#fff");
|
setupColorPicker("color-background", "color-background-btn", "color-background-text", loadedConfig.colors.background || "#fff");
|
||||||
setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
|
setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", loadedConfig.colors.browser_color || "#fff");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
function refreshFontDropdowns() {
|
function refreshFontDropdowns() {
|
||||||
setFontDropdown("font-primary", document.getElementById("font-primary").value, [
|
setFontDropdown("font-primary", loadedConfig.fonts?.primary?.name || "Lato", [
|
||||||
...googleFonts.map(f => f.family),
|
...googleFonts.map(f => f.family),
|
||||||
...localFonts
|
...localFonts
|
||||||
]);
|
]);
|
||||||
setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
|
setFontDropdown("font-secondary", loadedConfig.fonts?.secondary?.name || "Montserrat", [
|
||||||
...googleFonts.map(f => f.family),
|
...googleFonts.map(f => f.family),
|
||||||
...localFonts
|
...localFonts
|
||||||
]);
|
]);
|
||||||
|
setFallbackDropdown("font-primary-fallback", loadedConfig.fonts?.primary?.fallback || "sans-serif");
|
||||||
|
setFallbackDropdown("font-secondary-fallback", loadedConfig.fonts?.secondary?.fallback || "serif");
|
||||||
}
|
}
|
||||||
if (themeYaml.fonts) {
|
refreshFontDropdowns();
|
||||||
setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
|
|
||||||
...googleFonts.map(f => f.family),
|
|
||||||
...localFonts
|
|
||||||
]);
|
|
||||||
setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
|
|
||||||
setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
|
|
||||||
...googleFonts.map(f => f.family),
|
|
||||||
...localFonts
|
|
||||||
]);
|
|
||||||
setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font upload logic
|
// Font upload logic
|
||||||
const fontUploadInput = document.getElementById("font-upload");
|
const fontUploadInput = document.getElementById("font-upload");
|
||||||
const chooseFontBtn = document.getElementById("choose-font-btn");
|
const chooseFontBtn = document.getElementById("choose-font-btn");
|
||||||
const fontUploadStatus = document.getElementById("font-upload-status");
|
|
||||||
const localFontsList = document.getElementById("local-fonts-list");
|
const localFontsList = document.getElementById("local-fonts-list");
|
||||||
|
|
||||||
// Modal logic for font deletion
|
// Modal logic for font deletion
|
||||||
@ -178,11 +312,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
showToast("Only .woff and .woff2 fonts are allowed.", "error");
|
showToast("Only .woff and .woff2 fonts are allowed.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showLoader("Uploading font...");
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("theme", themeInfo.theme_name);
|
formData.append("theme", themeInfo.theme_name);
|
||||||
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
|
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
showToast("✅ Font uploaded!", "success");
|
showToast("✅ Font uploaded!", "success");
|
||||||
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
@ -219,9 +355,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
};
|
};
|
||||||
deleteFontModalConfirm.onclick = async () => {
|
deleteFontModalConfirm.onclick = async () => {
|
||||||
if (!fontToDelete) return;
|
if (!fontToDelete) return;
|
||||||
|
showLoader("Removing font...");
|
||||||
const result = await removeFont(themeInfo.theme_name, fontToDelete);
|
const result = await removeFont(themeInfo.theme_name, fontToDelete);
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
showToast("Font removed!", "✅ success");
|
showToast("Font removed!", "success");
|
||||||
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
refreshLocalFonts();
|
refreshLocalFonts();
|
||||||
} else {
|
} else {
|
||||||
@ -272,11 +410,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
showToast("Invalid file type for favicon.", "error");
|
showToast("Invalid file type for favicon.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showLoader("Uploading favicon...");
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("theme", themeInfo.theme_name);
|
formData.append("theme", themeInfo.theme_name);
|
||||||
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
|
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
faviconInput.value = result.filename;
|
faviconInput.value = result.filename;
|
||||||
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
|
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
|
||||||
@ -303,12 +443,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
deleteFaviconModalConfirm.onclick = async () => {
|
deleteFaviconModalConfirm.onclick = async () => {
|
||||||
|
showLoader("Removing favicon...");
|
||||||
const res = await fetch("/api/favicon/remove", {
|
const res = await fetch("/api/favicon/remove", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme: themeInfo.theme_name })
|
body: JSON.stringify({ theme: themeInfo.theme_name })
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
hideLoader();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
faviconInput.value = "";
|
faviconInput.value = "";
|
||||||
updateFaviconPreview("");
|
updateFaviconPreview("");
|
||||||
@ -320,9 +462,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeYaml.favicon && themeYaml.favicon.path) {
|
if (loadedConfig.favicon && loadedConfig.favicon.path) {
|
||||||
faviconInput.value = themeYaml.favicon.path;
|
faviconInput.value = loadedConfig.favicon.path;
|
||||||
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
|
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${loadedConfig.favicon.path}?t=${Date.now()}`);
|
||||||
} else {
|
} else {
|
||||||
updateFaviconPreview("");
|
updateFaviconPreview("");
|
||||||
}
|
}
|
||||||
@ -335,31 +477,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
if (addGoogleFontBtn) {
|
if (addGoogleFontBtn) {
|
||||||
addGoogleFontBtn.addEventListener("click", async () => {
|
addGoogleFontBtn.addEventListener("click", async () => {
|
||||||
googleFonts.push({ family: "", weights: [] });
|
googleFonts.push({ family: "", weights: [] });
|
||||||
// Save immediately to backend
|
|
||||||
await fetch("/api/theme-google-fonts", {
|
await fetch("/api/theme-google-fonts", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
||||||
});
|
});
|
||||||
// Fetch updated theme info and refresh dropdowns
|
|
||||||
const updatedThemeInfo = await fetchThemeInfo();
|
const updatedThemeInfo = await fetchThemeInfo();
|
||||||
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
||||||
googleFonts.length = 0;
|
googleFonts.length = 0;
|
||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const googleFontsFields = document.getElementById("google-fonts-fields");
|
const googleFontsFields = document.getElementById("google-fonts-fields");
|
||||||
if (googleFontsFields) {
|
if (googleFontsFields) {
|
||||||
// Save on blur for family/weights fields
|
|
||||||
googleFontsFields.addEventListener("blur", async (e) => {
|
googleFontsFields.addEventListener("blur", async (e) => {
|
||||||
if (
|
if (
|
||||||
e.target.name &&
|
e.target.name &&
|
||||||
(e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
|
(e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
|
||||||
) {
|
) {
|
||||||
// Update googleFonts array from the form fields
|
|
||||||
const fontFields = googleFontsFields.querySelectorAll(".input-field");
|
const fontFields = googleFontsFields.querySelectorAll(".input-field");
|
||||||
googleFonts.length = 0;
|
googleFonts.length = 0;
|
||||||
fontFields.forEach(field => {
|
fontFields.forEach(field => {
|
||||||
@ -368,87 +507,102 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
.split(",").map(w => w.trim()).filter(Boolean);
|
.split(",").map(w => w.trim()).filter(Boolean);
|
||||||
googleFonts.push({ family, weights });
|
googleFonts.push({ family, weights });
|
||||||
});
|
});
|
||||||
// Save immediately to backend
|
|
||||||
await fetch("/api/theme-google-fonts", {
|
await fetch("/api/theme-google-fonts", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
||||||
});
|
});
|
||||||
// Fetch updated theme info and refresh dropdowns
|
|
||||||
const updatedThemeInfo = await fetchThemeInfo();
|
const updatedThemeInfo = await fetchThemeInfo();
|
||||||
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
||||||
googleFonts.length = 0;
|
googleFonts.length = 0;
|
||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
}
|
}
|
||||||
}, true); // Use capture phase to catch blur from children
|
}, true);
|
||||||
|
|
||||||
// Delegate remove button click for Google Fonts
|
|
||||||
googleFontsFields.addEventListener("click", async (e) => {
|
googleFontsFields.addEventListener("click", async (e) => {
|
||||||
if (e.target.classList.contains("remove-google-font")) {
|
if (e.target.classList.contains("remove-google-font")) {
|
||||||
const idx = Number(e.target.dataset.idx);
|
const idx = Number(e.target.dataset.idx);
|
||||||
googleFonts.splice(idx, 1);
|
googleFonts.splice(idx, 1);
|
||||||
// Save immediately to backend
|
|
||||||
await fetch("/api/theme-google-fonts", {
|
await fetch("/api/theme-google-fonts", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
|
||||||
});
|
});
|
||||||
// Fetch updated theme info and refresh dropdowns
|
|
||||||
const updatedThemeInfo = await fetchThemeInfo();
|
const updatedThemeInfo = await fetchThemeInfo();
|
||||||
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
|
||||||
googleFonts.length = 0;
|
googleFonts.length = 0;
|
||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form submit
|
// --- Section status listeners ---
|
||||||
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
|
[
|
||||||
|
{ form: document.getElementById("colors-form"), section: "colors" },
|
||||||
|
{ form: document.getElementById("google-fonts-form"), section: "google-fonts" },
|
||||||
|
{ form: document.getElementById("fonts-form"), section: "fonts" },
|
||||||
|
{ form: document.getElementById("favicon-form"), section: "favicon" }
|
||||||
|
].forEach(({ form, section }) => {
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener("input", () => updateSectionStatus(section, loadedConfig));
|
||||||
|
form.addEventListener("change", () => updateSectionStatus(section, loadedConfig));
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Section save handlers ---
|
||||||
|
[
|
||||||
|
{ form: document.getElementById("colors-form"), section: "colors" },
|
||||||
|
{ form: document.getElementById("google-fonts-form"), section: "google-fonts" },
|
||||||
|
{ form: document.getElementById("fonts-form"), section: "fonts" },
|
||||||
|
{ form: document.getElementById("favicon-form"), section: "favicon" }
|
||||||
|
].forEach(({ form, section }) => {
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {};
|
if (!form.reportValidity() || !isSectionComplete(section)) {
|
||||||
data.colors = {
|
showToast("❌ Please fill all required fields before saving.", "error");
|
||||||
primary: document.getElementById("color-primary-text").value,
|
updateSectionStatus(section, loadedConfig);
|
||||||
primary_dark: document.getElementById("color-primary-dark-text").value,
|
return;
|
||||||
secondary: document.getElementById("color-secondary-text").value,
|
|
||||||
accent: document.getElementById("color-accent-text").value,
|
|
||||||
text_dark: document.getElementById("color-text-dark-text").value,
|
|
||||||
background: document.getElementById("color-background-text").value,
|
|
||||||
browser_color: document.getElementById("color-browser-color-text").value
|
|
||||||
};
|
|
||||||
data.fonts = {
|
|
||||||
primary: {
|
|
||||||
name: document.getElementById("font-primary").value,
|
|
||||||
fallback: document.getElementById("font-primary-fallback").value
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
name: document.getElementById("font-secondary").value,
|
|
||||||
fallback: document.getElementById("font-secondary-fallback").value
|
|
||||||
}
|
}
|
||||||
};
|
// Merge with loadedConfig to avoid overwriting other sections
|
||||||
data.favicon = {
|
let payload = { ...loadedConfig };
|
||||||
path: faviconInput.value
|
switch (section) {
|
||||||
};
|
case "colors":
|
||||||
data.google_fonts = [];
|
payload.colors = getSectionValues("colors");
|
||||||
document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
|
break;
|
||||||
const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
|
case "google-fonts":
|
||||||
const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
|
payload.google_fonts = getSectionValues("google-fonts");
|
||||||
.split(",").map(w => w.trim()).filter(w => w);
|
break;
|
||||||
if (family) data.google_fonts.push({ family, weights });
|
case "fonts":
|
||||||
});
|
payload.fonts = getSectionValues("fonts");
|
||||||
|
break;
|
||||||
|
case "favicon":
|
||||||
|
payload.favicon = getSectionValues("favicon");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
showLoader("Saving...");
|
||||||
const res = await fetch("/api/theme-info", {
|
const res = await fetch("/api/theme-info", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
|
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: payload })
|
||||||
});
|
});
|
||||||
|
hideLoader();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showToast("✅ Theme saved!", "success");
|
showToast("✅ Section saved!", "success");
|
||||||
|
const updatedThemeInfo = await fetchThemeInfo();
|
||||||
|
loadedConfig = updatedThemeInfo.theme_yaml;
|
||||||
|
updateSectionStatus(section, loadedConfig);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error saving theme.", "error");
|
showToast("Error saving section.", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initial status update
|
||||||
|
["colors", "google-fonts", "fonts", "favicon"].forEach(section => updateSectionStatus(section, loadedConfig));
|
||||||
|
});
|
@ -1,41 +1,47 @@
|
|||||||
// --- Upload gallery images ---
|
// --- Loader helpers ---
|
||||||
document.getElementById('upload-gallery').addEventListener('change', async (e) => {
|
function showLoader(text = "Uploading...") {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) {
|
||||||
|
loader.classList.add("active");
|
||||||
|
document.getElementById("loader-text").textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideLoader() {
|
||||||
|
const loader = document.getElementById("global-loader");
|
||||||
|
if (loader) loader.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generic upload handler ---
|
||||||
|
function setupUpload(inputId, apiUrl, loaderText, successMsg, refreshFn) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (!input) return;
|
||||||
|
input.addEventListener('change', async (e) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
|
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();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
|
showToast(`✅ ${data.uploaded.length} ${successMsg}`, "success");
|
||||||
refreshGallery();
|
if (typeof refreshFn === "function") refreshFn();
|
||||||
} else showToast('Error: ' + data.error, "error");
|
} else showToast('Error: ' + data.error, "error");
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
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 ---
|
||||||
document.getElementById('upload-hero').addEventListener('change', async (e) => {
|
setupUpload('upload-gallery', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
|
||||||
const files = e.target.files;
|
setupUpload('upload-hero', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
|
||||||
if (!files.length) return;
|
setupUpload('upload-gallery-bottom', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
|
||||||
|
setupUpload('upload-hero-bottom', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
|
||||||
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();
|
|
||||||
if (res.ok) {
|
|
||||||
showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
|
|
||||||
refreshHero();
|
|
||||||
} else showToast('Error: ' + data.error, "error");
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
showToast('Server error!', "error");
|
|
||||||
} finally { e.target.value = ''; }
|
|
||||||
});
|
|
@ -1,48 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "template/base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
{% block title %}Lumeex - Site Info{% endblock %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta charset="UTF-8">
|
{% block content %}
|
||||||
<title>Lumeex</title>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Top bar -->
|
|
||||||
<div class="nav-bar">
|
|
||||||
<div class="content-inner nav">
|
|
||||||
<input type="checkbox" id="nav-check">
|
|
||||||
<div class="nav-header">
|
|
||||||
<div class="nav-title">
|
|
||||||
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="nav-btn">
|
|
||||||
<label for="nav-check">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="nav-links">
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
|
||||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
|
||||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<button id="build-btn" class="button">Build !</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Toast container for notifications -->
|
|
||||||
<div id="site-info" class="content-inner">
|
|
||||||
<div id="toast-container"></div>
|
|
||||||
<h1>Edit Site Info</h1>
|
<h1>Edit Site Info</h1>
|
||||||
<form id="site-info-form">
|
|
||||||
<!-- Info Section -->
|
<!-- Info Section -->
|
||||||
<fieldset>
|
<form id="info-form" autocomplete="off">
|
||||||
|
<fieldset id="info-section">
|
||||||
<h2>Info</h2>
|
<h2>Info</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Set the basic information for your site and SEO</p>
|
<p>Set the basic information for your site and SEO</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -70,10 +38,15 @@
|
|||||||
<input type="text" name="info.author" placeholder="Your Name" required>
|
<input type="text" name="info.author" placeholder="Your Name" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="info">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Social Section -->
|
<!-- Social Section -->
|
||||||
<fieldset>
|
<form id="social-form" autocomplete="off">
|
||||||
|
<fieldset id="social-section">
|
||||||
<h2>Social</h2>
|
<h2>Social</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Set your social media links and thumbnail for link sharing</p>
|
<p>Set your social media links and thumbnail for link sharing</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -83,16 +56,21 @@
|
|||||||
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
|
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
|
||||||
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
|
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
|
||||||
<div class="thumbnail-form">
|
<div class="thumbnail-form">
|
||||||
<input type="hidden" name="social.thumbnail" id="social-thumbnail">
|
<input type="hidden" name="social.thumbnail" id="social-thumbnail" required>
|
||||||
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
|
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
|
||||||
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
|
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="social">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Menu Section -->
|
<!-- Menu Section -->
|
||||||
<fieldset>
|
<form id="menu-form" autocomplete="off">
|
||||||
|
<fieldset id="menu-section">
|
||||||
<h2>Menu</h2>
|
<h2>Menu</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
|
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field" style="flex: 1 1 100%;">
|
<div class="input-field" style="flex: 1 1 100%;">
|
||||||
@ -100,10 +78,15 @@
|
|||||||
<button type="button" id="add-menu-item">+ Add menu item</button>
|
<button type="button" id="add-menu-item">+ Add menu item</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="menu">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Footer Section -->
|
<!-- Footer Section -->
|
||||||
<fieldset>
|
<form id="footer-form" autocomplete="off">
|
||||||
|
<fieldset id="footer-section">
|
||||||
<h2>Footer</h2>
|
<h2>Footer</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Set your copyright informations and legal link name</p>
|
<p>Set your copyright informations and legal link name</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -112,13 +95,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Legal Label</label>
|
<label>Legal Label</label>
|
||||||
<input type="text" name="footer.legal_label" re>
|
<input type="text" name="footer.legal_label" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="footer">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Legals Section -->
|
<!-- Legals Section -->
|
||||||
<fieldset>
|
<form id="legals-form" autocomplete="off">
|
||||||
|
<fieldset id="legals-section">
|
||||||
<h2>Legals</h2>
|
<h2>Legals</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Set your legal informations</p>
|
<p>Set your legal informations</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -139,15 +127,20 @@
|
|||||||
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
|
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="legals">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Build Section -->
|
<!-- Build Section -->
|
||||||
<fieldset>
|
<form id="build-form" autocomplete="off">
|
||||||
|
<fieldset id="build-section">
|
||||||
<h2>Build</h2>
|
<h2>Build</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
|
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Theme</label>
|
<label>Theme</label>
|
||||||
<select name="build.theme" id="theme-select" required></select>
|
<select name="build.theme" id="theme-select"></select>
|
||||||
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
|
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
|
||||||
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
|
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
|
||||||
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
|
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
|
||||||
@ -163,11 +156,26 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="build">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button type="submit">Save</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Stepper -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Steps</h2>
|
||||||
|
<p>Follow the steps to generate your static gallery</p>
|
||||||
|
<ul id="stepper">
|
||||||
|
<li><a href="/gallery-editor">Upload your photos</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a class="step-active" href="/site-info">Configure site info</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/theme-editor">Customize your theme</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Delete confirmation modal (now outside .content-inner) -->
|
<!-- Delete thumbnail confirmation modal-->
|
||||||
|
<div class="content-inner">
|
||||||
<div id="delete-modal" class="modal" style="display:none;">
|
<div id="delete-modal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span id="delete-modal-close" class="modal-close">×</span>
|
<span id="delete-modal-close" class="modal-close">×</span>
|
||||||
@ -179,7 +187,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Delete theme confirmation modal -->
|
<!-- Delete theme confirmation modal -->
|
||||||
|
<div class="content-inner">
|
||||||
<div id="delete-theme-modal" class="modal" style="display:none;">
|
<div id="delete-theme-modal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span id="delete-theme-modal-close" class="modal-close">×</span>
|
<span id="delete-theme-modal-close" class="modal-close">×</span>
|
||||||
@ -191,17 +201,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Build success modal -->
|
|
||||||
<div id="build-success-modal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
|
||||||
<h3>✅ Build completed!</h3>
|
|
||||||
<p>Your files are available in the output folder.</p>
|
|
||||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
|
||||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/build.js') }}"></script>
|
{% endblock %}
|
||||||
</body>
|
|
||||||
</html>
|
|
File diff suppressed because it is too large
Load Diff
89
src/webui/template/base.html
Normal file
89
src/webui/template/base.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Lumeex{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
||||||
|
<meta name="darkreader-lock">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Top bar -->
|
||||||
|
<div class="nav-bar">
|
||||||
|
<div class="content-inner nav">
|
||||||
|
<div class="nav-header">
|
||||||
|
<a href="/" class="nav-title">
|
||||||
|
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- Burger toggle input and label -->
|
||||||
|
<input type="checkbox" id="nav-toggle" class="nav-toggle" hidden>
|
||||||
|
<label for="nav-toggle" class="nav-burger">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
<div class="nav-links">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item"><a href="/gallery-editor">Gallery</a></li>
|
||||||
|
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
||||||
|
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button id="build-btn" class="button">🚀 Build!</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div class="content-inner first-content">
|
||||||
|
<div class="inner">
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
<!-- Page content -->
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
<!-- Build success modal -->
|
||||||
|
<div class="content-inner">
|
||||||
|
<div id="build-success-modal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="build-success-modal-close" class="modal-close">×</span>
|
||||||
|
<h3>✅ Build completed!</h3>
|
||||||
|
<p>Your files are available in the output folder.</p>
|
||||||
|
<div class="modal-actions-row">
|
||||||
|
<button id="preview-site-btn" class="modal-btn" data-preview-port="{{ preview_port }}">🔎 Preview Site</button>
|
||||||
|
<button id="download-zip-btn" class="modal-btn">📦 Download ZIP</button>
|
||||||
|
</div>
|
||||||
|
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer -->
|
||||||
|
<div id="footer">
|
||||||
|
<div class="content-inner">
|
||||||
|
<div class="inner">
|
||||||
|
<div class="footer-container">
|
||||||
|
<div class="footer-credit">
|
||||||
|
<p><a href="https//lumeex.djeex.fr"><span class="lum-first">Lum</span><span class="lum-second">eex</span> v{{ lumeex_version }}</a> — © 2025</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a class="footer-link documentation" href="https://lumeex.djeex.fr"><span class="icon"><img src="/img/favicon.svg"></span><span class="icon-text">Documentation</span></a>
|
||||||
|
<a class="footer-link gitea" href="https://git.djeex.fr/Djeex/lumeex"><span class="icon"><img src="/img/gitea.svg"></span><span class="icon-text">Giteex</span></a>
|
||||||
|
<a class="footer-link github" href="https://github.com/Djeex/lumeex"><span class="icon"><img src="/img/github.svg"></span><span class="icon-text">Github</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Loader -->
|
||||||
|
<div id="global-loader">
|
||||||
|
<div class="loader-inner">
|
||||||
|
<div class="loader-spinner"></div>
|
||||||
|
<div id="loader-text">Uploading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,158 +1,141 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "template/base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
{% block title %}Lumeex - Theme Editor{% endblock %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta charset="UTF-8">
|
{% block content %}
|
||||||
<title>Theme Editor</title>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Top bar -->
|
|
||||||
<div class="nav-bar">
|
|
||||||
<div class="content-inner nav">
|
|
||||||
<input type="checkbox" id="nav-check">
|
|
||||||
<div class="nav-header">
|
|
||||||
<div class="nav-title">
|
|
||||||
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="nav-btn">
|
|
||||||
<label for="nav-check">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="nav-links">
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
|
||||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
|
||||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<button id="build-btn" class="button">Build !</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Toast container for notifications -->
|
|
||||||
<div id="theme-editor" class="content-inner">
|
|
||||||
<div id="toast-container"></div>
|
|
||||||
<h1>Edit Theme</h1>
|
<h1>Edit Theme</h1>
|
||||||
<!-- Show current theme -->
|
<!-- Show current theme -->
|
||||||
<div class="theme-info">
|
<div class="theme-info">
|
||||||
<strong>Current theme:</strong> <span id="current-theme"></span>
|
<strong>Current theme:</strong> <span id="current-theme"></span>
|
||||||
</div>
|
</div>
|
||||||
<form id="theme-editor-form">
|
<div id="theme-editor-form">
|
||||||
<!-- Colors Section -->
|
<!-- Colors Section -->
|
||||||
<fieldset>
|
<form id="colors-form" autocomplete="off">
|
||||||
|
<fieldset id="color-picker">
|
||||||
<h2>Colors</h2>
|
<h2>Colors</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Set the color values for your theme</p>
|
<p>Set the color values for your theme</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<!-- Example for one color field, repeat for all -->
|
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Primary</label>
|
<label>Primary</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-primary-btn" class="color-btn"></button>
|
<button type="button" id="color-primary-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.primary" id="color-primary" class="color-input">
|
<input type="color" name="colors.primary" id="color-primary" class="color-input" required>
|
||||||
<input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
|
<input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Primary Dark</label>
|
<label>Primary Dark</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-primary-dark-btn" class="color-btn"></button>
|
<button type="button" id="color-primary-dark-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input">
|
<input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input" required>
|
||||||
<input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
|
<input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Secondary</label>
|
<label>Secondary</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-secondary-btn" class="color-btn"></button>
|
<button type="button" id="color-secondary-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.secondary" id="color-secondary" class="color-input">
|
<input type="color" name="colors.secondary" id="color-secondary" class="color-input" required>
|
||||||
<input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
|
<input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Accent</label>
|
<label>Accent</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-accent-btn" class="color-btn"></button>
|
<button type="button" id="color-accent-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.accent" id="color-accent" class="color-input">
|
<input type="color" name="colors.accent" id="color-accent" class="color-input" required>
|
||||||
<input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
|
<input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Text Dark</label>
|
<label>Text Dark</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-text-dark-btn" class="color-btn"></button>
|
<button type="button" id="color-text-dark-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.text_dark" id="color-text-dark" class="color-input">
|
<input type="color" name="colors.text_dark" id="color-text-dark" class="color-input" required>
|
||||||
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
|
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Background</label>
|
<label>Background</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-background-btn" class="color-btn"></button>
|
<button type="button" id="color-background-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.background" id="color-background" class="color-input">
|
<input type="color" name="colors.background" id="color-background" class="color-input" required>
|
||||||
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
|
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Browser Color</label>
|
<label>Browser Color</label>
|
||||||
<div class="fields color-fields">
|
<div class="fields color-fields">
|
||||||
<button type="button" id="color-browser-color-btn" class="color-btn"></button>
|
<button type="button" id="color-browser-color-btn" class="color-btn"></button>
|
||||||
<input type="color" name="colors.browser_color" id="color-browser-color" class="color-input">
|
<input type="color" name="colors.browser_color" id="color-browser-color" class="color-input" required>
|
||||||
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
|
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="colors">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
<!-- Google Fonts Section -->
|
<!-- Google Fonts Section -->
|
||||||
|
<form id="google-fonts-form" autocomplete="off">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Google Fonts</h2>
|
<h2>Google Fonts</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Add Google Fonts to your theme</p>
|
<p>Add Google Fonts to your theme</p>
|
||||||
<div class="fields" id="google-fonts-fields">
|
<div class="fields" id="google-fonts-fields">
|
||||||
<!-- JS will render font family and weights inputs here -->
|
<!-- JS will render font family and weights inputs here -->
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="add-google-font">Add Google Font</button>
|
<button type="button" id="add-google-font">Add Google Font</button>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="google-fonts">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
<!-- Custom Font Upload Section -->
|
<!-- Custom Font Upload Section -->
|
||||||
|
<form id="font-upload-form" autocomplete="off">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Upload Custom Font</h2>
|
<h2>Upload Custom Font</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Supported formats: .woff, .woff2</p>
|
<p>Supported formats: .woff, .woff2</p>
|
||||||
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
|
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
|
||||||
<div id="local-fonts-list" class="font-list"></div>
|
<div id="local-fonts-list" class="font-list"></div>
|
||||||
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
|
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
|
||||||
|
<!-- Save button not needed for upload-only section -->
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
<!-- Fonts Section -->
|
<!-- Fonts Section -->
|
||||||
|
<form id="fonts-form" autocomplete="off">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Fonts</h2>
|
<h2>Fonts</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Select where to apply your fonts</p>
|
<p>Select where to apply your fonts</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Primary Font</label>
|
<label>Primary Font</label>
|
||||||
<select name="fonts.primary.name" id="font-primary"></select>
|
<select name="fonts.primary.name" id="font-primary" required></select>
|
||||||
<label>Fallback</label>
|
<label>Fallback</label>
|
||||||
<select name="fonts.primary.fallback" id="font-primary-fallback">
|
<select name="fonts.primary.fallback" id="font-primary-fallback" required>
|
||||||
<option value="sans-serif">sans-serif</option>
|
<option value="sans-serif">sans-serif</option>
|
||||||
<option value="serif">serif</option>
|
<option value="serif">serif</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Secondary Font</label>
|
<label>Secondary Font</label>
|
||||||
<select name="fonts.secondary.name" id="font-secondary"></select>
|
<select name="fonts.secondary.name" id="font-secondary" required></select>
|
||||||
<label>Fallback</label>
|
<label>Fallback</label>
|
||||||
<select name="fonts.secondary.fallback" id="font-secondary-fallback">
|
<select name="fonts.secondary.fallback" id="font-secondary-fallback" required>
|
||||||
<option value="sans-serif">sans-serif</option>
|
<option value="sans-serif">sans-serif</option>
|
||||||
<option value="serif">serif</option>
|
<option value="serif">serif</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="fonts">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</form>
|
||||||
<!-- Favicon Section -->
|
<!-- Favicon Section -->
|
||||||
|
<form id="favicon-form" autocomplete="off">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Favicon</h2>
|
<h2>Favicon</h2>
|
||||||
|
<p class="section-status"></p>
|
||||||
<p>Supported formats: .png, .jpg, .jpeg</p>
|
<p>Supported formats: .png, .jpg, .jpeg</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -166,10 +149,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="section-save-btn" data-section="favicon">Save</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button type="submit">Save Theme</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Stepper -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Steps</h2>
|
||||||
|
<p>Follow the steps to generate your static gallery</p>
|
||||||
|
<ul id="stepper">
|
||||||
|
<li><a href="/gallery-editor">Upload your photos</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a href="/site-info">Configure site info</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
|
||||||
|
<div></div>
|
||||||
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<!-- Delete confirmation modal for favicon -->
|
<!-- Delete confirmation modal for favicon -->
|
||||||
<div id="delete-favicon-modal" class="modal" style="display:none;">
|
<div id="delete-favicon-modal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -194,17 +191,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Build success modal -->
|
|
||||||
<div id="build-success-modal" class="modal" style="display:none;">
|
{% endblock %}
|
||||||
<div class="modal-content">
|
|
||||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
{% block scripts %}
|
||||||
<h3>✅ Build completed!</h3>
|
|
||||||
<p>Your files are available in the output folder.</p>
|
|
||||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
|
||||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/build.js') }}"></script>
|
{% endblock %}
|
||||||
</body>
|
|
||||||
</html>
|
|
Reference in New Issue
Block a user