Compare commits
26 Commits
v2.0
...
5fb6ef18d1
Author | SHA1 | Date | |
---|---|---|---|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.*
|
.*
|
||||||
|
!.env
|
||||||
!.sh
|
!.sh
|
||||||
!.gitignore
|
!.gitignore
|
||||||
output/
|
output/
|
||||||
|
@ -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 {
|
||||||
|
@ -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,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: '#ffffff'
|
|
||||||
browser_color: '#ffffff'
|
|
||||||
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
|
@ -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,33 @@ 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
|
if [ ! -d "/app/config" ]; then
|
||||||
mkdir -p /app/config
|
mkdir -p /app/config
|
||||||
fi
|
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
|
# Recursively check all files and folders in /app/default
|
||||||
filename=$(basename "$file")
|
while IFS= read -r src; do
|
||||||
target="/app/config/$filename"
|
relpath="${src#/app/default/}"
|
||||||
|
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
|
||||||
|
cp -r "$src" "$target"
|
||||||
|
else
|
||||||
|
cp "$src" "$target"
|
||||||
|
fi
|
||||||
files_copied=true
|
files_copied=true
|
||||||
fi
|
fi
|
||||||
done
|
done < <(find /app/default -mindepth 1)
|
||||||
|
|
||||||
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,11 +47,13 @@ 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 preview 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=$!
|
||||||
|
|
||||||
echo "Starting Lumeex Flask webui..."
|
echo "[~] Starting Lumeex Flask webui..."
|
||||||
python3 -u -m src.py.webui.webui &
|
python3 -u -m src.py.webui.webui &
|
||||||
WEBUI_PID=$!
|
WEBUI_PID=$!
|
||||||
|
|
||||||
@ -72,15 +78,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,10 +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"
|
||||||
- "5000:5000"
|
- "${WEBUI_PORT:-5000}:5000"
|
||||||
|
|
@ -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 {
|
||||||
|
@ -30,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"
|
||||||
@ -72,9 +74,14 @@ 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
|
@app.context_processor
|
||||||
def inject_version():
|
def inject_version():
|
||||||
return dict(lumeex_version=lumeex_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")
|
||||||
@ -203,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 ---
|
||||||
@ -487,5 +506,6 @@ def download_output_zip():
|
|||||||
|
|
||||||
# --- Run server ---
|
# --- Run server ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.info("Starting WebUI at http://0.0.0.0:5000")
|
logging.info("[~] Starting WebUI at http://0.0.0.0:5000")
|
||||||
|
logging.info(f"[i] WebUI host port is set to {WEBUI_PORT}")
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
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>
|
||||||
|
@ -18,7 +18,16 @@
|
|||||||
<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 -->
|
||||||
@ -31,8 +40,27 @@
|
|||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
@ -92,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 = () => {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
let galleryImages = [];
|
let galleryImages = [];
|
||||||
let heroImages = [];
|
let heroImages = [];
|
||||||
let allTags = []; // global tag list
|
let allTags = []; // global tag list
|
||||||
|
let showOnlyUntagged = false;
|
||||||
|
|
||||||
// --- Load images from server on page load ---
|
// --- Load images from server on page load ---
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
@ -34,12 +35,20 @@ function updateAllTags() {
|
|||||||
function renderGallery() {
|
function renderGallery() {
|
||||||
const container = document.getElementById('gallery');
|
const container = document.getElementById('gallery');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
galleryImages.forEach((img, i) => {
|
|
||||||
|
// Filter images if toggle is on
|
||||||
|
let imagesToShow = galleryImages;
|
||||||
|
if (showOnlyUntagged) {
|
||||||
|
imagesToShow = galleryImages.filter(img => !img.tags || img.tags.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesToShow.forEach((img) => {
|
||||||
|
const i = galleryImages.indexOf(img); // Use real index for tag/delete actions
|
||||||
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">
|
||||||
@ -54,11 +63,51 @@ function renderGallery() {
|
|||||||
renderTags(i, img.tags || []);
|
renderTags(i, img.tags || []);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show/hide Remove All button
|
// Update gallery count (top)
|
||||||
|
const galleryCount = document.getElementById('gallery-count');
|
||||||
|
if (galleryCount) {
|
||||||
|
galleryCount.innerHTML = `<p>${imagesToShow.length} photos</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update gallery count (bottom)
|
||||||
|
const galleryCountBottom = document.getElementById('gallery-count-bottom');
|
||||||
|
if (galleryCountBottom) {
|
||||||
|
galleryCountBottom.innerHTML = `<p>${imagesToShow.length} photos</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide Remove All button (top)
|
||||||
const removeAllBtn = document.getElementById('remove-all-gallery');
|
const removeAllBtn = document.getElementById('remove-all-gallery');
|
||||||
if (removeAllBtn) {
|
if (removeAllBtn) {
|
||||||
removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
|
removeAllBtn.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show/hide bottom upload row
|
||||||
|
const bottomGalleryUpload = document.getElementById('bottom-gallery-upload');
|
||||||
|
if (bottomGalleryUpload) {
|
||||||
|
bottomGalleryUpload.style.display = imagesToShow.length > 0 ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide Remove All button (bottom)
|
||||||
|
const removeAllBtnBottom = document.getElementById('remove-all-gallery-bottom');
|
||||||
|
if (removeAllBtnBottom) {
|
||||||
|
removeAllBtnBottom.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade-in effect for loaded images
|
||||||
|
const fadeImages = document.querySelectorAll("img.fade-in-img");
|
||||||
|
fadeImages.forEach(img => {
|
||||||
|
const onLoad = () => {
|
||||||
|
img.classList.add("loaded");
|
||||||
|
};
|
||||||
|
if (img.complete && img.naturalHeight !== 0) {
|
||||||
|
onLoad();
|
||||||
|
} else {
|
||||||
|
img.addEventListener("load", onLoad, { once: true });
|
||||||
|
img.addEventListener("error", () => {
|
||||||
|
console.warn("Image failed to load:", img.dataset.src || img.src);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Render tags for a single image ---
|
// --- Render tags for a single image ---
|
||||||
@ -233,7 +282,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">
|
||||||
@ -244,11 +293,35 @@ function renderHero() {
|
|||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show/hide Remove All button
|
// Update hero count (top)
|
||||||
|
const heroCount = document.getElementById('hero-count');
|
||||||
|
if (heroCount) {
|
||||||
|
heroCount.innerHTML = `<p>${heroImages.length} photos</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hero count (bottom)
|
||||||
|
const heroCountBottom = document.getElementById('hero-count-bottom');
|
||||||
|
if (heroCountBottom) {
|
||||||
|
heroCountBottom.innerHTML = `<p>${heroImages.length} photos</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide Remove All button (top)
|
||||||
const removeAllBtn = document.getElementById('remove-all-hero');
|
const removeAllBtn = document.getElementById('remove-all-hero');
|
||||||
if (removeAllBtn) {
|
if (removeAllBtn) {
|
||||||
removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
|
removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show/hide bottom upload row
|
||||||
|
const bottomHeroUpload = document.getElementById('bottom-hero-upload');
|
||||||
|
if (bottomHeroUpload) {
|
||||||
|
bottomHeroUpload.style.display = heroImages.length > 0 ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide Remove All button (bottom)
|
||||||
|
const removeAllBtnBottom = document.getElementById('remove-all-hero-bottom');
|
||||||
|
if (removeAllBtnBottom) {
|
||||||
|
removeAllBtnBottom.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Save gallery to server ---
|
// --- Save gallery to server ---
|
||||||
@ -427,12 +500,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
|
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
|
||||||
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
|
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
|
||||||
|
|
||||||
|
// --- Radio toggle for gallery filter ---
|
||||||
|
const showAllRadio = document.getElementById('show-all-radio');
|
||||||
|
const showUntaggedRadio = document.getElementById('show-untagged-radio');
|
||||||
|
|
||||||
|
if (showAllRadio && showUntaggedRadio) {
|
||||||
|
showAllRadio.addEventListener('change', () => {
|
||||||
|
if (showAllRadio.checked) {
|
||||||
|
showOnlyUntagged = false;
|
||||||
|
renderGallery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
showUntaggedRadio.addEventListener('change', () => {
|
||||||
|
if (showUntaggedRadio.checked) {
|
||||||
|
showOnlyUntagged = true;
|
||||||
|
renderGallery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk delete buttons
|
// Bulk delete buttons
|
||||||
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
|
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
|
||||||
|
const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom');
|
||||||
const removeAllHeroBtn = document.getElementById('remove-all-hero');
|
const removeAllHeroBtn = document.getElementById('remove-all-hero');
|
||||||
|
const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom');
|
||||||
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
|
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
|
||||||
|
if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all');
|
||||||
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
|
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
|
||||||
|
if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Initialize ---
|
// --- Initialize ---
|
||||||
loadData();
|
loadData();
|
@ -26,14 +26,19 @@ function hideLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Form and menu logic
|
// --- Section Forms ---
|
||||||
const form = document.getElementById("site-info-form");
|
const infoForm = document.getElementById("info-form");
|
||||||
|
const socialForm = document.getElementById("social-form");
|
||||||
|
const menuForm = document.getElementById("menu-form");
|
||||||
|
const footerForm = document.getElementById("footer-form");
|
||||||
|
const legalsForm = document.getElementById("legals-form");
|
||||||
|
const buildForm = 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) => {
|
||||||
@ -50,7 +55,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update menu items from inputs
|
|
||||||
function updateMenuItemsFromInputs() {
|
function updateMenuItemsFromInputs() {
|
||||||
const inputs = menuList.querySelectorAll("input");
|
const inputs = menuList.querySelectorAll("input");
|
||||||
const items = [];
|
const items = [];
|
||||||
@ -62,12 +66,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
menuItems = items;
|
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) => {
|
||||||
@ -83,7 +86,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update IP paragraphs from textareas
|
|
||||||
function updateIpParagraphsFromInputs() {
|
function updateIpParagraphsFromInputs() {
|
||||||
const textareas = ipList.querySelectorAll("textarea");
|
const textareas = ipList.querySelectorAll("textarea");
|
||||||
ipParagraphs = Array.from(textareas).map(textarea => ({
|
ipParagraphs = Array.from(textareas).map(textarea => ({
|
||||||
@ -91,27 +93,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
})).filter(item => item.paragraph !== "");
|
})).filter(item => item.paragraph !== "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build options
|
// --- Build options ---
|
||||||
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
|
// --- 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 elements for delete confirmation ---
|
||||||
const deleteModal = document.getElementById("delete-modal");
|
const deleteModal = document.getElementById("delete-modal");
|
||||||
const deleteModalClose = document.getElementById("delete-modal-close");
|
const deleteModalClose = document.getElementById("delete-modal-close");
|
||||||
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
|
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
|
||||||
const deleteModalCancel = document.getElementById("delete-modal-cancel");
|
const deleteModalCancel = document.getElementById("delete-modal-cancel");
|
||||||
|
|
||||||
// Modal elements for theme deletion
|
// --- Modal elements for theme deletion ---
|
||||||
const deleteThemeModal = document.getElementById("delete-theme-modal");
|
const deleteThemeModal = document.getElementById("delete-theme-modal");
|
||||||
const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
|
const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
|
||||||
const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
|
const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
|
||||||
@ -119,7 +121,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
|
const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
|
||||||
let themeToDelete = null;
|
let themeToDelete = null;
|
||||||
|
|
||||||
// Show/hide thumbnail preview, remove button, and choose button
|
// --- 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 || "";
|
||||||
@ -133,12 +135,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose thumbnail button triggers file input
|
// --- 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)
|
// --- 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];
|
||||||
@ -156,17 +158,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
} else {
|
} else {
|
||||||
showToast("❌ Error uploading thumbnail", "error");
|
showToast("❌ Error uploading thumbnail", "error");
|
||||||
}
|
}
|
||||||
|
updateSectionStatus("social");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove thumbnail button triggers modal
|
// --- Remove thumbnail button triggers modal ---
|
||||||
if (removeThumbnailBtn) {
|
if (removeThumbnailBtn) {
|
||||||
removeThumbnailBtn.addEventListener("click", () => {
|
removeThumbnailBtn.addEventListener("click", () => {
|
||||||
deleteModal.style.display = "flex";
|
deleteModal.style.display = "flex";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal logic for thumbnail deletion
|
// --- Modal logic for thumbnail deletion ---
|
||||||
if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
|
if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
|
||||||
deleteModalClose.onclick = deleteModalCancel.onclick = () => {
|
deleteModalClose.onclick = deleteModalCancel.onclick = () => {
|
||||||
deleteModal.style.display = "none";
|
deleteModal.style.display = "none";
|
||||||
@ -187,10 +190,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
showToast("❌ Error removing thumbnail", "error");
|
showToast("❌ Error removing thumbnail", "error");
|
||||||
}
|
}
|
||||||
deleteModal.style.display = "none";
|
deleteModal.style.display = "none";
|
||||||
|
updateSectionStatus("social");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme upload logic (custom theme folder)
|
// --- Theme upload logic (custom theme folder) ---
|
||||||
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) {
|
||||||
@ -223,10 +227,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
} else {
|
} else {
|
||||||
showToast("❌ Error uploading theme", "error");
|
showToast("❌ Error uploading theme", "error");
|
||||||
}
|
}
|
||||||
|
updateSectionStatus("build");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove theme button triggers modal
|
// --- Remove theme button triggers modal ---
|
||||||
const removeThemeBtn = document.getElementById("remove-theme-btn");
|
const removeThemeBtn = document.getElementById("remove-theme-btn");
|
||||||
if (removeThemeBtn && themeSelect) {
|
if (removeThemeBtn && themeSelect) {
|
||||||
removeThemeBtn.addEventListener("click", () => {
|
removeThemeBtn.addEventListener("click", () => {
|
||||||
@ -242,7 +247,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal logic for theme deletion
|
// --- Modal logic for theme deletion ---
|
||||||
if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
|
if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
|
||||||
deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
|
deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
|
||||||
deleteThemeModal.style.display = "none";
|
deleteThemeModal.style.display = "none";
|
||||||
@ -283,10 +288,67 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
deleteThemeModal.style.display = "none";
|
deleteThemeModal.style.display = "none";
|
||||||
themeToDelete = null;
|
themeToDelete = null;
|
||||||
|
updateSectionStatus("build");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch theme list and populate select
|
// --- Fetch theme list and populate select, then load config and update build status ---
|
||||||
|
let loadedConfig = {};
|
||||||
|
function loadConfigAndUpdateBuildStatus() {
|
||||||
|
fetch("/api/site-info")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
loadedConfig = data;
|
||||||
|
// Info
|
||||||
|
if (infoForm) {
|
||||||
|
infoForm.elements["info.title"].value = data.info?.title || "";
|
||||||
|
infoForm.elements["info.subtitle"].value = data.info?.subtitle || "";
|
||||||
|
infoForm.elements["info.description"].value = data.info?.description || "";
|
||||||
|
infoForm.elements["info.canonical"].value = data.info?.canonical || "";
|
||||||
|
infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
|
||||||
|
infoForm.elements["info.author"].value = data.info?.author || "";
|
||||||
|
}
|
||||||
|
// Social
|
||||||
|
if (socialForm) {
|
||||||
|
socialForm.elements["social.instagram_url"].value = data.social?.instagram_url || "";
|
||||||
|
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
|
||||||
|
updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
|
||||||
|
}
|
||||||
|
// Menu
|
||||||
|
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
||||||
|
renderMenuItems();
|
||||||
|
// Footer
|
||||||
|
if (footerForm) {
|
||||||
|
footerForm.elements["footer.copyright"].value = data.footer?.copyright || "";
|
||||||
|
footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || "";
|
||||||
|
}
|
||||||
|
// Legals
|
||||||
|
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
||||||
|
? data.legals.intellectual_property
|
||||||
|
: [];
|
||||||
|
renderIpParagraphs();
|
||||||
|
if (legalsForm) {
|
||||||
|
legalsForm.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
||||||
|
legalsForm.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
|
||||||
|
legalsForm.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
||||||
|
}
|
||||||
|
// Build
|
||||||
|
if (themeSelect) {
|
||||||
|
themeSelect.value = data.build?.theme || "";
|
||||||
|
}
|
||||||
|
if (convertImagesCheckbox) {
|
||||||
|
convertImagesCheckbox.checked = !!data.build?.convert_images;
|
||||||
|
}
|
||||||
|
if (resizeImagesCheckbox) {
|
||||||
|
resizeImagesCheckbox.checked = !!data.build?.resize_images;
|
||||||
|
}
|
||||||
|
// Initial status update for all sections except build
|
||||||
|
["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
|
||||||
|
// For build, update status after theme select is set
|
||||||
|
updateSectionStatus("build");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (themeSelect) {
|
if (themeSelect) {
|
||||||
fetch("/api/themes")
|
fetch("/api/themes")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
@ -298,148 +360,268 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
option.textContent = theme;
|
option.textContent = theme;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
// Set selected value after loading config
|
// Now load config and update build status after theme select is ready
|
||||||
fetch("/api/site-info")
|
loadConfigAndUpdateBuildStatus();
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
themeSelect.value = data.build?.theme || "";
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// If no theme select, just load config
|
||||||
|
loadConfigAndUpdateBuildStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config from server and populate form
|
// --- Add menu item ---
|
||||||
if (form) {
|
|
||||||
fetch("/api/site-info")
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
|
||||||
? data.legals.intellectual_property
|
|
||||||
: [];
|
|
||||||
renderIpParagraphs();
|
|
||||||
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
|
||||||
renderMenuItems();
|
|
||||||
form.elements["info.title"].value = data.info?.title || "";
|
|
||||||
form.elements["info.subtitle"].value = data.info?.subtitle || "";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add menu item
|
|
||||||
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
|
// --- 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
|
// --- Update menuItems on input change ---
|
||||||
menuList.addEventListener("input", () => {
|
menuList.addEventListener("input", () => {
|
||||||
updateMenuItemsFromInputs();
|
updateMenuItemsFromInputs();
|
||||||
|
updateSectionStatus("menu");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add paragraph
|
// --- Add paragraph ---
|
||||||
if (addIpBtn) {
|
if (addIpBtn) {
|
||||||
addIpBtn.addEventListener("click", () => {
|
addIpBtn.addEventListener("click", () => {
|
||||||
ipParagraphs.push({ paragraph: "" });
|
ipParagraphs.push({ paragraph: "" });
|
||||||
renderIpParagraphs();
|
renderIpParagraphs();
|
||||||
|
updateSectionStatus("legals");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove paragraph
|
// --- 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
|
// --- 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: infoForm.elements["info.title"].value,
|
||||||
|
subtitle: infoForm.elements["info.subtitle"].value,
|
||||||
|
description: infoForm.elements["info.description"].value,
|
||||||
|
canonical: infoForm.elements["info.canonical"].value,
|
||||||
|
keywords: infoForm.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
|
||||||
|
author: infoForm.elements["info.author"].value
|
||||||
|
};
|
||||||
|
case "social":
|
||||||
|
return {
|
||||||
|
instagram_url: socialForm.elements["social.instagram_url"].value,
|
||||||
|
thumbnail: thumbnailInput ? thumbnailInput.value : ""
|
||||||
|
};
|
||||||
|
case "menu":
|
||||||
updateMenuItemsFromInputs();
|
updateMenuItemsFromInputs();
|
||||||
|
return { items: menuItems };
|
||||||
|
case "footer":
|
||||||
|
return {
|
||||||
|
copyright: footerForm.elements["footer.copyright"].value,
|
||||||
|
legal_label: footerForm.elements["footer.legal_label"].value
|
||||||
|
};
|
||||||
|
case "legals":
|
||||||
updateIpParagraphsFromInputs();
|
updateIpParagraphsFromInputs();
|
||||||
|
return {
|
||||||
// Check if thumbnail is set before saving (uploaded or present in input)
|
hoster_name: legalsForm.elements["legals.hoster_name"].value,
|
||||||
if (!thumbnailInput || !thumbnailInput.value) {
|
hoster_address: legalsForm.elements["legals.hoster_address"].value,
|
||||||
showLoader("Saving...");
|
hoster_contact: legalsForm.elements["legals.hoster_contact"].value,
|
||||||
showToast("❌ Thumbnail is required.", "error");
|
intellectual_property: ipParagraphs
|
||||||
hideLoader();
|
};
|
||||||
return;
|
case "build":
|
||||||
}
|
return {
|
||||||
|
|
||||||
const build = {
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// --- REMOVE loader for save ---
|
|
||||||
// showLoader("Saving...");
|
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; // Only check theme is present
|
||||||
|
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 ---
|
||||||
|
[
|
||||||
|
{ form: infoForm, section: "info" },
|
||||||
|
{ form: socialForm, section: "social" },
|
||||||
|
{ form: menuForm, section: "menu" },
|
||||||
|
{ form: footerForm, section: "footer" },
|
||||||
|
{ form: legalsForm, section: "legals" },
|
||||||
|
{ form: buildForm, section: "build" }
|
||||||
|
].forEach(({ form, section }) => {
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener("input", () => updateSectionStatus(section));
|
||||||
|
form.addEventListener("change", () => updateSectionStatus(section));
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Save section handler (form submit) ---
|
||||||
|
[
|
||||||
|
{ form: infoForm, section: "info" },
|
||||||
|
{ form: socialForm, section: "social" },
|
||||||
|
{ form: menuForm, section: "menu" },
|
||||||
|
{ form: footerForm, section: "footer" },
|
||||||
|
{ form: legalsForm, section: "legals" },
|
||||||
|
{ form: buildForm, section: "build" }
|
||||||
|
].forEach(({ form, section }) => {
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Native browser validation
|
||||||
|
if (!form.reportValidity()) {
|
||||||
|
showToast("❌ Please fill all required fields before saving.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Social section: check thumbnail
|
||||||
|
if (section === "social") {
|
||||||
|
if (!thumbnailInput || !thumbnailInput.value) {
|
||||||
|
showToast("❌ Thumbnail is required.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Menu section: check all menu items
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Legals section: check all paragraphs
|
||||||
|
if (section === "legals") {
|
||||||
|
updateIpParagraphsFromInputs();
|
||||||
|
if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
|
||||||
|
showToast("❌ Please fill all intellectual property paragraphs.", "error");
|
||||||
|
updateSectionStatus(section);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build payload for this section only
|
||||||
|
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" },
|
||||||
@ -447,10 +629,18 @@ 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");
|
||||||
|
// Reload config for this section
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -116,54 +116,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
|
||||||
@ -341,9 +460,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("");
|
||||||
}
|
}
|
||||||
@ -367,6 +486,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,6 +516,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@ -414,54 +535,72 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
googleFonts.push(...updatedGoogleFonts);
|
googleFonts.push(...updatedGoogleFonts);
|
||||||
renderGoogleFonts(googleFonts);
|
renderGoogleFonts(googleFonts);
|
||||||
refreshFontDropdowns();
|
refreshFontDropdowns();
|
||||||
|
updateSectionStatus("google-fonts", loadedConfig);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
|
// --- Section status listeners ---
|
||||||
|
[
|
||||||
|
{ 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();
|
||||||
showLoader("Saving theme...");
|
if (!form.reportValidity() || !isSectionComplete(section)) {
|
||||||
const data = {};
|
showToast("❌ Please fill all required fields before saving.", "error");
|
||||||
data.colors = {
|
updateSectionStatus(section, loadedConfig);
|
||||||
primary: document.getElementById("color-primary-text").value,
|
return;
|
||||||
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
|
|
||||||
};
|
|
||||||
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();
|
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));
|
||||||
|
});
|
@ -11,6 +11,7 @@ function hideLoader() {
|
|||||||
if (loader) loader.classList.remove("active");
|
if (loader) loader.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Upload gallery images ---
|
// --- Upload gallery images ---
|
||||||
const galleryInput = document.getElementById('upload-gallery');
|
const galleryInput = document.getElementById('upload-gallery');
|
||||||
if (galleryInput) {
|
if (galleryInput) {
|
||||||
@ -62,3 +63,55 @@ if (heroInput) {
|
|||||||
} finally { e.target.value = ''; }
|
} finally { e.target.value = ''; }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Upload gallery images (bottom button) ---
|
||||||
|
const galleryInputBottom = document.getElementById('upload-gallery-bottom');
|
||||||
|
if (galleryInputBottom) {
|
||||||
|
galleryInputBottom.addEventListener('change', async (e) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files.length) return;
|
||||||
|
showLoader("Uploading photos...");
|
||||||
|
const formData = new FormData();
|
||||||
|
for (const file of files) formData.append('files', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
hideLoader();
|
||||||
|
if (res.ok) {
|
||||||
|
showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
|
||||||
|
if (typeof refreshGallery === "function") refreshGallery();
|
||||||
|
} else showToast('Error: ' + data.error, "error");
|
||||||
|
} catch(err) {
|
||||||
|
hideLoader();
|
||||||
|
console.error(err);
|
||||||
|
showToast('Server error!', "error");
|
||||||
|
} finally { e.target.value = ''; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Upload hero images (bottom button) ---
|
||||||
|
const heroInputBottom = document.getElementById('upload-hero-bottom');
|
||||||
|
if (heroInputBottom) {
|
||||||
|
heroInputBottom.addEventListener('change', async (e) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files.length) return;
|
||||||
|
showLoader("Uploading hero photos...");
|
||||||
|
const formData = new FormData();
|
||||||
|
for (const file of files) formData.append('files', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
hideLoader();
|
||||||
|
if (res.ok) {
|
||||||
|
showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
|
||||||
|
if (typeof refreshHero === "function") refreshHero();
|
||||||
|
} else showToast('Error: ' + data.error, "error");
|
||||||
|
} catch(err) {
|
||||||
|
hideLoader();
|
||||||
|
console.error(err);
|
||||||
|
showToast('Server error!', "error");
|
||||||
|
} finally { e.target.value = ''; }
|
||||||
|
});
|
||||||
|
}
|
@ -5,10 +5,12 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<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">
|
||||||
@ -36,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">
|
||||||
@ -49,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%;">
|
||||||
@ -66,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">
|
||||||
@ -78,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">
|
||||||
@ -105,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>
|
||||||
@ -129,9 +156,11 @@
|
|||||||
</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">
|
<div class="section">
|
||||||
<h2>Steps</h2>
|
<h2>Steps</h2>
|
||||||
<p>Follow the steps to generate your static gallery</p>
|
<p>Follow the steps to generate your static gallery</p>
|
||||||
@ -145,7 +174,6 @@
|
|||||||
<li><button id="stepper-build">Generate your static site!</button></li>
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- Delete thumbnail confirmation modal-->
|
<!-- Delete thumbnail confirmation modal-->
|
||||||
<div class="content-inner">
|
<div class="content-inner">
|
||||||
<div id="delete-modal" class="modal" style="display:none;">
|
<div id="delete-modal" class="modal" style="display:none;">
|
||||||
|
@ -7,7 +7,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin:0px;
|
margin:0px;
|
||||||
width: 100vw;
|
min-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -236,11 +236,32 @@ h2 {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Filter Row --- */
|
||||||
|
.filter-row label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row input[type="radio"] {
|
||||||
|
accent-color: #55c3ec;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Gallery & Hero Grid --- */
|
/* --- Gallery & Hero Grid --- */
|
||||||
#gallery, #hero {
|
#gallery, #hero {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
margin: 30px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hero-count-bottom, #gallery-count-bottom {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-action-row {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +337,20 @@ h2 {
|
|||||||
.toast.success { background-color: #28a7468c; }
|
.toast.success { background-color: #28a7468c; }
|
||||||
.toast.error { background-color: #dc3545; }
|
.toast.error { background-color: #dc3545; }
|
||||||
|
|
||||||
|
/* img fade in */
|
||||||
|
|
||||||
|
.fade-in-img {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.02);
|
||||||
|
transition: opacity 1.2s ease-out, transform 1.2s ease-out;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-img.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Tags --- */
|
/* --- Tags --- */
|
||||||
.tag-input {
|
.tag-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -366,15 +401,18 @@ h2 {
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
display: none;
|
display: none;
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-input ul.suggestions li {
|
.tag-input ul.suggestions li {
|
||||||
padding: 6px 8px;
|
padding: 6px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-input ul.suggestions li:hover {
|
.tag-input ul.suggestions li:hover {
|
||||||
background-color: #007782;
|
background-color: #007782;
|
||||||
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-display {
|
.tags-display {
|
||||||
@ -406,7 +444,9 @@ h2 {
|
|||||||
background-color: #007782;
|
background-color: #007782;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions li {
|
.suggestions li {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -416,6 +456,14 @@ h2 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-actions-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center; /* Center horizontally */
|
||||||
|
align-items: center; /* Center vertically */
|
||||||
|
}
|
||||||
|
|
||||||
.flex-column {
|
.flex-column {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -521,18 +569,23 @@ h2 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Remove All Buttons --- */
|
/* --- Remove All Buttons --- */
|
||||||
#remove-all-hero, #remove-all-gallery {
|
#remove-all-hero, #remove-all-gallery, .bottom-remove-btn {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
color: white;
|
color: white;
|
||||||
display: none;
|
display: none;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottom-remove-btn {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
#remove-all-gallery:hover,
|
#remove-all-gallery:hover,
|
||||||
#remove-all-hero:hover {
|
#remove-all-hero:hover, .bottom-remove-btn:hover {
|
||||||
background: rgb(121, 26, 19);
|
background: rgb(121, 26, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,6 +594,7 @@ h2 {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,9 +642,9 @@ label {
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input, #theme-editor-form input,
|
form input,
|
||||||
#site-info-form textarea, #theme-editor-form textarea,
|
form textarea,
|
||||||
#site-info-form select, #theme-editor-form select {
|
form select {
|
||||||
background: #1f2223;
|
background: #1f2223;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
@ -612,27 +666,24 @@ label {
|
|||||||
gap: 0 18px;
|
gap: 0 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-info {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input::placeholder,
|
||||||
#site-info-form input::placeholder,
|
form textarea::placeholder {
|
||||||
#theme-editor-form input::placeholder,
|
|
||||||
#site-info-form textarea::placeholder,
|
|
||||||
#theme-editor-form textarea::placeholder {
|
|
||||||
color: #585858;
|
color: #585858;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input:focus,
|
form input:focus,
|
||||||
#theme-editor-form input:focus,
|
form textarea:focus,
|
||||||
#site-info-form textarea:focus,
|
form select:focus {
|
||||||
#theme-editor-form textarea:focus,
|
|
||||||
#site-info-form select:focus,
|
|
||||||
#theme-editor-form select:focus {
|
|
||||||
border-color: #585858;
|
border-color: #585858;
|
||||||
background: #161616;
|
background: #161616;
|
||||||
}
|
}
|
||||||
#site-info-form textarea,
|
|
||||||
#theme-editor-form textarea {
|
form textarea {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
@ -651,7 +702,7 @@ img#thumbnail-preview {
|
|||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
|
form button[type="submit"] {
|
||||||
background: linear-gradient(135deg, #26c4ff, #016074);
|
background: linear-gradient(135deg, #26c4ff, #016074);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -659,17 +710,18 @@ img#thumbnail-preview {
|
|||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
padding: 12px 32px;
|
padding: 12px 32px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
margin: 0 0 45px 0;
|
margin: 16px 0 0 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 16px rgba(38,196,255,0.15);
|
box-shadow: 0 4px 16px rgba(38,196,255,0.15);
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="submit"]:hover, #theme-editor-form button[type="submit"]:hover {
|
form button[type="submit"]:hover {
|
||||||
background: linear-gradient(135deg, #72d9ff, #26657e);
|
background: linear-gradient(135deg, #72d9ff, #26657e);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
|
form button[type="button"] {
|
||||||
background: #00000000;
|
background: #00000000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
@ -682,34 +734,36 @@ img#thumbnail-preview {
|
|||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
|
form button[type="button"]:hover{
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
|
form button.remove-menu-item,
|
||||||
|
form button.remove-ip-paragraph {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
|
form button.remove-menu-item:hover,
|
||||||
|
form button.remove-ip-paragraph:hover {
|
||||||
background: rgb(121, 26, 19);
|
background: rgb(121, 26, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
|
form button.remove-btn {
|
||||||
|
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
|
form button.remove-btn:hover {
|
||||||
background: rgb(121, 26, 19);
|
background: rgb(121, 26, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
|
form .thumbnail-form-label {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,6 +825,10 @@ fieldset p, .section p {
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-status {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Stepper --- */
|
/* --- Stepper --- */
|
||||||
|
|
||||||
#stepper {
|
#stepper {
|
||||||
@ -905,6 +963,7 @@ justify-content: center;
|
|||||||
|
|
||||||
#footer a {
|
#footer a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-credit .lum-first::before {
|
.footer-credit .lum-first::before {
|
||||||
@ -920,6 +979,7 @@ justify-content: center;
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-text {
|
.icon-text {
|
||||||
@ -994,12 +1054,12 @@ justify-content: center;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#stepper {
|
form button[type="button"], form button[type="submit"], #stepper li {
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#stepper li {
|
#stepper {
|
||||||
width: 100%;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container, .footer-links {
|
.footer-container, .footer-links {
|
||||||
@ -1020,14 +1080,16 @@ justify-content: center;
|
|||||||
/* Hide the default arrow */
|
/* Hide the default arrow */
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
#site-info-form, #theme-editor-form {
|
|
||||||
padding: 18px 8px;
|
|
||||||
}
|
|
||||||
.input-field {
|
.input-field {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#color-picker .input-field{
|
#color-picker .input-field{
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,10 @@
|
|||||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
<span id="build-success-modal-close" class="modal-close">×</span>
|
||||||
<h3>✅ Build completed!</h3>
|
<h3>✅ Build completed!</h3>
|
||||||
<p>Your files are available in the output folder.</p>
|
<p>Your files are available in the output folder.</p>
|
||||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
<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 id="zip-loader" style="display:none;">Creating ZIP...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<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 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://gitea.com/Djeex/lumeex"><span class="icon"><img src="/img/gitea.svg"></span><span class="icon-text">Giteex</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>
|
<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>
|
||||||
|
@ -9,116 +9,133 @@
|
|||||||
<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 -->
|
||||||
|
<form id="colors-form" autocomplete="off">
|
||||||
<fieldset id="color-picker">
|
<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">
|
||||||
@ -132,9 +149,11 @@
|
|||||||
</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>
|
||||||
|
<!-- Stepper -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Steps</h2>
|
<h2>Steps</h2>
|
||||||
<p>Follow the steps to generate your static gallery</p>
|
<p>Follow the steps to generate your static gallery</p>
|
||||||
@ -148,8 +167,6 @@
|
|||||||
<li><button id="stepper-build">Generate your static site!</button></li>
|
<li><button id="stepper-build">Generate your static site!</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</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">
|
||||||
|
Reference in New Issue
Block a user