Help
RSS
API
Feed
Maltego
Contact
Domain > hermannbredehorst.com
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2019-11-29
98.129.229.184
(
ClassC
)
2026-01-20
172.67.69.103
(
ClassC
)
Port 443
HTTP/1.1 200 OKDate: Tue, 20 Jan 2026 04:08:08 GMTContent-Type: text/html;charsetUTF-8Content-Length: 706976Connection: keep-aliveCache-Control: no-store, no-cache, must-revalidate, proxy-revalidateExpires: 0Pragma: no-cacheX-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; modeblockVary: accept-encodingReport-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?s2Xlm9x5i2CtToEBUvVogKM%2BRsOFbU9yju87D97T3ivNfDUHnzzr%2F7nIFtt9MEL3oYA2OPhxc07ckYBkUdYtlxiqs8q0OY7l3viCnIlpDHZsJug%3D%3D}}Nel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9c0bb57b2adc0bee-PDX !DOCTYPE html> html langen> head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> meta namehydra-site-id content9kccrxyp> !-- Primary Meta Tags --> title>Hermann Bredehorst/title> meta namedescription contentHermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL> meta namerobots contentindex, follow> meta nameauthor contenthermannbredehorst.com> meta namelanguage contenten> !-- Canonical URL --> link relcanonical hrefhttps://hermannbredehorst.com/> !-- Favicon --> !-- Open Graph / Facebook / iMessage / Discord --> meta propertyog:type contentarticle> meta propertyog:url contenthttps://hermannbredehorst.com/> meta propertyog:title contentHermann Bredehorst> meta propertyog:description contentHermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL> meta propertyog:site_name contenthermannbredehorst.com> meta propertyog:locale contenten_US> !-- Twitter / X Card --> meta nametwitter:card contentsummary_large_image> meta nametwitter:site content@hermannbredehorst.com> meta nametwitter:url contenthttps://hermannbredehorst.com/> meta nametwitter:title contentHermann Bredehorst> meta nametwitter:description contentHermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL> !-- Additional SEO Meta Tags --> meta nametheme-color content#000000> meta namemsapplication-TileColor content#000000> meta nameapple-mobile-web-app-capable contentyes> meta nameapple-mobile-web-app-status-bar-style contentdefault> meta nameapple-mobile-web-app-title contentHermann Bredehorst> !-- Security Headers --> meta http-equivX-Content-Type-Options contentnosniff> meta http-equivX-XSS-Protection content1; modeblock> script typeapplication/ld+json> { @context: https://schema.org, @type: WebPage, name: Hermann Bredehorst, description: Hermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL, url: https://hermannbredehorst.com/, datePublished: 2026-01-20T04:08:08.369Z, dateModified: 2026-01-20T04:08:08.369Z, inLanguage: en-US, isPartOf: { @type: WebSite, @id: https://hermannbredehorst.com/#website, name: hermannbredehorst.com, url: https://hermannbredehorst.com/, description: Hermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL, publisher: { @type: Organization, name: hermannbredehorst.com, url: https://hermannbredehorst.com/ } }, breadcrumb: { @type: BreadcrumbList, itemListElement: { @type: ListItem, position: 1, name: Home, item: https://hermannbredehorst.com/ }, { @type: ListItem, position: 2, name: Hermann Bredehorst, item: https://hermannbredehorst.com/ } }} /script> !-- Preview mode helper script --> script> // Immediately execute to patch request methods before any other script runs (function() { // Detect preview mode by checking hostname const isPreviewMode window.location.hostname preview.neonsky.app; if (isPreviewMode) { console.log(Preview Helper Preview mode detected); // Extract GUID from URL path (first segment) const pathParts window.location.pathname.split(/).filter(Boolean); const previewGuid pathParts.length > 0 ? pathParts0 : ; if (!previewGuid) { console.warn(Preview Helper No GUID found in path:, window.location.pathname); return; // Exit if no GUID found } console.log(Preview Helper Using GUID:, previewGuid); // Store these in window for other scripts window.isPreviewMode true; window.previewGuid previewGuid; window.isHydraEnvironment true; window.Parameters window.Parameters || {}; window.Parameters.isHydra true; // Helper function that adds the GUID to API URLs and relative links window.getApiUrl function(endpoint) { // For relative paths if (endpoint.startsWith(/api/)) { const newUrl / + previewGuid + endpoint; console.log(Preview Helper Transformed API URL:, endpoint, →, newUrl); return newUrl; } return endpoint; }; // Helper function that adds the GUID to any relative link window.getPreviewUrl function(url) { // For relative paths (starts with /) that arent already prefixed with GUID if (url.startsWith(/) && !url.startsWith(/ + previewGuid + /)) { const newUrl / + previewGuid + url; console.log(Preview Helper Transformed relative link:, url, →, newUrl); return newUrl; } return url; }; // PATCH FETCH API with enhanced URL handling const originalFetch window.fetch; window.fetch function(resource, init) { // Only transform string URLs if (typeof resource string) { // Case 1: Handle relative paths starting with /api/ if (resource.startsWith(/api/)) { const newUrl window.getApiUrl(resource); console.log(Preview Helper Patched fetch call (API):, resource, →, newUrl); return originalFetch(newUrl, init); } // Case 2: Handle other relative paths (for content/gallery links) else if (resource.startsWith(/) && !resource.startsWith(/ + previewGuid + /)) { const newUrl window.getPreviewUrl(resource); console.log(Preview Helper Patched fetch call (relative):, resource, →, newUrl); return originalFetch(newUrl, init); } // Case 3: Handle absolute URLs to the same domain containing /api/ else if (resource.includes(window.location.host) && resource.includes(/api/)) { try { const urlObj new URL(resource); if (urlObj.hostname window.location.hostname) { const path urlObj.pathname; const newPath / + previewGuid + path; urlObj.pathname newPath; const newUrl urlObj.toString(); console.log(Preview Helper Patched fetch call (absolute API):, resource, →, newUrl); return originalFetch(newUrl, init); } } catch (e) { console.error(Preview Helper Error processing URL:, e); } } // Case 4: Handle absolute URLs to the same domain (non-API) else if (resource.includes(window.location.host)) { try { const urlObj new URL(resource); if (urlObj.hostname window.location.hostname && !urlObj.pathname.startsWith(/ + previewGuid + /)) { const path urlObj.pathname; const newPath / + previewGuid + path; urlObj.pathname newPath; const newUrl urlObj.toString(); console.log(Preview Helper Patched fetch call (absolute):, resource, →, newUrl); return originalFetch(newUrl, init); } } catch (e) { console.error(Preview Helper Error processing URL:, e); } } } return originalFetch(resource, init); }; console.log(Preview Helper Patched fetch API); // PATCH XMLHttpRequest with enhanced URL handling const originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function(method, url, ...rest) { // Only transform string URLs if (typeof url string) { // Case 1: Handle relative paths starting with /api/ if (url.startsWith(/api/)) { const newUrl window.getApiUrl(url); console.log(Preview Helper Patched XMLHttpRequest (API):, url, →, newUrl); return originalOpen.call(this, method, newUrl, ...rest); } // Case 2: Handle other relative paths else if (url.startsWith(/) && !url.startsWith(/ + previewGuid + /)) { const newUrl window.getPreviewUrl(url); console.log(Preview Helper Patched XMLHttpRequest (relative):, url, →, newUrl); return originalOpen.call(this, method, newUrl, ...rest); } // Case 3: Handle absolute URLs to the same domain containing /api/ else if (url.includes(window.location.host) && url.includes(/api/)) { try { const urlObj new URL(url); if (urlObj.hostname window.location.hostname) { const path urlObj.pathname; const newPath / + previewGuid + path; urlObj.pathname newPath; const newUrl urlObj.toString(); console.log(Preview Helper Patched XMLHttpRequest (absolute API):, url, →, newUrl); return originalOpen.call(this, method, newUrl, ...rest); } } catch (e) { console.error(Preview Helper Error processing URL:, e); } } // Case 4: Handle absolute URLs to the same domain (non-API) else if (url.includes(window.location.host)) { try { const urlObj new URL(url); if (urlObj.hostname window.location.hostname && !urlObj.pathname.startsWith(/ + previewGuid + /)) { const path urlObj.pathname; const newPath / + previewGuid + path; urlObj.pathname newPath; const newUrl urlObj.toString(); console.log(Preview Helper Patched XMLHttpRequest (absolute):, url, →, newUrl); return originalOpen.call(this, method, newUrl, ...rest); } } catch (e) { console.error(Preview Helper Error processing URL:, e); } } } return originalOpen.call(this, method, url, ...rest); }; console.log(Preview Helper Patched XMLHttpRequest); // Note: Link click handling is done by the existing gallery link interceptor // The fetch and XMLHttpRequest patches above handle programmatic requests // Add debugging aid window.debugPreviewUrls function() { return { hostname: window.location.hostname, pathname: window.location.pathname, guid: window.previewGuid, isPreviewMode: window.isPreviewMode, sampleTransformations: { relativeApi: window.getApiUrl(/api/menu-styles), relativeLink: window.getPreviewUrl(/gallery), absolute: new URL(/api/save-config, window.location.origin).toString().replace(/api/, / + previewGuid + /api/) } }; }; // Test the transformation immediately console.log(Preview Helper Test API transformation:, window.getApiUrl(/api/menu-styles)); console.log(Preview Helper Test relative link transformation:, window.getPreviewUrl(/gallery)); console.log(Preview Helper Test absolute URL transformation:, new URL(/api/save-config, window.location.origin).toString().replace(/api/, / + previewGuid + /api/)); } })(); /script> link relstylesheet hrefhttps://cdn.neonsky.app/menu-styles-v260119-004.css?t1768882088369 onerrorthis.onerrornull; this.hrefthis.href.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>script srchttps://assets.lemonsqueezy.com/lemon.js defer>/script>script srchttps://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js>/script>script srchttps://cdn.jsdelivr.net/npm/magic-sdk>/script>script srchttps://cdn.jsdelivr.net/npm/@magic-ext/auth>/script>script srchttps://cdn.neonsky.app/style-customizer-v260119-004.js?t1768882088369 defer onerrorthis.onerrornull; this.srcthis.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>/script>script srchttps://cdn.neonsky.app/sidebar-manager-v260119-004.js?t1768882088369 defer onerrorthis.onerrornull; this.srcthis.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>/script>script srchttps://cdn.neonsky.app/image-uploader-v260119-004.js?t1768882088369 defer onerrorthis.onerrornull; this.srcthis.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>/script>script srchttps://cdn.neonsky.app/page-manager-v260119-004.js?t1768882088369 defer onerrorthis.onerrornull; this.srcthis.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>/script>link relstylesheet hrefhttps://cdn.neonsky.app/page-styles-v260119-004.css?t1768882088369 onerrorthis.onerrornull; this.hrefthis.href.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/);>link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css>!-- CDN Fallback Handler for CSS and JS files -->script>(function() { // Global flag: Set to true when any CDN resource fails and uses proxy window.useCdnProxy window.useCdnProxy || false; // Global base URL function: Returns proxy base if flag is set, otherwise CDN base window.getCdnBaseUrl function() { return window.useCdnProxy ? window.location.origin + /cdn-proxy/ : https://cdn.neonsky.app/; }; // Utility function to convert CDN URLs to proxy URLs window.convertCdnToProxyUrl function(cdnUrl) { if (!cdnUrl || !cdnUrl.includes(cdn.neonsky.app)) return cdnUrl; return cdnUrl.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); }; // Utility function to construct CDN URLs (uses proxy if flag is set) window.buildCdnUrl function(path) { return window.getCdnBaseUrl() + path; }; // Resource tracking for debugging window._resourceTracking window._resourceTracking || { resources: , proxyFlagChanges: , imageLoads: , networkErrors: }; // Track when proxy flag changes - use a wrapper function let proxyFlagChangeCount 0; let _useCdnProxyValue window.useCdnProxy || false; // Create a setter function that tracks changes window._setUseCdnProxy function(newValue, reason) { const oldValue _useCdnProxyValue; if (oldValue ! newValue) { proxyFlagChangeCount++; const changeInfo { timestamp: new Date().toISOString(), changeNumber: proxyFlagChangeCount, oldValue: oldValue, newValue: newValue, reason: reason || Unknown, stack: new Error().stack?.split(\n).slice(1, 6).join(\n) || No stack }; window._resourceTracking.proxyFlagChanges.push(changeInfo); console.error(🔍 PROXY_FLAG_CHANGE:, JSON.stringify(changeInfo, null, 2)); // Keep only last 50 changes if (window._resourceTracking.proxyFlagChanges.length > 50) { window._resourceTracking.proxyFlagChanges.shift(); } _useCdnProxyValue newValue; window.useCdnProxy newValue; } }; // Track resource loading function trackResource(url, type, status, details) { const entry { timestamp: new Date().toISOString(), url: url, type: type, // script, link, image, etc. status: status, // loading, success, failed, proxy, storage details: details || {}, useCdnProxy: window.useCdnProxy }; window._resourceTracking.resources.push(entry); console.error(📊 RESOURCE_TRACK:, JSON.stringify(entry, null, 2)); // Keep only last 200 resources if (window._resourceTracking.resources.length > 200) { window._resourceTracking.resources.shift(); } } // Track image loads function trackImageLoad(url, status, source) { const entry { timestamp: new Date().toISOString(), url: url, status: status, // loading, success, failed source: source, // cdn, proxy, storage useCdnProxy: window.useCdnProxy }; window._resourceTracking.imageLoads.push(entry); console.error(🖼️ IMAGE_LOAD:, JSON.stringify(entry, null, 2)); // Keep only last 100 image loads if (window._resourceTracking.imageLoads.length > 100) { window._resourceTracking.imageLoads.shift(); } } // Track network errors function trackNetworkError(url, error, context) { const entry { timestamp: new Date().toISOString(), url: url, error: { message: error?.message || Unknown error, name: error?.name || Unknown, type: error?.type || unknown, toString: String(error) }, context: context || {}, useCdnProxy: window.useCdnProxy }; window._resourceTracking.networkErrors.push(entry); console.error(❌ NETWORK_ERROR:, JSON.stringify(entry, null, 2)); // Keep only last 100 errors if (window._resourceTracking.networkErrors.length > 100) { window._resourceTracking.networkErrors.shift(); } } // Expose tracking functions globally window.trackResource trackResource; window.trackImageLoad trackImageLoad; window.trackNetworkError trackNetworkError; // Function to retry failed resources via proxy // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag // The proxy can handle both network errors and 404s, so its safer to err on the side of using it function retryViaProxy(element, originalSrc, errorEvent) { if (!originalSrc) originalSrc element.href || element.src; if (!originalSrc || !originalSrc.includes(cdn.neonsky.app)) return; // Prevent multiple retries if (element.getAttribute(data-retried)) { return; } element.setAttribute(data-retried, true); // Check if element still exists in DOM if (!element.parentNode) { console.warn(⚠️ Element no longer in DOM, skipping proxy retry:, originalSrc); return; } // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s // Well be aggressive and set the flag for any CDN failure to ensure images also use proxy // Check error message if available, but default to assuming network error const hasExplicitNetworkError errorEvent && errorEvent.message && ( errorEvent.message.includes(Failed to fetch) || errorEvent.message.includes(SSL) || errorEvent.message.includes(secure connection) || errorEvent.message.includes(network) || errorEvent.message.includes(ERR_) || errorEvent.message.includes(SSL_ERROR) ); // Track the error trackNetworkError(originalSrc, errorEvent, { elementType: element.tagName, hasExplicitNetworkError: hasExplicitNetworkError, errorMessage: errorEvent?.message || No error message, errorName: errorEvent?.name || No error name }); // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure) // This covers ISP-affected clients where SSL handshake fails before HTTP request const reason hasExplicitNetworkError ? Explicit network/SSL error detected : (!errorEvent || !errorEvent.message) ? Browser silent failure (likely SSL/network) : CDN resource failed (using proxy as fallback); if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) { window._setUseCdnProxy(true, reason); console.error(🚩 CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests); console.error( Error details:, errorEvent ? (errorEvent.message || No error message (likely SSL/network failure)) : No error event); } else { // Even for other errors, set the flag to be safe - proxy can handle 404s too window._setUseCdnProxy(true, reason); console.error(🚩 CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback); } const proxyUrl originalSrc.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); console.error(🔄 CDN failed, retrying via proxy:, originalSrc, ->, proxyUrl); // Track the retry trackResource(originalSrc, element.tagName.toLowerCase(), failed, { retryUrl: proxyUrl, error: errorEvent?.message || Unknown }); if (element.tagName SCRIPT) { const newScript document.createElement(script); newScript.src proxyUrl; newScript.defer element.defer; newScript.async element.async; newScript.setAttribute(data-retried, true); newScript.onload function() { trackResource(proxyUrl, script, proxy-success, { originalUrl: originalSrc }); }; newScript.onerror function() { trackResource(proxyUrl, script, proxy-failed, { originalUrl: originalSrc }); console.error(❌ Proxy also failed, trying storage:, proxyUrl); const storageUrl originalSrc.replace(cdn.neonsky.app, storage.neonsky.app); trackResource(storageUrl, script, storage-fallback, { originalUrl: originalSrc, proxyUrl: proxyUrl }); newScript.src storageUrl; newScript.onload function() { trackResource(storageUrl, script, storage-success, { originalUrl: originalSrc }); }; }; element.parentNode.replaceChild(newScript, element); } else if (element.tagName LINK) { const newLink document.createElement(link); newLink.rel element.rel; newLink.href proxyUrl; newLink.setAttribute(data-retried, true); newLink.onload function() { trackResource(proxyUrl, link, proxy-success, { originalUrl: originalSrc }); }; newLink.onerror function() { trackResource(proxyUrl, link, proxy-failed, { originalUrl: originalSrc }); console.error(❌ Proxy also failed, trying storage:, proxyUrl); const storageUrl originalSrc.replace(cdn.neonsky.app, storage.neonsky.app); trackResource(storageUrl, link, storage-fallback, { originalUrl: originalSrc, proxyUrl: proxyUrl }); newLink.href storageUrl; newLink.onload function() { trackResource(storageUrl, link, storage-success, { originalUrl: originalSrc }); }; }; element.parentNode.replaceChild(newLink, element); } } // Monitor script and link tags for errors document.addEventListener(error, function(e) { const target e.target; if ((target.tagName SCRIPT || target.tagName LINK) && target.src && target.src.includes(cdn.neonsky.app)) { retryViaProxy(target, target.src, e); } else if (target.tagName LINK && target.href && target.href.includes(cdn.neonsky.app)) { retryViaProxy(target, target.href, e); } }, true); // Also check existing scripts/links that may have failed before this script loaded // For ISP-affected clients, we assume CDN failures are network/SSL errors setTimeout(function() { document.querySelectorAll(scriptsrc*cdn.neonsky.app, linkhref*cdn.neonsky.app).forEach(function(el) { if (el.tagName SCRIPT && !el.getAttribute(data-loaded) && !el.getAttribute(data-retried)) { el.addEventListener(error, function(errorEvent) { // For ISP-affected clients, assume any CDN failure is a network/SSL error // The proxy can handle both network errors and 404s retryViaProxy(el, el.src, errorEvent); }); } else if (el.tagName LINK && !el.sheet && !el.getAttribute(data-loaded) && !el.getAttribute(data-retried)) { // CSS link failed if it has no stylesheet - check after a delay setTimeout(function() { if (!el.sheet && !el.getAttribute(data-retried)) { // For ISP-affected clients, assume any CDN failure is a network/SSL error retryViaProxy(el, el.href, null); } }, 2000); } }); }, 100);})();/script>!-- Log Capture System - S+J to download logs (works even if gallery doesnt load) -->script>(function() { use strict; // Only initialize if not already initialized if (window._indexLogCaptureInitialized) { return; } window._indexLogCaptureInitialized true; // Wait for DOM to be ready function initLogCapture() { // Store original console methods const originalConsole { log: console.log, warn: console.warn, error: console.error, info: console.info }; // Array to store all console messages window._capturedLogs window._capturedLogs || ; // Override console methods to capture logs log, warn, error, info.forEach(method > { consolemethod function(...args) { // Call original method originalConsolemethod.apply(console, args); // Capture the log with timestamp window._capturedLogs.push({ timestamp: new Date().toISOString(), level: method.toUpperCase(), message: args.map(arg > { if (typeof arg object) { try { return JSON.stringify(arg, null, 2); } catch (e) { return String(arg); } } return String(arg); }).join( ) }); // Keep only last 2000 logs to prevent memory issues if (window._capturedLogs.length > 2000) { window._capturedLogs.shift(); } }; }); // Add keyboard shortcut to download logs (S+J) let pressedKeys new Set(); document.addEventListener(keydown, function(event) { const key event.key.toLowerCase(); if (key s || key j) { pressedKeys.add(key); // Check if both S and J are pressed if (pressedKeys.has(s) && pressedKeys.has(j)) { event.preventDefault(); console.log(S+J detected - downloading logs...); // Show a brief visual indicator const indicator document.createElement(div); indicator.style.cssText position: fixed; top: 20px; right: 20px; z-index: 10000; + background: #4CAF50; color: white; padding: 10px 20px; + border-radius: 5px; font-family: Arial, sans-serif; + font-size: 14px; font-weight: bold; + box-shadow: 0 2px 10px rgba(0,0,0,0.3);; indicator.textContent 📥 Downloading logs...; document.body.appendChild(indicator); setTimeout(() > { if (indicator.parentNode) { indicator.parentNode.removeChild(indicator); } }, 2000); downloadLogs(); // Clear the set after triggering pressedKeys.clear(); } } }); document.addEventListener(keyup, function(event) { const key event.key.toLowerCase(); if (key s || key j) { pressedKeys.delete(key); } }); console.log(📋 Log capture system initialized. Press S+J simultaneously to download logs.); } // Download captured logs as a text file function downloadLogs() { try { // Get all scripts and links, categorized const allScripts Array.from(document.querySelectorAll(scriptsrc)).map(s > s.src); const allLinks Array.from(document.querySelectorAll(linkhref)).map(l > l.href); // Get additional debug info const debugInfo { url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), useCdnProxy: window.useCdnProxy || false, windowLocation: window.location.toString(), documentReadyState: document.readyState, // All scripts/links (not just CDN) allScriptsLoaded: allScripts, allLinksLoaded: allLinks, // CDN-specific (for backward compatibility and CDN debugging) scriptsLoaded: allScripts.filter(src > src.includes(cdn.neonsky.app)), linksLoaded: allLinks.filter(href > href.includes(cdn.neonsky.app)), // Gallery scripts (loaded from Worker) galleryScriptsLoaded: allScripts.filter(src > src.includes(/gallery-scripts/)), galleryLinksLoaded: allLinks.filter(href > href.includes(/gallery-scripts/)), // Failed resources failedScripts: Array.from(document.querySelectorAll(scriptsrc*cdn.neonsky.appdata-retried)).map(s > s.src), failedLinks: Array.from(document.querySelectorAll(linkhref*cdn.neonsky.appdata-retried)).map(l > l.href), // Proxy resources proxyScriptsLoaded: allScripts.filter(src > src.includes(/cdn-proxy/)), proxyLinksLoaded: allLinks.filter(href > href.includes(/cdn-proxy/)), // Resource tracking data resourceTracking: window._resourceTracking || { resources: , proxyFlagChanges: , imageLoads: , networkErrors: }, // Performance timing performanceTiming: performance.timing ? { navigationStart: performance.timing.navigationStart, domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, domInteractive: performance.timing.domInteractive - performance.timing.navigationStart } : null }; // Create log content const logContent INDEX.TS DEBUG LOGS , Generated: + debugInfo.timestamp, URL: + debugInfo.url, User Agent: + debugInfo.userAgent, useCdnProxy Flag: + debugInfo.useCdnProxy, Document Ready State: + debugInfo.documentReadyState, , , DEBUG INFO , JSON.stringify(debugInfo, null, 2), , RESOURCE TRACKING SUMMARY , Total Resources Tracked: + (debugInfo.resourceTracking.resources.length || 0), Proxy Flag Changes: + (debugInfo.resourceTracking.proxyFlagChanges.length || 0), Image Loads Tracked: + (debugInfo.resourceTracking.imageLoads.length || 0), Network Errors Tracked: + (debugInfo.resourceTracking.networkErrors.length || 0), , PROXY FLAG CHANGES , JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2), , RESOURCE LOADING TRACKING , JSON.stringify(debugInfo.resourceTracking.resources, null, 2), , IMAGE LOADING TRACKING , JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2), , NETWORK ERRORS , JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2), , CONSOLE LOGS , ...(window._capturedLogs || ).map(log > + log.timestamp + + log.level + : + log.message ) .join(\n); // Create and download file const blob new Blob(logContent, { type: text/plain }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download index-debug-logs- + new Date().toISOString().replace(/:./g, -) + .txt; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log(✅ Debug logs downloaded successfully!); } catch (error) { console.error(❌ Failed to download logs:, error); alert(Failed to download logs. Error: + (error.message || Unknown error)); } } // Initialize when DOM is ready if (document.readyState loading) { document.addEventListener(DOMContentLoaded, initLogCapture); } else { // DOM already ready, initialize immediately setTimeout(initLogCapture, 100); }})();/script>!-- Log Capture System - S+J to download logs -->script>(function() { use strict; // Only initialize if not already initialized if (window._indexLogCaptureInitialized) { return; } window._indexLogCaptureInitialized true; // Wait for DOM to be ready function initLogCapture() { // Store original console methods const originalConsole { log: console.log, warn: console.warn, error: console.error, info: console.info }; // Array to store all console messages window._capturedLogs window._capturedLogs || ; // Override console methods to capture logs log, warn, error, info.forEach(method > { consolemethod function(...args) { // Call original method originalConsolemethod.apply(console, args); // Capture the log with timestamp window._capturedLogs.push({ timestamp: new Date().toISOString(), level: method.toUpperCase(), message: args.map(arg > { if (typeof arg object) { try { return JSON.stringify(arg, null, 2); } catch (e) { return String(arg); } } return String(arg); }).join( ) }); // Keep only last 2000 logs to prevent memory issues if (window._capturedLogs.length > 2000) { window._capturedLogs.shift(); } }; }); // Add keyboard shortcut to download logs (S+J) let pressedKeys new Set(); document.addEventListener(keydown, function(event) { const key event.key.toLowerCase(); if (key s || key j) { pressedKeys.add(key); // Check if both S and J are pressed if (pressedKeys.has(s) && pressedKeys.has(j)) { event.preventDefault(); console.log(S+J detected - downloading logs...); // Show a brief visual indicator const indicator document.createElement(div); indicator.style.cssText position: fixed; top: 20px; right: 20px; z-index: 10000; + background: #4CAF50; color: white; padding: 10px 20px; + border-radius: 5px; font-family: Arial, sans-serif; + font-size: 14px; font-weight: bold; + box-shadow: 0 2px 10px rgba(0,0,0,0.3);; indicator.textContent 📥 Downloading logs...; document.body.appendChild(indicator); setTimeout(() > { if (indicator.parentNode) { indicator.parentNode.removeChild(indicator); } }, 2000); downloadLogs(); // Clear the set after triggering pressedKeys.clear(); } } }); document.addEventListener(keyup, function(event) { const key event.key.toLowerCase(); if (key s || key j) { pressedKeys.delete(key); } }); console.log(📋 Log capture system initialized. Press S+J simultaneously to download logs.); } // Download captured logs as a text file function downloadLogs() { try { // Get all scripts and links, categorized const allScripts Array.from(document.querySelectorAll(scriptsrc)).map(s > s.src); const allLinks Array.from(document.querySelectorAll(linkhref)).map(l > l.href); // Get additional debug info const debugInfo { url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), useCdnProxy: window.useCdnProxy || false, windowLocation: window.location.toString(), documentReadyState: document.readyState, // All scripts/links (not just CDN) allScriptsLoaded: allScripts, allLinksLoaded: allLinks, // CDN-specific (for backward compatibility and CDN debugging) scriptsLoaded: allScripts.filter(src > src.includes(cdn.neonsky.app)), linksLoaded: allLinks.filter(href > href.includes(cdn.neonsky.app)), // Gallery scripts (loaded from Worker) galleryScriptsLoaded: allScripts.filter(src > src.includes(/gallery-scripts/)), galleryLinksLoaded: allLinks.filter(href > href.includes(/gallery-scripts/)), // Failed resources failedScripts: Array.from(document.querySelectorAll(scriptsrc*cdn.neonsky.appdata-retried)).map(s > s.src), failedLinks: Array.from(document.querySelectorAll(linkhref*cdn.neonsky.appdata-retried)).map(l > l.href), // Proxy resources proxyScriptsLoaded: allScripts.filter(src > src.includes(/cdn-proxy/)), proxyLinksLoaded: allLinks.filter(href > href.includes(/cdn-proxy/)), // Resource tracking data resourceTracking: window._resourceTracking || { resources: , proxyFlagChanges: , imageLoads: , networkErrors: }, // Performance timing performanceTiming: performance.timing ? { navigationStart: performance.timing.navigationStart, domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, domInteractive: performance.timing.domInteractive - performance.timing.navigationStart } : null }; // Create log content const logContent INDEX.TS DEBUG LOGS , Generated: + debugInfo.timestamp, URL: + debugInfo.url, User Agent: + debugInfo.userAgent, useCdnProxy Flag: + debugInfo.useCdnProxy, Document Ready State: + debugInfo.documentReadyState, , , DEBUG INFO , JSON.stringify(debugInfo, null, 2), , RESOURCE TRACKING SUMMARY , Total Resources Tracked: + (debugInfo.resourceTracking.resources.length || 0), Proxy Flag Changes: + (debugInfo.resourceTracking.proxyFlagChanges.length || 0), Image Loads Tracked: + (debugInfo.resourceTracking.imageLoads.length || 0), Network Errors Tracked: + (debugInfo.resourceTracking.networkErrors.length || 0), , PROXY FLAG CHANGES , JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2), , RESOURCE LOADING TRACKING , JSON.stringify(debugInfo.resourceTracking.resources, null, 2), , IMAGE LOADING TRACKING , JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2), , NETWORK ERRORS , JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2), , CONSOLE LOGS , ...(window._capturedLogs || ).map(log > + log.timestamp + + log.level + : + log.message ) .join(\n); // Create and download file const blob new Blob(logContent, { type: text/plain }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download index-debug-logs- + new Date().toISOString().replace(/:./g, -) + .txt; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log(✅ Debug logs downloaded successfully!); } catch (error) { console.error(❌ Failed to download logs:, error); alert(Failed to download logs. Error: + (error.message || Unknown error)); } } // Initialize when DOM is ready if (document.readyState loading) { document.addEventListener(DOMContentLoaded, initLogCapture); } else { // DOM already ready, initialize immediately setTimeout(initLogCapture, 100); }})();/script>!-- Add auth token if available -->style>/* Override Pico.css body padding to prevent jumping between pages */body { padding: 0 !important; margin: 0 !important; display: block !important;}/* Scope Pico.css to only affect elements in edit mode */:root:not(data-themepico) {--pico-applied: dark;}/* Add data-themepico to body when in edit mode */body.edit-mode-active {data-theme: dark;}/* Custom styles to ensure Pico only affects editor UI, not content */.sidebar.editing button,.edit-controls button,.element-header button,.text-editor button,.image-uploader button {--pico-applied: dark;}/* Hide default browser-styling when Pico is active */data-themepico button.btn {appearance: dark;-webkit-appearance: dark;}/* Ensure content isnt affected by Pico styles */.gallery-container,.page-text,.sidebar-text {--pico-applied: none !important;}/* Cookie Notice Styles */.cookie-notice { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.9); color: white; padding: 15px 20px; z-index: 10000; display: flex; align-items: center; justify-content: space-between; font-size: 14px; line-height: 1.4; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);}.cookie-notice.hidden { display: none;}.cookie-notice-text { flex: 1; margin-right: 15px;}.cookie-notice-buttons { display: flex; gap: 10px; align-items: center;}.cookie-notice button { background: #4682B4; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s;}.cookie-notice button:hover { background: #3a6d96;}.cookie-notice .close-btn { background: transparent; color: #ccc; padding: 4px 8px; font-size: 18px; line-height: 1;}.cookie-notice .close-btn:hover { background: rgba(255, 255, 255, 0.1); color: white;}/* Prevent FOUC - hide body until layout is ready */body { visibility: hidden;}body.hydra-ready { visibility: visible;}/* Admin panel typography */.sidebar-element-form,.sidebar-element-form *,.add-form,.add-form *,.metadata-editor,.metadata-editor * { font-family: Hanken Grotesk, sans-serif; font-weight: 200; letter-spacing: 0.02em;}.sidebar-element-form h3,.sidebar-element-form label,.add-form label,.metadata-editor h3,.metadata-editor label { text-transform: uppercase; letter-spacing: 0.04em; font-weight: 300;}.sidebar-element-form .form-actions .btn,.sidebar-element-form button,.add-form .form-actions .btn,.add-form button,.metadata-editor .form-actions .btn,.metadata-editor button { text-transform: uppercase; letter-spacing: 0.08em; font-weight: 300;}.sidebar-element-form input,.sidebar-element-form select,.sidebar-element-form textarea,.sidebar-element-form button,.add-form input,.add-form select,.add-form textarea,.add-form button,.metadata-editor input,.metadata-editor select,.metadata-editor textarea,.metadata-editor button { font-family: Hanken Grotesk, sans-serif; font-weight: 200; letter-spacing: 0.02em; border-radius: 0;}/style>!-- Add auth token if available -->script>// Flag to prevent duplicate initializationwindow.hydraInitialized false;function ensureAdminPanelFont() { const head document.head || document.getElementsByTagName(head)0; if (!head) { return; } const linkDefinitions { id: admin-panels-font-preconnect-googleapis, rel: preconnect, href: https://fonts.googleapis.com }, { id: admin-panels-font-preconnect-gstatic, rel: preconnect, href: https://fonts.gstatic.com, crossOrigin: anonymous }, { id: admin-panels-font-stylesheet, rel: stylesheet, href: https://fonts.googleapis.com/css2?familyHanken+Grotesk:ital,wght@0,200;1,200&displayswap } ; for (const definition of linkDefinitions) { if (!document.getElementById(definition.id)) { const link document.createElement(link); link.id definition.id; link.rel definition.rel; link.href definition.href; if (definition.crossOrigin) { link.crossOrigin definition.crossOrigin; } head.appendChild(link); } }}ensureAdminPanelFont(); window.Parameters window.Parameters || {}; window.Parameters.siteId 9kccrxyp; window.Parameters.SiteId 9kccrxyp; // Also set capitalized version window.siteId 9kccrxyp;// Initialize menu styles from serverdocument.addEventListener(DOMContentLoaded, function() {// Apply initial menu styles from serverconst initialMenuStyles {fontFamily:Helvetica, sans-serif,fontSize:14px,color:#545454,activeColor:#bababa,hoverColor:#ffffff,verticalSpacing:2px,menuBackgroundColor:#000000,siteBackgroundColor:#000000,mobileHeaderHeight:60px,sidebarWidth:21vw,sidebarPaddingTop:20px,sidebarPaddingRight:0px,sidebarPaddingBottom:0px,sidebarPaddingLeft:0px,menuLayout:sidebar,disableRightClick:false,fontName:Helvetica};// Set CSS custom propertiesif (initialMenuStyles) { if (initialMenuStyles.fontFamily) { document.documentElement.style.setProperty(--menu-font-family, initialMenuStyles.fontFamily); } if (initialMenuStyles.fontSize) { document.documentElement.style.setProperty(--menu-font-size, initialMenuStyles.fontSize); } if (initialMenuStyles.color) { document.documentElement.style.setProperty(--menu-color, initialMenuStyles.color); } if (initialMenuStyles.activeColor) { document.documentElement.style.setProperty(--menu-active-color, initialMenuStyles.activeColor); } if (initialMenuStyles.hoverColor) { document.documentElement.style.setProperty(--menu-hover-color, initialMenuStyles.hoverColor); } if (initialMenuStyles.verticalSpacing) { document.documentElement.style.setProperty(--menu-vertical-spacing, initialMenuStyles.verticalSpacing); }}// Store initial styles in MenuStyleCustomizer if it existsif (window.MenuStyleCustomizer) { window.MenuStyleCustomizer.settings { ...window.MenuStyleCustomizer.defaults, ...initialMenuStyles };}});/script>script>let magic null;let userMetadata null;let isAuthenticated false;let isAdmin false;let didToken null;let siteId 9kccrxyp;// State Managementwindow.siteMetadata {title:Hermann Bredehorst,description:Hermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL,keywords:,googleAnalytics:,noIndex:false,author:hermannbredehorst.com,language:en,locale:en_US,siteName:hermannbredehorst.com,favicon:};console.log(Client Initialized site metadata:, window.siteMetadata);console.log(Client Google Analytics from metadataToUse:, );console.log(Client Google Analytics in window.siteMetadata:, window.siteMetadata.googleAnalytics);console.log(Client Content Language:, window.siteMetadata.contentLanguage);// State Managementlet galleries {id:1756256969823,title:Spacer,parentId:null,visible:true,isSpacer:true},{id:1756256990355,title:Spacer-copy,parentId:null,visible:true,isSpacer:true,slug:spacer-copy-2,url:/spacer-copy-2,isHomePage:false,position:1},{id:1756257184992,title:Hermann, start here...,isPage:true,isIntegrated:true,isSubmenu:false,pageId:page_1756257184992,visible:false,parentId:null,siteId:9kccrxyp,slug:hermann-start-here,url:/hermann-start-here,pageElements:{id:1756257205303,type:metadata,visible:true,position:1,metaTitle:,metaDescription:,metaKeywords:},{id:1756257215430,type:text,title:Text Block,visible:true,position:1,textContent:p> /p>p>Hi Hermann,/p>p>br>/p>p>To log in.../p>p>br>/p>p>1) Press the span style\color: rgb(255, 0, 200);\> /span>span style\color: rgb(136, 20, 7); font-size: 24px;\>+/span>span style\color: rgb(255, 160, 51); font-size: 24px;\> /span> key on your keyboard./p>p>2) Enter your email address./p>p>3) Enter the One Time Passcode that you receive via email/p>p>br>/p>p>...then click the Edit button in the top-left corner/p>p>br>/p>p>Theres a video how-to below. /p>p>Let us know if you need any help with this!/p>p>br>/p>p>Jayson/p>p>Neon Sky/p>p>br>/p>p>br>/p>,textWidth:53},{id:1756257292267,type:embedded-content,title:New Element,visible:true,position:2,contentUrl:https://share.descript.com/embed/FyubGmgsVIG,containerWidth:87,containerHeight:74,useAutoHeight:false}},{id:1756248557766,title:home,url:/home,isIntegrated:true,isPage:false,isSubmenu:false,visible:false,parentId:null,siteId:9kccrxyp,pageId:o2i8weoo,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:home,normalizedName:home,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:o2i8weoo,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:o2i8weoo,initialPageAlias:home,timestamp:1756249635961,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:true,autoplayDuration:3,autoplayTransition:1,showDescription:false,descriptionsOnDemand:false,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},metaTitle:,metaDescription:,hideMenuOnPage:false,passwordProtected:false,isHomePage:true,siteAlias:null,position:2},{id:1756248557768,title:International,isFolder:true,isCollapsed:true,visible:true,parentId:null,siteId:9kccrxyp,slug:international,importSource:folder,isSubmenu:true,isHomePage:false,position:3},{id:1756248557769,title:Kashmir,url:/kashmir,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:du469sfk,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:kashmir,normalizedName:kashmir,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:du469sfk,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>About 95% of the poppulation in Kashmir are Moselms. India and Pakistan, both nuclear powers, have fought two wars over the state and, twice in the past four years, have come close to a third. Fifty-six years ago, when the British left the subcontinent, the newly independent nations of Pakistan and India fought for Kashmir - Pakistanis felt Kashmirs Muslim majority meant it should be part of their Islamic state. But the war left two-thirds of Kashmir, including the heartland known as the Valley, ruled by India which maintains Kashmir is an integral part of its secular country. The result was a tug-of-war that has lasted five decades. In the late 1980s, following an election rigged by India, the Kashmiris revolted. Their demands were disparate, with factions battling for independence, for Islamicisation, for union with Pakistan. Indian repression and covert aid from Islamabad led to violence. Official figures say that since 1989 an estimated 45,000 people have died, unofficial figures put it at around 90,000. This year around 700 civilians, 500 Indian security men and at least 1,000 militants have died and thousands more have been wounded and maimed./p>,siteAlias:preview.neonsky.app,initialPageUuid:du469sfk,initialPageAlias:kashmir,timestamp:1756250016213,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,metaTitle:,metaDescription:,hideMenuOnPage:false,passwordProtected:false,siteAlias:null,position:4},{id:1756248557770,title:Srinagar Mental Hospital,url:/srinagar-mental-hospital,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:aef793mq,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:srinagar-mental-hospital,normalizedName:srinagar-mental-hospital,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:aef793mq,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Inpatients in the citys mental hospital. 18% of the cases are confirmed cases that result direct from witnessing horrifying and conflict related events. For ex. the execution of a relative at close range./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The CIA says Kashmir is the scene of the worlds most dangerous low-intensity conflict. India and Pakistan, both nuclear powers, have fought two wars over the state and, twice in the past four years, have come close to a third. Fifty-six years ago, when the British left the subcontinent, the newly independent nations of Pakistan and India fought for Kashmir - Pakistanis felt Kashmirs Muslim majority meant it should be part of their Islamic state. But the war left two-thirds of Kashmir, including the heartland known as the Valley, ruled by India which maintains Kashmir as an integral part of its secular country. The result was a tug-of-war that has lasted five decades. In the late 1980s, following an election rigged by India, the Kashmiris revolted. Their demands were disparate, with factions battling for independence, for Islamicisation, for union with Pakistan. Indian repression and covert aid from Islamabad led to violence. Official figures say that since 1989 an estimated 45,000 people have died, unofficial figures put it at around 90,000. This year 2003 around 700 civilians, 500 Indian security men and at least 1,000 militants have died and thousands more have been wounded and maimed./p>,siteAlias:preview.neonsky.app,initialPageUuid:aef793mq,initialPageAlias:srinagar-mental-hospital,timestamp:1756250067679,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:5},{id:1756248557771,title:Kashmir Revisited,url:/kashmir-revisited,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:zhq3qxpq,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:kashmir-revisited,normalizedName:kashmir-revisited,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:zhq3qxpq,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:zhq3qxpq,initialPageAlias:kashmir-revisited,timestamp:1756250110943,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:6},{id:1756248557772,title:Kenya,url:/kenya,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:4zxwhhah,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:kenya,normalizedName:kenya,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:4zxwhhah,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Life in Kenya remains a daily struggle months after post-election violence killed more than 800 people and forced some 300,000 from their homes following the contested re-election of President Mwai Kibaki. Around the world, the elections of December 2007 were widely viewed as flawed, with observers stating they did not meet regional or international standards. Subsequent protests escalated into ethnic violence and property destruction and today the nation is ruled by Kibaki and opposition candidate Raila Odinga, who became the nations second prime minister/p>,siteAlias:preview.neonsky.app,initialPageUuid:4zxwhhah,initialPageAlias:kenya,timestamp:1756250134886,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:7},{id:1756248557773,title:Nicaragua, Acahualinca,url:/nicaragua,-acahualinca,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:4ft9feud,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:nicaragua,-acahualinca,normalizedName:nicaragua,-acahualinca,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:4ft9feud,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Nicaragua, once treated with patronizing affection by the political left from east and west (as a model for developing socialist nations), is a forgotten country today./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>This story is about the people who live on the dump site of Managua which is called Acahualinca./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The circumstances under which the people are living are appalling. The water seeping through the ground of the dump goes through the settlement, which is surrounded by a lagoon of sewage. Malaria, Dengue Fever, and infections of the skin and the eyes are bothering the people./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>A lack of education, no health care (providing family planning), in combination with the blessings of the catholic church and her disapproval of birth control are producing a huge number of kids who are both children and parents./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Many of the teenagers inhale \Pega\ (glue), which makes them apathetic and hopeless./p>,siteAlias:preview.neonsky.app,initialPageUuid:4ft9feud,initialPageAlias:nicaragua,-acahualinca,timestamp:1756250275212,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:8},{id:1756248557774,title:Spain Illegal,url:/spain-illegal,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:wnigrjnx,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:spain-illegal,normalizedName:spain-illegal,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:wnigrjnx,horizontalScrollerText:h2>strong>Spain Illegal/strong>/h2>p>Illegal Immigration in Spain /p>p>According to official numbers about 31.000 illegal immigrants arrived this year by small and unsafe boats at the shores of the Islands, overwhelming the authorities and sparking Spanish criticism that EU states and the European Union in general are not doing enough to help Madrid stem the \flow\. The European Parliament called on Sept. 28th on the EU to create a special fund to help deal with the wave of illegal immigrants heading for the blocs shores. According to NGOs about 3000 would be immigrants died during the passage. /p>,siteAlias:preview.neonsky.app,initialPageUuid:wnigrjnx,initialPageAlias:spain-illegal,timestamp:1756250332015,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:9},{id:1756248557776,title:Thailand Tsunami,url:/thailand-tsunami,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557768,siteId:9kccrxyp,pageId:a2lldfat,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:thailand-tsunami,normalizedName:thailand-tsunami,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:a2lldfat,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>On Dec 26th 2004 an 9.3 earthquake that caused the tsunami occurred off the west coast of the Indonesian island of Sumatra./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>More than 300.000 people died in the region after the giant waves crashed onto the shores of south east Asian and east African countrys./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Thailand and its tourist resorts in Kao Lak lost more than 6500 lives, thousands are still missing. Many more lost everything and live as refugees in set up camps along the coast line. Residents of the destroyed areas continue to return to the sites of their former homes to view the damage and to reclaim personal property./p>,siteAlias:preview.neonsky.app,initialPageUuid:a2lldfat,initialPageAlias:thailand-tsunami,timestamp:1756250400378,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:false,descriptionsOnDemand:false,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:10},{id:1756248557777,title:Corporate,isFolder:true,isCollapsed:true,visible:true,parentId:null,siteId:9kccrxyp,slug:corporate,importSource:folder,isSubmenu:true,isHomePage:false,position:11},{id:1756248557778,title:BRIONI,url:/brioni,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557777,siteId:9kccrxyp,pageId:o5l8ysdl,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:brioni,normalizedName:brioni,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:o5l8ysdl,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:o5l8ysdl,initialPageAlias:brioni,timestamp:1756250422524,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:12},{id:1756248557779,title:Made In Germany,isFolder:true,isCollapsed:true,visible:true,parentId:null,siteId:9kccrxyp,slug:made-in-germany,importSource:folder,isSubmenu:true,isHomePage:false,position:13},{id:1756248557780,title:The Rise Of Germanys New Right,url:/the-rise-of-germanys-new-right,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:9eovrpag,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:the-rise-of-germanys-new-right,normalizedName:the-rise-of-germanys-new-right,galleryOptions:{siteAlias:preview.neonsky.app,initialPageUuid:9eovrpag,initialPageAlias:the-rise-of-germanys-new-right,timestamp:1756250512895,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>The rise of the German new right and the Afd Party/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The Alternative for Germany (German: Alternative für Deutschland, AfD) is a right-wing to far-right political party in Germany. Founded in April 2013i n Berlin by Eurosceptic and nationalist Professors like Bernd Luke it became Germanys first right-wing populist party./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>In 2013, the AfD narrowly missed the 5% electoral threshold to sit in the Bundestag during the 2013 federal election. In 2014 the party won seven seats in the European election as a member of the European Conservatives and Reformists. After securing representation in 14 of the 16 German state parliaments by October of 2017, the AfD became the third largest party in Germany after the 2017 federal election, claiming 94 seats in the Bundestag, a major breakthrough for the party as it was the first time a right wing party had won any seats in the Bundestag since over 50 years. The party is chaired by Jörg Meuthen; its lead candidates in the 2017 elections were AfD Co-Vice Chairman Alexander Gauland and Alice Weidel who now serves as the party group leader in the Bundestag./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Since about 2015, and the refugee crisis in Germany the AfD is increasingly open to working with far-right groups such as the Pegida movement. Portions of the AfD show racist, Islamophobic and/or anti-Semitic tendencies linked to far-right movements such as the Neo-Nazi party NPD and the Identitarian Movement. The strongest support is in the former GDR parts of the reunified country making the AfD in the State of Saxony the strongest party with 27 % of the votes in 2017 federal elections, toping Merkels CDU./p>,startInSingles:true,layoutType:singles,columns:3,spacing:2,manualCollectionName:GUID4db89e34d3312,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}},siteId:9kccrxyp},isHomePage:false,position:14},{id:1756248557781,title:Mannomannomann - The Schulz Story,url:/mannomannomann---the-schulz-story,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:tmwzmwd8,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:mannomannomann---the-schulz-story,normalizedName:mannomannomann---the-schulz-story,galleryOptions:{siteAlias:preview.neonsky.app,initialPageUuid:tmwzmwd8,initialPageAlias:mannomannomann---the-schulz-story,timestamp:1756251750088,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>When he first announced his candidacy for chancellor, ex-European Parliament President Martin Schulz rocketed upward in the opinion polls he then became leader of the German center-left Social Democratic Party SPD in March 2017 by a historic 100 % vote./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Polls also showed Schulz leading Merkel if Germans could elect their chancellor directly./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>But his slide during the campaign against Angela Merkel was just as dramatic./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The man who speaks besides his native German, 5 European languages and who was President of the European Parliament from 2012 to 2017and the Leader of the Progressive Alliance of Socialists and Democrats from 2004 to 2012/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>was described by conservative media and in social media as former alcohol addict, country bumpkin and just a former small town mayor of his West German home town Würselen./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The SPD party campaign was mainly focused on social justice in Germany but without a philosophical and academic vision and plan behind it./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>His speaker and speech writers in the campaign missed to highlight his former career and reputation for his leadership and work in Europe and as SPD party member in leading positions. He joined the party at the age of 19./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Since then he spoke up for decent wage agreements, secure and lasting jobs, employee participation in decision-making and the examination of the social justification for claims and payments./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>Read here the award winning story by Markus Feldenkirchen:/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>English:/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>a href\http://www.spiegel.de/international/germany/martin-schulz-inside-the-failed-campaign-a-1172210.html\ target\_blank\>http://www.spiegel.de/international/germany/martin-schulz-inside-the-failed-campaign-a-1172210.html/a>/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>German:/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>a href\http://www.spiegel.de/spiegel/martin-schulz-die-story-seiner-gescheiterten-kampagne-a-1170957.html\ target\_blank\>http://www.spiegel.de/spiegel/martin-schulz-die-story-seiner-gescheiterten-kampagne-a-1170957.html/a>/p>,startInSingles:true,layoutType:singles,columns:3,spacing:2,manualCollectionName:GUID4db89e34d3312,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}},siteId:9kccrxyp},isHomePage:false,siteAlias:null,position:15},{id:1756248557782,title:Ay Karamba,url:/ay-karamba,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:toac1xf2,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:ay-karamba,normalizedName:ay-karamba,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:toac1xf2,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Senegalese born Karambe Diaby , Social Democratic candidate for the German Parliament poses for a picture in his constituency in Halle/Saale. Diaby, a PhD in chemistry, canvassed the former hub for East Germanys chemical industry to become the first black member of the Bundestag in German history./p>,siteAlias:preview.neonsky.app,initialPageUuid:toac1xf2,initialPageAlias:ay-karamba,timestamp:1756252300616,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:16},{id:1756248557783,title:Noblesse oblige,url:/noblesse-oblige,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:vur6wq5c,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:noblesse-oblige,normalizedName:noblesse-oblige,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:vur6wq5c,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:vur6wq5c,initialPageAlias:noblesse-oblige,timestamp:1756252395415,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:17},{id:1756248557784,title:Shield And Sword,url:/shield-and-sword,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:wdpkr7dw,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:shield-and-sword,normalizedName:shield-and-sword,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:wdpkr7dw,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:wdpkr7dw,initialPageAlias:shield-and-sword,timestamp:1756252422717,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:18},{id:1756248557785,title:Bohème Sauvage,url:/boheme-sauvage,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:3myp59ol,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:boheme-sauvage,normalizedName:bohème-sauvage,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:3myp59ol,horizontalScrollerText:h2>strong>Bohème Sauvage/strong>/h2>p>Bohème Sauvage describes itself as a dazzling, glamorous and glittering hommage to the era of the Golden Twenties. Bohème Sauvage usually takes place once in a month in different locations in Berlin. The organizer, as well as the iniciator, inventor and host of Bohème Sauvage is Miss Else Edelstahl who says: ... It is about having a splendid and amusing time in a sophisticated way. Sumptious decorations ad to that, so does the entertainment on stage (cabaret, music, dancing-lessons, burlesque dance shows, magic, etc.), and the casino where people play with fake inflation money to win free absinth (or to just barter for a kiss). /p>,siteAlias:preview.neonsky.app,initialPageUuid:3myp59ol,initialPageAlias:boheme-sauvage,timestamp:1756253836795,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,metaTitle:,metaDescription:,hideMenuOnPage:false,passwordProtected:false,siteAlias:null,position:19},{id:1756248557786,title:Millionaire Fair,url:/millionaire-fair,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:1ieztujk,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:millionaire-fair,normalizedName:millionaire-fair,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:1ieztujk,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Customers gather at the invitation only VIP opening party of the Millionaire Fair in Munich. The four-day event is held for the first time in this city./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The products on sale include for ex. a fur covered safe, jewellery, luxury real estate. Cigar Company shows for the first time their Golden \Zeus\ cigar coated with pure gold, latest Audi and Maseratti sports cars are presented among other useless items that might appeal to the millionaire market./p>,siteAlias:preview.neonsky.app,initialPageUuid:1ieztujk,initialPageAlias:millionaire-fair,timestamp:1756253960667,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:20},{id:1756248557787,title:Sasha Walz \Dialoge 09\,url:/sasha-walz-\dialoge-09\,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:36cdk8gd,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:sasha-walz-\dialoge-09\,normalizedName:sasha-walz-\dialoge-09\,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:36cdk8gd,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Dancers perform during dress rehearsals of \Dialoge 09 - Neues Museum\ by Sasha Waltz at Neues Museum in Berlin./p>,siteAlias:preview.neonsky.app,initialPageUuid:36cdk8gd,initialPageAlias:sasha-walz-\dialoge-09\,timestamp:1756254048446,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:21},{id:1756248557788,title:Berlin Commute,url:/berlin-commute,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:40ppus1u,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:berlin-commute,normalizedName:berlin-commute,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:40ppus1u,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>It is a passive, noninvolved way of conquering a distance which mostly consists of waiting even when people actually become transported./p>,siteAlias:preview.neonsky.app,initialPageUuid:40ppus1u,initialPageAlias:berlin-commute,timestamp:1756254069008,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:22},{id:1756248557789,title:10 Cold Days In February,url:/10-cold-days-in-february,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:8lx9jsui,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:10-cold-days-in-february,normalizedName:10-cold-days-in-february,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:8lx9jsui,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>The Berlin International Film Festival , also called the Berlinale, is one of the worlds leading film festivals./p>,siteAlias:preview.neonsky.app,initialPageUuid:8lx9jsui,initialPageAlias:10-cold-days-in-february,timestamp:1756254180435,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:23},{id:1756248557791,title:Berlin Corona Covid19 Lockdown,url:/berlin-corona-covid19-lockdown,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:i8k4ssnx,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:berlin-corona-covid19-lockdown,normalizedName:berlin-corona-covid19-lockdown,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:i8k4ssnx,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:i8k4ssnx,initialPageAlias:berlin-corona-covid19-lockdown,timestamp:1756254206014,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:24},{id:1756248557792,title:Bavaria Felhorn Blast,url:/bavaria-felhorn-blast,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557779,siteId:9kccrxyp,pageId:c7tr5h8c,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:bavaria-felhorn-blast,normalizedName:bavaria-felhorn-blast,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:c7tr5h8c,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:c7tr5h8c,initialPageAlias:bavaria-felhorn-blast,timestamp:1756254227004,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:25},{id:1756248557793,title:Travel,isFolder:true,isCollapsed:true,visible:true,parentId:null,siteId:9kccrxyp,slug:travel,importSource:folder,isSubmenu:true,isHomePage:false,position:26},{id:1756248557794,title:Sicily,url:/sicily,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557793,siteId:9kccrxyp,pageId:gsusclmd,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:sicily,normalizedName:sicily,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:gsusclmd,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>Sicily is the largest island in the Mediterranean Sea. The greatest proportion of the wave of Italian immigration to the United States came from the \Mezzogiorno\, with by far the largest numbers from Sicily. Thus, in many cases the speech, customs, and culinary delights considered by Americans to be \Italian\ were actually Sicilian./p>,siteAlias:preview.neonsky.app,initialPageUuid:gsusclmd,initialPageAlias:sicily,timestamp:1756254254503,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:27},{id:1756248557795,title:Ongoing Projects,isFolder:true,isCollapsed:true,visible:true,parentId:null,siteId:9kccrxyp,slug:ongoing-projects,importSource:folder,isSubmenu:true,isHomePage:false,position:28},{id:1756248557796,title:Western Cape,url:/western-cape,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557795,siteId:9kccrxyp,pageId:crx5xy3c,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:western-cape,normalizedName:western-cape,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:crx5xy3c,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:crx5xy3c,initialPageAlias:western-cape,timestamp:1756254343501,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:29},{id:1756248557797,title:FridaysForFuture_Berlin,url:/fridaysforfuture_berlin,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557795,siteId:9kccrxyp,pageId:lde5bxub,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:fridaysforfuture_berlin,normalizedName:fridaysforfuture_berlin,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:lde5bxub,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:lde5bxub,initialPageAlias:fridaysforfuture_berlin,timestamp:1756254370902,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:false,descriptionsOnDemand:false,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:30},{id:1756248557798,title:SPREE,url:/spree,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557795,siteId:9kccrxyp,pageId:kveoafl5,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:spree,normalizedName:spree,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:kveoafl5,horizontalScrollerText:p style\line-height: 1; margin-bottom: 0.5em;\>It is the time of the Corona stop. Sitting in a house near the river, the days go by with the handbrake on./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>But the river keeps flowing, to Berlin and some molecules maybe even to New York./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>So the Spree, who actually lives there, what does it look like?/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>The theme of the river thus suddenly becomes a narrative space for a photographic journey that consists of different episodes, in which each story brings forth different stories and the lives of the protagonists seem to/p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>intertwine, probably without ever meeting each other./p>p>\n/p>p style\line-height: 1; margin-bottom: 0.5em;\>It is the course of the Spree River that connects these stories on their winding trail./p>,siteAlias:preview.neonsky.app,initialPageUuid:kveoafl5,initialPageAlias:spree,timestamp:1756254435977,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:true,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:31},{id:1756248557799,title:Youll never walk alone,url:/youll-never-walk-alone,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:1756248557795,siteId:9kccrxyp,pageId:d3hn8wi4,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:youll-never-walk-alone,normalizedName:youll-never-walk-alone,galleryOptions:{manualCollectionName:GUID4db89e34d3312,importedFromClassic:true,siteId:9kccrxyp,pageId:d3hn8wi4,horizontalScrollerText:p>br>/p>,siteAlias:preview.neonsky.app,initialPageUuid:d3hn8wi4,initialPageAlias:youll-never-walk-alone,timestamp:1756254457362,startInSingles:true,layoutType:singles,columns:3,spacing:2,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}}},isHomePage:false,siteAlias:null,position:32},{id:1756248557800,title:Portrait,url:/portrait,isIntegrated:true,isPage:false,isSubmenu:false,visible:true,parentId:null,siteId:9kccrxyp,pageId:wrlp6toj,classicGuid:4db89e34d3312,importSource:classic-gallery,slug:portrait,normalizedName:portrait,galleryOptions:{siteAlias:hermannbredehorst.com,initialPageUuid:wrlp6toj,initialPageAlias:portrait,timestamp:1757933784196,horizontalScrollerText:p>br>/p>,startInSingles:true,layoutType:singles,columns:3,spacing:2,manualCollectionName:GUID4db89e34d3312,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:true,descriptionsOnDemand:true,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID4db89e34d3312:{isClassicCollection:true,guid:GUID4db89e34d3312,metadata:{},totalItems:0}},siteId:9kccrxyp},isHomePage:false,siteAlias:null,position:33},{id:1756254565250,title:Publications,parentId:null,visible:true,isIntegrated:true,pageId:bbd3c17j,slug:publications,url:/publications,galleryOptions:{siteAlias:hermannbredehorst.com,initialPageUuid:bbd3c17j,initialPageAlias:publications,timestamp:1757933126620,horizontalScrollerText:p>br>/p>,startInSingles:true,layoutType:singles,columns:3,spacing:2,manualCollectionName:GUID9kccrxyp,autoplaySingles:false,autoplayDuration:4,autoplayTransition:1,showDescription:false,descriptionsOnDemand:false,showFilename:false,displayAllInfo:false,thumbnailNavigation:false,navigationMode:dynamicCursor,showTextBlock:false,fixedHeroImage:false,zoomInLightbox:false,lightboxOnMobile:false,fadeDuration:500,fadeInDuration:100,descriptionTextColor:0, 0, 0,gridImageOverlayColor:130, 130, 130,gridImageOverlayOpacity:0.5,lightboxBgColor:255, 255, 255,lightboxBgOpacity:1,lightboxCloseColor:0, 0, 0,lightboxArrowColor:0, 0, 0,useTitles:false,useLinks:false,desktopTitleDisplayMode:overlay,titleTextAlign:center,showDescriptionInOverlay:false,includeRolloverImageInOverlay:true,filterMenuEnabled:false,filterMenuCollectionNames:,filterMenuTitles:,filterMenuStyle:dropdown,rolloverSwap:false,rolloverCollectionNames:,openMultipleLightboxes:false,batchSize:20,jsonCollections:{GUID9kccrxyp:{isClassicCollection:true,guid:GUID9kccrxyp,metadata:{},totalItems:0}},siteId:9kccrxyp},siteId:9kccrxyp,siteAlias:null,position:34},{id:1756248557804,title:About,isPage:true,isIntegrated:true,isSubmenu:false,pageId:page_1756248557804,visible:true,parentId:null,siteId:9kccrxyp,slug:about,url:/about,pageElements:{id:1756248558704,type:text,title:Text Block,visible:true,position:0,textContent:p style\margin-bottom: 0.5em; line-height: 1;\>br>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Hermann Bredehorst is a photojournalist and award winning photographer based in Berlin, Germany and Cape Town SA, and is available for assignments./span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Currently in Cape Town SA/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>He is a regular contributer to Germanys leading news magazine DER SPIEGEL./span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Commissioned works have appeared in leading newspapers and magazines like, Die Zeit, Focus, Time, The New York Times, Los Angeles Times, Business Week, Le Monde, Le Journal du Dimanche, HUMO, LExpress, Bloomberg Markets./span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Corporate work had been assigned by companies such as: Siemens AG, Gillette, Exxon Mobile, Sagem, Gemalto, Shell, Brioni and on a regular basis the German NGOs Brot für die Welt and Diakonie Katastrophenhilfe./span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>br>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>span class\ql-cursor\>/span>/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2004 Selecion Premio Internacional de Fotografía Humanitaria Luis Valtueña for \Chachemira\/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2005 Grant VG Bild Kunst \Sin Papeles - Illegale Immigranten in Spanien\/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Rueckblende 2010 3rd price Herta Mueller/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2017 Short List 2nd Hansel Mieth Award Der SPIEGEL/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2020 Fotonostrum Magazine Issue No 6 14 pages feature \Hermann Bredehorst, Photography as Social and Historical Documentation\/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2020 Short List Monovisions Award \Fridays for Future Berlin\/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2020 Roma Life in the Time of Coronavirus, Group Exhibition, Corona Protest Berlin/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2020 IPA 2020 Honorable Mention Editorial / Press, Contemporary issues: Title: Protest Against Corona Restrictions, Berlin/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2020 IPA 2020 Honorable Mention, Editorial / Press, Contemporary issues/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2021 Spiederwawards Honorable Mention Fridays for Future Demo Berlin/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2021 Winner of BJP and 1854, Decade of Change - Greta Thunberg attends a \Fridays for Future\ protest, Berlin/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2021 Winner 1854 and British Journal of Photography in collaboration with New Art City, Berlin Lockdown/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2021 Winner BJP Edition365 Berlin Lockdown Series/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2021 Grant VG Bild/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2022 Two current personal projects have received a VG Bild fellowship./span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2023 Finalist Hellerau Portrait Award \Surface\ and Exhibition for SPREE/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2024 Shortlist Taylor Wessing Photo Portrait Prize 2024 - National Portrait Gallery London GB/span>/p>p style\line-height: 1;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>2024 Winner Black and White Spider Award Fridays for Future/span>/p>,textWidth:73},{id:1756248576404,type:metadata,visible:true,position:1,metaTitle:,metaDescription:,metaKeywords:},classicGuid:4db89e34d3312,importSource:classic-page,isHomePage:false,position:38},{id:1756248557805,title:Contact,isPage:true,isIntegrated:true,isSubmenu:false,pageId:page_1756248557805,visible:true,parentId:null,siteId:9kccrxyp,slug:contact,url:/contact,pageElements:{id:1756248558805,type:metadata,title:Metadata,visible:true,position:0,metaTitle:Contact,metaDescription:,metaKeywords:},{id:1756248560805,type:column-container,title:Two Column Layout,visible:true,position:1,columns:{id:1756248561105,hAlign:right,vAlign:middle,elements:{id:1756248560905,type:slideshow,title:Page Images,visible:true,position:0,slides:{imageUrl:https://storage.neonsky.app/4db89e34d3312/images/Artist_.jpg,imageKey:4db89e34d3312/images/Artist_.jpg,imageFilename:Artist_.jpg,imageType:image/jpeg,caption:,title:,byline:,dateline:,altText:},slideDuration:5000,transitionDuration:500,slideshowWidth:88,slideshowHeight:71,showFullImages:true}},{id:1756248561205,hAlign:left,vAlign:middle,elements:{id:1756248561005,type:text,title:Text Block,visible:true,position:0,textContent:p style\margin-bottom: 0.5em; line-height: 1;\>br>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Hermann Bredehorst is based in/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Berlin, Germany and Cape Town, South Africa/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>and available for assignments./span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Phone Germany: +49-179-5910129/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Phone South Africa: +27-63-730-4341/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>E-mail: /span>a href\mailto:contact@hermannbredehorst.com\ target\_blank\ style\font-size: 14px; color: rgb(69, 69, 69);\>contact@hermannbredehorst.com/a>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Representation:/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>a href\http://www.polarisimages.com\ target\_blank\ style\font-size: 14px; color: rgb(69, 69, 69);\>POLARIS IMAGES/a>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>New York, USA/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Phone: +1-212-9674556/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Contact person: JP Pappis/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>E-mail: jpp@polarisimages.com/span>/p>p style\line-height: 1;\>span style\color: rgb(69, 69, 69); font-size: 14px;\>\n/span>/p>p style\line-height: 1; margin-bottom: 0.5em;\>span style\font-size: 14px; color: rgb(69, 69, 69);\>Photo: uli klose/span>/p>,textWidth:90}},backgroundSlideshow:{enabled:false,slides:,slideDuration:5000,transitionDuration:500,slideshowHeight:50,showFullImages:false}},classicGuid:4db89e34d3312,importSource:classic-page,isHomePage:false,position:39},{id:1756256979488,title:Spacer-copy,parentId:null,visible:true,isSpacer:true,slug:spacer-copy-1,url:/spacer-copy-1,isHomePage:false,position:40},{id:1756256974801,title:Spacer-copy,parentId:null,visible:true,isSpacer:true,slug:spacer-copy,url:/spacer-copy,isHomePage:false,position:41},{id:1756257168261,title:Spacer-copy-copy,parentId:null,visible:true,isSpacer:true,slug:spacer-copy-copy-1,url:/spacer-copy-copy-1,isHomePage:false,position:42},{id:1756257163758,title:Spacer-copy-copy,parentId:null,visible:true,isSpacer:true,slug:spacer-copy-copy,url:/spacer-copy-copy,isHomePage:false,position:43};let activeGalleryId null;let sortableInstance null;let isEditing false;let initialPageInfo null;// Store the current menu layout typelet currentMenuLayout null;document.addEventListener(DOMContentLoaded, () > { if (window.hydraInitialized) { console.log(Hydra already initialized, skipping); return; } window.hydraInitialized true; document.body.classList.add(hydra-initialized); window.Parameters window.Parameters || {}; if (localStorage.getItem(hydra_is_admin) true) { document.body.classList.add(hydra-admin, hydra-authenticated); isAdmin true; isAuthenticated true; console.log(Restored admin status from localStorage); // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins // This ensures admins always see fresh data when opening new tabs try { console.log(Admin detected - clearing all caches except auth tokens...); // Save auth tokens first const savedAuthToken localStorage.getItem(hydra_auth_token); const savedAuthEmail localStorage.getItem(hydra_auth_email); const savedIsAdmin localStorage.getItem(hydra_is_admin); const savedDidToken localStorage.getItem(hydra_did_token); // Clear ALL localStorage localStorage.clear(); // Restore auth tokens if (savedAuthToken) localStorage.setItem(hydra_auth_token, savedAuthToken); if (savedAuthEmail) localStorage.setItem(hydra_auth_email, savedAuthEmail); if (savedIsAdmin) localStorage.setItem(hydra_is_admin, savedIsAdmin); if (savedDidToken) localStorage.setItem(hydra_did_token, savedDidToken); // Clear ALL sessionStorage (auth tokens are not stored here) sessionStorage.clear(); console.log(Cleared all caches except auth tokens for admin user); } catch (cacheError) { console.warn(Error clearing caches on admin page load:, cacheError); // Dont fail page load if cache clearing fails } } function ensureSidebarElementsAndRender() { console.log(Ensuring sidebar elements are available before rendering...); // Check if SidebarManager exists and has elements if (window.SidebarManager && (!window.SidebarManager.elements || window.SidebarManager.elements.length 0)) { // Try to initialize from siteConfig if available if (typeof siteConfig ! undefined && siteConfig.sitebarElements && siteConfig.sidebarElements.length > 0) { console.log(Initializing SidebarManager elements from siteConfig); window.SidebarManager.elements siteConfig.sidebarElements; } } // Determine layout and render let currentMenuLayout sidebar; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentMenuLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; } if (currentMenuLayout horizontal) { console.log(Rendering horizontal menu with ensured sidebar elements); renderHorizontalMenu(); } else { renderGalleries(); }} setTimeout(() > { console.log(Initializing galleries:, galleries.length); setTimeout(alignFolderStates, 100); // CHANGED: Use the new function instead of direct rendering setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init initMobileMenu(); // Handle direct URL navigation if (initialPageInfo) { console.log(Loading initial page from direct URL:, initialPageInfo); // Find the gallery by ID const gallery galleries.find(g > g.id initialPageInfo.id); if (gallery) { // Set active gallery ID activeGalleryId gallery.id; if (initialPageInfo.type page) { console.log(Loading page directly:, gallery.title); loadPage(gallery.id); } else { console.log(Loading gallery directly:, gallery.title); loadGallery(gallery.id); } } } else if (window.location.pathname /) { // No specific path, try to load home page console.log(No specific path, checking for home page); if (!loadHomePage() && galleries.length > 0) { // Default to first gallery if no home page is set loadGallery(galleries0.id); } } else if (galleries.length > 0) { // Path exists but no matching initialPageInfo, try to handle URL path handleURLNavigation(); } // Log what was rendered console.log(Menu content:, document.getElementById(galleryTree)?.innerHTML || Gallery tree not found); // Start with edit controls properly set toggleEditControlsVisibility(isAdmin && document.body.classList.contains(edit-mode-active)); // Explicitly hide the import classic form too const importForm document.getElementById(importClassicForm); if (importForm) { importForm.style.display none; importForm.classList.remove(visible); } // Try to restore token from localStorage if (!didToken) { didToken localStorage.getItem(hydra_auth_token); if (didToken) { console.log(Restored auth token from localStorage); } } // Initialize Magic and check authentication // Initialize sidebar elements if available if (window.SidebarManager) { // Pass the initial elements from server config if available if (true) { window.SidebarManager.elements {id:1756248419505,type:menu,title:Menu,visible:true,position:1},{id:1756255783852,type:image,title:Image,visible:true,position:0,imageData:https://storage.neonsky.app/9kccrxyp/1756255796863_hermannbredehorst.png,imageWidth:100%,imageAlignment:left},{id:1756256939437,type:social,title:Social,visible:true,position:2,socialIconSize:20,socialIcons:{type:instagram,url:https://instagram.com/hermannbredehorst/},socialAlignment:left},{id:1757082141226,type:image,title:Image,visible:true,position:3},{id:1757082177175,type:image,title:Image,visible:true,position:4}; } window.SidebarManager.init(); } // Initialize metadata from server configwindow.siteMetadata {title:Hermann Bredehorst,description:Hermann Bredehorst is an award winning photojournalist and photographer based in Berlin, Germany whose assignment work has appeared in TIME, New York Times, Business Week, Le Monde, Madame Figaro, he is a regular contributer to Germanys leading news magazine DER SPIEGEL,keywords:,googleAnalytics:,noIndex:false,author:hermannbredehorst.com,language:en,locale:en_US,siteName:hermannbredehorst.com,favicon:};console.log(Client Initialized site metadata (second init):, window.siteMetadata);console.log(Client Google Analytics from metadataToUse (second init):, );console.log(Client Google Analytics in window.siteMetadata (second init):, window.siteMetadata.googleAnalytics);console.log(Client Content Language (second init):, window.siteMetadata.contentLanguage); // Update copyright footer after metadata is set (important for main domain) if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter function) { window.SidebarManager.updateMetadataFooter(); } const submenuCheck document.getElementById(createSubmenu); const submenuTitleField document.getElementById(submenuTitle); const submenuTitleGroup submenuTitleField?.parentElement; if (submenuCheck && submenuTitleGroup) { submenuCheck.addEventListener(change, function() { submenuTitleGroup.style.display this.checked ? block : none; }); } // Check authentication state after initialization if (isAuthenticated && isAdmin) { console.log(Already authenticated and admin, showing edit UI); forceShowEditUI(); } else if (didToken) { console.log(Have token, checking admin status); checkAuth(); } }, 100); // Small delay to ensure everything is loaded setTimeout(() > { if (window.location.pathname / || (window.location.hostname preview.neonsky.app && window.location.pathname.split(/).length 2)) { // Root URL or preview URL with only GUID - check for home page if (!initialPageInfo) { // Only if no specific page was already loaded loadHomePage(); } } }, 500);});/** * Gets the first visible sidebar element (image or text) to be used as a logo or title. * @returns {object|null} The first suitable sidebar element or null. */function getFirstSidebarElementForHeader() { if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { const visibleElements window.SidebarManager.elements.filter(el > el.visible ! false); // Filter 1: visible if (visibleElements.length > 0) { const sortedElements visibleElements.sort((a, b) > a.position - b.position); for (let el of sortedElements) { if (el.type image || el.type text) { // Filter 2: type return el; } } // If loop finishes, no visible image or text found among visible elements console.log(getFirstSidebarElementForHeader Found visible elements, but none were type image/text.); return null; } else { console.log(getFirstSidebarElementForHeader No elements with visible ! false found in SidebarManager.elements.); return null; } } console.log(getFirstSidebarElementForHeader SidebarManager, its elements, or elements array is empty/undefined.); return null;}/** * Gets the first visible social icon element from sidebar elements. * @returns {object|null} The first suitable social element or null. */function getFirstSocialElementForHeader() { if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { const visibleElements window.SidebarManager.elements.filter(el > el.visible ! false); // Filter 1: visible if (visibleElements.length > 0) { const sortedElements visibleElements.sort((a, b) > a.position - b.position); for (let el of sortedElements) { if (el.type social) { // Filter for social type return el; } } // If loop finishes, no visible social element found console.log(getFirstSocialElementForHeader Found visible elements, but none were type social.); return null; } else { console.log(getFirstSocialElementForHeader No elements with visible ! false found in SidebarManager.elements.); return null; } } console.log(getFirstSocialElementForHeader SidebarManager, its elements, or elements array is empty/undefined.); return null;}function ensureCorrectLayoutApplied() { // Check if MenuStyleCustomizer and its necessary parts exist if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout function) { // Get the current layout setting, default to sidebar if not set const currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; console.log(`Ensuring layout is applied: ${currentLayout}`); // Apply the layout styles (CSS classes, display properties) window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // --- Force Header Structure Rebuild for Horizontal Layout --- if (currentLayout horizontal) { const mobileHeader document.querySelector(.mobile-header); if (mobileHeader) { let mobileHeaderContent mobileHeader.querySelector(.mobile-header-content); // Ensure the main content container exists if (!mobileHeaderContent) { mobileHeaderContent document.createElement(div); mobileHeaderContent.className mobile-header-content; const hamburgerBtn mobileHeader.querySelector(.hamburger-btn); if (hamburgerBtn) { mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn); } else { mobileHeader.appendChild(mobileHeaderContent); } } // Ensure specific sub-containers exist, creating them if necessary // This guarantees the structure is present before renderHorizontalMenu fills them. if (!mobileHeaderContent.querySelector(.horizontal-header-logo)) { const logoContainer document.createElement(div); logoContainer.className horizontal-header-logo; mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild); } if (!mobileHeaderContent.querySelector(.horizontal-menu-container)) { const menuContainer document.createElement(div); menuContainer.className horizontal-menu-container; const logoContainer mobileHeaderContent.querySelector(.horizontal-header-logo); if (logoContainer) { logoContainer.insertAdjacentElement(afterend, menuContainer); } else { // Fallback if logo container wasnt found/created mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild); } } // Apply flexbox styling to position social container at far right mobileHeaderContent.style.display flex; mobileHeaderContent.style.alignItems center; mobileHeaderContent.style.justifyContent space-between; mobileHeaderContent.style.width 100%; // Create a flex container for logo and menu items const logoMenuContainer mobileHeaderContent.querySelector(.logo-menu-container); if (!logoMenuContainer) { console.log(ensureCorrectLayoutApplied Creating logo-menu wrapper...); const logoMenuWrapper document.createElement(div); logoMenuWrapper.className logo-menu-container; logoMenuWrapper.style.display flex; logoMenuWrapper.style.alignItems center; logoMenuWrapper.style.gap 20px; // Move logo and menu into the wrapper const logoContainer mobileHeaderContent.querySelector(.horizontal-header-logo); const menuContainer mobileHeaderContent.querySelector(.horizontal-menu-container); console.log(ensureCorrectLayoutApplied Found logo container:, !!logoContainer); console.log(ensureCorrectLayoutApplied Found menu container:, !!menuContainer); if (logoContainer && menuContainer) { // Insert wrapper before the logo mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); // Move both containers into the wrapper logoMenuWrapper.appendChild(logoContainer); logoMenuWrapper.appendChild(menuContainer); console.log(ensureCorrectLayoutApplied Successfully moved logo and menu into wrapper); } else { console.warn(ensureCorrectLayoutApplied Missing logo or menu container, cannot create wrapper); } } else { console.log(ensureCorrectLayoutApplied Logo-menu wrapper already exists); } // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created) if (!mobileHeaderContent.querySelector(.horizontal-social-container)) { const socialContainer document.createElement(div); socialContainer.className horizontal-social-container; // Insert at the end (far right) of the mobile header content mobileHeaderContent.appendChild(socialContainer); } } } // --- End Header Structure Rebuild --- // Re-render the correct menu structure based on the applied layout if (currentLayout horizontal) { // If horizontal layout, render the horizontal menu if (typeof renderHorizontalMenu function) { renderHorizontalMenu(); } else { console.warn(renderHorizontalMenu function not found.); } // After rendering horizontal menu, update mobile header content // This ensures mobile header shows logo instead of menu items on mobile setTimeout(() > { if (typeof updateMobileHeaderContent function) { updateMobileHeaderContent(); } }, 100); } else { // For sidebar or top layouts, render the standard gallery tree if (typeof renderGalleries function) { renderGalleries(); } else { console.warn(renderGalleries function not found.); } // Update mobile header content for other layouts too setTimeout(() > { if (typeof updateMobileHeaderContent function) { updateMobileHeaderContent(); } }, 100); } } else { // Log a warning if the necessary components arent available console.warn(MenuStyleCustomizer or its methods not available to re-apply layout.); }}function applyLayoutStylesOnly() { // Check if MenuStyleCustomizer and its necessary parts exist if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout function) { // Get the current layout setting, default to sidebar if not set const currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`); // Apply the layout styles (CSS classes, display properties) window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles } else { console.warn(APPLY STYLES: MenuStyleCustomizer or its methods not available.); }}// Add this function to ensure sidebar content for mobilefunction ensureSidebarContentForMobile() { const isHorizontalLayout document.body.classList.contains(menu-layout-horizontal); const isMobileView window.innerWidth 768; if (isHorizontalLayout && isMobileView) { console.log(Ensuring sidebar content for mobile horizontal layout); // Make sure the sidebar tree is populated const galleryTreeContainer document.getElementById(galleryTree); if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() )) { console.log(Sidebar tree is empty, populating with galleries); // Render the galleries in the sidebar tree for mobile navigation if (typeof renderGalleries function) { renderGalleries(); } } // Also ensure sidebar elements are rendered if (window.SidebarManager && typeof window.SidebarManager.renderElements function) { console.log(Re-rendering sidebar elements for mobile); window.SidebarManager.renderElements(); } }}function renderTextLogo(container, htmlContentFromSidebarElement) { if (!container) { console.error(RenderTextLogo Error: Logo container not found.); return; } // Clear any existing content from the logo container. container.innerHTML ; // Directly inject the HTML content from the sidebar element. // This preserves the original HTML structure (like h1>, span>) and any inline styles. // The .horizontal-header-logo CSS should handle alignment (e.g., display: flex; align-items: center;). container.innerHTML htmlContentFromSidebarElement || div stylepadding: 0 10px; font-weight: bold;>Menu/div>; // Fallback text wrapped in a div // Optional: If the root element of htmlContentFromSidebarElement (e.g., an h1>) // has default margins that interfere with flex centering, you might need CSS like: // .horizontal-header-logo > h1 { margin: 0; } // This would typically go in your menu-styles.css file. console.log(`RenderTextLogo Text logo HTML rendered directly into container.`);}function renderHorizontalMenu() { console.log(RenderHorizontalMenu Function CALLED. Current edit mode:, typeof isEditing ! undefined ? isEditing : isEditing_undefined); const mobileHeader document.querySelector(.mobile-header); if (!mobileHeader) { console.error(RenderHorizontalMenu CRITICAL: .mobile-header element NOT FOUND.); return; } // Apply flexbox styling to position social container at far right const mobileHeaderContent mobileHeader.querySelector(.mobile-header-content); if (mobileHeaderContent) { mobileHeaderContent.style.display flex; mobileHeaderContent.style.alignItems center; mobileHeaderContent.style.justifyContent space-between; mobileHeaderContent.style.width 100%; // Create a flex container for logo and menu items const logoMenuContainer mobileHeaderContent.querySelector(.logo-menu-container); if (!logoMenuContainer) { console.log(RenderHorizontalMenu Creating logo-menu wrapper...); const logoMenuWrapper document.createElement(div); logoMenuWrapper.className logo-menu-container; logoMenuWrapper.style.display flex; logoMenuWrapper.style.alignItems center; logoMenuWrapper.style.gap 20px; // Move logo and menu into the wrapper const logoContainer mobileHeaderContent.querySelector(.horizontal-header-logo); const menuContainer mobileHeaderContent.querySelector(.horizontal-menu-container); console.log(RenderHorizontalMenu Found logo container:, !!logoContainer); console.log(RenderHorizontalMenu Found menu container:, !!menuContainer); if (logoContainer && menuContainer) { // Insert wrapper before the logo mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); // Move both containers into the wrapper logoMenuWrapper.appendChild(logoContainer); logoMenuWrapper.appendChild(menuContainer); console.log(RenderHorizontalMenu Successfully moved logo and menu into wrapper); } else { console.warn(RenderHorizontalMenu Missing logo or menu container, cannot create wrapper); } } else { console.log(RenderHorizontalMenu Logo-menu wrapper already exists); } } const logoContainer mobileHeader.querySelector(.horizontal-header-logo); if (!logoContainer) { console.error(RenderHorizontalMenu CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header.); return; } console.log(RenderHorizontalMenu Found .horizontal-header-logo container); logoContainer.innerHTML ; // Clear existing logo content first // FIXED: Check if were on mobile - if so, dont render the horizontal logo const isMobileView window.innerWidth 768; // Adjust breakpoint as needed if (isMobileView) { console.log(RenderHorizontalMenu Mobile view detected - skipping horizontal logo rendering); // On mobile, the logo will be handled by updateMobileHeaderContent() } else { console.log(RenderHorizontalMenu Desktop view - rendering horizontal logo); // Get the logo element data let logoElementData null; try { logoElementData getFirstSidebarElementForHeader(); console.log(RenderHorizontalMenu getFirstSidebarElementForHeader() returned:, JSON.stringify(logoElementData)); } catch (error) { console.error(RenderHorizontalMenu Error getting sidebar element:, error); logoElementData getFirstSidebarElementFallback(); } if (logoElementData) { if (logoElementData.type image && (logoElementData.imageData || logoElementData.imageUrl)) { console.log(RenderHorizontalMenu Attempting to render IMAGE logo with src:, logoElementData.imageData || logoElementData.imageUrl); const img document.createElement(img); img.src logoElementData.imageUrl || logoElementData.imageData; img.alt logoElementData.title || Site Logo; // Apply comprehensive styling to ensure visibility img.style.display block; // --- FIX --- // The line below was removed. It was applying a fixed max-height of 50px, // overriding your CSS. Removing it allows your stylesheet to take control. // img.style.maxHeight 50px; img.style.width auto; img.style.objectFit contain; img.style.minWidth 1px; img.style.minHeight 1px; img.style.visibility visible; img.style.opacity 1; // Enhanced event listeners for debugging img.onload () > { console.log(RenderHorizontalMenu Image successfully LOADED); }; img.onerror (error) > { console.error(RenderHorizontalMenu Image FAILED to load:, error); renderTextLogo(logoContainer, logoElementData.title || Logo); }; try { // Handle linking (similar to sidebar-manager.js) // Only add links in view mode, not edit mode const isEditing window.SidebarManager?.isEditing || document.body.classList.contains(edit-mode-active) || (typeof window.isInEditMode function && window.isInEditMode()); if (logoElementData.linkType && logoElementData.linkType ! none && !isEditing) { const link document.createElement(a); link.target logoElementData.linkTarget || _self; let finalLinkHref #; let isValidLink false; if (logoElementData.linkType external && logoElementData.linkUrl) { finalLinkHref logoElementData.linkUrl; if (link.target _blank) { link.rel noopener noreferrer; } isValidLink true; } else if (logoElementData.linkType internal && logoElementData.linkPageId) { const allGalleries window.galleries || (typeof galleries ! undefined ? galleries : ); const targetPage allGalleries.find(g > g.id parseInt(logoElementData.linkPageId)); if (targetPage) { // Handle preview mode URLs const isPreviewMode window.location.hostname preview.neonsky.app; if (isPreviewMode) { const pathParts window.location.pathname.split(/).filter(Boolean); const siteGuid pathParts0; if (siteGuid && targetPage.slug) { finalLinkHref / + siteGuid + / + targetPage.slug; isValidLink true; } else if (targetPage.url && targetPage.url.startsWith(/)) { finalLinkHref targetPage.url; isValidLink true; } } else { if (targetPage.slug) { finalLinkHref / + targetPage.slug; isValidLink true; } else if (targetPage.url && targetPage.url.startsWith(/)) { finalLinkHref targetPage.url; isValidLink true; } } } } if (isValidLink) { link.href finalLinkHref; // For internal links, add click handler to use site navigation if (logoElementData.linkType internal && logoElementData.linkPageId) { link.addEventListener(click, (e) > { e.preventDefault(); const allGalleries window.galleries || (typeof galleries ! undefined ? galleries : ); const targetPage allGalleries.find(g > g.id parseInt(logoElementData.linkPageId)); if (targetPage) { if (targetPage.isPage && typeof window.loadPage function) { window.loadPage(targetPage.id); } else if (typeof window.loadGallery function) { window.loadGallery(targetPage.id); } else { // Fallback to standard navigation window.location.href finalLinkHref; } } else { // Fallback to standard navigation if page not found window.location.href finalLinkHref; } }); } link.appendChild(img); logoContainer.appendChild(link); console.log(RenderHorizontalMenu SUCCESSFULLY appended linked image to logoContainer.); } else { logoContainer.appendChild(img); console.log(RenderHorizontalMenu SUCCESSFULLY appended image to logoContainer.); } } else { logoContainer.appendChild(img); console.log(RenderHorizontalMenu SUCCESSFULLY appended image to logoContainer.); } } catch (e) { console.error(RenderHorizontalMenu Error during logoContainer.appendChild(img):, e); renderTextLogo(logoContainer, logoElementData.title || Logo); } } else if (logoElementData.type text && logoElementData.textContent) { console.log(RenderHorizontalMenu Attempting to render TEXT logo:, logoElementData.textContent); renderTextLogo(logoContainer, logoElementData.textContent); } else { console.log(RenderHorizontalMenu Using title as fallback logo); renderTextLogo(logoContainer, logoElementData.title || Menu); } } else { console.log(RenderHorizontalMenu No logoElementData available. Using default text.); renderTextLogo(logoContainer, Menu); } } // Render menu items (only on desktop, not on mobile) const menuContainer mobileHeader.querySelector(.horizontal-menu-container); if (!menuContainer) { console.error(RenderHorizontalMenu .horizontal-menu-container NOT FOUND within .mobile-header.); } else { menuContainer.innerHTML ; // Only populate menu items on desktop, not on mobile if (!isMobileView) { let galleryTree ; if (typeof createGalleryTree function && typeof galleries ! undefined) { galleryTree createGalleryTree(galleries); } else { console.warn(RenderHorizontalMenu createGalleryTree function or galleries array is undefined.); } galleryTree.forEach(item > { const menuItemElement createHorizontalMenuItem(item); if (menuItemElement) { menuContainer.appendChild(menuItemElement); } }); } else { console.log(RenderHorizontalMenu Mobile view - skipping horizontal menu item population); } } // Render social icons (only on desktop, not on mobile) let socialContainer mobileHeader.querySelector(.horizontal-social-container); if (!socialContainer) { // Fallback: create the social container if it doesnt exist console.log(RenderHorizontalMenu .horizontal-social-container not found, creating it...); const mobileHeaderContent mobileHeader.querySelector(.mobile-header-content); if (mobileHeaderContent) { socialContainer document.createElement(div); socialContainer.className horizontal-social-container; mobileHeaderContent.appendChild(socialContainer); console.log(RenderHorizontalMenu Created .horizontal-social-container); } else { console.error(RenderHorizontalMenu .mobile-header-content not found, cannot create social container); } } if (socialContainer) { socialContainer.innerHTML ; // Only populate social icons on desktop, not on mobile if (!isMobileView) { const socialElement getFirstSocialElementForHeader(); if (socialElement && socialElement.type social) { console.log(RenderHorizontalMenu Found social element, rendering social icons); renderSocialIcons(socialContainer, socialElement); } else { console.log(RenderHorizontalMenu No social element found or element is not social type); // Container remains empty - no impact on layout } } else { console.log(RenderHorizontalMenu Mobile view - skipping social icons population); } } if (typeof updateActiveStatesHorizontal function) { updateActiveStatesHorizontal(); } setTimeout(() > { ensureSidebarContentForMobile();}, 100); console.log(RenderHorizontalMenu Function COMPLETED.);}/** * Renders social icons in the horizontal header container. * @param {HTMLElement} container - The container to render social icons in * @param {object} socialElement - The social element data from sidebar */function renderSocialIcons(container, socialElement) { console.log(RenderSocialIcons Rendering social icons:, socialElement); try { // Safety check: ensure we have a valid social element if (!socialElement || socialElement.type ! social) { console.log(RenderSocialIcons Invalid social element - skipping rendering); return; } // Find the existing sidebar social icons and clone them const sidebarSocialContainer document.querySelector(.sidebar-social-container); if (sidebarSocialContainer) { const sidebarSocialLinks sidebarSocialContainer.querySelectorAll(ahref); sidebarSocialLinks.forEach(link > { // Clone the entire link element with all its styling and functionality const clonedLink link.cloneNode(true); // Remove any edit-specific classes or attributes clonedLink.classList.remove(edit-social-icon, delete-social-icon); // Ensure it opens in new tab clonedLink.target _blank; clonedLink.rel noopener noreferrer; // Add horizontal menu specific class clonedLink.classList.add(horizontal-social-link); // Apply horizontal menu specific styling - let CSS handle most styling clonedLink.style.cssText display: flex !important; align-items: center !important; justify-content: center !important; width: 30px !important; height: 30px !important; color: var(--menu-color, #333) !important; text-decoration: none !important; flex-shrink: 0 !important;; // Style the icon wrapper to match horizontal menu sizing const iconWrapper clonedLink.querySelector(.social-icon-wrapper); if (iconWrapper) { iconWrapper.style.cssText display: flex !important; align-items: center !important; justify-content: center !important;; } // Style the SVG to use proper sizing const svg clonedLink.querySelector(svg); if (svg) { svg.style.width 20px; svg.style.height 20px; svg.setAttribute(width, 20px); svg.setAttribute(height, 20px); svg.setAttribute(preserveAspectRatio, xMidYMid meet); svg.style.color inherit; svg.style.fill currentColor; svg.style.stroke currentColor; } // Remove any click handlers that might interfere const newLink clonedLink.cloneNode(true); container.appendChild(newLink); console.log(RenderSocialIcons Cloned sidebar social link:, newLink.href); }); } else { console.log(RenderSocialIcons No sidebar social container found to clone from); } } catch (error) { console.error(RenderSocialIcons Error rendering social icons:, error); }}/** * Gets the appropriate icon text or class for a social platform. * @param {string} platform - The social platform name * @returns {string} The icon text or class */function getSocialIconText(platform) { const iconMap { facebook: f, twitter: t, instagram: i, linkedin: in, youtube: yt, pinterest: p, tiktok: tt, snapchat: sc, whatsapp: wa, telegram: tg, discord: d, github: gh, email: @, phone: 📞, website: 🌐 }; return iconMapplatform.toLowerCase() || •;}/** * Creates a single top-level horizontal menu item and its submenu if it has children. * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal) * @param {object} galleryItem - The gallery item data. * @returns {HTMLElement | null} The created menu item element, or null if not rendered. */function createHorizontalMenuItem(galleryItem) { if (galleryItem.visible false || galleryItem.isSpacer) { return null; } const menuItem document.createElement(div); menuItem.className horizontal-menu-item; menuItem.dataset.id String(galleryItem.id); menuItem.setAttribute(data-visible, galleryItem.visible ! false ? true : false); const titleSpan document.createElement(span); titleSpan.textContent galleryItem.title; menuItem.appendChild(titleSpan); // Ensure activeGalleryId is treated as a number for comparison during initial render const currentGlobalActiveId parseInt(String(window.activeGalleryId || activeGalleryId)); if (galleryItem.id currentGlobalActiveId) { menuItem.classList.add(active); } let leaveTimer; // For mouseleave timeout to close dropdown if (galleryItem.children && galleryItem.children.length > 0) { const visibleChildren galleryItem.children.filter(child > child.visible ! false && !child.isSpacer); if (visibleChildren.length > 0) { menuItem.classList.add(has-children); const toggleIconContainer document.createElement(span); toggleIconContainer.className submenu-toggle-horizontal; const svgIcon document.createElementNS(http://www.w3.org/2000/svg, svg); svgIcon.setAttribute(class, icon toggle-icon); svgIcon.setAttribute(viewBox, 0 0 24 24); svgIcon.setAttribute(fill, none); svgIcon.setAttribute(stroke, currentColor); svgIcon.setAttribute(stroke-width, 2); const polyline document.createElementNS(http://www.w3.org/2000/svg, polyline); polyline.setAttribute(points, 9 6 15 12 9 18); // Right-pointing arrow svgIcon.appendChild(polyline); toggleIconContainer.appendChild(svgIcon); menuItem.appendChild(toggleIconContainer); const subMenu document.createElement(div); subMenu.className horizontal-dropdown; visibleChildren.forEach((childItem) > { const subMenuItemElement createHorizontalSubMenuItem(childItem); // Recursive call if (subMenuItemElement) { subMenu.appendChild(subMenuItemElement); } }); if (subMenu.hasChildNodes()) { menuItem.appendChild(subMenu); menuItem.addEventListener(mouseenter, function() { clearTimeout(leaveTimer); document.querySelectorAll(.horizontal-menu-item.expanded).forEach((openItem) > { if (openItem ! this) { openItem.classList.remove(expanded); const otherIcon openItem.querySelector(.icon.toggle-icon); if (otherIcon) otherIcon.classList.remove(rotated); } }); this.classList.add(expanded); const currentIcon this.querySelector(.icon.toggle-icon); if (currentIcon) currentIcon.classList.add(rotated); }); menuItem.addEventListener(mouseleave, function(event) { // Check if were moving to the dropdown or a flyout const relatedTarget event.relatedTarget; const isMovingToDropdown relatedTarget && ( relatedTarget.closest(.horizontal-dropdown) || relatedTarget.classList.contains(horizontal-submenu-item) || relatedTarget.closest(.horizontal-submenu-item) ); const isMovingToFlyout relatedTarget && ( relatedTarget.classList.contains(horizontal-nested-dropdown) || relatedTarget.closest(.horizontal-nested-dropdown) ); if (!isMovingToDropdown && !isMovingToFlyout) { console.log(Main menu item mouseleave - starting timer); leaveTimer setTimeout(() > { // Check if theres an active flyout - if so, keep parent open // Use the same multiple detection methods as the submenu handler const activeFlyout document.querySelector(.horizontal-nested-dropdownstyle*display: block) || document.querySelector(.horizontal-nested-dropdownstyle*visibility: visible) || document.querySelector(.horizontal-nested-dropdown:not(style*display: none)); console.log(Main menu timer fired - active flyout:, activeFlyout); if (!activeFlyout) { console.log(Main menu closing dropdown - no active flyout); this.classList.remove(expanded); const currentIcon this.querySelector(.icon.toggle-icon); if (currentIcon) currentIcon.classList.remove(rotated); } else { console.log(Main menu keeping dropdown open - flyout is active); } }, 200); } else { console.log(Main menu item mouseleave - not starting timer (moving to dropdown or flyout)); } }); subMenu.addEventListener(mouseenter, function() { const parentMenuItem this.closest(.horizontal-menu-item); if (parentMenuItem && parentMenuItem.leaveTimer) { clearTimeout(parentMenuItem.leaveTimer); parentMenuItem.leaveTimer null; } }); subMenu.addEventListener(mouseleave, function(event) { const parentMenuItem this.closest(.horizontal-menu-item); if (parentMenuItem) { // Check if were moving to another element within the same dropdown const relatedTarget event.relatedTarget; console.log(SubMenu mouseleave - relatedTarget:, relatedTarget); console.log(SubMenu mouseleave - relatedTarget classes:, relatedTarget ? relatedTarget.className : null); // Check if were moving to another element within the same dropdown const currentDropdown this.closest(.horizontal-dropdown) || this.closest(.horizontal-submenu)?.closest(.horizontal-dropdown); console.log(Current dropdown found:, !!currentDropdown); console.log(Current dropdown element:, currentDropdown); console.log(RelatedTarget:, relatedTarget); console.log(RelatedTarget closest dropdown:, relatedTarget ? relatedTarget.closest(.horizontal-dropdown) : null); // Check if relatedTarget is within the same dropdown container const isMovingWithinDropdown relatedTarget && currentDropdown && ( relatedTarget.closest(.horizontal-dropdown) currentDropdown || relatedTarget.classList.contains(horizontal-submenu-item) || relatedTarget.closest(.horizontal-submenu-item) || currentDropdown.contains(relatedTarget) ); // Alternative approach: check if relatedTarget is within the same parent menu item const parentMenuContainer this.closest(.horizontal-menu-item); const isMovingWithinParent relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget); console.log(Is moving within parent menu item:, isMovingWithinParent); console.log(Current dropdown contains relatedTarget:, currentDropdown ? currentDropdown.contains(relatedTarget) : no dropdown); console.log(RelatedTarget closest dropdown currentDropdown:, relatedTarget && currentDropdown ? relatedTarget.closest(.horizontal-dropdown) currentDropdown : null); // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside) // Get the parent menu items dropdown container const parentMenuItem this.closest(.horizontal-menu-item); console.log(Parent menu item found:, !!parentMenuItem); const dropdownContainer parentMenuItem ? parentMenuItem.querySelector(.horizontal-dropdown) : null; console.log(Dropdown container found:, !!dropdownContainer); if (dropdownContainer) { console.log(Dropdown container classes:, dropdownContainer.className); console.log(Dropdown container display:, window.getComputedStyle(dropdownContainer).display); console.log(Dropdown container position:, window.getComputedStyle(dropdownContainer).position); } // Get bounds from the visible dropdown (not the hidden one) let dropdownRect { left: 0, right: 0, top: 0, bottom: 0 }; if (dropdownContainer) { const computedStyle window.getComputedStyle(dropdownContainer); if (computedStyle.display ! none) { dropdownRect dropdownContainer.getBoundingClientRect(); } else { // If dropdown is hidden, try to get bounds from the parent menu item const parentRect parentMenuItem.getBoundingClientRect(); dropdownRect parentRect; } } const mouseX event.clientX; const mouseY event.clientY; const isMouseInDropdownBounds mouseX > dropdownRect.left && mouseX dropdownRect.right && mouseY > dropdownRect.top && mouseY dropdownRect.bottom; console.log(Dropdown bounds:, { left: dropdownRect.left, right: dropdownRect.right, top: dropdownRect.top, bottom: dropdownRect.bottom, mouseX, mouseY, mouseInBounds: mouseX > dropdownRect.left && mouseX dropdownRect.right && mouseY > dropdownRect.top && mouseY dropdownRect.bottom }); // If theres an active flyout, always keep the parent dropdown open // Check multiple ways to detect active flyouts const activeFlyout document.querySelector(.horizontal-nested-dropdownstyle*display: block) || document.querySelector(.horizontal-nested-dropdownstyle*visibility: visible) || document.querySelector(.horizontal-nested-dropdown:not(style*display: none)); console.log(Active flyout found:, !!activeFlyout); if (activeFlyout) { console.log(Active flyout display style:, activeFlyout.style.display); console.log(Active flyout computed display:, window.getComputedStyle(activeFlyout).display); console.log(Active flyout visibility style:, activeFlyout.style.visibility); } const hasActiveFlyout !!activeFlyout; // If the dropdown is hidden (display: none), we cant get accurate bounds // So lets use a simpler approach - if were moving to another submenu item, keep open const isMovingToSubmenuItem relatedTarget && ( relatedTarget.classList.contains(horizontal-submenu-item) || relatedTarget.closest(.horizontal-submenu-item) ); console.log(Is moving to submenu item:, isMovingToSubmenuItem); console.log(RelatedTarget is submenu item:, relatedTarget ? relatedTarget.classList.contains(horizontal-submenu-item) : null); console.log(RelatedTarget has submenu item ancestor:, relatedTarget ? !!relatedTarget.closest(.horizontal-submenu-item) : null); // If were moving to another submenu item within the same dropdown, always keep open // OR if theres an active flyout (regardless of where the mouse is), keep open // Simple approach: if mouse is within dropdown bounds OR theres an active flyout, keep open const shouldKeepOpen isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem; console.log(Is moving within dropdown:, isMovingWithinDropdown); console.log(Is mouse in dropdown bounds:, isMouseInDropdownBounds); console.log(Has active flyout:, hasActiveFlyout); console.log(Should keep open:, shouldKeepOpen); if (!shouldKeepOpen) { console.log(SubMenu mouseleave - starting timer (moving outside dropdown)); parentMenuItem.leaveTimer setTimeout(() > { // Check if theres an active flyout - if so, keep parent open const activeFlyout document.querySelector(.horizontal-nested-dropdownstyle*display: block); console.log(Timer fired - active flyout:, activeFlyout); if (!activeFlyout) { console.log(Closing parent dropdown - no active flyout); parentMenuItem.classList.remove(expanded); const currentIcon parentMenuItem.querySelector(.icon.toggle-icon); if (currentIcon) currentIcon.classList.remove(rotated); } else { console.log(Keeping parent dropdown open - flyout is active); } }, 200); } else { console.log(SubMenu mouseleave - not starting timer (moving within dropdown)); } } }); } else { menuItem.classList.remove(has-children); if (toggleIconContainer.parentNode) { toggleIconContainer.remove(); } } } } // Click listener for items that are direct navigation links (not folders with children) if (!menuItem.classList.contains(has-children)) { menuItem.addEventListener(click, async (event) > { event.stopPropagation(); const clickedItemId galleryItem.id; // Capture the ID of the clicked item // Check if its an external URL first if (galleryItem.isExternal && galleryItem.url) { window.open(galleryItem.url, _blank, noopener,noreferrer); return; } if (galleryItem.isPage) { if (typeof loadPage function) { loadPage(clickedItemId, event); // loadPage is synchronous if (typeof updateActiveStatesHorizontal function) { console.log(Click Handler (Page): Calling updateActiveStatesHorizontal for ID:, clickedItemId); // Use a minimal delay even for sync operations if page rendering has its own async microtasks setTimeout(() > updateActiveStatesHorizontal(clickedItemId), 50); } } } else { // Its a gallery if (typeof loadGallery function) { console.log(Click Handler (Gallery): Awaiting loadGallery for ID:, clickedItemId); await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery console.log(Click Handler (Gallery): loadGallery completed for ID:, clickedItemId, . Calling updateActiveStatesHorizontal with delay.); if (typeof updateActiveStatesHorizontal function) { setTimeout(() > { console.log(Delayed Update from Click Calling updateActiveStatesHorizontal for gallery ID:, clickedItemId); updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId }, 100); // Delay to allow gallery system to settle } } } // Close any other open dropdowns and flyouts document.querySelectorAll(.horizontal-menu-item.expanded).forEach((openItem) > { // Dont close the parent if a child within its dropdown was clicked (already handled by submenu click) if (openItem ! menuItem.closest(.horizontal-menu-item.expanded)) { openItem.classList.remove(expanded); const icon openItem.querySelector(.icon.toggle-icon); if (icon) icon.classList.remove(rotated); } }); // Close all flyouts document.querySelectorAll(.horizontal-nested-dropdown).forEach((flyout) > { flyout.style.display none; flyout.style.opacity 0; flyout.style.visibility hidden; }); // Remove expanded class from all submenu items document.querySelectorAll(.horizontal-submenu-item.expanded).forEach((subItem) > { subItem.classList.remove(expanded); }); }); } return menuItem;}/*** Creates a single horizontal submenu item.* (UPDATED to handle nested children and async loadGallery)* @param {object} galleryItem - The gallery item data for the submenu.* @returns {HTMLElement | null} The created submenu item element, or null if not rendered.*/function createHorizontalSubMenuItem(galleryItem) { if (galleryItem.visible false || galleryItem.isSpacer) { return null; } const subMenuItem document.createElement(li); subMenuItem.className horizontal-submenu-item; subMenuItem.dataset.id String(galleryItem.id); subMenuItem.setAttribute(data-visible, galleryItem.visible ! false ? true : false); // Create the text content const titleSpan document.createElement(span); titleSpan.textContent galleryItem.title; subMenuItem.appendChild(titleSpan); const currentGlobalActiveId parseInt(String(window.activeGalleryId || activeGalleryId)); if (galleryItem.id currentGlobalActiveId) { subMenuItem.classList.add(active); } // Check if this submenu item has children (nested sub-sub items) if (galleryItem.children && galleryItem.children.length > 0) { const visibleChildren galleryItem.children.filter(child > child.visible ! false && !child.isSpacer); if (visibleChildren.length > 0) { subMenuItem.classList.add(has-children); // Add arrow indicator for nested items (same as top-level menu) const arrowSpan document.createElement(span); arrowSpan.className submenu-arrow; const svgIcon document.createElementNS(http://www.w3.org/2000/svg, svg); svgIcon.setAttribute(class, icon toggle-icon); svgIcon.setAttribute(viewBox, 0 0 24 24); svgIcon.setAttribute(fill, none); svgIcon.setAttribute(stroke, currentColor); svgIcon.setAttribute(stroke-width, 2); const polyline document.createElementNS(http://www.w3.org/2000/svg, polyline); polyline.setAttribute(points, 9 6 15 12 9 18); // Right-pointing arrow svgIcon.appendChild(polyline); arrowSpan.appendChild(svgIcon); subMenuItem.appendChild(arrowSpan); // Create nested submenu as a true flyout const nestedSubMenu document.createElement(div); nestedSubMenu.className horizontal-nested-dropdown; // Create a list container for the nested items const nestedList document.createElement(ul); nestedList.className horizontal-nested-list; visibleChildren.forEach((childItem) > { const nestedSubMenuItem createHorizontalSubMenuItem(childItem); // Recursive call if (nestedSubMenuItem) { nestedList.appendChild(nestedSubMenuItem); } }); nestedSubMenu.appendChild(nestedList); if (nestedSubMenu.hasChildNodes()) { // Append to document body for true flyout behavior document.body.appendChild(nestedSubMenu); // Store reference to the flyout for cleanup const flyoutId flyout- + Date.now() + - + Math.random(); subMenuItem.setAttribute(data-flyout-id, flyoutId); nestedSubMenu.setAttribute(data-flyout-id, flyoutId); console.log(Created flyout with ID:, flyoutId, for item:, galleryItem.title); // Debug console.log(Flyout element:, nestedSubMenu); // Debug // Add hover events for nested submenu let nestedLeaveTimer; subMenuItem.addEventListener(mouseenter, function() { if (this.nestedLeaveTimer) { clearTimeout(this.nestedLeaveTimer); } // Keep the parent dropdown open by preventing its mouseleave timer const parentMenuItem this.closest(.horizontal-menu-item); if (parentMenuItem) { // Clear any existing leave timer on the parent if (parentMenuItem.leaveTimer) { clearTimeout(parentMenuItem.leaveTimer); } } // Close other nested dropdowns document.querySelectorAll(.horizontal-submenu-item.expanded).forEach((openItem) > { if (openItem ! this) { openItem.classList.remove(expanded); // Hide other flyouts const otherFlyoutId openItem.getAttribute(data-flyout-id); if (otherFlyoutId) { const otherFlyout document.querySelector(data-flyout-id + otherFlyoutId + ); if (otherFlyout) { otherFlyout.style.display none; otherFlyout.style.opacity 0; otherFlyout.style.visibility hidden; } } } }); this.classList.add(expanded); // Show and position the nested dropdown as a true flyout const flyoutId this.getAttribute(data-flyout-id); console.log(Flyout ID:, flyoutId); // Debug if (flyoutId) { // Find the actual flyout container (not the submenu item) const nestedDropdown document.querySelector(.horizontal-nested-dropdowndata-flyout-id + flyoutId + ); console.log(Found nested dropdown:, nestedDropdown); // Debug if (nestedDropdown) { const rect this.getBoundingClientRect(); const parentDropdown this.closest(.horizontal-dropdown); const parentRect parentDropdown ? parentDropdown.getBoundingClientRect() : rect; console.log(Positioning flyout - rect:, rect, parentRect:, parentRect); // Debug // Position to the right of the parent dropdown (touching) const leftPos parentRect.right - 1; // Slight overlap to eliminate gap const topPos rect.top; nestedDropdown.style.left leftPos + px; nestedDropdown.style.top topPos + px; nestedDropdown.style.display block; nestedDropdown.style.opacity 1; nestedDropdown.style.visibility visible; nestedDropdown.style.position fixed; // Ensure fixed positioning nestedDropdown.style.zIndex 1251; // Ensure high z-index console.log(Flyout shown - display: block, opacity: 1, visibility: visible); console.log(Set flyout position:, leftPos, topPos); // Debug // Check if it would go off-screen and adjust if needed setTimeout(() > { const dropdownRect nestedDropdown.getBoundingClientRect(); if (dropdownRect.right > window.innerWidth) { // Position to the left of the parent dropdown instead const newLeftPos parentRect.left - dropdownRect.width - 5; nestedDropdown.style.left newLeftPos + px; console.log(Adjusted flyout position (left side):, newLeftPos); // Debug } }, 10); } else { console.error(Nested dropdown not found for flyout ID:, flyoutId); // Debug } } else { console.error(No flyout ID found for submenu item); // Debug } }); subMenuItem.addEventListener(mouseleave, function(event) { // Check if were moving to the flyout const flyoutId this.getAttribute(data-flyout-id); const relatedTarget event.relatedTarget; const isMovingToFlyout relatedTarget && flyoutId && relatedTarget.closest(.horizontal-nested-dropdowndata-flyout-id + flyoutId + ); // Also check if were moving within the dropdown const parentMenuItem this.closest(.horizontal-menu-item); const isMovingWithinDropdown relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget); console.log(Submenu item mouseleave - moving to flyout:, isMovingToFlyout); console.log(Submenu item mouseleave - moving within dropdown:, isMovingWithinDropdown); if (!isMovingToFlyout && !isMovingWithinDropdown) { this.nestedLeaveTimer setTimeout(() > { this.classList.remove(expanded); // Hide the flyout if (flyoutId) { const nestedDropdown document.querySelector(.horizontal-nested-dropdowndata-flyout-id + flyoutId + ); if (nestedDropdown) { nestedDropdown.style.display none; nestedDropdown.style.opacity 0; nestedDropdown.style.visibility hidden; } } // Now that the flyout is closed, allow the parent dropdown to close const parentMenuItem this.closest(.horizontal-menu-item); if (parentMenuItem) { setTimeout(() > { const activeFlyout document.querySelector(.horizontal-nested-dropdownstyle*display: block); if (!activeFlyout) { parentMenuItem.classList.remove(expanded); const currentIcon parentMenuItem.querySelector(.icon.toggle-icon); if (currentIcon) currentIcon.classList.remove(rotated); } }, 50); } }, 200); } else { console.log(Submenu item mouseleave - not starting timer (moving to flyout)); } }); nestedSubMenu.addEventListener(mouseenter, function() { console.log(Flyout mouseenter - clearing timers); clearTimeout(nestedLeaveTimer); // Clear the parent dropdowns leave timer to keep it open const flyoutId this.getAttribute(data-flyout-id); if (flyoutId) { const parentSubItem document.querySelector(.horizontal-submenu-itemdata-flyout-id + flyoutId + ); if (parentSubItem) { const parentMenuItem parentSubItem.closest(.horizontal-menu-item); if (parentMenuItem && parentMenuItem.leaveTimer) { console.log(Clearing parent menu item leave timer); clearTimeout(parentMenuItem.leaveTimer); parentMenuItem.leaveTimer null; } else { console.log(No parent menu item leave timer to clear); } if (parentSubItem.nestedLeaveTimer) { console.log(Clearing submenu item nested leave timer); clearTimeout(parentSubItem.nestedLeaveTimer); parentSubItem.nestedLeaveTimer null; } } } }); nestedSubMenu.addEventListener(mouseleave, function() { const flyoutId this.getAttribute(data-flyout-id); if (flyoutId) { const parentSubItem document.querySelector(.horizontal-submenu-itemdata-flyout-id + flyoutId + ); if (parentSubItem) { nestedLeaveTimer setTimeout(() > { parentSubItem.classList.remove(expanded); this.style.display none; this.style.opacity 0; this.style.visibility hidden; }, 200); } } }); } } } // Click listener for navigation (only if no children or if its a direct link) if (!subMenuItem.classList.contains(has-children) || galleryItem.isPage) { subMenuItem.addEventListener(click, async (event) > { event.stopPropagation(); const clickedItemId galleryItem.id; // Check if its an external URL first if (galleryItem.isExternal && galleryItem.url) { window.open(galleryItem.url, _blank, noopener,noreferrer); return; } if (galleryItem.isPage) { if (typeof loadPage function) { loadPage(clickedItemId, event); if (typeof updateActiveStatesHorizontal function) { console.log(SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:, clickedItemId); setTimeout(() > updateActiveStatesHorizontal(clickedItemId), 50); } } } else { // Its a gallery if (typeof loadGallery function) { console.log(SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:, clickedItemId); await loadGallery(clickedItemId, event); console.log(SubMenu Click Handler (Gallery): loadGallery completed for ID:, clickedItemId, . Calling updateActiveStatesHorizontal with delay.); if (typeof updateActiveStatesHorizontal function) { setTimeout(() > { console.log(Delayed Update from SubMenu Click Calling updateActiveStatesHorizontal for gallery ID:, clickedItemId); updateActiveStatesHorizontal(clickedItemId); }, 100); } } } // Close all parent dropdowns and flyouts after navigation document.querySelectorAll(.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded).forEach((openItem) > { openItem.classList.remove(expanded); const icon openItem.querySelector(.icon.toggle-icon); if (icon) icon.classList.remove(rotated); }); // Close all flyouts document.querySelectorAll(.horizontal-nested-dropdown).forEach((flyout) > { flyout.style.display none; flyout.style.opacity 0; flyout.style.visibility hidden; }); }); } return subMenuItem;}/*** Updates the active class for items in the horizontal menu.*/function updateActiveStatesHorizontal(forcedActiveId null) { // Prioritize forcedActiveId if provided, otherwise use global state. // Ensure the ID is treated as a number for comparison. const targetId forcedActiveId ! null ? parseInt(String(forcedActiveId)) : parseInt(String(window.activeGalleryId || activeGalleryId)); console.log(Horizontal Menu Update Initiated. Target Active ID:, targetId, (forcedActiveId ! null ? `(Forced: ${forcedActiveId})` : (Global)), Type:, typeof targetId); const menuItems document.querySelectorAll(.horizontal-menu-item, .horizontal-submenu-item); if (menuItems.length 0 && !isNaN(targetId)) { // Check if targetId is a valid number console.warn(Horizontal Menu Update No horizontal menu items found in DOM to update. Target ID was:, targetId); return; } let activeItemSet false; menuItems.forEach(item > { const itemIdStr item.dataset.id; if (!itemIdStr) { // console.warn(Horizontal Menu Update Menu item found without a data-id attribute:, item); return; } const itemId parseInt(itemIdStr); if (itemId targetId) { if (!item.classList.contains(active)) { item.classList.add(active); console.log(`Horizontal Menu Update ADDED .active to item ID ${itemId} (${item.textContent.trim()}) using target ID ${targetId}`); } activeItemSet true; } else { if (item.classList.contains(active)) { item.classList.remove(active); console.log(`Horizontal Menu Update REMOVED .active from item ID ${itemId} (${item.textContent.trim()}) using target ID ${targetId}`); } } }); if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number console.warn(`Horizontal Menu Update Target Active ID was ${targetId}, but NO matching horizontal menu item was made active. Check data-id attributes and item visibility.`); } const activeSubItem document.querySelector(.horizontal-submenu-item.active); if (activeSubItem) { const parentTopItem activeSubItem.closest(.horizontal-menu-item.has-children); if (parentTopItem && !parentTopItem.classList.contains(active)) { // parentTopItem.classList.add(active-parent); // Optional class for styling parent } } console.log(Horizontal Menu Update Completed for Target ID:, targetId);} // Listener for closing dropdowns when clicking outside document.addEventListener(click, function(event) { if (currentMenuLayout horizontal) { const openSubmenus document.querySelectorAll(.horizontal-menu-item.expanded); let clickedInsideSubmenuOrParent false; openSubmenus.forEach(submenuContainer > { // Check if click is on the submenu container OR its direct parent menu item if (submenuContainer.contains(event.target) || (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains(horizontal-menu-item))) { clickedInsideSubmenuOrParent true; } }); // If click is on another top-level item that is NOT expanded, also dont close const targetMenuItem event.target.closest(.horizontal-menu-item); if (targetMenuItem && !targetMenuItem.classList.contains(expanded) && targetMenuItem.classList.contains(has-children)) { clickedInsideSubmenuOrParent true; // Dont close if clicking another parent to open it } if (!clickedInsideSubmenuOrParent) { openSubmenus.forEach(submenuContainer > { submenuContainer.classList.remove(expanded); }); } } });document.addEventListener(menuLayoutChanged, function(e) { const newLayout e.detail.layout; console.log(EVENT: menuLayoutChanged detected. New layout:, newLayout, Current isEditing state:, isEditing); // Update the global currentMenuLayout variable if you have one if (typeof currentMenuLayout ! undefined) { currentMenuLayout newLayout; } const sidebar document.querySelector(.sidebar); // Ensure sidebar DOM element is selected // Always clear and re-render the appropriate menu structure for the new layout. // This is important for style changes (like font size) that affect dimensions. const galleryTreeContainer document.getElementById(galleryTree); const mobileHeaderContent document.querySelector(.mobile-header-content); if (galleryTreeContainer) { galleryTreeContainer.innerHTML ; // Clear existing tree } if (mobileHeaderContent) { const horizontalMenuContainer mobileHeaderContent.querySelector(.horizontal-menu-container); if (horizontalMenuContainer) { horizontalMenuContainer.innerHTML ; // Clear only items, not the container itself } } // Render the correct menu structure based on the new layout if (newLayout horizontal) { if (typeof renderHorizontalMenu function) { renderHorizontalMenu(); } // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal } else { // sidebar or top if (typeof renderGalleries function) { renderGalleries(); } // CSS rules will handle sidebar visibility } // If currently in edit mode, ensure the UI components for editing are correctly displayed // for the new layout, without fully toggling the mode off and on. if (isEditing && typeof window.updateGlobalEditState function) { console.log(menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.); // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode. // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar. window.updateGlobalEditState(true); // 2. Re-initialize sortables. Its often safer to destroy existing ones first. if (typeof initializeNestedSortables function) { if (typeof destroyNestedSortables function) { destroyNestedSortables(); } setTimeout(initializeNestedSortables, 100); // Delay for DOM updates } // Also reinitialize for SidebarManager if it exists and handles its own sortables if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables function) { setTimeout(() > window.SidebarManager._reinitializeNestedSortables(), 150); } // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible const editControls document.querySelector(.edit-controls); if (editControls) { editControls.style.display flex; // Or your default display type for these controls editControls.classList.add(visible); } // 4. If the layout is now horizontal, the sidebar (for editing) should be visible. // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }` // should handle this. This log is for confirmation. if (newLayout horizontal && sidebar) { console.log(menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.); } // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set. if (window.PageManager && typeof window.PageManager.setEditMode function && window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) { window.PageManager.setEditMode(true); } } else if (!isEditing) { // If not in edit mode, CSS should handle the view state. // This block can be used for any explicit view mode adjustments if CSS isnt sufficient. console.log(menuLayoutChanged: Not in edit mode. CSS will handle view state.); } // Update mobile title and close mobile menu if open, regardless of edit mode if (typeof updateMobileTitle function) { updateMobileTitle(); } if (typeof closeMobileMenu function) { closeMobileMenu(); }});// Make sure getFirstSidebarElementForHeader is available if called from other scriptsif (typeof window.getFirstSidebarElementForHeader undefined) { window.getFirstSidebarElementForHeader getFirstSidebarElementForHeader;}// Make sure getFirstSocialElementForHeader is available if called from other scriptsif (typeof window.getFirstSocialElementForHeader undefined) { window.getFirstSocialElementForHeader getFirstSocialElementForHeader;}// Make sure renderSocialIcons is available if called from other scriptsif (typeof window.renderSocialIcons undefined) { window.renderSocialIcons renderSocialIcons;}if (typeof window.renderHorizontalMenu undefined) { window.renderHorizontalMenu renderHorizontalMenu;}if (typeof window.createHorizontalMenuItem undefined) { window.createHorizontalMenuItem createHorizontalMenuItem;}if (typeof window.createHorizontalSubMenuItem undefined) { window.createHorizontalSubMenuItem createHorizontalSubMenuItem;}if (typeof window.updateActiveStatesHorizontal undefined) { window.updateActiveStatesHorizontal updateActiveStatesHorizontal;}// Ensure toggleEditMode is globally available if its the primary one.if (typeof window.toggleEditMode undefined || window.toggleEditMode.toString().length 100) { // Heuristic to check if its a placeholder window.toggleEditMode toggleEditMode;}// Helper function to toggle edit controls visibilityfunction toggleEditControlsVisibility(show) { const editControls document.querySelector(.edit-controls); if (editControls) { if (show) { editControls.classList.add(visible); editControls.style.display flex; } else { editControls.classList.remove(visible); editControls.style.display none; } }}document.addEventListener(keydown, function(e) { if (e.key + || e.keyCode 187 && e.shiftKey) { // Check if user is already logged in and is admin const token window.didToken || localStorage.getItem(hydra_auth_token); const isAdmin window.isAdmin || localStorage.getItem(hydra_is_admin) true; if (token && isAdmin) { // User is already logged in - toggle edit mode instead of showing login prompt console.log(User already logged in - toggling edit mode); if (typeof window.toggleEditMode function) { window.toggleEditMode(); } else if (typeof toggleEditMode function) { toggleEditMode(); } else { console.warn(toggleEditMode function not found); } } else { // User is not logged in - show login prompt startOTPLogin(); } e.preventDefault(); }});// Email OTP Authentication with Enhanced Debugging// Add this function to the client-side code// Improved client-side backup function with better token handlingasync function createLoginBackup() { try { console.log(Attempting to create login backup...); // Get authentication info from multiple possible sources let token window.didToken || localStorage.getItem(hydra_auth_token); const email localStorage.getItem(hydra_auth_email) || (window.userMetadata ? window.userMetadata.email : ) || ; // Check if we have authentication if (!token) { console.error(No auth token available for backup); return; } if (!email) { console.error(No email available for backup - make sure it was stored during login); return; } console.log(`Creating backup with token (${token.length} chars) for ${email}`); // Format the token correctly - VERY IMPORTANT // If its already a hydra token (starts with hydra:), use it as is // Otherwise, add the hydra: prefix const formattedToken token.startsWith(hydra:) ? token : `hydra:${token}`; console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`); // Get the API URL with preview handling if needed const backupUrl typeof getApiUrl function ? getApiUrl(/api/create-backup) : /api/create-backup; console.log(`Sending backup request to: ${backupUrl}`); // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header const response await fetch(backupUrl, { method: POST, headers: { Authorization: `Bearer ${formattedToken}`, Content-Type: application/json, X-User-Email: email, // Include email as fallback X-API-Request: true // Mark as API request } }); // Handle response if (!response.ok) { const errorText await response.text(); let errorData; try { // Try to parse as JSON errorData JSON.parse(errorText); console.error(Backup failed:, errorData.error, errorData); } catch { // Not JSON, log raw text console.error(Backup failed with status, response.status, errorText); } return; } // Parse successful response const result await response.json(); console.log(`✅ Backup created successfully: ${result.backup}`); } catch (error) { console.error(Error in backup process:, error); }}// Initialize Magic with detailed loggingfunction initMagic() { try { // If Magic is already initialized, return it if (window.magic) { return window.magic; } console.log(Initializing Magic SDK...); // Initialize with minimal options const magic new Magic(pk_live_E0B68BA6DCA8C75F, { testMode: false }); // Store reference globally window.magic magic; console.log(Magic SDK initialized successfully); return magic; } catch (error) { console.error(Error initializing Magic:, error); throw error; }}// Start OTP login flow with proper event handlingasync function startOTPLogin() { try { // Initialize Magic const magic initMagic(); // Get email from user const email prompt(Please enter your email:); if (!email) return; // User cancelled console.log(Starting Email OTP login for:, email); showLoadingOverlay(Sending verification code...); // Check if Magics event enum types are available - for newer Magic SDK versions // If not available, fall back to string-based events const useEnums typeof LoginWithEmailOTPEventOnReceived ! undefined && typeof LoginWithEmailOTPEventEmit ! undefined; console.log(Using enum-based events:, useEnums); // Create a timeout handler let emailSentTimeout setTimeout(() > { console.log(No email-otp-sent event received, using fallback UI); hideLoadingOverlay(); showOTPInputOverlay(email); }, 10000); // Create OTP login handle with showUI: false const handle magic.auth.loginWithEmailOTP({ email, showUI: false, deviceCheckUI: false }); // Track authentication state window.pendingAuth { email, handle }; // Listen for events - using both enum-based and string-based event handling console.log(Setting up OTP event handlers...); // Email OTP sent event if (useEnums) { handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () > { console.log(EVENT: EmailOTPSent (enum) - Email was sent successfully); clearTimeout(emailSentTimeout); hideLoadingOverlay(); showOTPInputOverlay(email); }); } // Also listen for string-based event (for backward compatibility) handle.on(email-otp-sent, () > { console.log(EVENT: email-otp-sent - Email was sent successfully); clearTimeout(emailSentTimeout); hideLoadingOverlay(); showOTPInputOverlay(email); }); // Invalid OTP event if (useEnums) { handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () > { console.log(EVENT: InvalidEmailOtp (enum) - Invalid code entered); showErrorMessage(Invalid verification code. Please try again.); updateOTPInputForRetry(); }); } // Also listen for string-based event handle.on(invalid-email-otp, () > { console.log(EVENT: invalid-email-otp - Invalid code entered); showErrorMessage(Invalid verification code. Please try again.); updateOTPInputForRetry(); }); function handleLoginSuccess(result) { console.log(OTP login successful!); hideOTPInputOverlay(); hideLoadingOverlay(); // Store the didToken window.didToken result; console.log(DID Token received:, result ? (result.substring(0, 20) + ...) : none); // Set isAuthenticated globally window.isAuthenticated true; // Get user metadata magic.user.getInfo().then(userInfo > { window.userMetadata userInfo; console.log(User info retrieved:, userInfo); // Call checkAdminStatus with the token return checkAdminStatus(result); }).then(() > { console.log(Admin status check completed); // Show success message showSuccessMessage(Login successful!); // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy // 1. Add state classes to body document.body.classList.add(hydra-authenticated); if (window.isAdmin) { document.body.classList.add(hydra-admin); } // 2. Direct DOM manipulation const logoutButton document.getElementById(logoutButton); const editButton document.getElementById(editButton); // Create new buttons if they dont exist if (!logoutButton) { console.log(Creating logout button); const newLogoutButton document.createElement(button); newLogoutButton.id logoutButton; newLogoutButton.className btn; newLogoutButton.textContent Logout; newLogoutButton.onclick logout; newLogoutButton.style.display block; // Add to sidebar header const sidebarHeader document.querySelector(.sidebar-header); if (sidebarHeader) { sidebarHeader.appendChild(newLogoutButton); } else { document.body.appendChild(newLogoutButton); } } else { // Force display of existing button logoutButton.style.cssText display: block !important; visibility: visible !important; opacity: 1 !important;; } if (!editButton && window.isAdmin) { console.log(Creating edit button); const newEditButton document.createElement(button); newEditButton.id editButton; newEditButton.className btn btn-primary; newEditButton.innerHTML ` svg classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width2> path dM11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7>/path> path dM18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z>/path> /svg> Edit `; newEditButton.onclick toggleEditMode; newEditButton.style.display block; // Add to sidebar header const sidebarHeader document.querySelector(.sidebar-header); if (sidebarHeader) { sidebarHeader.appendChild(newEditButton); } else { document.body.appendChild(newEditButton); } } else if (editButton && window.isAdmin) { // Force display of existing button editButton.style.cssText display: block !important; visibility: visible !important; opacity: 1 !important;; } // 3. Dispatch event for other listeners document.dispatchEvent(new CustomEvent(login-complete, { detail: { isAdmin: window.isAdmin, email: window.userMetadata ? window.userMetadata.email : null } })); // Clear pending auth window.pendingAuth null; }).catch(error > { console.error(Error in login success handling:, error); showErrorMessage(Error completing login: + error.message); });} // Completion event handle.on(done, async (result) > { console.log(OTP login successful!); hideOTPInputOverlay(); hideLoadingOverlay(); handleLoginSuccess(); // Store the didToken window.didToken result; console.log(DID Token received:, result ? (result.substring(0, 20) + ...) : none); // Set isAuthenticated globally window.isAuthenticated true; document.body.classList.add(hydra-authenticated); // Get user metadata try { const userInfo await magic.user.getInfo(); window.userMetadata userInfo; console.log(User info retrieved:, userInfo); } catch (error) { console.error(Error getting user info:, error); } // Call checkAdminStatus with the token try { if (typeof checkAdminStatus function) { await checkAdminStatus(result); } else { console.warn(checkAdminStatus function not available); } } catch (error) { console.error(Error checking admin status:, error); } // Show success message showSuccessMessage(Login successful!); // Get the ID token (JWT) for API calls - this contains user info try { const idToken await magic.user.getIdToken(); console.log(ID Token received, length:, idToken ? idToken.length : 0); console.log(ID Token format check - contains dots:, idToken ? idToken.includes(.) : false); console.log(ID Token prefix:, idToken ? idToken.substring(0, 20) + ... : none); // Check if this is actually a JWT token (should contain dots) if (idToken && idToken.includes(.)) { console.log(Got proper JWT token from Magic.link); localStorage.setItem(hydra_auth_token, idToken); } else { console.log(ID token is not JWT format, trying to get user info...); // If not JWT, get user metadata and create a simple token const metadata await magic.user.getInfo(); console.log(User metadata:, metadata); if (metadata && metadata.email) { // Create a simple token with user email const userToken { email: metadata.email, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) }; // Create a proper JWT-like structure: header.payload.signature const header btoa(JSON.stringify({ typ: JWT, alg: none })); const payload btoa(JSON.stringify(userToken)); const signature nosignature; const jwtToken header + . + payload + . + signature; console.log(Created JWT-like token with email:, metadata.email); localStorage.setItem(hydra_auth_token, jwtToken); localStorage.setItem(hydra_auth_email, metadata.email); } else { throw new Error(Could not get user metadata); } } // Also store DID token separately if needed localStorage.setItem(hydra_did_token, result); } catch (error) { console.error(Error getting ID token:, error); // Fallback to DID token if ID token fails localStorage.setItem(hydra_auth_token, result); } // Update UI directly with classes document.body.classList.add(hydra-authenticated); if (isAdmin) { document.body.classList.add(hydra-admin); } // Clear pending auth window.pendingAuth null; // Run existing checkAuth if available if (typeof checkAuth function) { console.log(Running existing checkAuth function); setTimeout(checkAuth, 500); } }); // Error event handle.on(error, (error) > { console.error(EVENT: error - OTP login error:, error); hideLoadingOverlay(); hideOTPInputOverlay(); showErrorMessage(Login failed: + (error.message || Unknown error)); // Clear timeout if still active clearTimeout(emailSentTimeout); // Clean up window.pendingAuth null; }); console.log(OTP event handlers set up successfully); } catch (error) { console.error(Error starting OTP login:, error); hideLoadingOverlay(); hideOTPInputOverlay(); showErrorMessage(Error starting login: + error.message); }}// Submit OTP code with support for enum-based eventsfunction submitOTP(code) { try { // Validate code input if (!code || code.trim() ) { showErrorMessage(Please enter the verification code.); return; } // Check if we have a pending authentication if (!window.pendingAuth || !window.pendingAuth.handle) { showErrorMessage(No active login session. Please try again.); hideOTPInputOverlay(); return; } console.log(Submitting OTP code...); showLoadingOverlay(Verifying code...); // Check if enum types are available const useEnums typeof LoginWithEmailOTPEventEmit ! undefined; // Verify the OTP code if (useEnums) { window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code); } else { window.pendingAuth.handle.emit(verify-email-otp, code); } } catch (error) { console.error(Error submitting OTP:, error); hideLoadingOverlay(); showErrorMessage(Error verifying code: + error.message); }}// Cancel OTP login with support for enum-based eventsfunction cancelOTPLogin() { try { // Check if we have a pending authentication if (window.pendingAuth && window.pendingAuth.handle) { // Check if enum types are available const useEnums typeof LoginWithEmailOTPEventEmit ! undefined; // Emit cancel event if (useEnums) { window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel); } else { window.pendingAuth.handle.emit(cancel); } console.log(OTP login cancelled); } // Clean up window.pendingAuth null; // Hide any overlays hideOTPInputOverlay(); hideLoadingOverlay(); } catch (error) { console.error(Error cancelling OTP login:, error); }}// Update OTP input for retryfunction updateOTPInputForRetry() { const otpInput document.getElementById(otp-input); if (otpInput) { otpInput.value ; otpInput.focus(); // Add a shake animation for visual feedback otpInput.classList.add(shake); // Remove the animation class after it completes setTimeout(() > { otpInput.classList.remove(shake); }, 500); } // Hide loading overlay if its visible hideLoadingOverlay();}// Show OTP input overlay - improved versionfunction showOTPInputOverlay(email) { // Remove existing overlay if any hideOTPInputOverlay(); // Create overlay const overlay document.createElement(div); overlay.id otp-input-overlay; overlay.style.position fixed; overlay.style.top 0; overlay.style.left 0; overlay.style.width 100%; overlay.style.height 100%; overlay.style.backgroundColor rgba(0, 0, 0, 0.7); overlay.style.display flex; overlay.style.alignItems center; overlay.style.justifyContent center; overlay.style.zIndex 10000; // Create content box const content document.createElement(div); content.style.backgroundColor white; content.style.padding 30px; content.style.borderRadius 0px; content.style.maxWidth 400px; content.style.width 90%; content.style.textAlign center; content.style.boxShadow 0 4px 20px rgba(0,0,0,0.3); // Add logo const logo document.createElement(img); logo.src https://cdn.neonsky.app/neon-sky-logo.png; logo.alt Neon Sky Logo; logo.style.width 250px; logo.style.marginBottom 15px; // Add title const title document.createElement(h3); title.textContent ; title.style.margin 0 0 15px 0; title.style.fontSize 20px; // Add description const description document.createElement(p); description.innerHTML `Weve sent a verification code to strong>${email}/strong>.br>Please check your email and enter the code below:`; description.style.marginBottom 20px; description.style.fontSize 14px; description.style.lineHeight 1.4; description.style.color #555; // Append elements content.appendChild(logo); // Add logo at the top content.appendChild(title); content.appendChild(description); overlay.appendChild(content); document.body.appendChild(overlay); // Create form const form document.createElement(form); form.onsubmit function(e) { e.preventDefault(); const otpInput document.getElementById(otp-input); if (otpInput && otpInput.value) { submitOTP(otpInput.value); } }; // Add animation styles if (!document.getElementById(otp-animation-styles)) { const style document.createElement(style); style.id otp-animation-styles; style.textContent ` @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .shake { animation: shake 0.5s; } `; document.head.appendChild(style); } // Create input const input document.createElement(input); input.id otp-input; input.type text; input.placeholder Enter code; input.pattern 0-9*; // Numbers only input.inputMode numeric; // Show numeric keyboard on mobile input.autocomplete one-time-code; // For OTP autocomplete input.style.width 100%; input.style.padding 12px; input.style.fontSize 20px; input.style.textAlign center; input.style.letterSpacing 4px; input.style.fontWeight bold; input.style.border 2px solid #ddd; input.style.borderRadius 0px; input.style.marginBottom 20px; input.style.boxSizing border-box; form.appendChild(input); // Create button container const buttonContainer document.createElement(div); buttonContainer.style.display flex; buttonContainer.style.justifyContent space-between; buttonContainer.style.gap 10px; // Create verify button const verifyButton document.createElement(button); verifyButton.type submit; verifyButton.textContent Verify; verifyButton.style.flex 1; verifyButton.style.padding 10px; verifyButton.style.backgroundColor #4682B4; verifyButton.style.color white; verifyButton.style.border none; verifyButton.style.borderRadius 0px; verifyButton.style.fontSize 16px; verifyButton.style.cursor pointer; buttonContainer.appendChild(verifyButton); // Create cancel button const cancelButton document.createElement(button); cancelButton.type button; cancelButton.textContent Cancel; cancelButton.style.flex 1; cancelButton.style.padding 10px; cancelButton.style.backgroundColor #f1f1f1; cancelButton.style.color #333; cancelButton.style.border none; cancelButton.style.borderRadius 0px; cancelButton.style.fontSize 16px; cancelButton.style.cursor pointer; cancelButton.onclick function() { cancelOTPLogin(); }; buttonContainer.appendChild(cancelButton); form.appendChild(buttonContainer); // Add resend option const resendContainer document.createElement(div); resendContainer.style.marginTop 15px; resendContainer.style.fontSize 14px; resendContainer.style.color #666; const resendText document.createElement(span); resendText.textContent Didnt receive the code? ; const resendLink document.createElement(a); resendLink.textContent Resend; resendLink.href #; resendLink.style.color #4682B4; resendLink.style.textDecoration none; resendLink.onclick function(e) { e.preventDefault(); cancelOTPLogin(); setTimeout(() > { startOTPLogin(); }, 500); }; resendContainer.appendChild(resendText); resendContainer.appendChild(resendLink); form.appendChild(resendContainer); content.appendChild(form); overlay.appendChild(content); document.body.appendChild(overlay); // Focus the input setTimeout(() > { input.focus(); }, 100);}// Hide OTP input overlayfunction hideOTPInputOverlay() { const overlay document.getElementById(otp-input-overlay); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }}// Utility function to show loading overlayfunction showLoadingOverlay(message Loading...) { // Remove any existing overlay first hideLoadingOverlay(); const overlay document.createElement(div); overlay.id loading-overlay; overlay.style.position fixed; overlay.style.top 0; overlay.style.left 0; overlay.style.width 100%; overlay.style.height 100%; overlay.style.backgroundColor rgba(0, 0, 0, 0.7); overlay.style.display flex; overlay.style.alignItems center; overlay.style.justifyContent center; overlay.style.zIndex 10000; const content document.createElement(div); content.style.backgroundColor white; content.style.padding 30px; content.style.borderRadius 0px; content.style.textAlign center; content.style.maxWidth 400px; content.style.width 90%; // Add SVG loader instead of spinner const loaderContainer document.createElement(div); loaderContainer.style.margin 0 auto 20px auto; loaderContainer.style.color #444444; // Dark grey color // Add SVG animation loaderContainer.innerHTML ` svg xmlnshttp://www.w3.org/2000/svg width80 height80 viewBox0 0 24 24> defs> filter idsvgSpinnersGooeyBalls20> feGaussianBlur inSourceGraphic resulty stdDeviation1/> feColorMatrix iny resultz values1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7/> feBlend inSourceGraphic in2z/> /filter> /defs> g filterurl(#svgSpinnersGooeyBalls20)> circle cx5 cy12 r4 fillcurrentColor> animate attributeNamecx calcModespline dur2s keySplines.36,.62,.43,.99;.79,0,.58,.57 repeatCountindefinite values5;8;5/> /circle> circle cx19 cy12 r4 fillcurrentColor> animate attributeNamecx calcModespline dur2s keySplines.36,.62,.43,.99;.79,0,.58,.57 repeatCountindefinite values19;16;19/> /circle> animateTransform attributeNametransform dur0.75s repeatCountindefinite typerotate values0 12 12;360 12 12/> /g> /svg> `; content.appendChild(loaderContainer); // Add message const messageEl document.createElement(p); messageEl.textContent message; messageEl.style.margin 0; messageEl.style.fontSize 16px; content.appendChild(messageEl); overlay.appendChild(content); document.body.appendChild(overlay);}// Utility function to hide loading overlayfunction hideLoadingOverlay() { const overlay document.getElementById(loading-overlay); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }}// Utility function to show success messagefunction showSuccessMessage(message, duration 3000) { const toast document.createElement(div); toast.id success-toast; toast.style.position fixed; toast.style.top 20px; toast.style.left 50%; toast.style.transform translateX(-50%); toast.style.backgroundColor #4682B4; toast.style.color white; toast.style.padding 12px 24px; toast.style.borderRadius 0px; toast.style.boxShadow 0 4px 12px rgba(0,0,0,0.2); toast.style.zIndex 10000; toast.style.fontSize 16px; toast.textContent message; document.body.appendChild(toast); // Remove after duration setTimeout(() > { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, duration);}// Utility function to show error messagefunction showErrorMessage(message, duration 5000) { const toast document.createElement(div); toast.id error-toast; toast.style.position fixed; toast.style.top 20px; toast.style.left 50%; toast.style.transform translateX(-50%); toast.style.backgroundColor #e74c3c; toast.style.color white; toast.style.padding 12px 24px; toast.style.borderRadius 0px; toast.style.boxShadow 0 4px 12px rgba(0,0,0,0.2); toast.style.zIndex 10000; toast.style.fontSize 16px; toast.textContent message; document.body.appendChild(toast); // Remove after duration setTimeout(() > { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, duration);}// UPDATED: Check authentication status with class-based approachasync function checkAuth() { try { if (!magic) { console.log(Magic not initialized, skipping auth check); return; } // Prevent multiple simultaneous auth checks if (window._checkingAuth) return; window._checkingAuth true; console.log(Checking auth status...); isAuthenticated await magic.user.isLoggedIn(); console.log(isLoggedIn check:, isAuthenticated); if (isAuthenticated) { // Add the authenticated class document.body.classList.add(hydra-authenticated); // Get user metadata userMetadata await magic.user.getInfo(); console.log(User info:, userMetadata); // Get ID token for API calls (JWT format) didToken await magic.user.getIdToken(); console.log(ID token acquired, length:, didToken.length); // Store token in localStorage localStorage.setItem(hydra_auth_token, didToken); // Check if user is admin await checkAdminStatus(didToken); } else { resetAuthUI(); } // Release lock window._checkingAuth false; } catch (error) { console.error(Error checking authentication:, error); window._checkingAuth false; resetAuthUI(); }}async function checkAdminStatus(token) { try { console.log(Checking admin status...); // Add user email to headers if available let userEmail null; if (window.userMetadata && window.userMetadata.email) { userEmail window.userMetadata.email; console.log(Using email from userMetadata:, userEmail); } else { userEmail localStorage.getItem(hydra_auth_email); // Fallback to localStorage if (userEmail) console.log(Using email from localStorage:, userEmail); } if (!token) { console.log(No token provided to checkAdminStatus); return false; // Indicate failure } // Format the token correctly for the API call let formattedToken token; // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix if (!token.startsWith(hydra:) && !token.startsWith(Bearer )) { formattedToken `Bearer ${token}`; // Assume Magic/JWT needs Bearer } else if (token.startsWith(hydra:)) { formattedToken `Bearer ${token}`; // API expects Bearer even for hydra tokens } // If it already starts with Bearer, use as is // Get the API URL, handling preview mode const apiUrl typeof getApiUrl function ? getApiUrl(/api/check-admin) : /api/check-admin; console.log(Sending admin check request to:, apiUrl); // Create request headers const headers { Authorization: formattedToken, X-Hydra-Request: true // Indicate this might be part of Hydra flow }; if (userEmail) { headersX-User-Email userEmail; } console.log(Admin check headers:, { Authorization: `${formattedToken.substring(0, 20)}...`, X-User-Email: userEmail || Not available }); // Add timeout to the fetch request const controller new AbortController(); const timeoutId setTimeout(() > controller.abort(), 10000); // 10 second timeout let data; try { const response await fetch(apiUrl, { method: GET, headers: headers, signal: controller.signal }); clearTimeout(timeoutId); // Clear timeout if fetch succeeds console.log(Admin check response status:, response.status); // Get the response as text first for better debugging const responseText await response.text(); console.log(Admin check response length:, responseText.length); console.log(Admin check response text (first 100 chars):, responseText.substring(0, 100) + ...); try { data JSON.parse(responseText); console.log(Admin check data:, data); } catch (parseError) { console.error(Error parsing admin check response:, parseError); // Handle cases where the response might not be JSON (e.g., HTML error page) // For preview URLs, lets still assume admin to allow editing flow if (window.location.hostname preview.neonsky.app) { console.log(Preview URL detected, assuming admin status due to non-JSON response); isAdmin true; document.body.classList.add(hydra-admin); localStorage.setItem(hydra_is_admin, true); forceShowEditUI(); // Attempt to show UI return true; // Indicate potential success for preview } throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`); } if (!response.ok) { // Throw an error with details from the parsed JSON if available throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`); } // Process the admin status isAdmin data.isAdmin; window.isAdmin isAdmin; // Update global flag // Update classes based on admin status if (isAdmin) { document.body.classList.add(hydra-admin); localStorage.setItem(hydra_is_admin, true); // Store email if we got it from the server response if (data.email) { localStorage.setItem(hydra_auth_email, data.email); } else if (userEmail) { localStorage.setItem(hydra_auth_email, userEmail); // Store the email we used } } else { document.body.classList.remove(hydra-admin); localStorage.removeItem(hydra_is_admin); localStorage.removeItem(hydra_auth_email); // Clear email if not admin } // Update siteId if provided if (data.siteId) { siteId data.siteId; window.siteId siteId; if (window.Parameters) window.Parameters.siteId siteId; } // Store new token if provided (Hydra token) if (data.hydraToken) { console.log(Received hydra token:, data.hydraToken.substring(0, 10) + ...); // Store token globally and in localStorage using TokenManager if available if (window.TokenManager && typeof window.TokenManager.storeToken function) { window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || ); } else { // Fallback storage const cleanToken data.hydraToken.startsWith(hydra:) ? data.hydraToken.substring(6) : data.hydraToken; didToken cleanToken; localStorage.setItem(hydra_auth_token, cleanToken); if (data.email || userEmail) { localStorage.setItem(hydra_auth_email, data.email || userEmail); } } console.log(Stored credentials in localStorage); } // Update UI based on admin status if (isAdmin) { console.log(Admin status confirmed, showing edit UI); forceShowEditUI(); // Ensure UI elements are visible // --- MODIFICATION START --- // If layout is horizontal, re-render the menu to show controls let currentLayout sidebar; // Default if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; } if (currentLayout horizontal) { console.log(checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.); if (typeof renderHorizontalMenu function) { renderHorizontalMenu(); } else { console.warn(checkAdminStatus: renderHorizontalMenu function not found.); } } // --- MODIFICATION END --- createLoginBackup(); // Create backup after successful login/admin check } else { console.log(User is not admin, resetting UI.); resetAuthUI(); // Ensure non-admin UI state } return isAdmin; // Return the admin status } catch (fetchError) { clearTimeout(timeoutId); // Clear timeout on error as well console.error(Fetch error during admin check:, fetchError); // For preview URLs, still allow editing as a fallback if (window.location.hostname preview.neonsky.app) { console.log(Preview URL + fetch error, still enabling editing); isAdmin true; window.isAdmin true; document.body.classList.add(hydra-admin); localStorage.setItem(hydra_is_admin, true); forceShowEditUI(); return true; // Indicate potential success for preview } // For non-preview, throw the error to indicate failure throw fetchError; } } catch (error) { console.error(Error in checkAdminStatus:, error); // Dont show alert here, let the calling function handle UI feedback // Reset UI to non-admin state on error resetAuthUI(); return false; // Indicate failure }}// UPDATED: Force showing edit UI with class-based approachfunction forceShowEditUI() { console.log(Forcing admin/auth state for UI); // Ensure global state flags are set isAuthenticated true; isAdmin true; window.isAuthenticated true; window.isAdmin true; // Add admin/auth classes to body - CSS will handle button visibility document.body.classList.add(hydra-authenticated, hydra-admin); // Store admin status in localStorage localStorage.setItem(hydra_is_admin, true); // Show edit controls container ONLY if already in edit mode if (window.isInEditMode && window.isInEditMode()) { const editControls document.querySelector(.edit-controls); if (editControls) { editControls.classList.add(visible); editControls.style.display flex; // Make sure container is flex } // Ensure controls inside items are visible if already editing document.querySelectorAll(.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle).forEach(el > { if (el.closest(.sidebar.editing) || el.closest(.page-container.editing)) { el.style.display flex; } }); // Show logout button in edit controls when in edit mode const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.style.display flex; logoutButton.classList.add(visible); } } else { // Ensure edit controls container is hidden if not in edit mode const editControls document.querySelector(.edit-controls); if (editControls) { editControls.classList.remove(visible); editControls.style.display none; } // Hide logout button when not in edit mode const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.style.display none; logoutButton.classList.remove(visible); } } console.log(Admin/Auth classes set. CSS should handle button visibility.);}// UPDATED: Reset auth UI with class-based approach function resetAuthUI() { document.body.classList.remove(hydra-authenticated); document.body.classList.remove(hydra-admin); const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.style.display none; logoutButton.classList.remove(visible); } const editButton document.getElementById(editButton); if (editButton) { editButton.style.display none; editButton.classList.remove(visible); } // Remove admin status from localStorage localStorage.removeItem(hydra_is_admin); localStorage.removeItem(hydra_auth_token); if (isEditing) { toggleEditMode(); // Exit edit mode if active }}// Debug function - add to your code during testingfunction debugUIClasses() { console.log(Current body classes:, document.body.className); console.log(hydra-initialized:, document.body.classList.contains(hydra-initialized)); console.log(hydra-authenticated:, document.body.classList.contains(hydra-authenticated)); console.log(hydra-admin:, document.body.classList.contains(hydra-admin)); console.log(edit-mode-active:, document.body.classList.contains(edit-mode-active));}async function logout() { console.log(Logout initiated); try { // Show loading indication (optional) showLoadingOverlay(Logging out...); // 1. Properly logout from Magic SDK if (window.magic) { try { await window.magic.user.logout(); console.log(Magic SDK logout successful); } catch (magicError) { console.error(Error during Magic logout:, magicError); // Continue with other logout steps even if Magic SDK logout fails } } // 2. Reset all authentication state variables window.isAuthenticated false; window.isAdmin false; window.userMetadata null; window.didToken null; // 3. Clear all authentication data from localStorage localStorage.removeItem(hydra_is_admin); localStorage.removeItem(hydra_auth_token); localStorage.removeItem(hydra_auth_email); // 4. Reset all UI state - remove authentication classes from body document.body.classList.remove(hydra-authenticated); document.body.classList.remove(hydra-admin); // 5. If in edit mode, exit it first if (document.body.classList.contains(edit-mode-active)) { console.log(Exiting edit mode before logout); // If toggleEditMode exists, call it to exit edit mode if (typeof toggleEditMode function) { try { toggleEditMode(); } catch (editError) { console.error(Error exiting edit mode:, editError); } } // Ensure edit mode class is removed regardless document.body.classList.remove(edit-mode-active); } // 6. Ensure all edit UI elements are hidden // Hide edit and logout buttons const editButton document.getElementById(editButton); if (editButton) { editButton.classList.remove(visible); editButton.style.display none; } const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.classList.remove(visible); logoutButton.style.display none; } // Hide edit controls const editControls document.querySelector(.edit-controls); if (editControls) { editControls.style.display none; editControls.classList.remove(visible); } // Hide sidebar header if it should be hidden when logged out const sidebarHeader document.querySelector(.sidebar-header); if (sidebarHeader) { sidebarHeader.style.display none; } // Hide all element controls document.querySelectorAll(.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle).forEach(el > { el.style.display none; }); // 7. Reset editor state if applicable if (window.Parameters) { window.Parameters.isInEditor false; } // Hide any editing forms that might be open const forms document.querySelectorAll(.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm); forms.forEach(form > { form.style.display none; if (form.classList.contains(visible)) { form.classList.remove(visible); } }); // Remove editing class from sidebar const sidebar document.querySelector(.sidebar); if (sidebar) { sidebar.classList.remove(editing); } // 8. Clear any page-specific state if (window.PageManager && typeof window.PageManager.clearCurrentPage function) { window.PageManager.clearCurrentPage(); } // 9. Notify any components that need to know about logout document.dispatchEvent(new CustomEvent(user-logout, { detail: { timestamp: Date.now() } })); // 10. Show success message hideLoadingOverlay(); showSuccessMessage(Successfully logged out); console.log(Logout complete); // 11. Optional: Refresh the page after a short delay for a clean state // Uncomment the following lines if you want the page to refresh after logout /* setTimeout(() > { window.location.reload(); }, 1500); */ } catch (error) { console.error(Error during logout process:, error); hideLoadingOverlay(); showErrorMessage(Error during logout. Please refresh the page.); }}// Ensure the logout button is properly connected to the logout functiondocument.addEventListener(DOMContentLoaded, function() { const logoutButton document.getElementById(logoutButton); if (logoutButton) { // Remove any existing event listeners to avoid duplicates const newButton logoutButton.cloneNode(true); logoutButton.parentNode.replaceChild(newButton, logoutButton); // Add fresh event listener newButton.addEventListener(click, function(e) { e.preventDefault(); e.stopPropagation(); logout(); }); }});function stopAutoAdvanceTimer() { if (window.currentAutoAdvanceTimerId) { clearTimeout(window.currentAutoAdvanceTimerId); window.currentAutoAdvanceTimerId null; console.log(Auto-advance timer stopped.); }}window.stopAutoAdvanceTimer stopAutoAdvanceTimer;function toggleEditMode() { // Prevent multiple simultaneous calls if (window._editModeToggleInProgress) { console.log(Edit mode toggle already in progress, skipping); return; } window._editModeToggleInProgress true; console.log(toggleEditMode called. Current window.isEditing:, window.isEditing, Current window.isInEditMode():, window.isInEditMode ? window.isInEditMode() : undefined); stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance // Authentication and Authorization Checks if (!window.isAuthenticated && !(localStorage.getItem(hydra_is_admin) true)) { alert(You must be logged in to edit); window._editModeToggleInProgress false; return; } if (localStorage.getItem(hydra_is_admin) true && !window.isAdmin) { window.isAdmin true; // Synchronize global flag } if (!window.isAdmin) { alert(You must be an admin to edit); window._editModeToggleInProgress false; return; } const currentEditState window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing ! undefined ? window.isEditing : false); const newEditState !currentEditState; console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`); // Apply body class for edit mode styling if (newEditState) { console.log(Entering edit mode - setting body class and theme); document.body.setAttribute(data-theme, pico); // Optional: if Pico theme is used for edit mode document.body.classList.add(edit-mode-active); } else { console.log(Exiting edit mode - removing body class and theme); document.body.removeAttribute(data-theme); document.body.classList.remove(edit-mode-active); } // Update the global state and dispatch event if (typeof window.updateGlobalEditState function) { window.updateGlobalEditState(newEditState); } else { window.isEditing newEditState; // Fallback document.dispatchEvent(new CustomEvent(edit-mode-changed, { detail: { editing: newEditState } })); } const sidebarEditControls document.querySelector(.edit-controls); if (newEditState) { // Entering edit mode if (sidebarEditControls) { sidebarEditControls.style.display flex; sidebarEditControls.classList.add(visible); } // Show logout button in edit controls when entering edit mode const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.style.display flex; logoutButton.classList.add(visible); } // Always render the tree-like menu in the sidebar for editing if (typeof window.renderGalleries function) { console.log(toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.); window.renderGalleries(); // This will show all items, including those with visible:false } setTimeout(() > { if (typeof window.initializeNestedSortables function) { console.log(Initializing nested sortables for edit mode); window.initializeNestedSortables(); } if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables function) { window.SidebarManager._reinitializeNestedSortables(); } }, 150); // If the currently active item was invisible, reload it now that we are in edit mode. if(window.galleries && window.activeGalleryId){ const currentGalleryItem window.galleries.find(g > g.id window.activeGalleryId); if (currentGalleryItem && currentGalleryItem.visible false) { console.log(toggleEditMode (Edit): Active item was invisible, reloading it.); if (currentGalleryItem.isPage && window.loadPage) { window.loadPage(window.activeGalleryId); } else if(window.loadGallery) { window.loadGallery(window.activeGalleryId); } } } if(typeof window.showStyleEditor function) window.showStyleEditor(false); // Ensure style editor is hidden initially } else { // Exiting edit mode (going to Live view) if (sidebarEditControls) { sidebarEditControls.style.display none; sidebarEditControls.classList.remove(visible); } // Hide logout button when exiting edit mode const logoutButton document.getElementById(logoutButton); if (logoutButton) { logoutButton.style.display none; logoutButton.classList.remove(visible); } if(typeof window.closeAllForms function) window.closeAllForms(); if (typeof window.destroyNestedSortables function) { window.destroyNestedSortables(); } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) { window.sortableInstances.forEach(instance > instance.destroy()); window.sortableInstances ; } // Re-render the menu based on the actual view mode layout. // These functions should internally handle not showing items with visible:false. let currentLayout sidebar; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; } if (currentLayout horizontal && typeof window.renderHorizontalMenu function) { window.renderHorizontalMenu(); } else if (typeof window.renderGalleries function) { window.renderGalleries(); } // Handle the currently active gallery/page. if (window.galleries && window.activeGalleryId) { const currentGalleryItem window.galleries.find(g > g.id window.activeGalleryId); if (currentGalleryItem) { // The content for currentGalleryItem should remain loaded, // even if currentGalleryItem.visible is false. // The menu rendering above will hide it from the list if its invisible. // Apply page-specific menu hiding rules. const bodyEl document.body; // When exiting edit mode, isInEditMode() will effectively be false for this check. if (currentGalleryItem.hideMenuOnPage) { bodyEl.classList.add(menu-hidden-on-page); } else { bodyEl.classList.remove(menu-hidden-on-page); } // Ensure active states in the (potentially now filtered) menu are updated. // If the active item is invisible, it wont be marked active in the menu, which is fine. if(typeof window.updateActiveStates function) window.updateActiveStates(); if(typeof window.updateActiveStatesHorizontal function) window.updateActiveStatesHorizontal(); } else { // activeGalleryId points to a non-existent item, so clear it and the content. console.log(toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.); window.activeGalleryId null; const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) galleryContainer.innerHTML ; if(typeof window.updateActiveStates function) window.updateActiveStates(); if(typeof window.updateActiveStatesHorizontal function) window.updateActiveStatesHorizontal(); } } // If, after all that, no content is effectively active (e.g., initial load to root, // or activeGalleryId became null because the item was deleted) // then try to load a default page (home page or first visible gallery). const galleryContainer document.querySelector(.gallery-container); // Check if activeGalleryId is null OR if its set but the container is empty (e.g. item was deleted) const noActiveContent window.activeGalleryId null || (galleryContainer && galleryContainer.innerHTML.trim() ); if (noActiveContent && window.galleries) { console.log(toggleEditMode (Live): No active content, attempting to load default page.); const homePage window.galleries.find(g > g.isHomePage true); let loadedDefault false; if (homePage) { // Load home page regardless of its visibility status for this default loading logic console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`); if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault true; } else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault true; } } if (!loadedDefault) { // If no home page, load the first *visible* gallery/page in live view. const firstVisibleGallery window.galleries.find(g > g.visible ! false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`); if (firstVisibleGallery.isPage && window.loadPage) { window.loadPage(firstVisibleGallery.id); } else if(window.loadGallery) { window.loadGallery(firstVisibleGallery.id); } } else { console.log(toggleEditMode (Live): No home page and no visible items to load.); } } } } window._editModeToggleInProgress false;}/** * Improved toggleSidebarElementForm - Closes other forms first */function toggleSidebarElementForm() { const formId sidebarElementForm; const form document.getElementById(formId); if (!form) return; // If this form is already open, just close everything if (window.currentOpenForm formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Show the form form.style.display block; form.classList.add(visible); // Set this as the current open form window.currentOpenForm formId;}// New function to add a sidebar elementfunction addSidebarElement() {const typeInput document.querySelector(inputnamesidebarElementType:checked);if (!typeInput) {alert(Please select an element type);return;}const type typeInput.value;// Use the SidebarManager to add the elementif (window.SidebarManager) {const newElementId window.SidebarManager.addElement(type);toggleSidebarElementForm();if (newElementId) { // Save the changes to KV store saveGalleries().then(() > { console.log(`New ${type} element added successfully with ID: ${newElementId}`); }).catch(error > { console.error(Error saving new element:, error); });}} else {alert(SidebarManager not available);}}// Global variable to store metadatawindow.siteMetadata { title: , description: , googleAnalytics: , noIndex: false, favicon: , contentLanguage: en, // Default to English copyrightEnabled: false, copyrightText: };// Global variable to store uploaded favicon URL temporarilywindow.uploadedFaviconUrl ;function updateMetadataCopyrightFieldState() { const toggle document.getElementById(metadataCopyrightEnabled); const textInput document.getElementById(metadataCopyrightText); const isEnabled !!(toggle && toggle.checked); if (textInput) { textInput.disabled !isEnabled; } // Intentionally leave the surrounding form group in place; the disabled attribute on the input // provides the visual cue in PicoCSS.}/** * Improved toggleMetadataEditor - Closes other forms first */function toggleMetadataEditor() { const formId metadataEditor; const editor document.getElementById(formId); if (!editor) return; // If this form is already open, just close everything if (window.currentOpenForm formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Load current values before showing loadMetadataValues(); editor.style.display block; // Set this as the current open form window.currentOpenForm formId;}// Load metadata values into the formfunction loadMetadataValues() { // Get the current values from the global object const metadata window.siteMetadata || {}; console.log(loadMetadataValues: Loading metadata:, JSON.stringify(metadata)); console.log(loadMetadataValues: Google Analytics from metadata:, metadata.googleAnalytics); console.warn( FAVICON LOAD DEBUG ); console.warn(window.siteMetadata:, JSON.stringify(window.siteMetadata)); console.warn(metadata object:, JSON.stringify(metadata)); console.warn(metadata.favicon value:, metadata.favicon); console.warn(metadata.favicon type:, typeof metadata.favicon); // Populate form fields document.getElementById(metaTitle).value metadata.title || ; document.getElementById(metaDescription).value metadata.description || ; document.getElementById(googleAnalytics).value metadata.googleAnalytics || ; document.getElementById(noIndexToggle).checked metadata.noIndex || false; const copyrightToggle document.getElementById(metadataCopyrightEnabled); const copyrightInput document.getElementById(metadataCopyrightText); if (copyrightToggle) { const enabled !!metadata.copyrightEnabled; copyrightToggle.checked enabled; if (copyrightInput) { copyrightInput.value metadata.copyrightText || ; } } updateMetadataCopyrightFieldState(); // Set content language dropdown (defaults to en) const contentLanguageSelect document.getElementById(contentLanguage); if (contentLanguageSelect) { contentLanguageSelect.value metadata.contentLanguage || en; console.log(Loaded content language:, contentLanguageSelect.value); } const faviconInput document.getElementById(faviconUrl); if (faviconInput) { // Check if we have a recently uploaded favicon URL const faviconValue metadata.favicon || window.uploadedFaviconUrl || ; faviconInput.value faviconValue; console.warn(Set favicon input value to:, faviconInput.value); console.warn(Used global uploaded favicon URL:, window.uploadedFaviconUrl); } else { console.error(Favicon input element not found!); } // Show favicon preview if theres an existing favicon if (metadata.favicon) { console.warn(Showing favicon preview for:, metadata.favicon); showFaviconPreview(metadata.favicon); } else { console.warn(No favicon to preview); } console.log(loadMetadataValues: Google Analytics field value after setting:, document.getElementById(googleAnalytics).value);}// Save metadata valuesfunction saveMetadata() { // Get values from form const googleAnalyticsElement document.getElementById(googleAnalytics); console.log(Google Analytics element found:, !!googleAnalyticsElement); console.log(Google Analytics element value:, googleAnalyticsElement ? googleAnalyticsElement.value : ELEMENT NOT FOUND); const faviconInput document.getElementById(faviconUrl); const faviconValue faviconInput ? faviconInput.value : ; const contentLanguageSelect document.getElementById(contentLanguage); const contentLanguageValue contentLanguageSelect ? contentLanguageSelect.value : en; const copyrightToggle document.getElementById(metadataCopyrightEnabled); const copyrightInput document.getElementById(metadataCopyrightText); const copyrightEnabled copyrightToggle ? copyrightToggle.checked : false; const copyrightText copyrightInput ? copyrightInput.value.trim() : ; console.warn( FAVICON SAVE DEBUG ); console.warn(Favicon input element exists:, !!faviconInput); console.warn(Favicon input value:, faviconValue); console.warn(Favicon input value length:, faviconValue.length); console.warn( CONTENT LANGUAGE SAVE ); console.warn(Content language selected:, contentLanguageValue); const metadata { title: document.getElementById(metaTitle).value, description: document.getElementById(metaDescription).value, googleAnalytics: googleAnalyticsElement ? googleAnalyticsElement.value : , noIndex: document.getElementById(noIndexToggle).checked, favicon: faviconValue, contentLanguage: contentLanguageValue, // Get from dropdown copyrightEnabled, copyrightText }; console.warn(Metadata object being created:, JSON.stringify(metadata)); console.warn(Favicon in metadata object:, metadata.favicon); // Update global metadata object immediately window.siteMetadata metadata; console.log(Updated window.siteMetadata:, JSON.stringify(window.siteMetadata)); if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter function) { window.SidebarManager.updateMetadataFooter(); } // Ensure we have the latest galleries, sidebar elements, and styles // before constructing the customData object. // Synchronize galleries first if the function exists if (typeof synchronizeGalleries function) { synchronizeGalleries(); console.log(Galleries synchronized within saveMetadata); } // Retrieve current galleries (use window.galleries as the source of truth after sync) const currentGalleries window.galleries || galleries || ; console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`); // Retrieve current sidebar elements const currentSidebarElements window.SidebarManager ? window.SidebarManager.elements : ; // Retrieve current menu styles const currentMenuStyles window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {}; // Now construct customData using the retrieved current state if (window.saveGalleries) { const customData { galleries: currentGalleries, // Use the synchronized/retrieved galleries siteMetadata: metadata, // Use the metadata gathered from the form sidebarElements: currentSidebarElements, // Use the retrieved sidebar elements menuStyles: currentMenuStyles // Use the retrieved menu styles }; console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`); console.log(Metadata being saved:, JSON.stringify(metadata)); console.log(Google Analytics specifically:, metadata.googleAnalytics); console.log(Sidebar elements being saved:, customData.sidebarElements.length); console.log(Menu styles being saved:, JSON.stringify(customData.menuStyles)); // Save with our correctly constructed custom data object window.saveGalleries(customData) .then(() > { alert(Metadata saved successfully); toggleMetadataEditor(); // Hide the editor }) .catch(error > { console.error(Error saving metadata:, error); alert(Error saving metadata. Please try again.); }); } else { alert(Save function not available); }} // Function to add new entries to the style editorfunction showStyleEditor(display false) { // Check if style editor already exists let styleEditor document.getElementById(menuStyleEditor); if (!styleEditor) { // Get the edit controls element (to place the editor after it) const editControls document.querySelector(.edit-controls); if (editControls && window.MenuStyleCustomizer) { // Create a wrapper for the style editor styleEditor document.createElement(div); styleEditor.id menuStyleEditor; styleEditor.style.display none; // Insert the editor after the edit controls editControls.parentNode.insertBefore(styleEditor, editControls.nextSibling); // Create the style editor UI window.MenuStyleCustomizer.createStyleEditor(styleEditor); } } // Only show the editor if display parameter is true if (styleEditor && display) { styleEditor.style.display block; }}// New function to hide the style editorfunction hideStyleEditor() { const styleEditor document.getElementById(menuStyleEditor); if (styleEditor) { styleEditor.style.display none; }}/** * Improved toggleAddForm - Closes other forms first and ensures proper sizing */function toggleAddForm() { const formId addForm; const form document.getElementById(formId); if (!form) return; // If this form is already open, just close everything if (window.currentOpenForm formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Determine the appropriate max-height based on content // First check if were showing the page form by checking the radio button const isPageSelected document.querySelector(inputnamegalleryTypevaluepage)?.checked; // Allow extra space for the page form const maxHeight isPageSelected ? 600px : 500px; // Show the form with proper max height form.style.maxHeight maxHeight; form.style.overflow visible; // Ensure all content is visible form.classList.add(visible); // Also add inline styles to ensure visibility during animation form.style.padding 15px; // Force recalculation of max-height after UI is visible setTimeout(() > { // Get actual content height + padding const contentHeight form.scrollHeight + 30; // Add padding // Set max-height based on content with minimum of 500px form.style.maxHeight Math.max(contentHeight, 500) + px; console.log(`Setting add form max height to ${form.style.maxHeight}`); }, 50); // Populate parent options const parentSelect document.getElementById(galleryParent); if (parentSelect) { parentSelect.innerHTML option value>None (Top Level)/option>; // Add all galleries that could be parents (including submenus) galleries.forEach(gallery > { parentSelect.innerHTML + `option value${gallery.id}>${gallery.title}/option>`; }); } // Set this as the current open form window.currentOpenForm formId;}// Store the currently open form IDwindow.currentOpenForm null;/** * Close all editor forms with improved handling for add form */function closeAllForms() { // Add Form - Special handling to ensure proper animation const addForm document.getElementById(addForm); if (addForm) { addForm.classList.remove(visible); // Also force style reset after animation completes setTimeout(() > { if (!addForm.classList.contains(visible)) { addForm.style.maxHeight 0; addForm.style.overflow hidden; addForm.style.padding 0 15px; } }, 350); // Set slightly longer than CSS transition } // Style Editor const styleEditor document.getElementById(menuStyleEditor); if (styleEditor) { styleEditor.style.display none; } // Metadata Editor const metadataEditor document.getElementById(metadataEditor); if (metadataEditor) { metadataEditor.style.display none; } // Sidebar Element Form const sidebarElementForm document.getElementById(sidebarElementForm); if (sidebarElementForm) { sidebarElementForm.style.display none; sidebarElementForm.classList.remove(visible); } // Import Classic Form const importClassicForm document.getElementById(importClassicForm); if (importClassicForm) { importClassicForm.style.display none; importClassicForm.classList.remove(visible); } // Clear the current open form window.currentOpenForm null;}function downloadJson() { const data JSON.stringify(galleries, null, 2); const blob new Blob(data, { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download galleries.json; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);}let galleriesBackup ;/** * Improved version of initializeNestedSortables with a better nesting approach */function initializeNestedSortables() { console.log(Initializing nested sortables with improved nesting logic...); // Create a backup of galleries try { galleriesBackup JSON.parse(JSON.stringify(galleries)); } catch (error) { console.error(Error creating galleries backup:, error); galleriesBackup ; } // Find all nested sortable lists const nestedSortables document.querySelectorAll(.nested-sortable); if (nestedSortables.length 0) { console.warn(No nested sortable elements found! Check your HTML structure.); return; } // Helper: compute depth based on ancestor nested lists function computeDepth(liEl) { let depth 0; let node liEl; while (node) { const parentOl node.closest(ol.nested-sortable); if (!parentOl) break; const parentLi parentOl.closest(li); if (parentLi) { depth + 1; node parentLi; } else { break; } } // Top-level depth should be 0 return Math.max(depth - 1, 0); } // Helper: apply indentation styling to an li and its subtree function applyIndentationFromDepth(rootLi) { if (!rootLi) return; const applyToItem (li) > { const depth computeDepth(li); // Update data-nesting-level attribute li.setAttribute(data-nesting-level, depth); // Apply padding-left style const content li.querySelector(.gallery-item-content); if (content) { content.style.paddingLeft (depth * 16) + px; } }; applyToItem(rootLi); rootLi.querySelectorAll(li).forEach(applyToItem); } // Helper: ensure parent has proper state and nested list function ensureParentChildState(parentLi) { if (!parentLi) return null; parentLi.classList.add(has-children); let nestedList parentLi.querySelector(ol.nested-sortable); if (!nestedList) { nestedList document.createElement(ol); nestedList.className nested-sortable; parentLi.appendChild(nestedList); } return nestedList; } // Helper: clean parent if it has no children function cleanupParentIfEmpty(parentLi) { if (!parentLi) return; const nestedList parentLi.querySelector(ol.nested-sortable); if (nestedList && nestedList.children.length 0) { nestedList.remove(); parentLi.classList.remove(has-children); } } // Initialize each sortable with improved nesting settings let sortableInstances ; for (let i 0; i nestedSortables.length; i++) { try { const container nestedSortablesi; // Track where the placeholder indicates the item should be placed // This helps us detect when SortableJS incorrectly auto-nests based on mouse position let intendedPlacementContainer null; let intendedRelatedElement null; // Track last move to prevent oscillation let lastMoveTime 0; let lastMoveRelatedId null; const MOVE_DEBOUNCE_MS 50; // Minimum time between moves (in milliseconds) // Track if were dragging the last item in a folder (to prevent boundary oscillation) let isLastItemInFolder false; let draggedItemOriginalContainer null; // Track container changes to prevent rapid switching (hysteresis) let firstPositionFixLoopActive false; // Prevent multiple fix loops let lastContainerDecision null; // inside or outside let lastContainerDecisionTime 0; const CONTAINER_SWITCH_DEBOUNCE_MS 150; // Longer debounce for container switches const sortable new Sortable(container, { group: nested, animation: 150, // Reduced animation during drag to reduce lag easing: cubic-bezier(0.25, 0.46, 0.45, 0.94), // Smooth easing function fallbackOnBody: false, // Disable forced fallback on desktop for better performance forceFallback: false, // Use native drag on desktop // CRITICAL: These settings make nesting much easier // Higher swap threshold makes it easier to drag over items swapThreshold: 0.65, // Increased to reduce oscillation within same folder // IMPROVED NESTING SETTINGS // Used when determining if an item should be nested invertSwap: false, // Disable to reduce oscillation direction: vertical, // Lock direction to vertical to reduce oscillation // Use handle for dragging handle: .nested-sortable-handle, // Distinguish between click and drag delay: 120, delayOnTouchOnly: true, // Empty container settings emptyInsertThreshold: 10, // Add custom ghost class ghostClass: sortable-ghost, chosenClass: sortable-chosen, dragClass: sortable-drag, // Make sure we listen to external move events dragoverBubble: true, // Store original dimensions to prevent layout shifts preventOnFilter: false, // Better drag preview stability scroll: true, scrollSensitivity: 100, scrollSpeed: 20, // Reduce oscillation by making placeholder movement less sensitive forceFallback: false, // Use native drag for better performance fallbackTolerance: 0, // No tolerance - use exact position /** * NESTING LOGIC: Prevent auto-nesting based on hover * Instead, rely on SortableJSs placeholder position which is accurate * Well verify and correct placement in onEnd based on actual DOM position */ onMove: function(evt, originalEvent) { const dragged evt.dragged; const related evt.related; if (!dragged || !related) return true; // Safety check: Ensure items have data-id attributes // This can be missing if item was just added and DOM not fully updated if (!dragged.dataset.id || !related.dataset.id) { console.warn(⚠️ Drag Item missing data-id attribute, aborting drag, { draggedHasId: !!dragged.dataset.id, relatedHasId: !!related.dataset.id }); return false; } // Get the IDs to check for circular references const draggedId parseInt(dragged.dataset.id); const relatedId parseInt(related.dataset.id); // Dont allow dropping inside itself or its descendants if (draggedId relatedId || hasAncestor(relatedId, draggedId)) { return false; } const currentTime Date.now(); const rootContainer document.querySelector(#galleryTree > ol.nested-sortable); const draggedParent draggedItemOriginalContainer || dragged.parentElement; // Determine if were moving inside or outside a folder // IMPORTANT: Check if related element IS the folder item itself (parent li) // When targeting first position, SortableJS might use the folder item as related const isRelatedFolderItem related.classList && related.classList.contains(has-children); let actualRelatedParent related.parentElement; // If related IS the folder item, we need to get its nested sortable container if (isRelatedFolderItem) { const nestedSortable related.querySelector(ol.nested-sortable); if (nestedSortable) { actualRelatedParent nestedSortable; } } const relatedParent actualRelatedParent; const isMovingInsideFolder relatedParent && relatedParent.classList.contains(nested-sortable) && relatedParent ! rootContainer; const isMovingOutsideFolder !isRelatedFolderItem && relatedParent rootContainer; const currentDecision isMovingInsideFolder ? inside : (isMovingOutsideFolder ? outside : null); // Determine if were moving within the same folder - this is the key check // If related IS the folder item, were still moving within the same folder let isMovingWithinSameFolder (isMovingInsideFolder && draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer && relatedParent draggedParent) || // OR: if related is the folder item itself and were dragging from that folder (isRelatedFolderItem && draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer && related.querySelector(ol.nested-sortable) draggedParent); // Special case: If related is the folder item, were definitely trying to move within that folder // This happens when targeting first position - SortableJS uses the folder item as related if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer) { const folderNestedSortable related.querySelector(ol.nested-sortable); if (folderNestedSortable draggedParent) { // Were dragging within the folder, and related is the folder item // This means were targeting first position - FORCE it to be treated as same-folder move // FORCE same-folder detection - override any previous calculation isMovingWithinSameFolder true; } } // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens // (Dont log every single move - too noisy) if (draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer) { const folderItems Array.from(draggedParent.children); const draggedIndex folderItems.indexOf(dragged); const relatedIndex folderItems.indexOf(related); // Get placeholder element to see where it actually is const placeholder document.querySelector(.sortable-ghost); const placeholderParent placeholder ? placeholder.parentElement : null; const placeholderIndex placeholderParent && placeholderParent.classList.contains(nested-sortable) ? Array.from(placeholderParent.children).indexOf(placeholder) : -1; const folderItem draggedParent.closest(li.has-children); const folderItemId folderItem ? folderItem.dataset.id : null; const firstItemInFolder folderItems.length > 0 ? folderItems0 : null; const firstItemInFolderId firstItemInFolder ? firstItemInFolder.dataset.id : null; // Check if were trying to target first position // related could be the first item OR the folder item itself (when SortableJS uses folder as related) const isFirstPosition folderItems.length > 0 && related firstItemInFolder; const isRelatedFolderItemCheck related folderItem; const isTargetingFirstViaFolderItem isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean // When targeting first via folder item, placeholder should be at index 0 // But were seeing it at index 1 - this is the issue! const shouldBeAtFirstPosition isTargetingFirstViaFolderItem && placeholderIndex 1 && draggedIndex > 0; // Check if placeholder is outside the folder when it should be inside const placeholderIsOutside placeholderParent rootContainer || (placeholderParent && !placeholderParent.classList.contains(nested-sortable)); const shouldBeInside draggedIndex > 0; // Were dragging from inside // Only log when theres a potential issue or interesting state change const placeholderMismatch placeholderIndex ! relatedIndex && placeholderIndex > 0 && relatedIndex > 0; const jumpingOut !isMovingWithinSameFolder && draggedIndex > 0 && relatedIndex 0; const jumpingOutToRoot placeholderIsOutside && shouldBeInside; // First position issue: trying to target first but: // - placeholder is outside OR // - placeholder is at index 1 instead of 0 (when targeting first via folder item) const firstPositionIssue (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition); // ALWAYS log when targeting first position via folder item (this is the key case were debugging) // OR when placeholder jumps out when it should stay in // OR when placeholder should be at first position but isnt const shouldLog placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition; if (shouldLog) { console.error(🔍 Placeholder Position, { draggedId, draggedIndex, relatedId, relatedIndex, placeholderIndex, relatedElementTag: related.tagName, isRelatedFolderItem: isRelatedFolderItemCheck, isTargetingFirstViaFolderItem, isMovingWithinSameFolder, placeholderMismatch, jumpingOut, jumpingOutToRoot, isFirstPosition, placeholderIsOutside, placeholderParentType: placeholderParent rootContainer ? ROOT : (placeholderParent && placeholderParent.classList.contains(nested-sortable) ? FOLDER : OTHER), folderItemId, firstItemInFolderId, firstPositionIssue, shouldBeAtFirstPosition, expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex }); } } // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic // This allows smooth reordering just like items not in folders if (isMovingWithinSameFolder) { // Simple debounce only - same as root level items const timeSinceLastMove currentTime - lastMoveTime; // Only prevent very rapid oscillation to same element if (timeSinceLastMove MOVE_DEBOUNCE_MS * 0.5 && relatedId lastMoveRelatedId) { return false; } // Allow all moves within same folder - update tracking and continue // SPECIAL FIX: When targeting first position via folder item, fix placeholder position // SortableJS places it at index 1 when related is the folder item, but we want it at index 0 // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer) { const folderItems Array.from(draggedParent.children); const firstItem folderItems.length > 0 ? folderItems0 : null; // Only apply fix if were actually near first position (draggedIndex is small, indicating were moving up) // This prevents the fix from being too aggressive when moving to other positions const isNearFirstPosition draggedIndex 2; // Allow fix when within first 3 positions if (firstItem && firstItem ! dragged && isNearFirstPosition) { // Fix immediately (synchronously) first - dont wait for next frame const placeholder document.querySelector(.sortable-ghost); if (placeholder && placeholder.parentElement draggedParent) { const currentIndex Array.from(draggedParent.children).indexOf(placeholder); // Only fix if placeholder is at index 1 when were targeting first position // Dont force it if its at index 0 or beyond index 2 (user might be moving to position 2 or 3) if (currentIndex 1 && draggedIndex 1) { draggedParent.insertBefore(placeholder, firstItem); } } } else { // Not near first position - stop any active fix loop firstPositionFixLoopActive false; } } else { // Not targeting first position anymore - reset flag firstPositionFixLoopActive false; } lastMoveTime currentTime; lastMoveRelatedId relatedId; // Skip all the boundary case logic below - just continue to allow the move // Fall through to update intendedPlacementContainer and return true } else { // Only apply complex boundary logic for moves BETWEEN folders or to/from root // Hysteresis: Prevent rapid switching between inside/outside folder const targetFolderItems isMovingInsideFolder ? Array.from(relatedParent.children) : ; const isFirstPositionInFolder isMovingInsideFolder && targetFolderItems.length > 0 && related targetFolderItems0; if (currentDecision && lastContainerDecision && currentDecision ! lastContainerDecision) { const timeSinceContainerSwitch currentTime - lastContainerDecisionTime; // Make first position easier to target from outside if (isFirstPositionInFolder) { // Moving to first position from outside - use shorter debounce const effectiveDebounce CONTAINER_SWITCH_DEBOUNCE_MS * 0.3; if (timeSinceContainerSwitch effectiveDebounce) { // Only block if its very rapid (oscillation) if (timeSinceContainerSwitch MOVE_DEBOUNCE_MS) { return false; } } } else { // Normal container switch - apply full hysteresis if (timeSinceContainerSwitch CONTAINER_SWITCH_DEBOUNCE_MS) { return false; } } } } // Special handling for boundary cases (last item in folder, or dragging into folder) // BUT: Skip all this logic if were moving within the same folder (already handled above) if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains(nested-sortable) && draggedParent ! rootContainer) { const folderItem draggedParent.closest(li.has-children); const currentFolderItems Array.from(draggedParent.children); const isFirstPositionInCurrentFolder currentFolderItems.length > 0 && currentFolderItems0 ! dragged && (related currentFolderItems0 || (isMovingInsideFolder && relatedParent draggedParent && related currentFolderItems0)); // Case 0: This case is now handled earlier for same-folder moves // Only handle cross-folder moves here // Case 1: Dragging last item out of folder if (isLastItemInFolder && isMovingOutsideFolder) { const timeSinceLastMove currentTime - lastMoveTime; // BUT: If were trying to move to first position, dont block it // Check if the related element is the folder itself or right after it if (folderItem) { const folderNextSibling folderItem.nextElementSibling; // If were moving to position right after folder, it might be trying to go to first position // Allow it if its been a reasonable time if (folderNextSibling && related folderNextSibling) { // This is ambiguous - could be first position or outside // Require more time but not as much as pure outside move if (timeSinceLastMove MOVE_DEBOUNCE_MS * 2) { if (relatedId lastMoveRelatedId) { return false; } } } else { // Definitely moving outside - require longer debounce if (timeSinceLastMove MOVE_DEBOUNCE_MS * 2.5) { if (relatedId lastMoveRelatedId) { return false; // Reject if oscillating } } } } else { // No folder item found - apply standard debounce if (timeSinceLastMove MOVE_DEBOUNCE_MS * 2.5) { if (relatedId lastMoveRelatedId) { return false; } } } } // Case 2: Dragging item into folder from outside if (isMovingInsideFolder && relatedParent ! draggedParent) { const targetFolderItems Array.from(relatedParent.children); const isNearBottom targetFolderItems.length > 0 && (related targetFolderItemstargetFolderItems.length - 1 || targetFolderItems.indexOf(related) > targetFolderItems.length - 1); const isFirstPos targetFolderItems.length > 0 && related targetFolderItems0; // Moving to first position - make it easy if (isFirstPos) { const timeSinceLastMove currentTime - lastMoveTime; // Very short debounce for first position if (timeSinceLastMove MOVE_DEBOUNCE_MS * 0.3 && relatedId lastMoveRelatedId) { return false; } // Allow move to first position } else if (isNearBottom) { // For bottom position, still use some debounce to prevent oscillation const timeSinceLastMove currentTime - lastMoveTime; if (timeSinceLastMove MOVE_DEBOUNCE_MS * 1.5) { if (relatedId lastMoveRelatedId) { return false; } } } } // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within // This is tricky - SortableJS might detect it as outside when were actually targeting first/last if (isMovingOutsideFolder && folderItem && draggedParent draggedItemOriginalContainer) { // Were dragging from within a folder but detected as moving outside // Check if the related element is adjacent to the folder (which might indicate targeting first/last) const folderIndex Array.from(rootContainer.children).indexOf(folderItem); const relatedIndex Array.from(rootContainer.children).indexOf(related); // If related is immediately before or after folder, might be targeting first/last if (folderIndex > 0 && relatedIndex > 0) { const isAdjacentToFolder (relatedIndex folderIndex - 1) || (relatedIndex folderIndex + 1); if (isAdjacentToFolder) { // Might be targeting first/last position - be lenient const timeSinceLastMove currentTime - lastMoveTime; // Only block if its very rapid oscillation if (timeSinceLastMove MOVE_DEBOUNCE_MS * 0.4 && relatedId lastMoveRelatedId) { return false; } // Otherwise allow - user might be trying to target first/last position // Dont block based on this check } } } } // Case 3: Dragging from root into folder if (draggedParent rootContainer && isMovingInsideFolder) { const targetFolderItems Array.from(relatedParent.children); const isNearBottom targetFolderItems.length > 0 && (related targetFolderItemstargetFolderItems.length - 1 || targetFolderItems.indexOf(related) > targetFolderItems.length - 1); const isFirstPosition targetFolderItems.length > 0 && related targetFolderItems0; // Special handling for first position - make it easier to target if (isFirstPosition) { const timeSinceLastMove currentTime - lastMoveTime; // Make first position easier - only prevent very rapid oscillation if (timeSinceLastMove MOVE_DEBOUNCE_MS * 0.5) { if (relatedId lastMoveRelatedId) { return false; } } // Allow move to first position - dont block it // Continue to track but dont apply strict debouncing } else if (isNearBottom) { // Keep strict debouncing for bottom position const timeSinceLastMove currentTime - lastMoveTime; if (timeSinceLastMove MOVE_DEBOUNCE_MS * 2) { if (relatedId lastMoveRelatedId) { return false; } } } } // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position) // If were at first position and trying to move outside, require deliberate movement if (isMovingInsideFolder) { const currentFolderItems Array.from(relatedParent.children); const isFirstPos currentFolderItems.length > 0 && related currentFolderItems0; // If were moving FROM first position to outside (reverse hysteresis) if (isFirstPos && lastContainerDecision outside) { const timeSinceLastMove currentTime - lastMoveTime; // If we just moved outside, require more time before allowing back to first position // BUT: if its been a reasonable time, allow it (dont make it too hard) if (timeSinceLastMove MOVE_DEBOUNCE_MS * 0.8 && relatedId lastMoveRelatedId) { // Very rapid - might be oscillation return false; } } } // Case 5: Prevent oscillation between first and second position within folder // BUT: Only apply if NOT moving within same folder (to allow smooth reordering) if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) { const folderItems Array.from(relatedParent.children); const isFirstPos folderItems.length > 0 && related folderItems0; const isSecondPos folderItems.length > 1 && related folderItems1; // If moving from first to second (or vice versa) very rapidly, prevent oscillation if ((isFirstPos || isSecondPos) && lastMoveRelatedId) { const lastRelated document.querySelector(lidata-id + lastMoveRelatedId + ); if (lastRelated && lastRelated.parentElement relatedParent) { const lastRelatedIndex folderItems.indexOf(lastRelated); const isOscillating (isFirstPos && lastRelatedIndex 1) || (isSecondPos && lastRelatedIndex 0); if (isOscillating) { const timeSinceLastMove currentTime - lastMoveTime; // Allow moving to first position easily, but prevent rapid oscillation between first/second if (isSecondPos && timeSinceLastMove MOVE_DEBOUNCE_MS * 1.2) { return false; // Prevent jumping from first to second too easily } // But allow first position more easily if (isFirstPos && timeSinceLastMove MOVE_DEBOUNCE_MS * 0.6 && relatedId lastMoveRelatedId) { return false; // Only prevent very rapid oscillation } } } } } // Prevent rapid oscillation when dragging within same container // NOTE: For same-folder moves, we already updated tracking above, so skip this section if (!isMovingWithinSameFolder) { const timeSinceLastMove currentTime - lastMoveTime; // If moving to the same related element very quickly, reject to prevent oscillation if (timeSinceLastMove MOVE_DEBOUNCE_MS && relatedId lastMoveRelatedId) { return false; } // Update tracking variables (only for non-same-folder moves) lastMoveTime currentTime; lastMoveRelatedId relatedId; } // For same-folder moves, tracking was already updated in the earlier block // Update container decision tracking if (currentDecision) { if (currentDecision ! lastContainerDecision) { lastContainerDecisionTime currentTime; } lastContainerDecision currentDecision; } // Track where the placeholder indicates the item should be placed // The related element is next to where the placeholder will be positioned // Check if its at root level or nested level if (related) { intendedRelatedElement related; const relatedParent related.parentElement; const rootContainer document.querySelector(#galleryTree > ol.nested-sortable); // Determine if the placeholder is at root level or nested level if (relatedParent rootContainer) { // Placeholder is at root level (not nested) intendedPlacementContainer rootContainer; } else if (relatedParent && relatedParent.classList.contains(nested-sortable)) { // Placeholder is in a nested list (inside a folder) intendedPlacementContainer relatedParent; } // Update ghost element indentation to match the target position requestAnimationFrame(() > { const ghost document.querySelector(.sortable-ghost); if (ghost && intendedPlacementContainer) { const ghostContent ghost.querySelector(.gallery-item-content); if (ghostContent) { // Calculate the depth where the ghost will be placed // Use the related element to determine the depth (if it exists and is in the target container) let targetDepth 0; if (intendedPlacementContainer rootContainer) { targetDepth 0; // Root level } else if (related && related.parentElement intendedPlacementContainer) { // Use the related elements depth to determine target depth targetDepth computeDepth(related); } else { // Fallback: find first child in the container or calculate from parent folder const firstChild intendedPlacementContainer.querySelector(li); if (firstChild) { targetDepth computeDepth(firstChild); } else { const parentFolder intendedPlacementContainer.closest(li.has-children); if (parentFolder) { targetDepth computeDepth(parentFolder) + 1; } } } ghostContent.style.paddingLeft (targetDepth * 16) + px; } } }); } // Let SortableJS handle the placement based on placeholder position // Well verify and correct if needed in onEnd return true; // Allow the move }, // Capture indentation before drag starts (fires when item is chosen) onChoose: function(evt) { if (!evt.item) return; const draggedItem evt.item; const content draggedItem.querySelector(.gallery-item-content); // Store the original padding-left for the ghost if (content) { const computedStyle window.getComputedStyle(content); const originalPaddingLeft computedStyle.paddingLeft; draggedItem.dataset.originalPaddingLeft originalPaddingLeft; } }, // Visual feedback on drag start onStart: function(evt) { if (!evt.item) return; // Dont set dimensions on the dragged item - let it maintain natural size // Setting width/height can cause layout shifts and menu expansion const draggedItem evt.item; // Store original container and check if its the last item in folder draggedItemOriginalContainer draggedItem.parentElement; const rootContainer document.querySelector(#galleryTree > ol.nested-sortable); if (draggedItemOriginalContainer && draggedItemOriginalContainer.classList.contains(nested-sortable) && draggedItemOriginalContainer ! rootContainer) { const siblings Array.from(draggedItemOriginalContainer.children); isLastItemInFolder siblings.length > 0 && siblingssiblings.length - 1 draggedItem; } else { isLastItemInFolder false; } // Apply the stored padding-left to the ghost element // The ghost is created by SortableJS at this point requestAnimationFrame(() > { const ghost document.querySelector(.sortable-ghost); if (ghost) { const ghostContent ghost.querySelector(.gallery-item-content); const originalPaddingLeft draggedItem.dataset.originalPaddingLeft; if (ghostContent && originalPaddingLeft) { ghostContent.style.paddingLeft originalPaddingLeft; } } }); // Reset oscillation tracking variables lastMoveTime 0; lastMoveRelatedId null; lastContainerDecision null; lastContainerDecisionTime 0; // Create a backup try { window._dragStartElementIds ; document.querySelectorAll(.nested-sortable li).forEach(li > { if (li.dataset.id) { window._dragStartElementIds.push(li.dataset.id); } }); galleriesBackup JSON.parse(JSON.stringify(galleries)); } catch (error) { console.error(Error creating backup:, error); } // Add indicator classes document.body.classList.add(menu-item-dragging); // Add hover indicator class to submenu items document.querySelectorAll(.has-children).forEach(item > { item.classList.add(potential-parent); }); // Prevent layout recalculation during drag document.body.style.setProperty(--is-dragging, 1); }, // Cleanup on end onEnd: function(evt) { // Verify and correct placement based on placeholders intended position // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level const dragged evt.item; const previousParentLi evt.from ? evt.from.closest(li) : null; const rootContainer document.querySelector(#galleryTree > ol.nested-sortable); if (!dragged || !rootContainer) { intendedPlacementContainer null; intendedRelatedElement null; // Restore normal sizing dragged.style.width ; dragged.style.height ; dragged.style.minHeight ; document.body.style.removeProperty(--is-dragging); return; } // Get the actual parent container where SortableJS placed the item const actualParentList dragged.parentElement; // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing requestAnimationFrame(() > { dragged.style.width ; dragged.style.height ; dragged.style.minHeight ; document.body.style.removeProperty(--is-dragging); }); // Check if the intended placement (where placeholder showed) differs from actual placement // This happens when SortableJS auto-nests based on mouse position over a folder if (intendedPlacementContainer && actualParentList ! intendedPlacementContainer) { // The item ended up in a different container than the placeholder indicated // Move it to where the placeholder showed it should be (next to intendedRelatedElement) if (intendedPlacementContainer rootContainer) { // Placeholder was at root level - move item to root, positioned next to related element if (intendedRelatedElement && intendedRelatedElement.parentElement rootContainer) { // Insert before the related element (where placeholder was) rootContainer.insertBefore(dragged, intendedRelatedElement); console.log(Corrected placement: moved from folder to root level where placeholder indicated); } else if (intendedRelatedElement) { // Related element exists but not at root - find its folder and insert before it at root const folderItem intendedRelatedElement.closest(li.has-children); if (folderItem && folderItem.parentElement rootContainer) { rootContainer.insertBefore(dragged, folderItem); console.log(Corrected placement: moved to root level above folder where placeholder indicated); } else { rootContainer.appendChild(dragged); } } else { rootContainer.appendChild(dragged); } // Update indentation since its now at root applyIndentationFromDepth(dragged); } else { // Placeholder was in a nested list - move it there if (intendedRelatedElement && intendedRelatedElement.parentElement intendedPlacementContainer) { // Insert before the related element in the nested list intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement); } else { intendedPlacementContainer.appendChild(dragged); } console.log(Corrected placement: moved to nested list where placeholder indicated); // Update indentation for nested item applyIndentationFromDepth(dragged); // Ensure parent folder has proper structure const folderItem intendedPlacementContainer.closest(li.has-children); if (folderItem) { const nestedList folderItem.querySelector(ol.nested-sortable); if (nestedList && intendedPlacementContainer ! nestedList) { nestedList.appendChild(dragged); } const parentDepth computeDepth(folderItem); const nestedDepth parentDepth + 1; if (nestedList) { nestedList.className nested-sortable nested-level- + nestedDepth; } folderItem.classList.add(has-children); } } } else if (actualParentList && actualParentList.classList.contains(nested-sortable) && actualParentList ! rootContainer) { // Item is correctly in a nested list - ensure proper structure const folderItem actualParentList.closest(li.has-children); if (folderItem) { const nestedList folderItem.querySelector(ol.nested-sortable); if (nestedList && actualParentList ! nestedList) { nestedList.appendChild(dragged); } const parentDepth computeDepth(folderItem); const nestedDepth parentDepth + 1; if (nestedList) { nestedList.className nested-sortable nested-level- + nestedDepth; } folderItem.classList.add(has-children); } applyIndentationFromDepth(dragged); } else { // Item is correctly at root level applyIndentationFromDepth(dragged); } // Reset tracking variables intendedPlacementContainer null; intendedRelatedElement null; isLastItemInFolder false; draggedItemOriginalContainer null; lastContainerDecision null; lastContainerDecisionTime 0; // Apply indentation updates for ALL affected items after drag // Use requestAnimationFrame to batch DOM updates and prevent visual jumps requestAnimationFrame(() > { if (dragged) { // Update the dragged item and all its descendants applyIndentationFromDepth(dragged); // Update all items in the new container (siblings of dragged item) const newContainer dragged.parentElement; if (newContainer && newContainer.classList.contains(nested-sortable)) { const siblings Array.from(newContainer.children); siblings.forEach(sibling > { if (sibling ! dragged && sibling.tagName LI) { applyIndentationFromDepth(sibling); } }); } // Update all items in the old container (if it still exists and has items) if (evt.from && evt.from.classList.contains(nested-sortable)) { const oldSiblings Array.from(evt.from.children); oldSiblings.forEach(sibling > { if (sibling.tagName LI) { applyIndentationFromDepth(sibling); } }); } // Also update all items in the entire tree to ensure consistency // This handles edge cases where indentation might be off const allItems document.querySelectorAll(#galleryTree .nested-sortable li); allItems.forEach(li > { const depth computeDepth(li); li.setAttribute(data-nesting-level, depth); const content li.querySelector(.gallery-item-content); if (content) { content.style.paddingLeft (depth * 16) + px; } }); } }); // Clean up previous parent if it lost its last child if (previousParentLi) { cleanupParentIfEmpty(previousParentLi); } // Remove all indicator classes document.body.classList.remove(menu-item-dragging); document.querySelectorAll(.potential-parent, .will-accept-child, .will-be-nested).forEach(el > { el.classList.remove(potential-parent, will-accept-child, will-be-nested); }); // Standard cleanup for ghost elements document.querySelectorAll(.sortable-ghost, .sortable-chosen, .sortable-drag).forEach(item > { item.classList.remove(sortable-ghost, sortable-chosen, sortable-drag); }); // Check if drop was outside a valid container if (!evt.to || !evt.to.classList.contains(nested-sortable)) { console.warn(Item was dropped outside a valid container - reverting); restoreFromBackup(); return; } // Verify no elements were lost try { // Check DOM elements const endElementIds ; document.querySelectorAll(.nested-sortable li).forEach(li > { if (li.dataset.id) { endElementIds.push(li.dataset.id); } }); if (window._dragStartElementIds && window._dragStartElementIds.length > 0) { const startCount window._dragStartElementIds.length; const endCount endElementIds.length; if (endCount startCount) { console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`); restoreFromBackup(); return; } } // Update the data structure updateGalleryStructure(); // Verify data model const originalCount countGalleries(galleriesBackup); const newCount countGalleries(galleries); if (newCount originalCount) { console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`); restoreFromBackup(); return; } // Save changes if (window.saveGalleries) { window.saveGalleries().then(() > { console.log(Gallery structure saved successfully after drag); }).catch(error > { console.error(Error saving gallery structure after drag:, error); }); } } catch (error) { console.error(Error updating gallery structure:, error); restoreFromBackup(); } } }); sortableInstances.push(sortable); } catch (error) { console.error(`Error initializing sortable for container ${i}:`, error); } } return sortableInstances;}// NEW HELPER FUNCTION: Gets the nesting level of an elementfunction getElementNestingLevel(element) { let level 0; let current element; // Count how many nested-sortable parents this element has while (current && current.parentElement) { if (current.parentElement.classList && current.parentElement.classList.contains(nested-sortable)) { level++; } current current.parentElement; } return level;}// NEW HELPER FUNCTION: Determines if an item would become nestedfunction wouldBeNestedItem(draggedEl, relatedEl, event) { if (!relatedEl || !draggedEl) return false; // Get mouse position const mouseX event.clientX; // Get the bounding rect of the related element const rect relatedEl.getBoundingClientRect(); // Determine the nesting threshold - how far from the left edge // the mouse needs to be to consider it a nesting operation const nestingThreshold 25; // pixels from left edge // Check if mouse is within the nesting threshold from the left edge const distanceFromLeft mouseX - rect.left; // Check if we should nest (mouse is within threshold of left edge) const wouldNest distanceFromLeft nestingThreshold; // Log the calculation for debugging console.log(`Nesting check: distance${distanceFromLeft}px, threshold${nestingThreshold}px, would nest${wouldNest}`); return wouldNest;}/** * Helper function to find gallery by ID in a tree structure * @param {Array} galleries - The gallery array to search * @param {number} id - The ID to find * @returns {Object|null} - The found gallery or null */function findGalleryById(galleries, id) { if (!Array.isArray(galleries)) return null; // First check at the current level const directMatch galleries.find(g > g.id id); if (directMatch) return directMatch; // Then check in children for (const gallery of galleries) { if (gallery.children && gallery.children.length > 0) { const childMatch findGalleryById(gallery.children, id); if (childMatch) return childMatch; } } return null;}/** * Get all gallery IDs from a tree structure * @param {Array} galleries - Array of galleries * @returns {Array} - Flat array of all IDs */function getAllGalleryIds(galleries) { if (!Array.isArray(galleries)) return ; let ids ; galleries.forEach(gallery > { if (gallery && gallery.id) { ids.push(gallery.id); } // Add child IDs if any if (gallery.children && Array.isArray(gallery.children)) { ids ids.concat(getAllGalleryIds(gallery.children)); } }); return ids;}/** * Enhanced count function that handles edge cases * @param {Array} galleryArray - The array to count * @returns {number} - The total count of galleries */function countGalleries(galleryArray) { if (!Array.isArray(galleryArray)) return 0; return galleryArray.reduce((count, gallery) > { if (!gallery) return count; // Skip null/undefined items // Count this gallery let total 1; // If it has children, count them too (recursively) if (gallery.children && Array.isArray(gallery.children)) { total + countGalleries(gallery.children); } return count + total; }, 0);}/** * Enhanced backup restoration function with better debugging and notification */function restoreFromBackup() { console.warn(Restoring galleries from backup due to invalid operation); // Verify we have a valid backup if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length 0) { console.error(No valid backup available for restoration); // Create a backup from the current DOM as a last resort try { console.log(Attempting to rebuild from DOM structure...); rebuildGalleriesFromDOM(); return; } catch (rebuildError) { console.error(Failed to rebuild from DOM:, rebuildError); // Continue with normal user notification } } else { // Restore from backup galleries JSON.parse(JSON.stringify(galleriesBackup)); } // Re-render the tree with the restored data renderGalleries(); // Force reinitialize nested sortables after restoration setTimeout(() > { initializeNestedSortables(); }, 300);}/** * Emergency recovery function that tries to rebuild galleries from DOM * This is a last resort when backup restoration fails */function rebuildGalleriesFromDOM() { console.log(EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure); // Create a new galleries array const rebuiltGalleries ; // This will track items weve already processed const processedIds new Set(); // Function to process a container and its children function processContainer(container, parentId null) { // Get all immediate list items const items container.querySelectorAll(:scope > li); items.forEach(item > { // Get item ID from data attribute const id parseInt(item.dataset.id); if (isNaN(id)) return; // Skip if no valid ID // Skip already processed items to avoid duplicates if (processedIds.has(id)) return; processedIds.add(id); // Try to find the original gallery data let galleryData null; // First check current galleries array const existingGallery findGalleryById(galleries, id); if (existingGallery) { galleryData { ...existingGallery }; delete galleryData.children; // Well rebuild the hierarchy } // If not found, check if we have a backup else if (galleriesBackup && Array.isArray(galleriesBackup)) { const backupGallery findGalleryById(galleriesBackup, id); if (backupGallery) { galleryData { ...backupGallery }; delete galleryData.children; // Well rebuild the hierarchy } } // If we still dont have data, create minimal placeholder if (!galleryData) { // Try to extract title from DOM const titleElement item.querySelector(.menu-item); const title titleElement ? titleElement.textContent.trim() : `Item ${id}`; galleryData { id: id, title: title, visible: !item.classList.contains(hidden-gallery) }; } // Update parent ID to match DOM structure galleryData.parentId parentId; // Add to rebuilt galleries rebuiltGalleries.push(galleryData); // Process children if any const childContainer item.querySelector(ol.nested-sortable); if (childContainer) { processContainer(childContainer, id); } }); } // Start with root container const rootContainer document.querySelector(#galleryTree > ol.nested-sortable); if (rootContainer) { processContainer(rootContainer); // Replace global galleries array with our rebuilt one galleries rebuiltGalleries; // Re-render with the rebuilt data renderGalleries(); console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`); return true; } else { console.error(RECOVERY FAILED: Root container not found); return false; }}// Make sure these functions are available to the global scopewindow.initializeNestedSortables initializeNestedSortables;window.restoreFromBackup restoreFromBackup;window.getElementNestingLevel getElementNestingLevel;window.wouldBeNestedItem wouldBeNestedItem;function hasDescendant(itemId, possibleDescendantId) {// Check if possibleDescendantId is a descendant of itemIdconst children galleries.filter(g > g.parentId itemId);if (children.some(child > child.id possibleDescendantId)) {return true;}return children.some(child > hasDescendant(child.id, possibleDescendantId));}function updateGalleryStructure() {// Create a new array to hold the updated gallery structureconst newGalleries ;// Function to recursively process nested listsfunction processNestedList(container, parentId null) {if (!container || !container.children) { console.warn(Invalid container in processNestedList, container); return;}const items container.children;Array.from(items).forEach(item > { if (item.tagName ! LI) return; // Skip non-list items const id parseInt(item.dataset.id); if (isNaN(id)) { console.warn(Invalid ID in list item:, item); return; } const gallery galleries.find(g > g.id id); if (gallery) { // Create a new gallery object without children const newGallery { ...gallery }; if (newGallery.children) delete newGallery.children; // Update parent ID newGallery.parentId parentId; // Add to new galleries array newGalleries.push(newGallery); // Process any nested OL with class nested-sortable within this item const nestedList item.querySelector(ol.nested-sortable); if (nestedList) { processNestedList(nestedList, id); } } else { console.warn(`Gallery with ID ${id} not found in galleries array`); }});}// Start processing from the root listconst rootList document.querySelector(#galleryTree > ol.nested-sortable);if (rootList) {try { processNestedList(rootList); // Verify that we havent lost any galleries if (newGalleries.length galleries.length) { console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`); // Find missing galleries const originalIds galleries.map(g > g.id); const newIds newGalleries.map(g > g.id); const missingIds originalIds.filter(id > !newIds.includes(id)); if (missingIds.length > 0) { console.error(Missing gallery IDs:, missingIds); // Add missing galleries to the new structure missingIds.forEach(id > { const missingGallery galleries.find(g > g.id id); if (missingGallery) { // Add it as a top-level item const newGallery { ...missingGallery, parentId: null }; if (newGallery.children) delete newGallery.children; newGalleries.push(newGallery); console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`); } }); } } // Update the galleries array with the new structure galleries newGalleries; // Save the updated structure saveGalleries();} catch (error) { console.error(Error in updateGalleryStructure:, error); throw error; // Re-throw to trigger the backup restore}} else {console.warn(Root list not found);throw new Error(Root list not found); // Throw error to trigger backup restore}}function toggleSubmenu(id, event) {// Ensure the event doesnt interfere with drag operationsevent.stopPropagation();const listItem document.querySelector(`lidata-id${id}`);if (listItem) {// Toggle the expanded classlistItem.classList.toggle(expanded);// Find the submenu toggle icon and rotate itconst toggleIcon listItem.querySelector(.toggle-icon);if (toggleIcon) { toggleIcon.classList.toggle(rotated);}}}/** * Improved slugify function with robust character handling * @param {string} text - The text to convert to a slug * @returns {string} A URL-friendly slug */function slugify(text) { // Guard against null or empty input if (!text || text.trim() ) { return page- + Date.now(); } // First convert to lowercase let slug text.toLowerCase(); // Replace spaces with hyphens slug slug.split( ).join(-); // Filter out unwanted characters (keep only a-z, 0-9, and hyphens) let filtered ; for (let i 0; i slug.length; i++) { const char slug.charAt(i); if ((char > a && char z) || (char > 0 && char 9) || char -) { filtered + char; } } slug filtered; /* // Clean up multiple hyphens while (slug.indexOf(--) ! -1) { slug slug.replace(--, -); } */ // Remove leading and trailing hyphens if (slug.startsWith(-)) { slug slug.substring(1); } if (slug.endsWith(-)) { slug slug.substring(0, slug.length - 1); } // If slug is empty after processing, use a default if (!slug || slug -) { return page- + Date.now(); } return slug;}// Update the addPage function to use our new robust slugify function/** * Adds a new page to the galleries array * @returns {number|null} The new page ID or null if error */function addPage() { try { // Get the page title const titleInput document.getElementById(galleryTitle); if (!titleInput) { console.error(Page title input not found); return; } const title titleInput.value.trim(); // Validate title is not empty if (!title) { alert(Please enter a page title); return; } // Get parent ID with error handling const parentSelect document.getElementById(galleryParent); const parentId parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null; // Generate a unique ID for the page const id Date.now(); const pageId `page_${id}`; // Calculate slug using our more robust slugify function const pageSlug slugify(title); // Create the page object with explicit properties const page { id: id, title: title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageId, visible: true, parentId: parentId, siteId: siteId, // Set slug and URL explicitly slug: pageSlug, url: / + pageSlug }; console.log(`Creating new page with ID: ${id}, title: ${title}`); // Add to galleries array galleries.push(page); // CRITICAL: Ensure window.galleries is synchronized if (typeof window.galleries undefined) { window.galleries galleries; } else { // Make sure the page is in window.galleries too const existingIndex window.galleries.findIndex(g > g.id id); if (existingIndex > 0) { window.galleriesexistingIndex page; } else { window.galleries.push(page); } } // Initialize empty page elements array in PageManager if (window.PageManager) { window.PageManager.elementspageId ; } // Update UI first renderGalleries(); clearAddForm(); toggleAddForm(); // Set as active page console.log(`Setting new page ${page.title} as active`); activeGalleryId id; window.activeGalleryId id; // Update active states in the menu updateActiveStates(); // Save to server saveGalleries().then(() > { console.log(`Page ${page.title} saved successfully`); // Double check that our gallery is still in the array const pageExists galleries.some(g > g.id id); console.log(`Page exists in galleries array: ${pageExists}`); if (pageExists) { // Load the new page after a slight delay to ensure DOM is updated setTimeout(() > { console.log(`Loading new page ${page.title} with ID: ${id}`); loadPage(id); }, 50); } else { console.error(`Page with ID ${id} not found in galleries array after save`); } }).catch(error > { alert(Error saving page. Please try again.); }); return id; } catch (error) { console.error(Error adding page:, error); alert(Error adding page. Please try again.); return null; }}/** * Ensures a slug is unique among existing galleries * @param {string} slug - The base slug to check * @param {Array} existingGalleries - Array of existing gallery configs * @returns {string} A unique slug */function ensureUniqueSlug(slug, existingGalleries) { // Guard against invalid slugs if (!slug || slug -) { slug page- + Date.now(); } let finalSlug slug; let counter 1; // Check if slug already exists while (existingGalleries.some(g > g.slug finalSlug)) { finalSlug `${slug}-${counter}`; counter++; } return finalSlug;}/** * Adds a new gallery based on the selected type * @returns {number|null} The new gallery ID or null if error */function addGallery() { try { // Get the gallery title const titleInput document.getElementById(galleryTitle); if (!titleInput) { console.error(Gallery title input not found); return; } const title titleInput.value.trim(); // Get the selected gallery type const galleryTypeRadio document.querySelector(inputnamegalleryType:checked); if (!galleryTypeRadio) { console.error(No gallery type selected); alert(Please select a gallery type); return; } const galleryType galleryTypeRadio.value; console.log(Selected gallery type:, galleryType); // If this is a page, use the addPage function if (galleryType page) { console.log(Creating page instead of gallery); return addPage(); } // For spacer type, we dont require a title if (galleryType ! spacer && !title) { alert(Please enter a gallery title); return; } // Continue with gallery creation logic // Generate unique ID const id Date.now(); // Get parent gallery if selected const parentSelect document.getElementById(galleryParent); const parentId parentSelect.value ? parseInt(parentSelect.value) : null; // Get URL if this is an external gallery const galleryUrl document.getElementById(galleryUrl); const url galleryTypeRadio.value external && galleryUrl ? galleryUrl.value.trim() : ; // Set basic properties const gallery { id: id, title: title || Spacer, // Use Spacer as internal title if no title provided parentId: parentId, visible: true }; // Set type-specific properties if (galleryTypeRadio.value spacer) { // Spacer properties gallery.isSpacer true; gallery.title title || Spacer; // For internal reference } else if (galleryTypeRadio.value integrated) { // Generate pageId for integrated galleries gallery.isIntegrated true; gallery.pageId generatePageId(); // Generate and ensure unique slug const baseSlug slugify(title); gallery.slug ensureUniqueSlug(baseSlug, galleries); gallery.url / + gallery.slug; // Get the siteId from multiple possible sources const siteId window.Parameters?.siteId || window.siteId || document.querySelector(metanamehydra-site-id)?.getAttribute(content) || ; // Add galleryOptions with GUIDsiteId as the manualCollectionName gallery.galleryOptions { manualCollectionName: `GUID${siteId}`, isIntegratedGallery: true, pageId: gallery.pageId, // Store pageId for reference siteId: siteId }; } else if (galleryTypeRadio.value folder) { gallery.isFolder true; gallery.isCollapsed true; // Make folders closed by default } else if (galleryTypeRadio.value external) { // External URL gallery.url url; gallery.isExternal true; // Add this flag to identify external URLs } console.log(`Creating new gallery with ID: ${id}, title: ${title}`); // Add to galleries array galleries.push(gallery); // CRITICAL: Ensure window.galleries is synchronized if (typeof window.galleries undefined) { window.galleries galleries; } else { // Make sure the gallery is in window.galleries too const existingIndex window.galleries.findIndex(g > g.id id); if (existingIndex > 0) { window.galleriesexistingIndex gallery; } else { window.galleries.push(gallery); } } // Update UI first renderGalleries(); clearAddForm(); toggleAddForm(); // For non-folder types, activate and load the new gallery immediately if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer) { console.log(`Setting new gallery ${gallery.title} as active`); // Set as active gallery activeGalleryId id; window.activeGalleryId id; // Update active states in the menu updateActiveStates(); } // Save galleries after UI update saveGalleries().then(() > { console.log(`Gallery ${gallery.title} saved successfully`); // Double check that our gallery is still in the array const galleryExists galleries.some(g > g.id id); console.log(`Gallery exists in galleries array: ${galleryExists}`); // For non-folder types, load the new gallery content after a slight delay if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible ! false) { setTimeout(() > { console.log(`Loading new gallery ${gallery.title} with ID: ${id}`); if (gallery.isIntegrated) { loadGallery(id); } else if (gallery.isExternal && gallery.url) { // For external URLs, set the iframe src const frame document.getElementById(galleryFrame); if (frame) frame.src gallery.url; } }, 50); } }).catch(error > { console.error(`Error saving gallery ${gallery.title}:`, error); alert(Error saving gallery. Please try again.); }); // Return the new gallery ID return id; } catch (error) { console.error(Error adding gallery:, error); alert(Error adding gallery. Please try again.); }}function debugDOMState(message) {console.log(`DEBUG ${message}`);const galleryContainer document.querySelector(.gallery-container);const iframe document.getElementById(galleryFrame);const pageContainers document.querySelectorAll(.page-container);console.log(`- Gallery container: ${galleryContainer ? found : not found}`);console.log(`- iframe: ${iframe ? found : not found}, display: ${iframe?.style.display}`);console.log(`- Page containers: ${pageContainers.length} found`);pageContainers.forEach((container, index) > {console.log(` - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`);});}/*** Global helper function to check if were in edit mode* This centralizes the logic for detecting edit mode across all components*/window.isInEditMode function() {// Method 1: Check global isEditing variableif (typeof window.isEditing boolean) {return window.isEditing;}// Method 2: Check sidebar editing classconst sidebar document.querySelector(.sidebar);if (sidebar && sidebar.classList.contains(editing)) {return true;}// Method 3: Check Parameters.isInEditorif (window.Parameters && typeof window.Parameters.isInEditor boolean) {return window.Parameters.isInEditor;}// Method 4: Check if any page containers have editing classconst editingContainers document.querySelectorAll(.page-container.editing);if (editingContainers && editingContainers.length > 0) {return true;}// Default to false if no indicators foundreturn false;};/*** This function synchronizes edit mode state across all components*/window.updateGlobalEditState function(isEditing) {console.log(Updating global edit state:, isEditing);// Update global variablewindow.isEditing isEditing;// Update Parameters object if it existsif (window.Parameters) {window.Parameters.isInEditor isEditing;}// Update sidebar classconst sidebar document.querySelector(.sidebar);if (sidebar) {if (isEditing) { sidebar.classList.add(editing);} else { sidebar.classList.remove(editing);}}// Update page containersconst pageContainers document.querySelectorAll(.page-container);pageContainers.forEach(container > {if (isEditing) { container.classList.add(editing);} else { container.classList.remove(editing);}});// Notify all components via eventdocument.dispatchEvent(new CustomEvent(edit-mode-changed, {detail: { editing: isEditing }}));};/** * loadPage function that safely handles undefined galleries * @param {number} id - The gallery ID * @param {Event} event - Optional event object */function loadPage(id, event){ if (event) { event.preventDefault(); event.stopPropagation(); const now Date.now(); const lastCallTime window._lastPageLoadTime || 0; window._lastPageLoadTime now; if (now - lastCallTime 100) { console.log(Ignoring duplicate loadPage call); return; } } console.log(loadPage: Loading page with gallery ID:, id); stopAutoAdvanceTimer(); let galleriesData window.galleries || galleries; const gallery findGalleryById(galleriesData, id); if (!gallery) { console.warn(No gallery found with ID:, id, for page load.); return; } if (typeof window.removeGalleryScriptsWithPause function) { window.removeGalleryScriptsWithPause(); } window.activeGalleryId id; activeGalleryId id; const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) { galleryContainer.innerHTML ; } if (!gallery.pageId) gallery.pageId `page_${id}`; if (!gallery.isPage) gallery.isPage true; if (!window._pageIdToGalleryId) window._pageIdToGalleryId {}; window._pageIdToGalleryIdgallery.pageId id; if (gallery.pageElements && Array.isArray(gallery.pageElements)) { if (window.PageManager && window.PageManager.elements) { window.PageManager.elementsgallery.pageId ...gallery.pageElements; } } else if (window.PageManager && window.PageManager.elements && window.PageManager.elementsgallery.pageId && window.PageManager.elementsgallery.pageId.length > 0) { gallery.pageElements ...window.PageManager.elementsgallery.pageId; } if (window.PageManager && typeof window.PageManager.loadPage function) { try { window.PageManager.loadPage(gallery.pageId); const isCurrentlyEditing typeof isInEditMode function ? isInEditMode() : false; if (isCurrentlyEditing && typeof window.PageManager.setEditMode function) { window.PageManager.setEditMode(isCurrentlyEditing); } } catch (error) { console.error(Error loading page with PageManager:, error); } } else { console.error(PageManager not found or loadPage method not available); } // Apply menu visibility const bodyEl document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add(menu-hidden-on-page); } else { bodyEl.classList.remove(menu-hidden-on-page); // Ensure menu is visible if in edit mode or not hidden } if (typeof updateActiveStates function) updateActiveStates(); if (typeof updateMobileTitle function) updateMobileTitle(); if (typeof closeMobileMenu function) closeMobileMenu(); // Close the gallery options panel when navigating to a different page if (typeof window.closeOptionsPanel function) { window.closeOptionsPanel(); } if (typeof updateURLWithGallerySlug function) updateURLWithGallerySlug(gallery);}function generatePageId() {const characters abcdefghijklmnopqrstuvwxyz0123456789;let result ;const charactersLength characters.length;for (let i 0; i 8; i++) { result + characters.charAt(Math.floor(Math.random() * charactersLength));}return result;};function normalizeNameForComparison(name) {if (!name) {console.log(Empty name passed to normalization);return ;}console.log(`Normalizing name: ${name}`);// Step 1: Convert to lowercaselet normalized name.toLowerCase();console.log(`After lowercase: ${normalized}`);// Step 2: Replace spaces with hyphens (without regex)normalized normalized.split( ).join(-);console.log(`After space replacement: ${normalized}`);// Step 3: Filter out unwanted characters (without regex)let filtered ;for (let i 0; i normalized.length; i++) {const char normalized.charAt(i);// Keep only a-z, 0-9, and hyphensif ((char > a && char z) || (char > 0 && char 9) || char -) { filtered + char;}}normalized filtered;console.log(`After character filtering: ${normalized}`);// Step 4: Clean up multiple hyphens (without regex)while (normalized.indexOf(--) ! -1) {normalized normalized.split(--).join(-);}// Step 5: Remove leading and trailing hyphens (without regex)if (normalized.charAt(0) -) {normalized normalized.substring(1);}if (normalized.charAt(normalized.length - 1) -) {normalized normalized.substring(0, normalized.length - 1);}// Check if the result is validif (!normalized || normalized -) {console.log(`WARNING: Normalization produced invalid result: ${normalized}`);} else {console.log(`Final normalized slug: ${normalized}`);}return normalized;}function ensureUniqueSlug(slug, existingGalleries) {if (!slug) return gallery;let finalSlug slug;let counter 1;// Check if slug exists in any galleryconst slugExists function(s) {for (let i 0; i existingGalleries.length; i++) { if (existingGalleriesi.slug s) { return true; }}return false;};// Keep incrementing counter until uniquewhile (slugExists(finalSlug)) {finalSlug slug + - + counter;counter++;}return finalSlug;}/** * Gathers the current complete site configuration from the client-side state * and triggers a download of the data as a single JSON file. * This provides a simple way to back up or export the current site structure. */function downloadCurrentSiteJSON() { try { console.log(Gathering current site data for download...); // Get the current hostname to use in the filename. const hostname window.location.hostname.replace(www., ); // --- FIX: Ensure we get the most up-to-date menu styles --- // The MenuStyleCustomizer.settings object is the most reliable source // for the current styles being used on the client. const currentMenuStyles window.MenuStyleCustomizer?.settings || {}; // Get current galleries const currentGalleries window.galleries || galleries || ; // --- STREAMLINED: Process galleries without including image data --- const enhancedGalleries currentGalleries.map(gallery > { // If this is a Classic collection gallery, add note about manual retrieval if (gallery.galleryOptions?.manualCollectionName?.startsWith(GUID)) { const classicGuid gallery.galleryOptions.manualCollectionName.substring(5); console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`); return { ...gallery, _note: `This gallery references Classic collection GUID${classicGuid}. To get the full image data with captions, you may need to manually fetch the Classic collection JSON from: https://storage.neonsky.app/sites:site_${classicGuid}.json`, _classicGuid: classicGuid, _requiresClassicData: true }; } return gallery; }); // Assemble the complete site configuration object from global state variables. // This structure mirrors what the `save-config` API endpoint expects, ensuring // the exported file is a complete and re-importable replica. const siteData { // The core menu/page structure with enhanced Classic collection data. galleries: enhancedGalleries, // Admin emails are managed server-side for security. We export the list if its // available on the client from a previous check, but the server remains the // source of truth. A re-import will not overwrite the admin list. adminEmails: window.siteConfig?.adminEmails || , // The unique ID for the site. siteId: window.siteId || window.Parameters?.siteId || , // Custom styling rules for the menu. menuStyles: currentMenuStyles, // All elements configured to appear in the sidebar. sidebarElements: window.SidebarManager?.elements || , // Global site metadata for SEO and analytics. siteMetadata: window.siteMetadata || {}, // Add metadata about Classic collections that need manual data retrieval _classicCollections: enhancedGalleries .filter(g > g._requiresClassicData) .map(g > ({ title: g.title, guid: g._classicGuid, note: g._note })) }; console.log(Enhanced site data collected for export:, siteData); // Convert the JavaScript object to a formatted JSON string. const jsonString JSON.stringify(siteData, null, 2); // Create a Blob, which is a file-like object of immutable, raw data. const blob new Blob(jsonString, { type: application/json }); // Create a temporary link element to trigger the browsers download functionality. const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download `${hostname}-config.json`; // e.g., yoursite.com-config.json document.body.appendChild(a); a.click(); // Clean up by removing the temporary link and revoking the object URL to free up memory. document.body.removeChild(a); URL.revokeObjectURL(url); console.log(Enhanced JSON download initiated successfully.); // Show a helpful message about Classic collections const classicCount siteData._classicCollections?.length || 0; if (classicCount > 0) { console.log(`Export Summary: ${classicCount} Classic collection(s) detected. The exported JSON includes gallery structure and settings, but for full image data with captions, you may need to manually fetch the Classic collection JSON files.`); } } catch (error) { console.error(Error creating JSON download:, error); alert(An error occurred while preparing the download. Please check the console.); }}// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array)function buildHydraClassicNdjson(classicGuid, images) { const header { title: Untitled, description: , version: 1, isClassic: true, isClassicCollection: true, classicGuid: GUID + String(classicGuid || ) }; const imageArray Array.isArray(images) ? images.map(function(img, idx) { var path ; if (img) { path img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || ; } // Derive filename base from any provided path/filename and normalize extension to .jpg var fnameBase ; if (typeof path string && path.length > 0) { var slash path.lastIndexOf(/); var raw slash > 0 ? path.substring(slash + 1) : path; // remove query/hash var q raw.indexOf(?); if (q > 0) raw raw.substring(0, q); var h raw.indexOf(#); if (h > 0) raw raw.substring(0, h); // strip extension if present var dot raw.lastIndexOf(.); fnameBase dot > 0 ? raw.substring(0, dot) : raw; } else if (img && typeof img.filename string) { var rf img.filename; var q2 rf.indexOf(?); if (q2 > 0) rf rf.substring(0, q2); var h2 rf.indexOf(#); if (h2 > 0) rf rf.substring(0, h2); var d2 rf.lastIndexOf(.); fnameBase d2 > 0 ? rf.substring(0, d2) : rf; } var filename fnameBase ? (fnameBase + .jpg) : ; // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename} // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that sites path var guidRaw String(classicGuid || ).replace(/^GUID/, ); var cdnImageUrl filename && guidRaw ? (https://cdn.neonsky.app/ + guidRaw + /images/ + filename) : path; var rec { caption: (img && (img.caption || img.description)) || , description: (img && (img.description || img.caption)) || , isPreview: idx 1, title: (img && img.title) || , alt-text: (img && (imgalt-text || img.altText || )) || , image: cdnImageUrl || , link: , link-text: , filename: filename, isClassic: true, isClassicCollection: true, classicGuid: GUID + String(classicGuid || ) }; return rec; }) : ; // Build NDJSON as: header line, imageMetadata array line, then one line per full image record var body { imageMetadata: imageArray }; var out JSON.stringify(header), JSON.stringify(body); for (var k 0; k imageArray.length; k++) { out.push(JSON.stringify(imageArrayk)); } return out.join(\n) + \n;}async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) { // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if 800KB) const fileName String(siteId || ) + _ + String(pageId || ) + .ndjson; const flyUrl https://hydra-press-v2.fly.dev/publish; const irysUrl https://hydra-press-v2.fly.dev/upload-irys; console.error(Starting dual NDJSON upload for:, fileName); console.error(Content length:, ndjsonContent.length, characters); // COMPRESSION: Compress for Irys only (not Tigris) console.error(🔄 Compressing NDJSON with gzip for Irys...); const encoder new TextEncoder(); const ndjsonBytes encoder.encode(ndjsonContent); const compressedStream new Response(ndjsonBytes).body.pipeThrough(new CompressionStream(gzip)); const compressedBlob await new Response(compressedStream).blob(); const compressedBytes new Uint8Array(await compressedBlob.arrayBuffer()); const originalSize ndjsonBytes.length; const compressedSize compressedBytes.length; const compressionRatio ((1 - compressedSize / originalSize) * 100).toFixed(2); console.error(📊 Compression results:, { original: originalSize + bytes ( + (originalSize / 1024).toFixed(2) + KB), compressed: compressedSize + bytes ( + (compressedSize / 1024).toFixed(2) + KB), ratio: compressionRatio + % }); const IRYS_SIZE_LIMIT 800 * 1024; // 800KB limit for compressed Irys upload const shouldUploadToIrys compressedSize IRYS_SIZE_LIMIT; console.error(Irys upload eligible: + shouldUploadToIrys + (compressed: + (compressedSize / 1024).toFixed(2) + KB)); // STEP 1: Upload to Irys (non-blocking, compressed if 800KB) let irysResult null; if (shouldUploadToIrys) { try { console.error(🔄 Starting Irys upload (compressed, non-blocking)...); // Convert compressed bytes to base64 const compressedBase64 btoa(String.fromCharCode(...compressedBytes)); const irysResp await fetch(irysUrl, { method: POST, headers: { content-type: application/json }, body: JSON.stringify({ compressedData: compressedBase64, originalSize: originalSize, compressedSize: compressedSize, siteId: siteId, pageId: pageId }) }); if (irysResp.ok) { irysResult await irysResp.json(); console.error(✅ Irys upload successful:, irysResult.txId); console.error( Irys Gateway:, irysResult.urls?.irys); console.error( ar.io Gateway:, irysResult.urls?.arweave); // Log wallet balance and cost if (irysResult.walletBalance ! undefined) { console.error(💰 Wallet Balance:, irysResult.walletBalance.toFixed(6), AR); } if (irysResult.uploadCost ! undefined) { const costUSD (irysResult.uploadCost * 20).toFixed(4); if (irysResult.uploadCost 0) { console.error(💸 Upload Cost: FREE (0 AR)); } else { console.error(💸 Upload Cost:, irysResult.uploadCost.toFixed(6), AR (~$ + costUSD + USD at $20/AR)); } } } else { const errorText await irysResp.text().catch(() > Unknown error); console.warn(⚠️ Irys upload failed (non-critical):, irysResp.status, errorText); } } catch (irysError) { console.warn(⚠️ Irys upload error (non-critical):, irysError.message); // Continue - Irys failure doesnt block Fly.io upload } } else { console.error(⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)); } // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed) try { console.error(🔄 Starting Fly.io upload (uncompressed)...); const flyResp await fetch(flyUrl, { method: POST, headers: { content-type: application/json }, body: JSON.stringify({ ndjsonContent, dataFileName: fileName }) }); console.log(Server response status:, flyResp.status); if (!flyResp.ok) { const errorText await flyResp.text().catch(() > Unknown error); console.error(❌ Fly.io upload failed, flyResp.status, for, fileName + :, errorText); return { success: false, irysResult: null }; } const flyData await flyResp.json().catch(() > null); if (flyData && flyData.urls) { console.log(✅ Fly.io upload successful:, flyData.urls); console.log( Tigris URL:, flyData.urls.urlTigris); // Return comprehensive result const result { success: true, flyUrls: flyData.urls, irysResult: irysResult, uploadedTo: irysResult ? fly, irys : fly, fileSize: compressedSize, // Irys compressed size originalFileSize: originalSize // Original uncompressed size }; console.log(📦 Upload complete:, { destinations: result.uploadedTo.join( + ), txId: irysResult?.txId || N/A, flyUrl: flyData.urls.urlTigris }); return result; } else { console.warn(Fly.io upload response missing URLs:, flyData); return { success: false, irysResult: null }; } } catch (flyError) { console.error(❌ Fly.io upload error for, fileName + :, flyError); return { success: false, irysResult: null }; }}async function importClassicGalleries() { // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS function cleanDescriptionLeadingLines(text) { if (!text || typeof text ! string) return text; let result text; // Remove leading empty TEXTFORMAT blocks: TEXTFORMAT>P>FONT>/FONT>/P>/TEXTFORMAT> result result.replace( new RegExp( ^(\s*\TEXTFORMAT^\>*\>\s*\P^\>*\>\s*\FONT^\>*\>\s*\\/FONT\>\s*\\/P\>\s*\\/TEXTFORMAT\>\s*)+, i ), ); // Remove leading empty P tags result result.replace( new RegExp(^(\s*\P^\>*\>\s*\\/P\>\s*)+, i), ); // Remove leading empty FONT tags result result.replace( new RegExp(^(\s*\FONT^\>*\>\s*\\/FONT\>\s*)+, i), ); // Remove leading BR tags result result.replace( new RegExp(^(\s*\br\s*\/\?>\s*)+, i), ); // Remove any remaining leading whitespace return result.replace(new RegExp(^\s+, i), ).trim(); } // Browser-scoped helper: fix multi-line links (template-safe escaped regex) function fixMultilineLinks(text) { if (!text || typeof text ! string) return text; return text.replace( new RegExp(a^>*>.*?Click.*?\/a>a^>*>.*?HERE.*?\/a>a^>*>.*?to Add to Cart.*?\/a>, gis), function (match) { const first match.match(new RegExp(a href(^*) target(^*), i)); if (first) { return a href + first1 + target + first2 + >Click HERE to Add to Cart/a>; } return match; } ); } // Browser-scoped helper: convert plain text link phrase to HTML link function convertPlainTextLinks(text) { if (!text || typeof text ! string) return text; return text.replace( new RegExp(Click HERE to Add to Cart, g), a href# target_blank>Click HERE to Add to Cart/a> ); } // Get input elements, including the new checkbox const guidInputEl document.getElementById(classicGuid); const pastedJsonEl document.getElementById(pastedJson); const parentIdInputEl document.getElementById(importParent); const createSubmenuCheckboxEl document.getElementById(createSubmenu); const submenuTitleInputEl document.getElementById(submenuTitle); const replaceMenuCheckboxEl document.getElementById(replaceMenuCheckbox); // Get values from inputs const guidInput guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : ; const pastedJson pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : ; const replaceMenu replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false; let parentId parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null; let createSubmenu createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false; let submenuTitle submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : ; // If replacing the menu, ignore parent/submenu settings if (replaceMenu) { parentId null; createSubmenu false; } // Validate submenu title if checkbox is checked if (createSubmenu && !submenuTitle) { alert(Please provide a title for the submenu); return; } // Show status display const statusEl document.getElementById(importStatus); const progressBarEl statusEl ? statusEl.querySelector(.progress-bar) : null; const statusMessageEl statusEl ? statusEl.querySelector(.status-message) : null; const progressBar progressBarEl instanceof HTMLElement ? progressBarEl : null; const statusMessage statusMessageEl instanceof HTMLElement ? statusMessageEl : null; if (statusEl && progressBar && statusMessage) { statusEl.style.display block; statusEl.classList.add(importing); progressBar.style.width 10%; statusMessage.textContent Preparing to import...; } try { let classicData; let classicGuid; // Will hold the primary GUID for image path lookups // --- LOGIC TO GET DATA: Prioritize Pasted JSON --- if (pastedJson) { if (statusMessage) statusMessage.textContent Parsing pasted JSON...; classicData JSON.parse(pastedJson); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error(Pasted JSON is invalid: galleries array not found.); } // Use the first available GUID from the pasted data as the default for images classicGuid classicData.galleries0?.classicGuid || classicData.siteId; } else if (guidInput) { // Check if this is the new import_ format if (guidInput.startsWith(import_)) { if (statusMessage) statusMessage.textContent Fetching data via import_ format...; const importGuid guidInput.substring(7); // Remove import_ prefix const jsonUrl `https://hydra.neonsky.app/json-imports/${guidInput}.json`; const response await fetch(jsonUrl); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); classicData await response.json(); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error(Fetched import JSON is invalid: galleries array not found.); } // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata if (statusMessage) statusMessage.textContent Fetching image metadata from original JSON...; const originalJsonUrl `https://cdn.neonsky.app/sites:site_${importGuid}.json`; try { const originalResponse await fetch(originalJsonUrl); if (originalResponse.ok) { const originalData await originalResponse.json(); // Get the default language from site configuration let defaultLanguageID en; // fallback if (originalData.languages && Array.isArray(originalData.languages)) { const defaultLang originalData.languages.find(lang > lang.isDefault true); if (defaultLang) { defaultLanguageID defaultLang.id; } } // Merge image metadata from original JSON into import JSON galleries if (originalData.galleries && Array.isArray(originalData.galleries)) { classicData.galleries classicData.galleries.map(importGallery > { // Find matching gallery in original data by name or categoryID const originalGallery originalData.galleries.find(orig > orig.title importGallery.title || // Primary match by title orig.name importGallery.title || // Fallback: orig.name matches import.title orig.categoryID importGallery.categoryID // Fallback: categoryID match ); if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) { // Process all images from original gallery with metadata extraction const processedImages originalGallery.images.map(originalImage > { // Apply EXACT same metadata processing logic as Edition Publisher let title originalImage.title || ; let dateline originalImage.dateline || ; let caption originalImage.caption || ; // Check languageVersions array for metadata (this is where most data is stored) if (originalImage.languageVersions && originalImage.languageVersions.length > 0) { // Look for sites default language first, then fall back to first available let langData originalImage.languageVersions.find(lang > lang.languageID defaultLanguageID); if (!langData) { langData originalImage.languageVersions0; } // For language-enabled JSON files, prioritize languageVersions data // Only fall back to top-level if languageVersions data is empty if (!title || title.trim() ) title langData.title || ; if (!dateline || dateline.trim() ) dateline langData.dateline || ; if (!caption || caption.trim() ) caption langData.caption || ; } // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals) let description caption; if (caption && caption.includes(TEXTFORMAT)) { try { // Use properly escaped HTML characters for template literal context description caption .replace(/\TEXTFORMAT^\>*\>/g, ) .replace(/\\/TEXTFORMAT\>/g, ) .replace(/\P^\>*\>/g, \p\>) .replace(/\\/P\>/g, \\/p\>) .replace(/\FONT^\>*\>/g, ) .replace(/\\/FONT\>/g, ) .replace(/\A HREF(^*)^\>*\>/g, \a href$1 target_blank\>) .replace(/\\/A\>/g, \\/a\>) .replace(/\U\>/g, \u\>) .replace(/\\/U\>/g, \\/u\>) .replace(/\B\>/g, \strong\>) .replace(/\\/B\>/g, \\/strong\>) .replace(/\I\>/g, \em\>) .replace(/\\/I\>/g, \\/em\>) .replace(/"/g, ) .replace(/'/g, ) .replace(/\s+/g, ) .trim(); } catch (error) { console.warn(TEXTFORMAT conversion failed, using original caption:, error); description caption; } } // Prepend dateline to description if it exists (convert to uppercase) if (dateline) { const uppercaseDateline dateline.toUpperCase(); description `${uppercaseDateline} ${description}`.trim(); } // Apply description fixes description cleanDescriptionLeadingLines(description); description fixMultilineLinks(description); description convertPlainTextLinks(description); const imageEntry { // Match EXACT structure from Edition Publisher hydraImageEntry image: originalImage.image || originalImage.urls?.original || , caption: description, // Use processed description as caption description: description, // ALSO set description field (like Edition Publisher) title: title, alt-text: title, // Use title as alt-text like Edition Publisher link: originalImage.urls?.original || , link-text: originalImage.urls?.original || , isPreview: false, filename: originalImage.filename || , originalUrl: originalImage.image || originalImage.urls?.original || , dateline: dateline }; return imageEntry; }); return { ...importGallery, // Keep import gallery structure (order, settings, etc.) images: processedImages // Replace with processed original images }; } return importGallery; }); } } else { console.warn(Could not fetch original JSON for metadata, using import data only); } } catch (error) { console.warn(Error fetching original JSON for metadata:, error); } // Extract original GUID for image path lookups // If importGuid has a suffix (e.g., 4bd5ebf7bda9d-all), extract the base GUID // GUIDs are typically 13 characters, so split on common separators let baseGuid importGuid; const guidMatch importGuid.match(/^(a-f0-9{13})(?:-.*)?$/i); if (guidMatch) { baseGuid guidMatch1; } else { // Fallback: try to extract first 13-character hex string const hexMatch importGuid.match(/^(a-f0-9{13})/i); if (hexMatch) { baseGuid hexMatch1; } } // Use the base GUID for image path lookups (e.g., /images/filename.jpg) classicGuid baseGuid; console.log(Successfully loaded hybrid import data with import GUID:, importGuid); console.log(Using base GUID for image paths:, classicGuid); } else { // Standard GUID format - existing logic if (statusMessage) statusMessage.textContent Fetching data via GUID...; classicGuid guidInput.startsWith(GUID) ? guidInput.substring(5) : guidInput; console.log(Detected standard GUID format, GUID:, classicGuid); const jsonUrl `https://storage.neonsky.app/sites:site_${classicGuid}.json`; console.log(Fetching classic JSON from:, jsonUrl); const response await fetch(jsonUrl); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); classicData await response.json(); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error(Fetched JSON is invalid: galleries array not found.); } console.log(Successfully loaded classic data with GUID:, classicGuid); } } else { alert(Please either paste site JSON or enter a Classic GUID to import.); if (statusEl) statusEl.style.display none; return; } const galleriesForImport classicData.galleries; if (statusMessage) statusMessage.textContent `Found ${galleriesForImport.length} items to import...`; // --- HIERARCHY RECONSTRUCTION LOGIC --- const currentSiteId window.Parameters?.siteId || siteId; let submenuParentId parentId; const tempNewItems ; const originalIdToNewIdMap {}; // Create the optional top-level submenu if requested if (createSubmenu) { const submenuItem { id: Date.now(), title: submenuTitle, isSubmenu: true, isCollapsed: true, visible: true, parentId: parentId, siteId: currentSiteId, children: }; galleries.push(submenuItem); submenuParentId submenuItem.id; } // --- Pass 1: Create new items with correct data and map old IDs to new IDs --- for (let index 0; index galleriesForImport.length; index++) { const classicItem galleriesForImportindex; const newItemId Date.now() + index + 100; originalIdToNewIdMapclassicItem.id newItemId; if (progressBar) progressBar.style.width `${40 + Math.round(index * 50 / galleriesForImport.length)}%`; if (statusMessage) statusMessage.textContent `Processing item ${index + 1} of ${galleriesForImport.length}...`; const title classicItem.title || classicItem.languageVersions?.0?.title || classicItem.name || `Item ${classicItem.categoryID}`; let slug; // If pasting from JSON, *always* regenerate the slug from the title to ensure its clean. // Otherwise (for GUID import), trust the existing slug if its there. if (pastedJson) { slug normalizeNameForComparison(title); } else if (classicItem.slug && classicItem.slug.trim() ! ) { slug classicItem.slug; } else { slug normalizeNameForComparison(title); }const finalSlug ensureUniqueSlug(slug || gallery, ...galleries, ...tempNewItems); // Ensure uniqueness // Determine the GUID to use for this specific items image paths const itemGuid classicItem.classicGuid || classicGuid; let newItem; // --- Check if its an External URL --- if (classicItem.isExternal true && classicItem.url) { console.log(`Creating external URL menu item: ${title} -> ${classicItem.url}`); newItem { id: newItemId, title, url: classicItem.url, isExternal: true, visible: classicItem.visible ! false, parentId: null, siteId: currentSiteId, slug: finalSlug, importSource: external-url }; } // --- Check if its a Page and build its elements --- else if (classicItem.isPage true || classicItem.type 3 || classicItem.behaviorID 3) { const pageUniqueId `page_${newItemId}`; let pageElements ; // // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format // if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) { console.log(`Using existing pageElements for page: ${title}`); // Check if we have both pageElements and images - create two-column layout if (classicItem.images && classicItem.images.length > 0) { console.log(`Creating two-column layout for page: ${title} with ${classicItem.images.length} images`); // Create metadata element pageElements.push({ id: newItemId + 1000, type: metadata, title: Metadata, visible: true, position: 0, metaTitle: classicItem.meta?.title || title, metaDescription: classicItem.meta?.description || , metaKeywords: classicItem.meta?.keywords || }); // Create two-column container const leftColumnElements ; const rightColumnElements ; // Add images to left column const slides classicItem.images.map((img) > { const imageFilename (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || .jpg); const imageKey `${itemGuid}/images/${imageFilename}`; const imageUrl `https://storage.neonsky.app/${imageKey}`; return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || image/jpeg, caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ))), title: img.title || , byline: img.byline || , dateline: img.dateline || , altText: img.metadata?.alt_text || }; }); leftColumnElements.push({ id: newItemId + 3100, type: slideshow, title: Page Images, visible: true, position: 0, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true }); // Add text content to right column classicItem.pageElements.forEach((el, elIndex) > { if (el.type text && el.content) { rightColumnElements.push({ id: newItemId + 3200 + elIndex, type: text, title: Text Block, visible: true, position: elIndex, textContent: el.content, textWidth: 70 }); } }); const columns { id: newItemId + 3300, hAlign: right, vAlign: middle, elements: leftColumnElements }, { id: newItemId + 3400, hAlign: left, vAlign: middle, elements: rightColumnElements } ; pageElements.push({ id: newItemId + 3000, type: column-container, title: Two Column Layout, visible: true, position: 1, columns, backgroundSlideshow: { enabled: false, slides: , slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false } }); } else { // No images, just transform the pageElements as before pageElements classicItem.pageElements.map((el, elIndex) > { const elementId Date.now() + index + 1000 + elIndex; // Handle import_guid.json format (type + content) if (el.type && el.content) { if (el.type text) { return { id: elementId, type: text, title: Text Block, visible: true, position: elIndex, textContent: el.content, textWidth: 100 }; } else if (el.type metadata) { return { id: elementId, type: metadata, title: Metadata, visible: true, position: elIndex, metaTitle: el.metaTitle || title, metaDescription: el.metaDescription || , metaKeywords: el.metaKeywords || }; } } // Handle existing format (already has id, position, etc.) return { ...el, id: elementId, position: el.position ! undefined ? el.position : elIndex }; }); } } else { // Otherwise (for GUID-based import), generate the page structure from scratch. console.log(`Generating new pageElements for page: ${title}`); pageElements.push({ id: newItemId + 1000, type: metadata, title: Metadata, visible: true, position: 0, metaTitle: classicItem.meta?.title || title, metaDescription: classicItem.meta?.description || , metaKeywords: classicItem.meta?.keywords || }); // Add images if they exist in the page if (classicItem.images && classicItem.images.length > 0) { const slides classicItem.images.map((img) > { const imageFilename (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || .jpg); const imageKey `${itemGuid}/images/${imageFilename}`; const imageUrl `https://storage.neonsky.app/${imageKey}`; return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || image/jpeg, caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ))), title: img.title || , byline: img.byline || , dateline: img.dateline || , altText: img.metadata?.alt_text || }; }); pageElements.push({ id: newItemId + 1500, type: slideshow, title: Page Images, visible: true, position: 1, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true }); } pageElements.push({ id: newItemId + 2000, type: text, title: Text Block Spacer, visible: true, position: 2, textContent: p> /p>p>br>/p>, textWidth: 100 }); const columns ; const leftColumnElements ; const rightColumnElements ; if (classicItem.images && classicItem.images.length > 0) { const slides classicItem.images.map((img) > { const imageFilename (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || .jpg); const imageKey `${itemGuid}/images/${imageFilename}`; const imageUrl `https://storage.neonsky.app/${imageKey}`; // Preserve all image metadata including captions, titles, etc. return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || image/jpeg, // Preserve caption information - handle both direct and language version storage patterns caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.0?.caption || ))), title: img.title || img.languageVersions?.0?.title || , byline: img.byline || img.languageVersions?.0?.byline || , dateline: img.dateline || img.languageVersions?.0?.dateline || , altText: img.metadata?.alt_text || , // Preserve any additional metadata metaTitle: img.metadata?.meta_title || , metaDescription: img.metadata?.meta_description || , metaKeywords: img.metadata?.meta_keywords || , // Preserve gallery-specific data sortOrder: img.galleryData?.sortOrder || 0, linkURL: img.galleryData?.linkURL || null, linkTarget: img.galleryData?.linkTarget || _self }; }); leftColumnElements.push({ id: newItemId + 3100, type: slideshow, title: Image Slideshow, visible: true, position: 0, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true }); } let rawDetails classicItem.details || classicItem.languageVersions?.0?.details || ; if (rawDetails && typeof rawDetails string) { // // CHANGE 2: Fix for Invalid regular expression flags error. // Changed from /.../gi literal to new RegExp(..., gi) constructor // to prevent syntax errors. // let processedText rawDetails.replace(new RegExp(TEXTFORMAT.*?>, gi), ).replace(new RegExp(/TEXTFORMAT>, gi), ); processedText processedText.replace(new RegExp(P ALIGN.*?, gi), p).replace(new RegExp(/P>, gi), /p>); processedText processedText.replace(new RegExp(FONT.*?>, gi), ).replace(new RegExp(/FONT>, gi), ); processedText processedText.replace(new RegExp(B>, gi), strong>).replace(new RegExp(/B>, gi), /strong>); processedText processedText.replace(/</g, ).replace(/>/g, >).replace(/&/g, &); // Apply description fixes to processed text processedText cleanDescriptionLeadingLines(processedText); processedText fixMultilineLinks(processedText); processedText convertPlainTextLinks(processedText); rightColumnElements.push({ id: newItemId + 3200, type: text, title: Text Block Content, visible: true, position: 0, textContent: processedText, textWidth: 70 }); } columns.push({ id: newItemId + 3300, hAlign: right, vAlign: middle, elements: leftColumnElements }); columns.push({ id: newItemId + 3400, hAlign: left, vAlign: middle, elements: rightColumnElements }); pageElements.push({ id: newItemId + 3000, type: column-container, title: Imported Columns, visible: true, position: 2, columns, backgroundSlideshow: { enabled: false, slides: , slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false } }); } newItem { id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible ! false, parentId: null, siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: classic-page }; } else if (classicItem.isFolder true) { // --- Handle folders from import_guid.json format --- console.log(`Creating folder: ${title}`); newItem { id: newItemId, title, isFolder: true, isCollapsed: classicItem.isCollapsed ! false, visible: classicItem.visible ! false, parentId: null, siteId: currentSiteId, slug: finalSlug, importSource: folder }; } else { // --- This is a Gallery --- const galleryPageId generatePageId(); // --- ENHANCED: Try to fetch and include actual image data with captions --- let enhancedGalleryOptions { manualCollectionName: `GUID${itemGuid}`, categoryId: classicItem.categoryID, importedFromClassic: true, siteId: currentSiteId, pageId: galleryPageId }; // --- ENHANCED: Preserve gallery display settings from Classic JSON --- if (classicItem.galleryOptions) { console.log(`Preserving gallery settings for: ${title}`); // Preserve layout and display settings const preservedSettings startInSingles, layoutType, columns, spacing, showDescription, showFilename, displayAllInfo, navigationMode, showTextBlock, fixedHeroImage, zoomInLightbox, lightboxOnMobile, fadeDuration, fadeInDuration, descriptionTextColor, gridImageOverlayColor, gridImageOverlayOpacity, lightboxBgColor, lightboxBgOpacity, lightboxCloseColor, lightboxArrowColor, useTitles, useLinks, desktopTitleDisplayMode, titleTextAlign, showDescriptionInOverlay, includeRolloverImageInOverlay, filterMenuEnabled, filterMenuCollectionNames, filterMenuTitles, filterMenuStyle, rolloverSwap, rolloverCollectionNames, openMultipleLightboxes, batchSize, autoplaySingles, autoplayDuration, autoplayTransition ; preservedSettings.forEach(setting > { if (classicItem.galleryOptionssetting ! undefined) { enhancedGalleryOptionssetting classicItem.galleryOptionssetting; } }); // Preserve text content if (classicItem.galleryOptions.horizontalScrollerText) { enhancedGalleryOptions.horizontalScrollerText classicItem.galleryOptions.horizontalScrollerText; } // Preserve JSON collections if present if (classicItem.galleryOptions.jsonCollections) { enhancedGalleryOptions.jsonCollections classicItem.galleryOptions.jsonCollections; } else { // Create jsonCollections structure with processed image metadata const collectionKey GUID + itemGuid; const processedImages classicItem.images || ; enhancedGalleryOptions.jsonCollections { collectionKey: { isClassicCollection: true, guid: collectionKey, metadata: processedImages, totalItems: processedImages.length } }; console.log(Created jsonCollections for + title + with + processedImages.length + images); // Build and try to upload NDJSON for this gallery. // If upload succeeds, switch to metadata {} so frontend fetches NDJSON. // Skip NDJSON generation for galleries with no images to avoid creating empty files if (processedImages.length > 0) { try { console.log(Building and uploading NDJSON for gallery:, title, ( + processedImages.length + images)); const ndjsonContent buildHydraClassicNdjson(itemGuid, processedImages); console.log(Generated NDJSON content length:, ndjsonContent.length, characters); const uploadResult await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); if (uploadResult && uploadResult.success) { try { enhancedGalleryOptions.jsonCollectionscollectionKey.metadata {}; enhancedGalleryOptions.jsonCollectionscollectionKey.totalItems processedImages.length; console.log(NDJSON uploaded successfully for + title + ; switched metadata to {}); // Store dual upload metadata if Irys was successful if (uploadResult.irysResult && uploadResult.irysResult.txId) { enhancedGalleryOptions.txId uploadResult.irysResult.txId; enhancedGalleryOptions.permaURL uploadResult.irysResult.txId; // Plain txId for racing enhancedGalleryOptions.isPerma true; enhancedGalleryOptions.uploadedTo uploadResult.uploadedTo; enhancedGalleryOptions.fileSize uploadResult.fileSize; enhancedGalleryOptions.siteId currentSiteId; enhancedGalleryOptions.pageId galleryPageId; console.log(✅ Dual upload complete - Irys txId:, uploadResult.irysResult.txId); console.log( Uploaded to:, uploadResult.uploadedTo.join( + )); } else { console.log(✅ Single upload (Fly.io only) - No Irys data); } } catch (applyErr) { console.warn(Failed to switch metadata to {} after NDJSON upload:, applyErr); } } else { console.warn(NDJSON upload failed for + title + ; keeping inline metadata array); } } catch (e) { console.warn(NDJSON build/upload skipped for + title + due to error:, e); } } else { console.log(Skipping NDJSON generation for + title + - no images to process); } } // Preserve additional metadata if (classicItem.galleryOptions.siteAlias) { enhancedGalleryOptions.siteAlias classicItem.galleryOptions.siteAlias; } if (classicItem.galleryOptions.initialPageUuid) { enhancedGalleryOptions.initialPageUuid classicItem.galleryOptions.initialPageUuid; } if (classicItem.galleryOptions.initialPageAlias) { enhancedGalleryOptions.initialPageAlias classicItem.galleryOptions.initialPageAlias; } if (classicItem.galleryOptions.timestamp) { enhancedGalleryOptions.timestamp classicItem.galleryOptions.timestamp; } } // Note: For import from pasted JSON, we dont include image data to avoid overwriting existing captions // Image data is only included when importing from Classic GUID directly console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`); newItem { id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible ! false, parentId: null, siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: classic-gallery, slug: finalSlug, normalizedName: finalSlug, galleryOptions: enhancedGalleryOptions }; } tempNewItems.push(newItem); } // --- Pass 2: Link children to parents using the ID map --- tempNewItems.forEach(newItem > { const originalId Object.keys(originalIdToNewIdMap).find(key > originalIdToNewIdMapkey newItem.id); const originalItem galleriesForImport.find(g > String(g.id) String(originalId)); if (originalItem && originalItem.parentId) { const newParentId originalIdToNewIdMaporiginalItem.parentId; if (newParentId) { newItem.parentId newParentId; const parentItem tempNewItems.find(p > p.id newParentId); if (parentItem) { parentItem.isSubmenu true; parentItem.isCollapsed true; } } else { newItem.parentId submenuParentId; } } else { newItem.parentId submenuParentId; } }); if (replaceMenu) { console.log(Replacing existing menu structure.); // If replacing, overwrite the entire galleries array. galleries tempNewItems; } else { console.log(Appending to existing menu structure.); // If not replacing, push the new items to the existing array. galleries.push(...tempNewItems); } if (progressBar) progressBar.style.width 90%; if (statusMessage) statusMessage.textContent Saving changes...; // Pass import flag to skip localStorage for large import operations await saveGalleries({ isImport: true }); if (progressBar) progressBar.style.width 100%; if (statusMessage) statusMessage.textContent `Successfully imported ${tempNewItems.length} items!`; if (statusEl) statusEl.classList.remove(importing); setTimeout(() > { renderGalleries(); toggleImportClassicForm(); if (progressBar) progressBar.style.width 0; if (statusEl) statusEl.style.display none; if (confirm(Import complete! Would you like to reload the page to ensure everything is displayed correctly?)) { window.location.reload(); } }, 1000); } catch (error) { console.error(Error importing classic galleries:, error); if (statusMessage) statusMessage.textContent `Error: ${error.message}`; if (progressBar) { progressBar.style.width 100%; progressBar.style.backgroundColor #dc3545; } if (statusEl) statusEl.classList.remove(importing); }}/** * Improved toggleImportClassicForm - Closes other forms first */function toggleImportClassicForm() { // First check if user is logged in and in edit mode if (!isAuthenticated || !isAdmin || !window.isInEditMode()) { alert(You must be logged in as an admin and in edit mode to import galleries.); return; } const formId importClassicForm; const form document.getElementById(formId); if (!form) { console.error(Import form element not found); return; } // If this form is already open, just close everything if (window.currentOpenForm formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Show the form form.style.display block; // Use setTimeout to allow the display change to take effect before adding the class setTimeout(() > { form.classList.add(visible); }, 10); // Populate parent options const parentSelect document.getElementById(importParent); if (parentSelect) { parentSelect.innerHTML option value>Top Level (No Parent)/option>; // Add all galleries that could be parents (including submenus) galleries.forEach(gallery > { // Only include items that are not already marked as imported if (!gallery.importSource) { parentSelect.innerHTML + `option value${gallery.id}>${gallery.title}/option>`; } }); } // Set default values const submenuTitle document.getElementById(submenuTitle); if (submenuTitle && !submenuTitle.value) { submenuTitle.value Classic Galleries; } // Reset any previous import status const statusEl document.getElementById(importStatus); if (statusEl) { statusEl.style.display none; const progressBar statusEl.querySelector(.progress-bar); if (progressBar) { progressBar.style.width 0; progressBar.style.backgroundColor #4682B4; // Reset to blue } } // Set this as the current open form window.currentOpenForm formId;}// Add event listener for the radio buttons to adjust height when page is selecteddocument.addEventListener(DOMContentLoaded, function() { const galleryTypeRadios document.querySelectorAll(inputnamegalleryType); galleryTypeRadios.forEach(radio > { radio.addEventListener(change, function() { const addForm document.getElementById(addForm); if (addForm && addForm.classList.contains(visible)) { const isPageSelected this.value page; // Give more height for page type if (isPageSelected) { addForm.style.maxHeight 600px; } else { // For other types, measure content height const contentHeight addForm.scrollHeight + 30; addForm.style.maxHeight Math.max(contentHeight, 500) + px; } } }); });});/** * Improved toggleStyleEditor - Closes other forms first */function toggleStyleEditor(display false) { const formId menuStyleEditor; // If this form is already open or display is false, just close everything if (window.currentOpenForm formId || display false) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Check if style editor already exists let styleEditor document.getElementById(formId); if (!styleEditor) { // Get the edit controls element (to place the editor after it) const editControls document.querySelector(.edit-controls); if (editControls && window.MenuStyleCustomizer) { // Create a wrapper for the style editor styleEditor document.createElement(div); styleEditor.id formId; styleEditor.style.display none; // Insert the editor after the edit controls editControls.parentNode.insertBefore(styleEditor, editControls.nextSibling); // Create the style editor UI window.MenuStyleCustomizer.createStyleEditor(styleEditor); } } // Show the editor if (styleEditor) { styleEditor.style.display block; // Set this as the current open form window.currentOpenForm formId; }}/** * This function closes all forms when escape key is pressed */document.addEventListener(keydown, function(e) { if (e.key Escape && window.currentOpenForm) { closeAllForms(); }});/** * This function closes all forms when clicking outside any form */document.addEventListener(click, function(e) { // Only process if a form is open if (!window.currentOpenForm) return; // Get the current open form element const currentForm document.getElementById(window.currentOpenForm); if (!currentForm) return; // Check if the click was inside the form or on a form toggle button const isInsideForm currentForm.contains(e.target); const isFormToggleButton e.target.closest(onclick*toggle) ! null; // If clicked outside the form and not on a toggle button, close all forms if (!isInsideForm && !isFormToggleButton) { closeAllForms(); }});// Override the original functions with our improved versionswindow.toggleAddForm toggleAddForm;window.toggleStyleEditor toggleStyleEditor;window.toggleMetadataEditor toggleMetadataEditor;window.toggleSidebarElementForm toggleSidebarElementForm;window.toggleImportClassicForm toggleImportClassicForm;window.closeAllForms closeAllForms;/** * Opens the edit form for a gallery item. * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu) * @param {number} id - The ID of the gallery item. * @param {Event} event - The click event. */function editGallery(id, event) { event.stopPropagation(); // Prevent event bubbling const gallery findGalleryById(galleries, id); const li document.querySelector(`lidata-id${id}`); if (gallery && li) { const showUrlField gallery.isPage || gallery.isIntegrated; const showExternalUrlField gallery.isExternal; const currentSlug gallery.slug || (gallery.url && gallery.url.startsWith(/) ? gallery.url.substring(1) : ); const currentExternalUrl gallery.url || ; // Clear existing content and rebuild form to ensure event listeners are fresh if needed li.innerHTML ; // Clear previous form if any const formDiv document.createElement(div); formDiv.className edit-form; formDiv.innerHTML ` div classform-group> label>Title:/label> input typetext value${gallery.title || } classedit-title> /div> ${showUrlField ? ` div classform-group> label>URL Slug:/label> input typetext value${currentSlug} classedit-slug placeholdere.g., my-gallery> small>Path after domain name (e.g., yoursite.comb>/my-gallery/b>). Use lowercase letters, numbers, and hyphens./small> /div> ` : } ${showExternalUrlField ? ` div classform-group> label>External URL:/label> input typetext value${currentExternalUrl} classedit-external-url placeholderhttps://example.com> small>Full URL for the external link (e.g., https://example.com/page)./small> /div> ` : } div classform-group> label>Meta Title (Optional):/label> input typetext value${gallery.metaTitle || } classedit-meta-title placeholderTitle for search results (if different from main title)> small>Overrides site title for this item in search results/browser tabs./small> /div> div classform-group> label>Meta Description (Optional):/label> textarea classedit-meta-description placeholderDescription for search results (150-160 chars recommended)>${gallery.metaDescription || }/textarea> small>Overrides site description for this item in search results./small> /div> div classform-group> label styledisplay: flex; align-items: center; gap: 8px;> input typecheckbox classedit-hide-menu ${gallery.hideMenuOnPage ? checked : } stylewidth: auto; margin-bottom: 0;> Hide menu when viewing this page /label> small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode./small> /div> div classform-group> label styledisplay: flex; align-items: center; gap: 8px;> input typecheckbox classedit-password-protected ${gallery.passwordProtected ? checked : } stylewidth: auto; margin-bottom: 0;> Password protect this page /label> small>Visitors will need to enter a password to access this page./small> /div> div classform-group password-field styledisplay: ${gallery.passwordProtected ? block : none};> label>Password:/label> input typepassword value${gallery.password || } classedit-password placeholderEnter password> small>Password visitors must enter to access this page./small> /div> div classedit-form-actions> button classbtn btn-primary idsaveEditBtn-${id}>Save/button> button classbtn idcancelEditBtn-${id}>Cancel/button> /div> br> `; li.appendChild(formDiv); // Append the new form // Add event listeners programmatically const saveButton formDiv.querySelector(`#saveEditBtn-${id}`); if (saveButton) { saveButton.addEventListener(click, (e) > saveEdit(id, e)); } const cancelButton formDiv.querySelector(`#cancelEditBtn-${id}`); if (cancelButton) { cancelButton.addEventListener(click, () > renderGalleries()); } // Add password protection toggle functionality const passwordProtectedCheckbox formDiv.querySelector(.edit-password-protected); const passwordField formDiv.querySelector(.password-field); if (passwordProtectedCheckbox && passwordField) { passwordProtectedCheckbox.addEventListener(change, function() { passwordField.style.display this.checked ? block : none; }); } li.classList.add(edit-mode); } else { console.error(Could not find gallery or list item for ID:, id); }}/** * Handles click on a folder item to show/hide its contents * @param {number} id - The gallery ID * @param {Event} event - The click event */function toggleFolder(id, event) { // Ensure the event doesnt interfere with drag operations event.stopPropagation(); console.log(Toggling folder:, id); const gallery galleries.find(g > g.id id); if (!gallery) return; // Toggle the collapsed state gallery.isCollapsed !gallery.isCollapsed; // Find the list item in the DOM const listItem document.querySelector(`lidata-id${id}`); if (listItem) { // Toggle the expanded class listItem.classList.toggle(expanded, !gallery.isCollapsed); // Find the submenu toggle icon and rotate it const toggleIcon listItem.querySelector(.toggle-icon); if (toggleIcon) { toggleIcon.classList.toggle(rotated, !gallery.isCollapsed); } const folderSpan listItem.querySelector(.menu-itemdata-foldertrue); } // Only save if in edit mode if (window.isInEditMode && window.isInEditMode()) { saveGalleries(); } else { // For non-edit mode, optionally save folder states to localStorage for persistence try { const folderStates JSON.parse(localStorage.getItem(folder_states) || {}); folderStatesid !gallery.isCollapsed; localStorage.setItem(folder_states, JSON.stringify(folderStates)); } catch (e) { console.error(Error saving folder state to localStorage:, e); } }}window.toggleFolder toggleFolder;/** * Toggle password protection for a gallery/page * @param {number} id - The gallery ID * @param {Event} event - The click event */function toggleGalleryPasswordProtection(id, event) { event.stopPropagation(); const gallery findGalleryById(galleries, id); if (!gallery) { console.error(Could not find gallery for password protection toggle, ID:, id); return; } if (gallery.passwordProtected) { // Remove password protection if (confirm(Remove password protection from this page?)) { gallery.passwordProtected false; delete gallery.password; saveGalleries(); renderGalleries(); } } else { // Add password protection const password prompt(Enter a password for this page:); if (password && password.trim()) { gallery.passwordProtected true; gallery.password password.trim(); saveGalleries(); renderGalleries(); } }}window.toggleGalleryPasswordProtection toggleGalleryPasswordProtection;/** * Saves the edited gallery item details. * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic) * @param {number} id - The ID of the gallery item. * @param {Event} event - The click event. */function saveEdit(id, event) { event.stopPropagation(); const li document.querySelector(`lidata-id${id}`); if (!li) { console.error(Could not find list item for saveEdit, ID:, id); return; } const titleInput li.querySelector(.edit-title); const slugInput li.querySelector(.edit-slug); const externalUrlInput li.querySelector(.edit-external-url); const metaTitleInput li.querySelector(.edit-meta-title); const metaDescriptionInput li.querySelector(.edit-meta-description); const hideMenuInput li.querySelector(.edit-hide-menu); const passwordProtectedInput li.querySelector(.edit-password-protected); const passwordInput li.querySelector(.edit-password); const title titleInput ? titleInput.value.trim() : ; const newSlugRaw slugInput ? slugInput.value.trim() : null; const externalUrl externalUrlInput ? externalUrlInput.value.trim() : ; const metaTitle metaTitleInput ? metaTitleInput.value.trim() : ; const metaDescription metaDescriptionInput ? metaDescriptionInput.value.trim() : ; const hideMenu hideMenuInput ? hideMenuInput.checked : false; const passwordProtected passwordProtectedInput ? passwordProtectedInput.checked : false; const password passwordInput ? passwordInput.value.trim() : ; const gallery findGalleryById(galleries, id); if (!gallery) { console.error(Could not find gallery object for saveEdit, ID:, id); renderGalleries(); return; } if (!title && !gallery.isSpacer) { alert(Please enter a title); return; } // Validate password protection if (passwordProtected && !password) { alert(Please enter a password when password protection is enabled); return; } gallery.title title; gallery.metaTitle metaTitle; gallery.metaDescription metaDescription; gallery.hideMenuOnPage hideMenu; // Save the new property // Save password protection settings gallery.passwordProtected passwordProtected; if (passwordProtected) { gallery.password password; } else { delete gallery.password; // Remove password if protection is disabled } if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw ! null) { const originalSlug gallery.slug || (gallery.url && gallery.url.startsWith(/) ? gallery.url.substring(1) : ); let sanitizedSlug slugify(newSlugRaw); if (!sanitizedSlug) { sanitizedSlug slugify(gallery.title); } if (sanitizedSlug ! originalSlug) { const otherGalleries galleries.filter(g > g.id ! id); gallery.slug ensureUniqueSlug(sanitizedSlug, otherGalleries); gallery.url `/${gallery.slug}`; } else { if (gallery.url ! `/${gallery.slug}`) { gallery.url `/${gallery.slug}`; } } } // Update external URL if this is an external link if (gallery.isExternal && externalUrl) { gallery.url externalUrl; } saveGalleries(); renderGalleries(); // If the currently active gallery was just edited, re-apply menu visibility if (gallery.id activeGalleryId) { const bodyEl document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { bodyEl.classList.add(menu-hidden-on-page); } else { bodyEl.classList.remove(menu-hidden-on-page); } }}// Helper function to check if a gallery is an ancestor of anotherfunction hasAncestor(galleryId, ancestorId) {if (galleryId ancestorId) return true;const gallery galleries.find(g > g.id galleryId);if (!gallery || !gallery.parentId) return false;if (gallery.parentId ancestorId) return true;return hasAncestor(gallery.parentId, ancestorId);}function deleteGallery(id, event) { event.stopPropagation(); if (confirm(Are you sure you want to delete this gallery?)) { galleries galleries.filter(g > g.id ! id); if (activeGalleryId id) { activeGalleryId null; document.getElementById(galleryFrame).src ; } saveGalleries(); renderGalleries(); }}function debugNestedStructure() {console.group(Current Gallery Structure);function printGallery(gallery, level 0) {const indent .repeat(level);console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || none})`);if (gallery.children && gallery.children.length > 0) { gallery.children.forEach(child > printGallery(child, level + 1));}} // Create a tree structure for debuggingconst galleryTree createGalleryTree(galleries);galleryTree.forEach(gallery > printGallery(gallery));console.groupEnd();}function toggleHome(id, event) { event.stopPropagation(); console.log(Setting home page to gallery:, id); // Find gallery const gallery galleries.find(g > g.id id); if (!gallery) return; // Check if this gallery is already set as home const alreadyHome gallery.isHomePage true; // Set all galleries to not be the home page galleries.forEach(g > { g.isHomePage false; }); // Toggle this gallerys home status - if it was already home, were unsetting it gallery.isHomePage !alreadyHome; // Save changes saveGalleries(); // Update UI renderGalleries(); // If this gallery is now the home page, display a message if (gallery.isHomePage) { const title gallery.title || This page; showSuccessMessage(`${title} is now set as the Home page`); }}// Function to toggle gallery visibilityfunction toggleVisibility(id, event) { event.stopPropagation(); const gallery galleries.find(g > g.id id); if (gallery) { // Toggle the visibility gallery.visible gallery.visible false ? true : false; // Save the change to server saveGalleries(); // Update UI - important to toggle the class on the button itself const toggle event.currentTarget; if (toggle) { if (gallery.visible false) { toggle.classList.add(hidden); // Update the SVG icon toggle.querySelector(svg).innerHTML path dM17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24>/path>line x11 y11 x223 y223>/line>; } else { toggle.classList.remove(hidden); // Update the SVG icon toggle.querySelector(svg).innerHTML path dM1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z>/path>circle cx12 cy12 r3>/circle>; } } // Update parent row class const listItem document.querySelector(`lidata-id${id}`); if (listItem) { if (gallery.visible false) { listItem.classList.add(hidden-gallery); } else { listItem.classList.remove(hidden-gallery); } } // Only clear iframe if not in edit mode if (id activeGalleryId && gallery.visible false && !isEditing) { document.getElementById(galleryFrame).src ; } // Re-render menus based on current layout const currentLayout document.body.classList.contains(menu-layout-horizontal) ? horizontal : document.body.classList.contains(menu-layout-top) ? top : sidebar; if (currentLayout horizontal && typeof window.renderHorizontalMenu function) { window.renderHorizontalMenu(); } // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied // This is especially important when horizontal layout is active but were viewing the sidebar in edit mode if (typeof window.renderGalleries function) { window.renderGalleries(); } else if (typeof renderGalleries function) { renderGalleries(); } }}/*** Simplified loadGallery function that follows the same content swap pattern* @param {number} id - The gallery ID* @param {Event} event - Optional event object*/// Enhanced loadGallery function with script cleanupasync function loadGallery(id, event) {// Stop event propagation if providedif (event) { event.stopPropagation();}const galleryContainer document.querySelector(.gallery-container);if (galleryContainer) { // Apply immediate hiding styles galleryContainer.style.opacity 0;}console.log(Loading gallery with ID:, id);// Find gallery by IDconst gallery findGalleryById(galleries, id); // Assuming `galleries` is accessibleif (!gallery) { console.warn(No gallery found with ID:, id); if (galleryContainer) { galleryContainer.style.opacity 1; } return;}// Check if this is a page, if so use loadPage insteadif (gallery.isPage true) { // Use the loadPage function defined in this scope (or window.loadPage if you prefer) return loadPage(id, event);}// Skip submenu itemsif (gallery.isSubmenu) { console.log(Gallery is submenu, skipping URL update); toggleSubmenu(id, event || { stopPropagation: () > {} }); // Assuming toggleSubmenu is available if (galleryContainer) { galleryContainer.style.opacity 1; } // Show container return;}console.log(Found gallery:, gallery.title);// CRITICAL: Set active gallery ID consistently in both global and window scopeconsole.log(Setting active gallery ID to:, id, (Previous value:, (window.activeGalleryId || activeGalleryId), ));window.activeGalleryId id;activeGalleryId id; // Ensure both variables are synchronized// Save reference to which gallery were loading (for later verification)window.currentLoadingGalleryId id;// CRITICAL: Thorough cleanup with await to ensure completionif (typeof window.removeGalleryScriptsWithPause function) { await window.removeGalleryScriptsWithPause();}// Clear all existing content from gallery containerif (!galleryContainer) { console.error(Gallery container not found); return;}// Force clear all contentgalleryContainer.innerHTML ; // CRITICAL: Double-check active gallery ID before updating UI if (window.activeGalleryId ! id || activeGalleryId ! id) { console.warn(Active gallery ID changed unexpectedly before UI update, restoring to:, id); window.activeGalleryId id; activeGalleryId id; }// Apply menu visibility based on the gallerys setting and edit modeconst bodyEl document.body;if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add(menu-hidden-on-page);} else { bodyEl.classList.remove(menu-hidden-on-page); // Ensure menu is visible if in edit mode or not hidden}// Update UI state using explicit window function references or local fallbacksif (typeof window.updateActiveStates function) { window.updateActiveStates();} else if (typeof updateActiveStates function) { updateActiveStates();}if (typeof window.updateMobileTitle function) { window.updateMobileTitle();} else if (typeof updateMobileTitle function) { updateMobileTitle();}if (typeof window.closeMobileMenu function) { window.closeMobileMenu();} else if (typeof closeMobileMenu function) { closeMobileMenu();}// Update URL with gallery slugif (typeof window.updateURLWithGallerySlug function) { window.updateURLWithGallerySlug(gallery);} else if (typeof updateURLWithGallerySlug function) { updateURLWithGallerySlug(gallery);} // Create gallery container and load contentconst neonGalleryContainer document.createElement(div);neonGalleryContainer.id neon-gallery-container;neonGalleryContainer.className gallery-direct-content;neonGalleryContainer.setAttribute(data-gallery-id, gallery.id.toString());neonGalleryContainer.style.width 100%;neonGalleryContainer.style.height 100%;galleryContainer.appendChild(neonGalleryContainer);// Add a cache-busting parameter to ensure script is freshly loadedconst timestamp Date.now();// Set up gallery parameterswindow.Parameters { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available isInEditor: window.isEditing || false, // Assuming window.isEditing is available siteId: window.siteId || gallery.siteId || , // Assuming window.siteId is available isHydra: true, galleryInstanceId: timestamp, // Added loadedGalleryId as per your original function loadedGalleryId: gallery.id};// Debug: Check what galleryOptions containsconsole.error(🔍 INDEX.TS DEBUG - Gallery:, gallery.title, has isPerma:, gallery.galleryOptions?.isPerma, permaURL:, gallery.galleryOptions?.permaURL, loadTxId:, gallery.galleryOptions?.loadTxId, loadPermaURL:, gallery.galleryOptions?.loadPermaURL);// Set up gallery configwindow.neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: gallery.galleryOptions?.manualCollectionName || mod, layoutType: gallery.galleryOptions?.layoutType || grid, // Spread gallery options AFTER setting defaults to ensure current gallerys settings take precedence // This ensures that only properties from the current gallery are included, not from previous galleries ...(gallery.galleryOptions || {}), // CRITICAL: Explicitly include Load Network fields to ensure theyre passed to gallery script // These fields are required for the gallery to determine if it should use Load Network racing loadTxId: gallery.galleryOptions?.loadTxId || null, loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, siteId: window.siteId || gallery.siteId || , // Assuming window.siteId is available // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread galleryId: gallery.id, galleryInstanceId: timestamp};console.error(🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:, window.neonGalleryConfig.isPerma, permaURL:, window.neonGalleryConfig.permaURL, loadTxId:, window.neonGalleryConfig.loadTxId, loadPermaURL:, window.neonGalleryConfig.loadPermaURL);// Create and add the gallery script with cache-busting// Use Worker route (avoids CDN SSL issues)const script document.createElement(script);const galleryScriptsBase window.location.origin + /gallery-scripts/;script.src galleryScriptsBase + neon-gallery-main-v260119-004.js; // Using versioned filescript.setAttribute(data-gallery-id, gallery.id.toString()); // Keep data-gallery-id attributescript.setAttribute(data-timestamp, timestamp.toString()); // Keep data-timestamp attributeconsole.log(Loading gallery-main script from Worker:, script.src);script.onerror function() { console.error(❌ Gallery script failed, retrying via proxy:, script.src); const proxyUrl script.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); script.src proxyUrl; script.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); script.src script.src.replace(cdn.neonsky.app, storage.neonsky.app); };};// Added script.onload from your original functionscript.onload function() { if (window.currentLoadingGalleryId ! gallery.id) { console.warn(Gallery ID mismatch! Expected:, gallery.id, Current:, window.currentLoadingGalleryId); } console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);};if (galleryContainer) { setTimeout(() > { galleryContainer.style.opacity 1; }, 50); // Small delay to ensure content is ready }console.log(`Loading fresh gallery script for: ${gallery.title}`);document.body.appendChild(script);}function removeGalleryScriptsWithPause() { return new Promise(resolve > { console.log(Performing comprehensive gallery cleanup (v2 - refined selectors)); // 1. Capture and clear any gallery data in localStorage (remains the same) try { const localStorageKeys Object.keys(localStorage); const galleryStorageKeys localStorageKeys.filter(key > key.includes(gallery) || key.includes(neon) || key.includes(lightbox) ); galleryStorageKeys.forEach(key > { localStorage.removeItem(key); }); } catch (e) { console.warn(Error clearing localStorage:, e); } // @ts-ignore window._galleryPageContext null; // (remains the same) // 3. Remove all gallery-specific CSS styles (remains the same) const galleryStyles document.querySelectorAll(styledata-gallery, linkhref*neon-gallery); galleryStyles.forEach(style > { if (style && style.parentNode) { // console.log(Removing gallery style element); // Console log can be verbose, optionally keep style.parentNode.removeChild(style); } }); // 4. Remove lightbox elements (more specific selectors now) // These are specific IDs and classes for lightbox components. const lightboxComponentSelectors #neon-lightbox, // Main lightbox container by ID .neon-lightbox, // Alternative class for main container .lightbox-container,// General container class if used // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements ; const lightboxElements document.querySelectorAll(lightboxComponentSelectors.join(, )); lightboxElements.forEach(lightboxEl > { if (lightboxEl && lightboxEl.parentNode && lightboxEl ! document.body && lightboxEl ! document.documentElement) { // console.log(Removing lightbox component:, lightboxEl.id || lightboxEl.className); lightboxEl.parentNode.removeChild(lightboxEl); } }); // 5. Remove other gallery-specific DOM elements // REMOVED: class*lightbox, id*lightbox from this list // The selector class*neon- is still broad but less likely to match body unless you add neon- class to body. const galleryElementSelectors .neon-gallery-wrapper, .gallery-container .fullscreen-overlay, .neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, .neon-thumbnails, .neon-image, .neon-caption, .neon-controls, .neon-pagination, class*neon-, // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues. data-gallery-id, data-image-id, .image-grid, .image-masonry // Ensure none of these selectors inadvertently match document.body or critical layout containers ; const galleryElements document.querySelectorAll(galleryElementSelectors.join(, )); if (galleryElements.length > 0) { galleryElements.forEach(el > { // CRITICAL FIX: Add checks to ensure we dont remove body or html if (el && el.parentNode && el ! document.body && el ! document.documentElement) { // console.log(Removing gallery-specific DOM element:, el.id || el.className); el.parentNode.removeChild(el); } else if (el document.body || el document.documentElement) { console.warn(`CRITICAL WARNING Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`); } }); } // 6. Find and remove ALL gallery scripts (remains the same) const scripts document.querySelectorAll( scriptsrc*neon-gallery-main-hydra, scriptsrc*neon-gallery-main-dev.js, + scriptsrc*gallery, scriptdata-gallery-id, scriptdata-gallery, scriptdata-timestamp ); scripts.forEach(script > { if (script && script.parentNode) { // console.log(Removing script:, script.src || script.getAttribute(data-gallery-id)); script.parentNode.removeChild(script); } }); // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries) const optionsScripts document.querySelectorAll( scriptsrc*galleryOptions, scriptdata-options-js ); optionsScripts.forEach(script > { if (script && script.parentNode) { console.log(Removing gallery options script:, script.src || script.getAttribute(data-options-js)); script.parentNode.removeChild(script); } }); // NOTE: We intentionally do NOT remove gallery options CSS here // The CSS is needed across galleries and doesnt cause conflicts like scripts do setTimeout(() > { const remainingScripts document.querySelectorAll( scriptsrc*neon-gallery-main-hydra, scriptsrc*neon-gallery-main-dev.js, + scriptsrc*gallery, scriptdata-gallery-id, scriptdata-gallery, scriptdata-timestamp ); if (remainingScripts.length > 0) { // console.warn(`DEBUG ${remainingScripts.length} gallery scripts still found after removal attempt.`); } }, 50); // 7. Reset ALL known global state variables used by the gallery (remains the same) const resetGlobals neonGalleryInitComplete, neonGalleryInitInProgress, neonGalleryLoaded, neonGalleryConfig, neonGalleryState, neonGalleryCache, neonGalleryImages, neonGallerySettings, neonLightbox, neonGalleryEventListeners, neonGalleryInstance, currentGalleryId, galleryData, imageCache, thumbnailCache, ; resetGlobals.forEach(prop > { // @ts-ignore if (windowprop ! undefined) { // @ts-ignore windowprop null; } }); // 7b. Reset gallery options panel state // Reset optionsVisible flag if it exists on window (for cross-module access) // @ts-ignore if (window.optionsVisible ! undefined) { // @ts-ignore window.optionsVisible false; } // Reset options-related globals const optionsGlobals generateOptionsUI, openOptionsOverlay, showOptionsPanel, hideOptionsPanel, toggleOptionsOverlay, optionsVisible ; optionsGlobals.forEach(prop > { // @ts-ignore if (windowprop ! undefined && typeof windowprop ! function) { // Only reset non-function properties (like flags) // Functions should remain available // @ts-ignore if (prop optionsVisible) { // @ts-ignore windowprop false; } } }); // Short pause before resolving setTimeout(resolve, 150); });}// Fixed clearAddForm function with proper variable namesfunction clearAddForm() {// Clear the title inputconst titleInput document.getElementById(galleryTitle);if (titleInput) {titleInput.value ;}// Clear the URL inputconst urlInput document.getElementById(galleryUrl);if (urlInput) {urlInput.value ;}// Reset the parent selectionconst parentSelect document.getElementById(galleryParent);if (parentSelect) {parentSelect.value ;}// Reset radio buttons to externalconst externalRadio document.querySelector(inputnamegalleryTypevalueexternal);if (externalRadio) {externalRadio.checked true;}// Show URL input container - using a different variable name!const urlInputContainer document.getElementById(urlInputContainer);if (urlInputContainer) {urlInputContainer.style.display block;} else {console.log(URL input container not found);}}/** * Ensures window.galleries is synchronized with the global galleries variable * Call this function after any modification to gallery settings * * @returns {boolean} True if synchronization was successful *//** * Enhanced synchronizeGalleries function that checks ALL galleries * not just imported ones, and ensures settings are preserved * @returns {boolean} Success indicator */function synchronizeGalleries() { console.log(Starting enhanced gallery synchronization...); if (typeof galleries ! undefined && Array.isArray(galleries)) { // First check if window.galleries exists and is different from galleries const needsSync !window.galleries || !Array.isArray(window.galleries) || window.galleries.length ! galleries.length; // ENHANCED: Check ALL galleries for options preservation, not just imported ones if (!needsSync && window.galleries.length > 0) { // Find all galleries with galleryOptions const galleriesWithOptions galleries.filter(g > g.galleryOptions && Object.keys(g.galleryOptions).length > 4 ); if (galleriesWithOptions.length > 0) { // Check ALL galleries with substantial options, not just a sample let syncNeeded false; // Use for loop so we can break early if we find a mismatch for (let i 0; i galleriesWithOptions.length; i++) { const gallery galleriesWithOptionsi; const windowGallery window.galleries.find(g > g.id gallery.id); if (windowGallery) { // Get options depth const galleryOptionsDepth gallery.galleryOptions ? Object.keys(gallery.galleryOptions).length : 0; const windowOptionsDepth windowGallery.galleryOptions ? Object.keys(windowGallery.galleryOptions).length : 0; // If options depth differs significantly, we need to sync if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) { console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`); console.log(`Options count: global${galleryOptionsDepth}, window${windowOptionsDepth}`); syncNeeded true; break; // No need to check more galleries } // Additional check: Look for specific important gallery settings that should be preserved // Only do this check for galleries with substantial settings if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) { // Critical keys that indicate gallery customization const criticalKeys columns, spacing, layoutType, startInSingles, autoplaySingles, lightboxBgColor; // Check if any critical keys are missing in window.galleries const missingKeys criticalKeys.filter(key > gallery.galleryOptionskey ! undefined && windowGallery.galleryOptionskey undefined ); if (missingKeys.length > 0) { console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`); console.log(`Missing keys: ${missingKeys.join(, )}`); syncNeeded true; break; // No need to check more galleries } } } } // If we detected a need to sync, do it now if (syncNeeded) { // Ensure we preserve rich gallery settings window.galleries galleries.map(gallery > { const windowGallery window.galleries.find(g > g.id gallery.id); // If both have galleryOptions, merge them to ensure all settings are preserved if (windowGallery && windowGallery.galleryOptions && gallery.galleryOptions) { const globalOptionsDepth Object.keys(gallery.galleryOptions).length; const windowOptionsDepth Object.keys(windowGallery.galleryOptions).length; // If window gallery has more settings, merge them with global settings if (windowOptionsDepth > globalOptionsDepth) { return { ...gallery, galleryOptions: { ...windowGallery.galleryOptions, // Start with window settings ...gallery.galleryOptions // Override with any new global settings } }; } } // Otherwise use global gallery as is return gallery; }); console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); return true; } } } // If we determined sync is needed (or as a precaution), update window.galleries if (needsSync || !window.galleries) { window.galleries ...galleries; // Create a shallow copy to ensure different reference console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); } else { console.log(Gallery synchronization check: No sync needed); } // Verify that all galleries with rich settings have their full options preserved const richGalleries galleries.filter(g > g.galleryOptions && Object.keys(g.galleryOptions).length > 5 ); console.log(`Status: Found ${richGalleries.length} galleries with rich settings`); // Final verification - make sure both references are identical if (window.galleries ! galleries) { console.warn(References not identical after sync - forcing reference equality); window.galleries galleries; // Force reference equality return true; } return true; } else { console.warn(Cannot synchronize galleries: global galleries variable is undefined or not an array); // If we have window.galleries but no global galleries, try to reverse-sync if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) { console.log(Attempting reverse sync: updating global galleries from window.galleries); try { // This is risky and should only happen in unusual circumstances galleries window.galleries; return true; } catch (e) { console.error(Could not reverse-sync galleries:, e); } } return false; }}/** * Loads NDJSON content from the original gallery * @param {string} siteId - The site ID * @param {string} pageId - The page ID of the original gallery * @returns {Promisestring|null>} - The NDJSON content or null if not found/error */async function loadGalleryNDJSON(siteId, pageId) { console.error(NDJSON Duplication loadGalleryNDJSON called with siteId:, siteId, pageId:, pageId); if (!siteId || !pageId) { console.error(NDJSON Duplication ERROR: Cannot load NDJSON - missing siteId or pageId, { siteId: siteId, pageId: pageId }); return null; } try { const ndjsonFilename `${siteId}_${pageId}.ndjson`; const url `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`; console.error(NDJSON Duplication Attempting to load NDJSON from:, url); console.error(NDJSON Duplication Filename:, ndjsonFilename); const response await fetch(url); console.error(NDJSON Duplication Fetch response status:, response.status, response.statusText); if (!response.ok) { if (response.status 404) { console.error(NDJSON Duplication ERROR: NDJSON file not found (404) - gallery may not have been published yet); console.error(NDJSON Duplication Attempted URL:, url); return null; } const errorText await response.text().catch(() > Could not read error text); console.error(NDJSON Duplication ERROR: Failed to load NDJSON:, response.status, response.statusText, errorText); throw new Error(`Failed to load NDJSON: ${response.statusText}`); } const ndjsonContent await response.text(); console.error(NDJSON Duplication SUCCESS: Loaded NDJSON, length:, ndjsonContent.length, characters); console.error(NDJSON Duplication First 200 chars:, ndjsonContent.substring(0, 200)); return ndjsonContent; } catch (error) { console.error(NDJSON Duplication ERROR: Exception loading NDJSON:, error); console.error(NDJSON Duplication Error message:, error.message); console.error(NDJSON Duplication Error stack:, error.stack); return null; }}/** * Duplicates and publishes NDJSON for a gallery * @param {Object} originalGallery - The original gallery object * @param {Object} newGallery - The new gallery object with new pageId * @returns {Promiseboolean>} - Success indicator */async function duplicateGalleryNDJSON(originalGallery, newGallery) { console.error(NDJSON Duplication STARTING duplicateGalleryNDJSON ); console.error(NDJSON Duplication Original gallery:, { id: originalGallery.id, title: originalGallery.title, pageId: originalGallery.pageId, classicGuid: originalGallery.classicGuid, hasGalleryOptions: !!originalGallery.galleryOptions, manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, isClassicCollection: originalGallery.galleryOptions?.isClassicCollection }); console.error(NDJSON Duplication New gallery:, { id: newGallery.id, title: newGallery.title, pageId: newGallery.pageId }); try { // Check if this is an NDJSON-based gallery (not classic collection) // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files // Only skip if its explicitly marked as a classic collection OR has no pageId (which means no NDJSON) // Having a pageId means it uses NDJSON files, so we should duplicate them const isClassicCollection originalGallery.galleryOptions?.isClassicCollection true; console.error(NDJSON Duplication Classic collection check:, { hasClassicGuid: !!originalGallery.classicGuid, manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith(GUID), isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection, hasPageId: !!originalGallery.pageId, isClassicCollection: isClassicCollection, willSkip: isClassicCollection && !originalGallery.pageId }); // Only skip if explicitly marked as classic collection AND has no pageId // If it has a pageId, it uses NDJSON files and we should duplicate them if (isClassicCollection && !originalGallery.pageId) { console.error(NDJSON Duplication SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication); return true; // Not an error, just not applicable } // Check if original gallery has a pageId if (!originalGallery.pageId) { console.error(NDJSON Duplication SKIP: Original gallery has no pageId, skipping NDJSON duplication); console.error(NDJSON Duplication Original gallery keys:, Object.keys(originalGallery)); return true; // Not an error, just not applicable } // Get siteId const siteId window.siteId || originalGallery.siteId; console.error(NDJSON Duplication SiteId resolution:, { windowSiteId: window.siteId, gallerySiteId: originalGallery.siteId, resolvedSiteId: siteId }); if (!siteId) { console.error(NDJSON Duplication ERROR: Cannot duplicate NDJSON - missing siteId); console.error(NDJSON Duplication window.siteId:, window.siteId); console.error(NDJSON Duplication originalGallery.siteId:, originalGallery.siteId); return false; } // Load original NDJSON console.error(NDJSON Duplication Loading original NDJSON for duplication...); console.error(NDJSON Duplication Using siteId:, siteId, pageId:, originalGallery.pageId); const originalNdjson await loadGalleryNDJSON(siteId, originalGallery.pageId); if (!originalNdjson) { console.error(NDJSON Duplication ERROR: Could not load original NDJSON, gallery may not have been published yet); console.error(NDJSON Duplication This means the original gallery\s NDJSON file was not found); return false; } // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher console.error(NDJSON Duplication Publishing duplicated NDJSON with new pageId:, newGallery.pageId); const dataFileName `${siteId}_${newGallery.pageId}.ndjson`; console.error(NDJSON Duplication New filename:, dataFileName); console.error(NDJSON Duplication NDJSON content length:, originalNdjson.length); try { // Call the publish endpoint (same as edition publisher) console.error(NDJSON Duplication Calling publish endpoint: https://hydra-press-v2.fly.dev/publish); const response await fetch(https://hydra-press-v2.fly.dev/publish, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ ndjsonContent: originalNdjson, dataFileName: dataFileName }), }); console.error(NDJSON Duplication Publish response status:, response.status, response.statusText); console.error(NDJSON Duplication Response headers:, Object.fromEntries(response.headers.entries())); if (!response.ok) { const errorText await response.text().catch(() > Unknown error); console.error(NDJSON Duplication ERROR: Failed to publish duplicated NDJSON:, response.status, errorText); console.error(NDJSON Duplication Response status:, response.status); console.error(NDJSON Duplication Response statusText:, response.statusText); return false; } const result await response.json(); console.error(NDJSON Duplication SUCCESS: Published duplicated NDJSON); console.error(NDJSON Duplication Publish result:, JSON.stringify(result, null, 2)); console.error(NDJSON Duplication Publish result URLs:, result.urls); // Note: The publish endpoint doesnt return Irys txId, so we dont update galleryOptions // with txId/permaURL here. If Irys upload is needed, it would need to be done separately. console.error(NDJSON Duplication duplicateGalleryNDJSON COMPLETED SUCCESSFULLY ); return true; } catch (error) { console.error(NDJSON Duplication ERROR: Exception publishing duplicated NDJSON:, error); console.error(NDJSON Duplication Error message:, error.message); console.error(NDJSON Duplication Error stack:, error.stack); return false; } } catch (error) { console.error(NDJSON Duplication ERROR: Unexpected error in duplicateGalleryNDJSON:, error); console.error(NDJSON Duplication Error message:, error.message); console.error(NDJSON Duplication Error stack:, error.stack); return false; }}/** * Creates and shows a loading overlay for gallery duplication * @returns {Object} Object with hide function to remove the overlay */function showDuplicationOverlay() { // Remove any existing overlay first const existingOverlay document.getElementById(duplication-overlay); if (existingOverlay) { existingOverlay.remove(); } // Add styles if not already present if (!document.querySelector(#duplication-overlay-styles)) { const style document.createElement(style); style.id duplication-overlay-styles; style.textContent ` #duplication-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 99999999; display: flex; align-items: center; justify-content: center; cursor: not-allowed; pointer-events: all; } #duplication-overlay .duplication-message { background: rgba(255, 255, 255, 0.95); padding: 30px 50px; font-family: Arial, sans-serif; font-size: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; flex-direction: column; align-items: center; gap: 20px; } #duplication-overlay .duplication-spinner { width: 80px; height: 80px; color: #444444; } `; document.head.appendChild(style); } const overlay document.createElement(div); overlay.id duplication-overlay; const message document.createElement(div); message.className duplication-message; const spinner document.createElement(div); spinner.className duplication-spinner; spinner.innerHTML ` svg xmlnshttp://www.w3.org/2000/svg width80 height80 viewBox0 0 24 24> defs> filter idsvgSpinnersGooeyBalls20> feGaussianBlur inSourceGraphic resulty stdDeviation1/> feColorMatrix iny resultz values1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7/> feBlend inSourceGraphic in2z/> /filter> /defs> g filterurl(#svgSpinnersGooeyBalls20)> circle cx5 cy12 r4 fillcurrentColor> animate attributeNamecx calcModespline dur2s keySplines.36,.62,.43,.99;.79,0,.58,.57 repeatCountindefinite values5;8;5/> /circle> circle cx19 cy12 r4 fillcurrentColor> animate attributeNamecx calcModespline dur2s keySplines.36,.62,.43,.99;.79,0,.58,.57 repeatCountindefinite values19;16;19/> /circle> animateTransform attributeNametransform dur0.75s repeatCountindefinite typerotate values0 12 12;360 12 12/> /g> /svg> `; const text document.createElement(div); text.textContent Duplicating gallery...; text.style.textAlign center; message.appendChild(spinner); message.appendChild(text); overlay.appendChild(message); document.body.appendChild(overlay); return { hide: function() { if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } }, updateMessage: function(newMessage) { text.textContent newMessage; } };}/** * Duplicates a gallery or page item. * @param {number} id - The ID of the item to duplicate. * @param {Event} event - The click event. */async function duplicateGalleryItem(id, event) { event.stopPropagation(); console.log(`Duplicating item with ID: ${id}`); // Show loading overlay const overlay showDuplicationOverlay(); // Find the original item in the global galleries array const originalItem findGalleryById(galleries, id); // Use helper if available, otherwise simple find if (!originalItem) { console.error(`Cannot duplicate: Item with ID ${id} not found.`); overlay.hide(); alert(Error: Could not find the item to duplicate.); return; } try { // --- Create a Deep Copy --- // Using JSON.parse/stringify is a common way for simple object structures // Be cautious if your objects have Dates, functions, Maps, Sets, etc. const newItem JSON.parse(JSON.stringify(originalItem)); // --- Modify Copied Item --- newItem.id Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID newItem.title `${originalItem.title}-copy`; // Append -copy to title // Generate new pageId if its a page or integrated gallery if (newItem.isPage || newItem.isIntegrated) { newItem.pageId generatePageId(); // Assign a new unique pageId } // Generate new slug and URL const newBaseSlug slugify(newItem.title); // Use the new title newItem.slug ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness newItem.url `/${newItem.slug}`; // Update URL based on new slug // Reset children (dont duplicate children for simplicity) newItem.children ; // Deep clone galleryOptions and pageElements, update pageId if needed if (newItem.galleryOptions) { newItem.galleryOptions JSON.parse(JSON.stringify(newItem.galleryOptions)); // Update pageId within options if it exists and matches the old one if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId originalItem.pageId) { newItem.galleryOptions.pageId newItem.pageId; } } if (newItem.pageElements) { newItem.pageElements JSON.parse(JSON.stringify(newItem.pageElements)); // Give new IDs to duplicated page elements newItem.pageElements.forEach(el > { el.id Date.now() + Math.floor(Math.random() * 10000); }); } // Reset home page status newItem.isHomePage false; // --- Duplicate NDJSON if applicable --- // This must happen before saving the gallery console.error(Gallery Duplication Checking if NDJSON duplication is needed...); console.error(Gallery Duplication newItem.pageId:, newItem.pageId); console.error(Gallery Duplication originalItem.pageId:, originalItem.pageId); if (newItem.pageId && originalItem.pageId) { overlay.updateMessage(Duplicating gallery files...); console.error(Gallery Duplication Both galleries have pageIds, attempting to duplicate NDJSON file...); const ndjsonDuplicated await duplicateGalleryNDJSON(originalItem, newItem); if (!ndjsonDuplicated) { console.error(Gallery Duplication WARNING: NDJSON duplication failed, but continuing with gallery duplication); console.error(Gallery Duplication User can republish the gallery later if needed); // Dont block the duplication if NDJSON fails - user can republish later } else { console.error(Gallery Duplication SUCCESS: NDJSON duplication completed successfully); } } else { console.error(Gallery Duplication SKIP: Skipping NDJSON duplication:, { hasNewPageId: !!newItem.pageId, hasOriginalPageId: !!originalItem.pageId, reason: !newItem.pageId ? New gallery has no pageId : Original gallery has no pageId }); } overlay.updateMessage(Saving duplicated gallery...); // --- Insert into Galleries Array --- // Find the index of the original item const originalIndex galleries.findIndex(item > item.id id); if (originalIndex > -1) { // Insert the new item right after the original galleries.splice(originalIndex + 1, 0, newItem); console.log(`Inserted duplicate ${newItem.title} after ${originalItem.title}`); // Update positions for all items after the insertion point for (let i originalIndex + 1; i galleries.length; i++) { galleriesi.position i; // Re-assign position based on new index } } else { // Fallback: Add to the end if original couldnt be found (shouldnt happen) console.warn(`Original item ${id} not found in array, adding duplicate to the end.`); newItem.position galleries.length; galleries.push(newItem); } console.log(Rendering galleries immediately after duplication...); renderGalleries(); // Update active states if necessary (though unlikely needed here) updateActiveStates(); // --- Save and Refresh --- console.log(Saving duplicated item...); saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh .then(success > { overlay.hide(); // Hide overlay before page refresh if (success) { console.log(Duplicate saved successfully. Page will refresh.); // No need to call renderGalleries() here because saveGalleries will trigger a reload. } else { console.error(Failed to save the duplicated item.); alert(Error: Could not save the duplicated item.); // Optionally try to revert the galleries array change here if save fails } }) .catch(error > { overlay.hide(); // Hide overlay on error console.error(Error saving duplicated item:, error); alert(Error saving duplicated item.); }); } catch (error) { overlay.hide(); // Hide overlay on error console.error(Error duplicating item:, error); alert(An error occurred while duplicating the item.); }}/** * saveGalleries function that always saves FULL data * Ensures gallery settings are preserved in both localStorage and server * * @param {Object} customData - Optional custom data to save instead of galleries * @returns {Promiseboolean>} - Success indicator */async function saveGalleries(customData) { if (!isAuthenticated && localStorage.getItem(hydra_is_admin) ! true) { alert(You must be logged in as an admin to save changes); return false; } // Prevent multiple simultaneous saves if (window._saveInProgress) { console.log(Save already in progress, skipping); return false; } window._saveInProgress true; try { // CRITICAL: Synchronize galleries before saving to ensure we use the latest data if (typeof synchronizeGalleries function) { synchronizeGalleries(); console.log(Galleries synchronized before saving); } // Try to get a valid token from multiple sources let token null; // First try the global didToken (traditional approach) if (didToken) { console.log(Using global didToken for save operation); token didToken; } // Next try TokenManager if available else if (window.TokenManager && typeof window.TokenManager.getToken function) { token window.TokenManager.getToken(); // Remove Bearer prefix if present (well add it later) if (token && token.startsWith(Bearer )) { token token.substring(7); } console.log(Using TokenManager token for save operation, length:, token ? token.length : 0); } // Fallback to localStorage else if (localStorage.getItem(hydra_auth_token)) { token localStorage.getItem(hydra_auth_token); console.log(Using localStorage token for save operation, length:, token.length); } // Last resort: try to get a fresh token else if (magic && magic.user) { try { console.log(Attempting to get fresh token from Magic); token await magic.user.getIdToken(); console.log(Obtained fresh token from Magic:, token ? success : failed); } catch (e) { console.error(Error getting token from Magic:, e); } } if (!token) { throw new Error(Could not find a valid authentication token. Please log in again.); } // IMPORTANT: Try to save to localStorage with isolated error handling // This ensures localStorage failures dont prevent server saves if (!customData) { // Only try localStorage for regular gallery saves try { // FIXED: Always save the FULL galleries data to localStorage localStorage.setItem(galleries, JSON.stringify(galleries)); console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`); } catch (storageError) { // Now storage errors are isolated and non-fatal console.warn(LocalStorage quota exceeded, proceeding without local backup, storageError); } } else if (customData && customData.isImport) { // Skip localStorage for import operations - data too large console.log(Skipping localStorage for import operation - data too large for local storage); } // IMPORTANT: Ensure all pages have proper URLs and slugs before saving galleries.forEach(gallery > { if (gallery.isPage true) { const pageSlug slugify(gallery.title); // Make sure slug and URL are set correctly if (!gallery.slug || gallery.slug ) { gallery.slug pageSlug; } // Make sure URL points to slug if (!gallery.url || gallery.url / || !gallery.url.includes(gallery.slug)) { gallery.url `/${pageSlug}`; } } }); // Set up the data to save - ensure were using the complete data let siteData; console.log(saveGalleries: customData received:, JSON.stringify(customData)); console.log(saveGalleries: siteMetadata in customData:, customData?.siteMetadata); console.log(saveGalleries: TITLE in customData:, customData?.siteMetadata?.title); console.log(saveGalleries: GOOGLE ANALYTICS in customData:, customData?.siteMetadata?.googleAnalytics); if (customData) { // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar // If customData contains galleries (which it should for sidebar-initiated saves), // merge them with any existing settings to ensure nothing is lost if (customData.galleries && Array.isArray(customData.galleries)) { // First, ensure we preserve all current gallery settings from the existing galleries const existingGalleries (typeof galleries ! undefined ? galleries : window.galleries) || ; // Create merged galleries array with preserved settings const mergedGalleries customData.galleries.map(gallery > { const existingGallery existingGalleries.find(g > g.id gallery.id); // If this gallery exists in the current dataset, ensure all galleryOptions are preserved if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) { // Calculate settings depth const existingOptionsDepth Object.keys(existingGallery.galleryOptions).length; const serverOptionsDepth Object.keys(gallery.galleryOptions).length; // If server has fewer settings, merge them with existing settings if (serverOptionsDepth existingOptionsDepth && existingOptionsDepth > 5) { console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); // Create a merged gallery with preserved settings return { ...gallery, galleryOptions: { ...existingGallery.galleryOptions, // Start with all existing settings ...gallery.galleryOptions // Override with any new server settings } }; } } // If no preservation needed, use server gallery as is return gallery; }); // Use the merged galleries in customData siteData { ...customData, galleries: mergedGalleries }; console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`); console.log(saveGalleries: siteData after merge:, JSON.stringify(siteData)); console.log(saveGalleries: siteMetadata after merge:, siteData.siteMetadata); console.log(saveGalleries: TITLE after merge:, siteData.siteMetadata?.title); console.log(saveGalleries: GOOGLE ANALYTICS after merge:, siteData.siteMetadata?.googleAnalytics); } else { siteData customData; } } else { siteData { galleries: galleries }; if (window.siteMetadata) { siteData.siteMetadata window.siteMetadata; } if (window.SidebarManager) { siteData.sidebarElements window.SidebarManager.elements; } } // Get the save URL const saveUrl typeof getApiUrl function ? getApiUrl(/api/save-config) : /api/save-config; // Get user email from various sources const userEmail (window.userMetadata && window.userMetadata.email) || localStorage.getItem(hydra_auth_email) || ; // Ensure token has proper format const formattedToken token.startsWith(hydra:) ? token : (token.includes(.) ? token : `hydra:${token}`); console.log(Making server save request...); console.log(saveGalleries: Final siteData being sent to server:, JSON.stringify(siteData)); console.log(saveGalleries: Final siteMetadata being sent:, siteData.siteMetadata); console.log(saveGalleries: FINAL TITLE being sent:, siteData.siteMetadata?.title); console.log(saveGalleries: FINAL GOOGLE ANALYTICS being sent:, siteData.siteMetadata?.googleAnalytics); // Make the request const response await fetch(saveUrl, { method: POST, headers: { Content-Type: application/json, Authorization: `Bearer ${formattedToken}`, X-User-Email: userEmail, X-API-Request: true // Always include this header for API requests }, body: JSON.stringify(siteData) }); console.log(Save response status:, response.status); // Process the response const responseText await response.text(); console.log(Server response text (first 500 chars):, responseText.substring(0, 500)); let responseData; try { responseData JSON.parse(responseText); console.log(Server response parsed successfully:, responseData); if (responseData.debug) { console.log(Server debug info:, responseData.debug); console.log(Server debug - requestBodyKeys:, responseData.debug.requestBodyKeys); console.log(Server debug - hasSiteMetadata:, responseData.debug.hasSiteMetadata); console.log(Server debug - siteMetadata:, responseData.debug.siteMetadata); console.log(Server debug - finalSiteMetadata:, responseData.debug.finalSiteMetadata); console.log(Server debug - googleAnalytics:, responseData.debug.googleAnalytics); } } catch (jsonError) { if (response.status 200 && responseText.trim().startsWith(!DOCTYPE html>)) { responseData { success: true }; } else { throw new Error(Invalid response format: + responseText.substring(0, 100)); } } if (!response.ok && !responseData.success) { throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); } console.log(Configuration saved successfully); // Store new token if provided if (responseData.hydraToken) { console.log(Received new hydra token from server); // Update the global didToken didToken responseData.hydraToken.startsWith(hydra:) ? responseData.hydraToken.substring(6) : responseData.hydraToken; // Update localStorage localStorage.setItem(hydra_auth_token, didToken); // Update TokenManager if available if (window.TokenManager && typeof window.TokenManager.storeToken function) { window.TokenManager.storeToken(responseData.hydraToken, userEmail); } } // Notify sidebar manager document.dispatchEvent(new CustomEvent(sidebar-save-requested)); // Process server response data if needed if (responseData.galleries) { // If server returns updated galleries data, update the global galleries variable if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) { // CRITICAL FIX: Before replacing galleries with server response, // preserve any gallery settings that might be lost in the response const updatedGalleries responseData.galleries.map(serverGallery > { // Find the matching gallery in our current data const existingGallery galleries.find(g > g.id serverGallery.id); // Check if we need to preserve gallery settings if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) { // Calculate settings depth const existingOptionsDepth Object.keys(existingGallery.galleryOptions).length; const serverOptionsDepth Object.keys(serverGallery.galleryOptions).length; // If server has fewer settings, merge them with existing settings if (serverOptionsDepth existingOptionsDepth && existingOptionsDepth > 5) { console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); // Create a merged gallery with preserved settings return { ...serverGallery, galleryOptions: { ...existingGallery.galleryOptions, // Start with all existing settings ...serverGallery.galleryOptions // Override with any new server settings } }; } } // If no preservation needed, use server gallery as is return serverGallery; }); // Update the galleries variable with our preserved data galleries updatedGalleries; console.log(`Updated galleries from server response: ${galleries.length} galleries`); // IMPORTANT: Synchronize again after receiving server response if (typeof synchronizeGalleries function) { synchronizeGalleries(); console.log(Galleries re-synchronized after server response); } // Update localStorage with full data try { localStorage.setItem(galleries, JSON.stringify(galleries)); console.log(Updated localStorage with server gallery data (FULL format)); } catch (e) { console.warn(Could not update localStorage with server gallery data, e); } } } // CRITICAL FIX: Clear all sessionStorage caches after successful save // This ensures that when users open the site in a new tab, they get fresh data try { console.log(Clearing sessionStorage caches after successful save...); const keysToRemove ; for (let i 0; i sessionStorage.length; i++) { const key sessionStorage.key(i); if (key && (key.includes(gallery_data_) || key.includes(neonGallery_))) { keysToRemove.push(key); } } keysToRemove.forEach(key > { sessionStorage.removeItem(key); console.log(Cleared sessionStorage key: + key); }); console.log(Cleared + keysToRemove.length + sessionStorage cache entries); } catch (cacheError) { console.warn(Error clearing sessionStorage cache:, cacheError); // Dont fail the save if cache clearing fails } window._saveInProgress false; return true; } catch (error) { console.error(Error saving configuration:, error); alert(Failed to save changes: + error.message); window._saveInProgress false; return false; }}// Helper function to get API URL for preview sitesfunction getApiUrl(endpoint) { // Check if were on a preview URL const isPreviewUrl window.location.hostname preview.neonsky.app; if (isPreviewUrl) { // Extract the site GUID from the path (first segment after domain) const pathParts window.location.pathname.split(/).filter(Boolean); if (pathParts.length > 0) { const siteGuid pathParts0; // Include the site GUID in the API URL return `/${siteGuid}${endpoint}`; } } // Regular case - return the endpoint as is return endpoint;}function updateActiveStates() { // Get the most current active gallery ID, ensuring consistency between both variables const currentActiveId window.activeGalleryId || activeGalleryId; // Ensure both variables are synchronized window.activeGalleryId currentActiveId; activeGalleryId currentActiveId; console.log(updateActiveStates: Using active gallery ID:, currentActiveId); // Special case for invisible home page - dont try to highlight it in the menu const activeGallery galleries.find(g > g.id currentActiveId); if (activeGallery && activeGallery.visible false && activeGallery.isHomePage true) { console.log(Active gallery is invisible home page - not highlighting in menu); // Were not returning here, so it will still update the visibility classes // but wont try to find an element to mark as active } // Fix selector issue - look for both .tree and .sidebar document.querySelectorAll(.tree li, .sidebar li).forEach(li > { if (!li.dataset.id) return; // Skip items without data-id attribute const id parseInt(li.dataset.id); if (isNaN(id)) return; // Skip if ID is not a number const gallery galleries.find(g > g.id id); // Update active state using the most current ID const isActive id currentActiveId; // Add or remove the active class if (isActive) { li.classList.add(active); console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : Unknown})`); } else { li.classList.remove(active); } // Update visibility class if gallery exists if (gallery) { li.classList.toggle(hidden-gallery, gallery.visible false); // Update visibility toggle icon if it exists const toggleButton li.querySelector(.visibility-toggle); if (toggleButton) { toggleButton.classList.toggle(hidden, gallery.visible false); } } }); // Verify that the active state was actually set for the current activeGalleryId const activeElements document.querySelectorAll(.active); if (activeElements.length 0 && currentActiveId) { // Only log a warning if were not dealing with an invisible home page const activeGallery galleries.find(g > g.id currentActiveId); if (!(activeGallery && activeGallery.visible false && activeGallery.isHomePage true)) { console.warn(`No active elements found after update for ID: ${currentActiveId}`); } } // Log active items for debugging console.log(Active gallery ID after update:, currentActiveId); activeElements.forEach(el > console.log(Active element:, el.textContent.trim()) );}function debugGalleryStructure() {console.group(Gallery Structure Debug);// Log the raw data structureconsole.log(Raw galleries array:, JSON.parse(JSON.stringify(galleries)));// Log the tree structureconst treeStructure createGalleryTree(galleries);// Helper to print the treefunction printTree(items, level 0) {const indent .repeat(level);items.forEach(item > { console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || ROOT})`); if (item.children && item.children.length > 0) { printTree(item.children, level + 1); }});}console.log(Tree structure:);printTree(treeStructure);// Log the DOM structureconsole.log(DOM structure:);const treeEl document.getElementById(galleryTree);console.log(treeEl);console.groupEnd();}function createGalleryTree(galleries) {console.log(Creating gallery tree with preservation safeguards...);// First, create a map of galleries by id for quick lookupconst galleryMap {};const processedIds new Set();// First pass: Create clean copies without childrengalleries.forEach(gallery > {if (!gallery || !gallery.id) return; // Skip invalid galleries// Create a copy of the gallery object without the children fieldconst cleanGallery { ...gallery };delete cleanGallery.children;// Initialize an empty children arraycleanGallery.children ;// Add to map - even if weve seen this ID before, keep the latest versionif (processedIds.has(gallery.id)) { console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`);}processedIds.add(gallery.id);galleryMapgallery.id cleanGallery;});// Second pass: Assign children to their parentsconst rootGalleries ;// To detect already-processed childrenconst childrenAssigned new Set();galleries.forEach(gallery > {if (!gallery || !gallery.id) return; // Skip invalid galleriesconst galleryId gallery.id;// Skip if this gallery isnt in our map (could be invalid)if (!galleryMapgalleryId) return;// Skip if weve already assigned this gallery as a childif (childrenAssigned.has(galleryId)) return;if (gallery.parentId && galleryMapgallery.parentId) { // This is a child gallery, but first check it doesnt create a circular reference // Find all parents in the chain up to the root let currentParent galleryMapgallery.parentId; let safeToAdd true; let chainIds galleryId, gallery.parentId; while (currentParent.parentId) { // If weve seen this ID in the chain, it would create a circular reference if (chainIds.includes(currentParent.parentId)) { console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`); safeToAdd false; break; } chainIds.push(currentParent.parentId); currentParent galleryMapcurrentParent.parentId; // If parent doesnt exist in the map, break the chain if (!currentParent) break; } if (safeToAdd) { // Safe to add as child galleryMapgallery.parentId.children.push(galleryMapgalleryId); childrenAssigned.add(galleryId); } else { // Not safe - make it a root gallery rootGalleries.push(galleryMapgalleryId); }} else { // This is a root gallery (no parent or parent doesnt exist) if (!childrenAssigned.has(galleryId)) { rootGalleries.push(galleryMapgalleryId); }}});// Final verificationconst allGalleryIds new Set(galleries.map(g > g.id));const treeGalleryIds new Set();// Function to collect all IDs in the treeconst collectIds (items) > {items.forEach(item > { treeGalleryIds.add(item.id); if (item.children && item.children.length > 0) { collectIds(item.children); }});};collectIds(rootGalleries);// Check if any galleries are missing from the treeconst missingIds ;allGalleryIds.forEach(id > {if (!treeGalleryIds.has(id)) { missingIds.push(id);}});if (missingIds.length > 0) {console.warn(`${missingIds.length} galleries are missing from the tree structure`);// Add the missing galleries as root itemsmissingIds.forEach(id > { const gallery galleryMapid; if (gallery && !childrenAssigned.has(id)) { console.log(`Adding missing gallery to root: ${gallery.title}`); rootGalleries.push(gallery); }});}console.log(`Created tree with ${rootGalleries.length} root galleries`);return rootGalleries;}function alignFolderStates() { console.log(Aligning folder data states with collapsed visual appearance); let changed 0; galleries.forEach(gallery > { if ((gallery.isFolder true || gallery.isSubmenu true) && gallery.isCollapsed false) { gallery.isCollapsed true; changed++; } }); }function renderGalleryItem(gallery, level 0, maxLevel 10) { // Prevent infinite recursion by limiting depth if (level > maxLevel) { console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`); return ; } // Add debug info to console console.log(`Rendering gallery item: ${gallery.title}, isEditing${isEditing}, level${level}`); // Skip hidden items in view mode (but not spacers) if (!isEditing && gallery.visible false && !gallery.isSpacer) { return ; } const isPage gallery.isPage true;const isFolder gallery.isFolder true || gallery.isSubmenu true; // Support both folder and legacy submenuconst isSpacer gallery.isSpacer true;const isExternal gallery.isExternal true; // Determine if item has childrenconst hasChildren isFolder || gallery.children?.length > 0;const hasChildrenClass hasChildren ? has-children : ;// Standard collapsed state checkconst isCollapsed gallery.isCollapsed true;// Simple expanded class logic - if the folder is not collapsed, add expanded classconst isExpandedClass isFolder && !isCollapsed ? expanded : ;// Add additional classes for special typesconst spacerClass isSpacer ? spacer-item : ;const externalClass isExternal ? external-link : ;const folderClass isFolder ? folder-item : ; const duplicateIconSvg ` svg xmlnshttp://www.w3.org/2000/svg(http://www.w3.org/2000/svg) width24 height24 viewBox0 0 24 24> g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor> path dM16.964 8.982c-.003-2.95-.047-4.478-.906-5.524a4 4 0 0 0-.553-.554C14.4 2 12.76 2 9.48 2s-4.92 0-6.024.905a4 4 0 0 0-.553.554C1.998 4.56 1.998 6.2 1.998 9.48s0 4.92.906 6.023q.25.304.553.553c1.047.86 2.575.904 5.525.906/> path dm14.028 9.025l2.966-.043m-2.98 13.02l2.966-.043m4.992-7.937l-.028 2.96M9.01 14.036l-.028 2.96m2.505-7.971c-.832.149-2.17.302-2.477 2.024m10.485 10.91c.835-.137 2.174-.27 2.508-1.986M19.495 9.025c.832.149 2.17.302 2.477 2.024M11.5 21.957c-.833-.148-2.17-.301-2.478-2.023/> /g> /svg> `; // NEW: Add nesting level as a data attribute for styling const nestingLevelAttr `data-nesting-level${level}`; // Create the gallery item HTML with enhanced nesting indicators const html `li data-id${gallery.id} ${nestingLevelAttr} class${gallery.id activeGalleryId ? active : } ${gallery.visible false ? hidden-gallery : } ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass}> div classgallery-item-content nested-sortable-handle> ${hasChildren ? ` span classsubmenu-toggle onclicktoggleFolder(${gallery.id}, event)> svg classicon toggle-icon ${!isCollapsed ? rotated : } viewBox0 0 24 24 fillnone strokecurrentColor stroke-width2> polyline points9 6 15 12 9 18>/polyline> /svg> /span> ` : } !-- Menu item with different rendering based on type --> ${isSpacer ? ` !-- Spacer with non-breaking space to ensure height --> span classmenu-item spacer> /span> ` : isExternal ? ` !-- External URL uses an anchor tag with target_blank --> a href${gallery.url} classmenu-item external-link target_blank relnoopener noreferrer> ${gallery.title} /a> ` : isFolder ? ` !-- Folder item with toggle functionality --> span classmenu-item folder data-foldertrue onclicktoggleFolder(${gallery.id}, event) tabindex0> ${gallery.title} /span> ` : ` !-- Regular menu item with click handler --> span classmenu-item onclick${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}> ${gallery.title} /span> `} div classcontrols> ${!isSpacer ? ` button classbtn visibility-toggle ${gallery.visible false ? hidden : } onclicktoggleVisibility(${gallery.id}, event)> svg classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width1> ${gallery.visible ! false ? path dM1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z>/path>circle cx12 cy12 r3>/circle> : path dM17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24>/path>line x11 y11 x223 y223>/line> } /svg> /button> button classbtn password-toggle ${gallery.passwordProtected ? protected : } onclicktoggleGalleryPasswordProtection(${gallery.id}, event) title${gallery.passwordProtected ? Remove Password Protection : Add Password Protection}> svg xmlnshttp://www.w3.org/2000/svg classicon width24 height24 viewBox0 0 24 24> ${gallery.passwordProtected ? g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor>path dM12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345/>path dM7.5 9V6.5a4.5 4.5 0 0 1 9 0V9/>/g> : path fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 dM12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345M7.5 9V6.5A4.5 4.5 0 0 1 12 2c1.96 0 3.5 1.5 4 3 colorcurrentColor/> } /svg> /button> button classbtn edit-btn onclickeditGallery(${gallery.id}, event)> svg classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width1> path dM11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7>/path> path dM18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z>/path> /svg> /button> ` : } button classbtn duplicate-btn onclickduplicateGalleryItem(${gallery.id}, event) titleDuplicate> ${duplicateIconSvg} /button> button classbtn delete-btn onclickdeleteGallery(${gallery.id}, event)> svg classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width1> polyline points3 6 5 6 21 6>/polyline> path dM19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2>/path> /svg> /button>${!isSpacer ? `!-- Add home toggle button -->button classbtn home-toggle ${gallery.isHomePage ? active : } onclicktoggleHome(${gallery.id}, event) title${gallery.isHomePage ? Currently set as Home page : Set as Home page}> svg classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width1> ${gallery.isHomePage ? path fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 dm3.966 5.798l.603-1.401c.501-1.163.751-1.745 1.242-2.07C6.302 2 6.927 2 8.177 2h7.646c1.25 0 1.875 0 2.366.326c.49.326.741.908 1.242 2.07l.603 1.402c.817 1.896 1.225 2.844.788 3.523S19.366 10 17.328 10H6.671c-2.037 0-3.056 0-3.493-.679s-.029-1.627.788-3.523M4.5 10v5c0 1.886 0 2.828.586 3.414c.471.472 1.174.564 2.414.582M19.5 10v5c0 1.886 0 2.828-.586 3.414c-.471.472-1.174.564-2.414.582M13 14l-2.594 3.067c-.317.41-.475.616-.377.775c.098.158.383.158.954.158h2.034c.57 0 .856 0 .954.158c.098.159-.06.364-.377.775L10.984 22M10 2l-1 8m5-8l1 8M4 6h16 colorcurrentColor/> : g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor>path dM12 17h.009M20 8.5v5c0 3.771 0 5.657-1.172 6.828S15.771 21.5 12 21.5s-5.657 0-6.828-1.172S4 17.271 4 13.5v-5/>path dm22 10.5l-4.343-4.165C14.99 3.778 13.657 2.5 12 2.5S9.01 3.778 6.343 6.335L2 10.5/>/g> } /svg>/button>` : } /div> /div> ol classnested-sortable ${level > 0 ? nested-level- + level : }> ${gallery.children && gallery.children.length > 0 ? gallery.children.map(child > renderGalleryItem(child, level + 1, maxLevel)).join() : } /ol>/li>`; return html;}// Additional helper function to add visual indicators during drag operationsfunction addDragVisualFeedback() { // Add event listener for drag operations document.addEventListener(dragover, function(e) { // Find the closest nested sortable list const nearestList e.target.closest(.nested-sortable); if (!nearestList) return; // Remove active-drop-target class from all lists document.querySelectorAll(.active-drop-target).forEach(el > { el.classList.remove(active-drop-target); }); // Add it to the current list nearestList.classList.add(active-drop-target); // Calculate if this would be a nested position // This is a simplified calculation - SortableJS has its own logic, // but this helps provide additional visual feedback const rect nearestList.getBoundingClientRect(); const distanceFromLeft e.clientX - rect.left; // If were close to the left edge of a nested list, add a class if (distanceFromLeft 30) { nearestList.classList.add(potential-parent); } else { nearestList.classList.remove(potential-parent); } }); // Remove classes when drag ends document.addEventListener(dragend, function() { document.querySelectorAll(.active-drop-target, .potential-parent).forEach(el > { el.classList.remove(active-drop-target, potential-parent); }); });}// Make sure the addDragVisualFeedback function is called when the document is readydocument.addEventListener(DOMContentLoaded, function() { // Initialize the visual feedback helper addDragVisualFeedback(); // If were in edit mode, make sure the nested sortables are initialized if (document.body.classList.contains(edit-mode-active)) { setTimeout(function() { initializeNestedSortables(); }, 300); }});// Update the toggleEditMode function to ensure proper initialization of sortablesconst originalToggleEditMode window.toggleEditMode;window.toggleEditMode function() { originalToggleEditMode.apply(this, arguments); // Call original function stopAutoAdvanceTimer(); const sidebar document.querySelector(.sidebar); const editControls document.querySelector(.edit-controls); if (currentMenuLayout horizontal) { if (document.body.classList.contains(edit-mode-active)) { // In horizontal layout and edit mode is ON: show sidebar and edit controls if(sidebar) sidebar.style.display block; // Or flex if its a flex container if(editControls) editControls.style.display flex; // Re-render the sidebar menu for editing renderGalleries(); } else { // In horizontal layout and edit mode is OFF: hide sidebar if(sidebar) sidebar.style.display none; // Re-render the horizontal menu for viewing renderHorizontalMenu(); } } else { // For other layouts (sidebar, top), ensure sidebar is visible if(sidebar) sidebar.style.display block; // Or flex }};function verifyGalleryStructure() {console.log(Verifying gallery structure integrity...);// Count number of galleriesconst galleryCount galleries.length;console.log(`Total galleries in flat array: ${galleryCount}`);// Create tree and count galleries in treeconst tree createGalleryTree(...galleries);let treeCount 0;function countGalleriesInTree(items) {items.forEach(item > { treeCount++; if (item.children && item.children.length > 0) { countGalleriesInTree(item.children); }});}countGalleriesInTree(tree);console.log(`Total galleries in tree structure: ${treeCount}`);if (treeCount ! galleryCount) {console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`);return false;} else {console.log(Gallery structure is consistent);return true;}}function renderGalleries() {console.log(Rendering galleries...); // IMPORTANT: Synchronize with global edit state first const sidebar document.querySelector(.sidebar); if (sidebar && sidebar.classList.contains(editing)) { console.log(Edit mode detected from sidebar classes, ensuring isEditing is true); isEditing true; }try {// Only fix circular references if absolutely necessaryconst foundCircular fixCircularReferences();// Only create a new tree if we had to fix circular referenceslet galleryTree;if (foundCircular) { console.log(Creating fresh gallery tree after fixing circular references); galleryTree createGalleryTree(galleries);} else { console.log(Using existing structure to create gallery tree); galleryTree createGalleryTree(galleries);}// Render the tree - IMPORTANT: Add null checkconst tree document.getElementById(galleryTree);if (!tree) { console.warn(galleryTree element not found, cannot render galleries); return; // Exit the function if the element doesnt exist}// Render the tree with maximum depth protectiontree.innerHTML ` ol classnested-sortable> ${galleryTree.map(gallery > renderGalleryItem(gallery, 0, 20)).join()} /ol>`;// Initialize sortable for all nested sortable elementsif (isEditing) { // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables // This prevents drag issues when items are added and immediately dragged requestAnimationFrame(() > { initializeNestedSortables(); });}console.log(Galleries rendered successfully);// Verify structure integrity after renderingverifyGalleryStructure();} catch (error) {console.error(Error rendering galleries:, error);}}function fixCircularReferences() {console.log(Selectively checking for circular references...);// Only process galleries that need checking:// 1. Imported galleries (theyre new and might cause issues)// 2. Galleries with extremely deep nesting// 3. Actual circular referencesconst processed new Set();// First pass: Identify actual circular references without modifying structureconst circularIds new Set();const suspiciousIds new Set();galleries.forEach(gallery > {if (!gallery || !gallery.id) return;// Skip galleries weve already processedif (processed.has(gallery.id)) return;// Check for self-reference - this is always wrongif (gallery.parentId gallery.id) { console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`); circularIds.add(gallery.id); return;}// Skip galleries without parentsif (!gallery.parentId) { processed.add(gallery.id); return;}// Follow the parent chain to detect circular referencesconst parentChain gallery.id;let currentId gallery.parentId;let loopCount 0;let foundCircular false;while (currentId && loopCount 20) { // Reasonable limit to prevent infinite loops loopCount++; // If weve seen this ID before, its a circular reference if (parentChain.includes(currentId)) { console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`); circularIds.add(gallery.id); foundCircular true; break; } parentChain.push(currentId); // Move up to the next parent const parent galleries.find(g > g.id currentId); currentId parent?.parentId; // If we cant find the parent, we can stop if (!parent) break;}// If we hit our loop limit but didnt find a circular reference,// its suspicious but not definitely circularif (loopCount > 20 && !foundCircular) { console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`); suspiciousIds.add(gallery.id);}// Mark this gallery and all its ancestors as processedparentChain.forEach(id > processed.add(id));});// Second pass: Only fix the identified circular referencesif (circularIds.size > 0) {console.log(`Fixing ${circularIds.size} confirmed circular references`);galleries.forEach(gallery > { if (circularIds.has(gallery.id)) { console.log(`Breaking circular reference for gallery: ${gallery.title}`); gallery.parentId null; }});} else {console.log(No circular references found);}// For suspicious galleries, dont automatically fix themif (suspiciousIds.size > 0) {console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`);}return circularIds.size > 0; // Return true if we fixed anything}// Deep linking implementation that uses the existing slug property// Function to get the slug for a galleryfunction getGallerySlug(gallery) {// First try to use the existing slug propertyif (gallery.slug) {console.log(`Using existing gallery slug: ${gallery.slug}`);return gallery.slug;}// If no slug exists, generate one from the title as fallbackif (gallery.title) {const generatedSlug gallery.title .toLowerCase() .replace(/^ws-/g, ) .replace(/s+/g, -) .replace(/--+/g, -) .trim(); console.log(`Generated slug from title: ${generatedSlug}`);return generatedSlug || gallery;}// Default fallbackconsole.warn(Gallery has no slug or title, using default);return gallery;}// Function to update the URL when a gallery is loaded// Update the URL when a gallery is loadedfunction updateURLWithGallerySlug(gallery) { if (!gallery) return; // Get the slug for this gallery/page let slug gallery.slug; if (!slug && gallery.title) { slug gallery.title.toLowerCase().replace(/s+/g, -).replace(/^w-+/g, ).replace(/--+/g, -).trim(); } // Check if were in preview mode (add this new code) const isPreviewMode window.location.hostname preview.neonsky.app; // Check if current history state already has this galleryId const currentState window.history.state; const currentPath window.location.pathname; const isAlreadyCurrentPage currentState && currentState.galleryId gallery.id && ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath / + slug)); if (isPreviewMode) { // Extract GUID from current URL path - first segment after domain const pathParts window.location.pathname.split(/).filter(Boolean); const siteGuid pathParts0; // Create history entry if we have a valid GUID and slug, and its not already the current state if (siteGuid && slug) { const targetPath / + siteGuid + / + slug; // Only push if URL is different OR if state doesnt match (allows multiple navigations) if (currentPath ! targetPath || !isAlreadyCurrentPage) { console.log(🔍 updateURLWithGallerySlug Updating URL to + targetPath + (preview mode), galleryId: + gallery.id); // Update browser URL without reloading, preserving the GUID window.history.pushState( { galleryId: gallery.id }, gallery.title || Gallery, targetPath ); // Update page title document.title (gallery.title || Gallery) + - + window.location.hostname; } else { console.log(🔍 updateURLWithGallerySlug URL and state already match, skipping history update (preview mode)); } } return; // Exit early, weve handled the preview case } // Regular (non-preview) URL handling if (slug) { const targetPath / + slug; // Only push if URL is different OR if state doesnt match (allows multiple navigations) if (currentPath ! targetPath || !isAlreadyCurrentPage) { console.log(🔍 updateURLWithGallerySlug Updating URL to + targetPath + , galleryId: + gallery.id); window.history.pushState( { galleryId: gallery.id }, gallery.title || Gallery, targetPath ); document.title (gallery.title || Gallery) + - + window.location.hostname; } else { console.log(🔍 updateURLWithGallerySlug URL and state already match, skipping history update); } }}function loadHomePage() { // Find a gallery marked as home page (regardless of visibility) const homePage galleries.find(gallery > gallery.isHomePage true); if (homePage) { console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible ! false})`); // Update active gallery ID activeGalleryId homePage.id; // Add a special flag to indicate were loading the home page // This allows us to bypass visibility checks window._loadingHomePage true; // Determine if this is a page or gallery and load appropriately if (homePage.isPage) { loadPage(homePage.id); } else { loadGallery(homePage.id); } // Clear the flag after loading setTimeout(() > { window._loadingHomePage false; }, 100); // Update UI state updateActiveStates(); updateMobileTitle(); if (typeof closeMobileMenu function) closeMobileMenu(); return true; } console.log(No home page defined); return false;}// 4. Update the handleURLNavigation function to check for home page// Find the existing handleURLNavigation function and modify as follows:// In index.tsfunction handleURLNavigation() { const path window.location.pathname.substring(1); // Remove leading slash const isPreviewMode window.location.hostname preview.neonsky.app; let slug path; let siteGuid null; if (isPreviewMode && path) { const pathParts path.split(/); if (pathParts.length > 1) { siteGuid pathParts0; // Path could be just GUID/ or GUID/slug } if (pathParts.length > 2) { slug pathParts1; // Slug is the part after GUID console.log(`Preview URL detected, using slug: ${slug} under GUID: ${siteGuid}`); } else { // Only GUID is present, or path is empty after GUID slug ; // Treat as root/home for the given GUID console.log(`Preview URL with only GUID: ${siteGuid}, checking for home page or default.`); } } if (!slug) { // Handles root path (/) or preview URL with only GUID (/GUID/) console.log(No specific slug in path (root or GUID-only preview), checking for home page.); if (!loadHomePage() && galleries.length > 0) { const firstVisibleGallery galleries.find(g > g.visible ! false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log(No home page, loading first visible item:, firstVisibleGallery.title); if (firstVisibleGallery.isPage) { loadPage(firstVisibleGallery.id); } else { loadGallery(firstVisibleGallery.id); } } else { console.log(No home page and no visible items to load for this path.); } } } else { console.log(`Handling URL navigation for slug: ${slug}` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : )); // findGalleryByPath should be able to find the item using the slug, // handling preview mode internally if necessary or by being passed the correct slug. const matchingGallery findGalleryByPath(slug, isPreviewMode, siteGuid); if (matchingGallery) { console.log(Loading content from URL: + matchingGallery.title + (ID: + matchingGallery.id + )); console.log(🔍 handleURLNavigation Gallery properties - isPage:, matchingGallery.isPage, pageId:, matchingGallery.pageId, has pageElements:, !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements))); if (matchingGallery.isSubmenu) { console.log(Gallery is a submenu, skipping direct load. Parent expansion might be needed.); // Potentially, you might want to find and expand its parent here if the UI supports it. } else { // Set activeGalleryId *before* calling loadPage/loadGallery activeGalleryId matchingGallery.id; window.activeGalleryId matchingGallery.id; // Determine if this is a page by checking multiple indicators // Check isPage flag, pageId, or pageElements array const isPage matchingGallery.isPage true || (matchingGallery.pageId && matchingGallery.pageId.startsWith(page_)) || (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0); if (isPage) { console.log(🔍 handleURLNavigation Detected as PAGE, loading page: + matchingGallery.title + ); loadPage(matchingGallery.id); } else { console.log(🔍 handleURLNavigation Detected as GALLERY, loading gallery: + matchingGallery.title + ); loadGallery(matchingGallery.id); // This is async and should be awaited if subsequent logic depends on its completion. } } } else { console.log(`No matching gallery found for slug: ${slug}. Checking for home page as fallback.`); if (!loadHomePage()) { // Try loading home page const firstVisibleGallery galleries.find(g > g.visible ! false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log(`Loading first visible gallery instead: ${firstVisibleGallery.title}`); if (firstVisibleGallery.isPage) { loadPage(firstVisibleGallery.id); } else { loadGallery(firstVisibleGallery.id); } } else { console.log(No matching gallery, no home page, and no visible items to load.); // Optionally, display a 404 message in the gallery-container const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) { galleryContainer.innerHTML div>Page not found./div>; galleryContainer.style.opacity 1; } } } } } // Update UI states after initiating load. // These functions should ideally use the now-set activeGalleryId. if (typeof updateActiveStates function) updateActiveStates(); // For sidebar/tree if (typeof updateMobileTitle function) updateMobileTitle(); // For mobile header // **** ADDED FOR DEEP LINKING HORIZONTAL MENU **** // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load) let currentLayout sidebar; // Default assumption if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout window.MenuStyleCustomizer.settings.menuLayout; } else { // Fallback: check body class if MenuStyleCustomizer hasnt initialized settings if (document.body.classList.contains(menu-layout-horizontal)) { currentLayout horizontal; } } console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`); if (currentLayout horizontal) { if (typeof updateActiveStatesHorizontal function) { console.log(handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation.); // Its possible menu items arent rendered yet by renderHorizontalMenu if this is a very early call. // A small delay might be needed, or ensure renderHorizontalMenu has run. // For now, call it directly. If items arent there, it wont do anything harmful. setTimeout(() > { // Add a slight delay to allow menu rendering updateActiveStatesHorizontal(); }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered. } else { console.warn(handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout.); } }}/*** Enhanced loadPage function that properly cleans up gallery content* @param {number} id - The page ID* @param {Event} event - Optional event object*/window.loadPage function(id, event) { if (event) { event.preventDefault(); event.stopPropagation(); const now Date.now(); const lastCallTime window._lastPageLoadTime || 0; window._lastPageLoadTime now; if (now - lastCallTime 100) { console.log(Ignoring duplicate loadPage call); return; } } console.error(🔍 loadPage Loading page with gallery ID:, id); stopAutoAdvanceTimer(); let galleriesData window.galleries || galleries; const gallery findGalleryById(galleriesData, id); if (!gallery) { console.warn(No gallery found with ID:, id, for page load.); return; } if (typeof window.removeGalleryScriptsWithPause function) { window.removeGalleryScriptsWithPause(); } window.activeGalleryId id; activeGalleryId id; const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) { // Clear the container but ensure its visible galleryContainer.innerHTML ; galleryContainer.style.display block; galleryContainer.style.visibility visible; galleryContainer.style.opacity 1; console.log(🔍 loadPage Cleared and ensured gallery-container is visible); } else { console.error(🔍 loadPage gallery-container not found!); } if (!gallery.pageId) gallery.pageId `page_${id}`; if (!gallery.isPage) gallery.isPage true; if (!window._pageIdToGalleryId) window._pageIdToGalleryId {}; window._pageIdToGalleryIdgallery.pageId id; if (gallery.pageElements && Array.isArray(gallery.pageElements)) { if (window.PageManager && window.PageManager.elements) { window.PageManager.elementsgallery.pageId ...gallery.pageElements; } } else if (window.PageManager && window.PageManager.elements && window.PageManager.elementsgallery.pageId && window.PageManager.elementsgallery.pageId.length > 0) { gallery.pageElements ...window.PageManager.elementsgallery.pageId; } if (window.PageManager && typeof window.PageManager.loadPage function) { try { console.error(🔍 loadPage Calling PageManager.loadPage for pageId:, gallery.pageId); console.error(🔍 loadPage Gallery container exists:, !!galleryContainer, Container HTML length:, galleryContainer ? galleryContainer.innerHTML.length : 0); console.error(🔍 loadPage Page elements count:, gallery.pageElements ? gallery.pageElements.length : 0); console.error(🔍 loadPage PageManager.elements + gallery.pageId + exists:, !!(window.PageManager.elements && window.PageManager.elementsgallery.pageId)); // Small delay to ensure DOM is ready, especially when navigating back via history setTimeout(() > { // Verify container still exists const containerCheck document.querySelector(.gallery-container); console.error(🔍 loadPage Container check before loadPage:, !!containerCheck, Same as original:, containerCheck galleryContainer); // PageManager.loadPage is async - we need to properly handle it try { const loadPromise window.PageManager.loadPage(gallery.pageId); console.error(🔍 loadPage PageManager.loadPage called, returned:, typeof loadPromise, is promise:, loadPromise && typeof loadPromise.then function); if (loadPromise && typeof loadPromise.then function) { loadPromise.then(() > { console.error(🔍 loadPage PageManager.loadPage promise resolved); // Ensure container visibility after PageManager has initialized if (galleryContainer) { const pageContainer galleryContainer.querySelector(.page-container); if (pageContainer) { pageContainer.style.display block; pageContainer.style.visibility visible; pageContainer.style.opacity 1; const elementCount pageContainer.querySelectorAll(.page-element).length; console.error(🔍 loadPage Page container found and made visible, has, elementCount, elements); if (elementCount 0) { console.error(🔍 loadPage WARNING: Page container exists but has no elements!); console.error(🔍 loadPage PageManager.elements + gallery.pageId + :, window.PageManager.elementsgallery.pageId); } } else { console.error(❌ loadPage Page container not found after PageManager.loadPage); console.error(❌ loadPage Gallery container HTML:, galleryContainer.innerHTML.substring(0, 500)); console.error(❌ loadPage Gallery container children:, galleryContainer.children.length); } } else { console.error(❌ loadPage Gallery container is null after PageManager.loadPage); } }).catch((error) > { console.error(❌ loadPage Error in PageManager.loadPage promise:, error); console.error(❌ loadPage Error stack:, error.stack); }); } else { // If loadPage doesnt return a promise, check after a delay console.error(🔍 loadPage loadPage did not return a promise, checking after delay); setTimeout(() > { if (galleryContainer) { const pageContainer galleryContainer.querySelector(.page-container); if (pageContainer) { pageContainer.style.display block; pageContainer.style.visibility visible; pageContainer.style.opacity 1; console.error(🔍 loadPage Ensured page-container is visible (non-promise path)); } else { console.error(❌ loadPage Page container not found (non-promise path)); console.error(❌ loadPage Gallery container HTML:, galleryContainer.innerHTML.substring(0, 500)); } } }, 200); } } catch (loadError) { console.error(❌ loadPage Exception calling PageManager.loadPage:, loadError); console.error(❌ loadPage Error stack:, loadError.stack); } const isCurrentlyEditing typeof isInEditMode function ? isInEditMode() : false; if (isCurrentlyEditing && typeof window.PageManager.setEditMode function) { window.PageManager.setEditMode(isCurrentlyEditing); } }, 50); // Small delay to ensure DOM is ready } catch (error) { console.error(❌ loadPage Error in loadPage function:, error); console.error(❌ loadPage Error stack:, error.stack); } } else { console.error(❌ loadPage PageManager not found or loadPage method not available); console.error(❌ loadPage window.PageManager:, !!window.PageManager); console.error(❌ loadPage typeof loadPage:, window.PageManager ? typeof window.PageManager.loadPage : N/A); } // Apply menu visibility const bodyEl document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add(menu-hidden-on-page); } else { bodyEl.classList.remove(menu-hidden-on-page); // Ensure menu is visible if in edit mode or not hidden } if (typeof updateActiveStates function) updateActiveStates(); if (typeof updateMobileTitle function) updateMobileTitle(); if (typeof closeMobileMenu function) closeMobileMenu(); // Close the gallery options panel when navigating to a different page if (typeof window.closeOptionsPanel function) { window.closeOptionsPanel(); } if (typeof updateURLWithGallerySlug function) updateURLWithGallerySlug(gallery);}/*** Ensures all required gallery styles are loaded*/function ensureGalleryStylesLoaded() {// Use Worker route for gallery CSS (avoids CDN SSL issues)const galleryScriptsBase window.location.origin + /gallery-scripts/;const requiredStyles { id: neon-gallery-css, href: galleryScriptsBase + neon-gallery-hp-hydra-v260119-004.css },{ id: quill-css, href: https://cdn.quilljs.com/1.3.6/quill.snow.css };console.log(Loading gallery CSS from Worker:, galleryScriptsBase + neon-gallery-hp-hydra-v260119-004.css);requiredStyles.forEach(style > {if (!document.getElementById(style.id)) { const link document.createElement(link); link.id style.id; link.rel stylesheet; link.href style.href; if (style.href.includes(cdn.neonsky.app)) { link.onerror function() { console.error(❌ Gallery CSS failed, retrying via proxy:, link.href); // Set flag if not already set if (!window.useCdnProxy) { window.useCdnProxy true; console.error(🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests); } const proxyUrl link.href.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); link.href proxyUrl; link.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); link.href link.href.replace(cdn.neonsky.app, storage.neonsky.app); }; }; } document.head.appendChild(link); console.log(`Added gallery style: ${style.id}`);}});}/*** Ensures all required gallery scripts are loaded* @returns {Promise} A promise that resolves when all scripts are loaded*/function ensureGalleryScriptsLoaded() {const requiredScripts { id: crypto-js, src: https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js },{ id: quill-js, src: https://cdn.quilljs.com/1.3.6/quill.min.js }, { id: quill-integration, src: https://cdn.neonsky.app/quill-integration-v260119-004.js };const promises requiredScripts.map(script > {return new Promise((resolve, reject) > { // If script is already loaded, resolve immediately if (document.getElementById(script.id)) { resolve(); return; } // Create and load the script const scriptElement document.createElement(script); scriptElement.id script.id; scriptElement.src script.src; scriptElement.onload resolve; scriptElement.onerror () > reject(new Error(`Failed to load script: ${script.src}`)); document.head.appendChild(scriptElement);});});return Promise.all(promises);}/** * Creates a clean gallery context without using an iframe * @param {Object} gallery - The gallery object * @param {HTMLElement} container - The container to load the gallery into */function createGalleryContext(gallery, container) { console.log(Creating gallery context for:, gallery.title); // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script // This ensures styles are available even if NDJSON fails and falls back to classic JSON ensureGalleryStylesLoaded(); // Generate the page alias safely const pageAlias gallery.slug || gallery.title.toLowerCase().replace(/s+/g, -); // Get our gallery options const galleryOptions gallery.galleryOptions || {}; // Get token data let authToken null; // First try TokenManager if available if (window.TokenManager && typeof window.TokenManager.getToken function) { authToken window.TokenManager.getToken(); } // Then try global token else if (window.didToken) { authToken window.didToken; } // Set up Parameters for the gallery const galleryParameters { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: pageAlias, isInEditor: window.isEditing || false, siteId: window.siteId || , isHydra: true }; // If we have a token, add it if (authToken) { galleryParameters.hydraAuthToken authToken; } // Preserve original window.Parameters const originalParameters window.Parameters; // REMOVED: Loading indicator - no longer showing the loading animation // Load the main gallery script - Use Worker route (avoids CDN SSL issues) const galleryScript document.createElement(script); const galleryScriptsBase window.location.origin + /gallery-scripts/; galleryScript.src galleryScriptsBase + neon-gallery-main-v260119-004.js; console.log(Loading gallery-main script from Worker:, galleryScript.src); galleryScript.onerror function() { console.error(❌ Gallery script failed, retrying via proxy:, galleryScript.src); const proxyUrl galleryScript.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); galleryScript.src proxyUrl; galleryScript.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); galleryScript.src galleryScript.src.replace(cdn.neonsky.app, storage.neonsky.app); }; }; // Setup the gallery configuration const neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: mod, layoutType: grid, ...galleryOptions, siteId: window.siteId || }; // Execute in a safe way that minimizes global namespace pollution try { // Set up the global variables needed by the gallery script window.Parameters galleryParameters; window.neonGalleryConfig neonGalleryConfig; // Monitor for script load/error galleryScript.onload () > { console.log(Gallery script loaded successfully for:, gallery.title); // REMOVED: Dont need to remove the loading indicator since we dont add it }; galleryScript.onerror () > { console.error(Failed to load gallery script for:, gallery.title); //container.innerHTML div classgallery-error>Error loading gallery. Please try again later./div>; // Restore original parameters window.Parameters originalParameters; }; // Add the script to load the gallery document.body.appendChild(galleryScript); // Set up a cleanup function that will be called when another gallery is loaded container.cleanup () > { // Restore original parameters when cleaning up window.Parameters originalParameters; // Remove the gallery script to prevent conflicts galleryScript.remove(); // Clear main gallery container if (container.parentNode) { container.innerHTML ; } }; } catch (error) { console.error(Error creating gallery context:, error); //container.innerHTML div classgallery-error>Error initializing gallery. Please try again later./div>; // Restore original parameters window.Parameters originalParameters; }}/*** Only used for the PageManagers initializeDOM - ensures clean content swap*/function cleanGalleryContainer() {const galleryContainer document.querySelector(.gallery-container);if (galleryContainer) {galleryContainer.innerHTML ;}}/*** Initializes the page using the PageManager* To be used by PageManager.initializeDOM*/function initPageContent(pageId) {// First clear the container completelycleanGalleryContainer();// Then load the page contentif (window.PageManager && window.PageManager.loadPage) {window.PageManager.loadPage(pageId);}}/*** Cleanup gallery content when changing between galleries or pages*/function cleanupGalleryContent() {// Get the gallery containerconst galleryContainer document.querySelector(.gallery-container);if (!galleryContainer) return;// Find the direct gallery contentconst directContent galleryContainer.querySelector(.gallery-direct-content);if (directContent && typeof directContent.cleanup function) {// Call the cleanup functiondirectContent.cleanup();}// Clear the containergalleryContainer.innerHTML ;}/*** Simplified loadGallery function that follows the same content swap pattern* @param {number} id - The gallery ID* @param {Event} event - Optional event object*/// Override loadGallery to update URLwindow.loadGallery async function(id, event) {// Stop event propagation if providedif (event) { event.stopPropagation();}const galleryContainer document.querySelector(.gallery-container);if (galleryContainer) { // Apply immediate hiding styles galleryContainer.style.opacity 0;}console.log(Loading gallery with ID:, id);stopAutoAdvanceTimer();// Find gallery by IDconst gallery findGalleryById(galleries, id); // Assuming `galleries` is accessibleif (!gallery) { console.warn(No gallery found with ID:, id); if (galleryContainer) { galleryContainer.style.opacity 1; } // Show container if gallery not found return;}// Check if this is a page, if so use loadPage insteadif (gallery.isPage true) { // Use the loadPage function defined in this scope (or window.loadPage if you prefer) return loadPage(id, event);}// Skip submenu itemsif (gallery.isSubmenu) { console.log(Gallery is submenu, skipping URL update); toggleSubmenu(id, event || { stopPropagation: () > {} }); // Assuming toggleSubmenu is available if (galleryContainer) { galleryContainer.style.opacity 1; } // Show container return;}console.log(Found gallery:, gallery.title);// CRITICAL: Set active gallery ID consistently in both global and window scopeconsole.log(Setting active gallery ID to:, id, (Previous value:, (window.activeGalleryId || activeGalleryId), ));window.activeGalleryId id;activeGalleryId id; // Ensure both variables are synchronized// Save reference to which gallery were loading (for later verification)window.currentLoadingGalleryId id;// CRITICAL: Thorough cleanup with await to ensure completionif (typeof window.removeGalleryScriptsWithPause function) { await window.removeGalleryScriptsWithPause();}// Clear all existing content from gallery containerif (!galleryContainer) { console.error(Gallery container not found); return;}// Force clear all contentgalleryContainer.innerHTML ;// CRITICAL: Double-check active gallery ID before updating UIif (window.activeGalleryId ! id || activeGalleryId ! id) { console.warn(Active gallery ID changed unexpectedly before UI update, restoring to:, id); window.activeGalleryId id; activeGalleryId id;}// Apply menu visibility based on the gallerys setting and edit modeconst bodyEl document.body;if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add(menu-hidden-on-page);} else { bodyEl.classList.remove(menu-hidden-on-page); // Ensure menu is visible if in edit mode or not hidden}// Update UI state using explicit window function references or local fallbacksif (typeof window.updateActiveStates function) { window.updateActiveStates();} else if (typeof updateActiveStates function) { updateActiveStates();}if (typeof window.updateMobileTitle function) { window.updateMobileTitle();} else if (typeof updateMobileTitle function) { updateMobileTitle();}if (typeof window.closeMobileMenu function) { window.closeMobileMenu();} else if (typeof closeMobileMenu function) { closeMobileMenu();}// Update URL with gallery slugif (typeof window.updateURLWithGallerySlug function) { window.updateURLWithGallerySlug(gallery);} else if (typeof updateURLWithGallerySlug function) { updateURLWithGallerySlug(gallery);} // Create gallery container and load contentconst neonGalleryContainer document.createElement(div);neonGalleryContainer.id neon-gallery-container;neonGalleryContainer.className gallery-direct-content;neonGalleryContainer.setAttribute(data-gallery-id, gallery.id.toString());neonGalleryContainer.style.width 100%;neonGalleryContainer.style.height 100%;galleryContainer.appendChild(neonGalleryContainer);// Add a cache-busting parameter to ensure script is freshly loadedconst timestamp Date.now();// Set up gallery parameterswindow.Parameters { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available isInEditor: window.isEditing || false, // Assuming window.isEditing is available siteId: window.siteId || gallery.siteId || , // Assuming window.siteId is available isHydra: true, galleryInstanceId: timestamp, // Added loadedGalleryId as per your original function loadedGalleryId: gallery.id};// Debug: Check what galleryOptions containsconsole.error(🔍 INDEX.TS DEBUG - Gallery:, gallery.title, has isPerma:, gallery.galleryOptions?.isPerma, permaURL:, gallery.galleryOptions?.permaURL, loadTxId:, gallery.galleryOptions?.loadTxId, loadPermaURL:, gallery.galleryOptions?.loadPermaURL);// Set up gallery configwindow.neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: gallery.galleryOptions?.manualCollectionName || mod, layoutType: gallery.galleryOptions?.layoutType || grid, // Spread gallery options AFTER setting defaults to ensure current gallerys settings take precedence // This ensures that only properties from the current gallery are included, not from previous galleries ...(gallery.galleryOptions || {}), // CRITICAL: Explicitly include Load Network fields to ensure theyre passed to gallery script // These fields are required for the gallery to determine if it should use Load Network racing loadTxId: gallery.galleryOptions?.loadTxId || null, loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, siteId: window.siteId || gallery.siteId || , // Assuming window.siteId is available // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread galleryId: gallery.id, galleryInstanceId: timestamp};console.error(🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:, window.neonGalleryConfig.isPerma, permaURL:, window.neonGalleryConfig.permaURL, loadTxId:, window.neonGalleryConfig.loadTxId, loadPermaURL:, window.neonGalleryConfig.loadPermaURL);// Create and add the gallery script with cache-busting// Use Worker route (avoids CDN SSL issues)const script document.createElement(script);const galleryScriptsBase window.location.origin + /gallery-scripts/;script.src galleryScriptsBase + neon-gallery-main-v260119-004.js; // Using versioned filescript.setAttribute(data-gallery-id, gallery.id.toString()); // Keep data-gallery-id attributescript.setAttribute(data-timestamp, timestamp.toString()); // Keep data-timestamp attributeconsole.log(Loading gallery-main script from Worker:, script.src);script.onerror function() { console.error(❌ Gallery script failed, retrying via proxy:, script.src); const proxyUrl script.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); script.src proxyUrl; script.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); script.src script.src.replace(cdn.neonsky.app, storage.neonsky.app); };};// Added script.onload from your original functionscript.onload function() { if (window.currentLoadingGalleryId ! gallery.id) { console.warn(Gallery ID mismatch! Expected:, gallery.id, Current:, window.currentLoadingGalleryId); } console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);};if (galleryContainer) { setTimeout(() > { galleryContainer.style.opacity 1; }, 50); // Small delay to ensure content is ready }console.log(`Loading fresh gallery script for: ${gallery.title}`);document.body.appendChild(script);}// Function to ensure gallery container is visiblefunction ensureGalleryVisibility() { const galleryContainer document.querySelector(.gallery-container); if (galleryContainer && galleryContainer.style.opacity 0) { console.log(ensureGalleryVisibility: Setting gallery container opacity to 1); galleryContainer.style.opacity 1; }}// Handle browser back/forward navigationwindow.addEventListener(popstate, async function(event) { // Make it async console.error(🔍 Popstate Event triggered. State:, event.state, Current URL:, window.location.pathname); let galleryIdToLoad null; let galleryToLoad null; // First, try to get galleryId from event.state if (event.state && event.state.galleryId) { galleryIdToLoad event.state.galleryId; console.error(🔍 Popstate Found galleryId in event.state:, galleryIdToLoad); // Ensure galleries array is available and use findGalleryById helper const currentGalleries window.galleries || galleries || ; galleryToLoad findGalleryById(currentGalleries, galleryIdToLoad); } // If no gallery found from state, try to get it from the URL if (!galleryToLoad) { console.error(🔍 Popstate No galleryId in event.state or gallery not found, reading from URL:, window.location.pathname); if (typeof handleURLNavigation function) { // handleURLNavigation will parse the URL and load the appropriate page/gallery console.error(🔍 Popstate Calling handleURLNavigation to parse URL and load content); handleURLNavigation(); // This function should handle its own UI updates including active states. // After handleURLNavigation, update UI states setTimeout(() > { if (typeof updateActiveStates function) updateActiveStates(); if (typeof updateMobileTitle function) updateMobileTitle(); // Ensure gallery visibility after popstate navigation setTimeout(ensureGalleryVisibility, 1000); let currentLayout sidebar; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout window.MenuStyleCustomizer.settings.menuLayout; } else if (document.body.classList.contains(menu-layout-horizontal)) { currentLayout horizontal; } if (currentLayout horizontal) { if (typeof updateActiveStatesHorizontal function) { console.error(🔍 Popstate Calling updateActiveStatesHorizontal for horizontal layout with a delay.); setTimeout(() > { console.error(Delayed Update from Popstate Calling updateActiveStatesHorizontal.); updateActiveStatesHorizontal(); }, 150); } } }, 100); return; // Exit because handleURLNavigation will manage updates. } } // If we have a gallery from state, load it let contentLoaded false; if (galleryToLoad) { console.error(🔍 Popstate Found gallery in history: + galleryToLoad.title + (ID: + galleryToLoad.id + )); console.error(🔍 Popstate Gallery properties - isPage:, galleryToLoad.isPage, pageId:, galleryToLoad.pageId, has pageElements:, !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements))); activeGalleryId galleryToLoad.id; window.activeGalleryId galleryToLoad.id; const editModeActive typeof isEditing ! undefined ? isEditing : (typeof window.isInEditMode function ? window.isInEditMode() : false); // When navigating back via browser history, we should load the content regardless of visibility // The URL itself indicates the user should see this content. Visibility checks are for menu display, not navigation. // Only skip if its explicitly marked as not visible AND were not in edit mode AND its not a page const shouldLoad galleryToLoad.visible ! false || editModeActive || galleryToLoad.isPage true; console.error(🔍 Popstate Visibility check - visible:, galleryToLoad.visible, editModeActive:, editModeActive, isPage:, galleryToLoad.isPage, shouldLoad:, shouldLoad); if (shouldLoad) { // Determine if this is a page by checking multiple indicators // Check isPage flag, pageId, or pageElements array const isPage galleryToLoad.isPage true || (galleryToLoad.pageId && galleryToLoad.pageId.startsWith(page_)) || (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0); if (isPage) { console.error(🔍 Popstate Detected as PAGE, loading page + galleryToLoad.title + ); if (typeof loadPage function) { console.error(🔍 Popstate Calling loadPage for ID:, galleryToLoad.id); loadPage(galleryToLoad.id); contentLoaded true; } else { console.error(❌ Popstate loadPage function not available); } } else { console.error(🔍 Popstate Detected as GALLERY, loading gallery + galleryToLoad.title + ); if (typeof loadGallery function) { await loadGallery(galleryToLoad.id); // Await here contentLoaded true; } else { console.error(❌ Popstate loadGallery function not available); } } } else { console.error(🔍 Popstate Gallery + galleryToLoad.title + is not visible and not in edit mode. Clearing content.); const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) galleryContainer.innerHTML ; // activeGalleryId null; // Keep activeGalleryId to reflect URL, even if content not shown. } } else { console.error(❌ Popstate No gallery found and handleURLNavigation not available); } // Update UI states after loading (or attempting to load) content if (typeof updateActiveStates function) updateActiveStates(); if (typeof updateMobileTitle function) updateMobileTitle(); // Ensure gallery visibility after popstate navigation setTimeout(ensureGalleryVisibility, 1000); let currentLayout sidebar; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout window.MenuStyleCustomizer.settings.menuLayout; } else if (document.body.classList.contains(menu-layout-horizontal)) { currentLayout horizontal; } if (currentLayout horizontal) { if (typeof updateActiveStatesHorizontal function) { console.log(Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay.); setTimeout(() > { console.log(Delayed Update from Popstate Calling updateActiveStatesHorizontal.); updateActiveStatesHorizontal(); }, 150); // Slightly longer delay for popstate, as full content might be re-initializing } }});// Check for direct URL access on page loaddocument.addEventListener(DOMContentLoaded, function() {setTimeout(function() {console.log(Checking for deep links after DOM content loaded);if (window.location.pathname ! /) { handleURLNavigation();}// Ensure gallery visibility after URL navigationsetTimeout(ensureGalleryVisibility, 1000);}, 500);});document.addEventListener(page-save-requested, function(e) {const data e.detail;if (data && data.pageId && data.elements) {// Find the gallery/page that corresponds to this pageIdconst page galleries.find(g > g.pageId data.pageId);if (page) { // Store the page elements page.pageElements data.elements; // Save to server saveGalleries();}}});document.addEventListener(DOMContentLoaded, function() {// Preload the Quill editor dependencies when page loadsif (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies function) {console.log(Preloading Rich Text Editor dependencies);window.RichTextEditor._ensureEditorDependencies().then(() > { console.log(Rich Text Editor dependencies preloaded successfully);}).catch(error > { console.warn(Failed to preload Rich Text Editor:, error);});} else {console.warn(Rich Text Editor not available for preloading);// If not available yet, try again after a delaysetTimeout(() > { if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies function) { console.log(Retrying Rich Text Editor preload); window.RichTextEditor._ensureEditorDependencies().catch(e > console.warn(e)); }}, 2000);}});document.addEventListener(DOMContentLoaded, function() {const submenuCheck document.getElementById(createSubmenu);const submenuTitleField document.getElementById(submenuTitle);const submenuTitleGroup submenuTitleField?.parentElement;if (submenuCheck && submenuTitleGroup) {submenuCheck.addEventListener(change, function() { submenuTitleGroup.style.display this.checked ? block : none;});}});// Also check immediately if document is already loadedif (document.readyState ! loading) {setTimeout(function() {console.log(Document already loaded, checking for deep links);if (window.location.pathname ! /) { handleURLNavigation();}}, 300);}/script>/head>body classmenu-layout-sidebar> div classglobal-header-controls> button ideditButton classbtn-icon btn-primary onclicktoggleEditMode() styledisplay: none;> svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24> g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor> path dm16.214 4.982l1.402-1.401a1.982 1.982 0 0 1 2.803 2.803l-1.401 1.402m-2.804-2.804l-5.234 5.234c-1.045 1.046-1.568 1.568-1.924 2.205S8.342 14.561 8 16c1.438-.342 2.942-.7 3.579-1.056s1.16-.879 2.205-1.924l5.234-5.234m-2.804-2.804l2.804 2.804/> path dM21 12c0 4.243 0 6.364-1.318 7.682S16.242 21 12 21s-6.364 0-7.682-1.318S3 16.242 3 12s0-6.364 1.318-7.682S7.758 3 12 3/> /g> /svg> /button> /div> div classcontainer> div classmobile-header> div classmobile-header-content> div classhorizontal-header-logo>/div> div classhorizontal-menu-container>/div> /div> button idhamburgerBtn classhamburger-btn> span classhamburger-icon> span classhamburger-line>/span> span classhamburger-line>/span> span classhamburger-line>/span> /span> /button> /div> div idmobileBackdrop classmobile-backdrop>/div> div classsidebar> div classsidebar-header>/div> div classedit-controls> button classbtn-icon btn-primary onclicktoggleStyleEditor() data-custom-tooltipEdit Style> svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24> path fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 dm19 12.13l-6.061 6.077c-1.783 1.788-2.675 2.682-3.77 2.78q-.27.025-.543 0c-1.094-.098-1.986-.992-3.769-2.78l-2.02-2.026a2.87 2.87 0 0 1 0-4.052m16.163 0l-8.082-8.103M19 12.129H2.837m8.081-8.103l-8.081 8.103m8.081-8.103L8.898 2M22 20a2 2 0 1 1-4 0c0-1.105 2-3 2-3s2 1.895 2 3 colorcurrentColor/> /svg> /button> button classbtn-icon btn-primary onclicktoggleAddForm() data-custom-tooltipAdd Page> svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24> g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor> path dM13 2h.273c3.26 0 4.892 0 6.024.798c.324.228.612.5.855.805c.848 1.066.848 2.6.848 5.67v2.545c0 2.963 0 4.445-.469 5.628c-.754 1.903-2.348 3.403-4.37 4.113c-1.257.441-2.83.441-5.98.441c-1.798 0-2.698 0-3.416-.252c-1.155-.406-2.066-1.263-2.497-2.35C4 18.722 4 17.875 4 16.182V12/> path dM21 12a3.333 3.333 0 0 1-3.333 3.333c-.666 0-1.451-.116-2.098.057a1.67 1.67 0 0 0-1.179 1.179c-.173.647-.057 1.432-.057 2.098A3.333 3.333 0 0 1 11 22m0-16H3m4-4v8/> /g> /svg> /button> button classbtn-icon btn-primary onclicktoggleSidebarElementForm() data-custom-tooltipAdd Element> svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24> path fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 dM18 2v8m4-4h-8M2 6c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.093C3.9 2 4.6 2 6 2s2.1 0 2.635.272a2.5 2.5 0 0 1 1.093 1.093C10 3.9 10 4.6 10 6s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.093C8.1 10 7.4 10 6 10s-2.1 0-2.635-.272a2.5 2.5 0 0 1-1.093-1.093C2 8.1 2 7.4 2 6m0 12c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.092C3.9 14 4.6 14 6 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.093 1.092C10 15.9 10 16.6 10 18s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.092C8.1 22 7.4 22 6 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.093-1.092C2 20.1 2 19.4 2 18m12 0c0-1.4 0-2.1.273-2.635a2.5 2.5 0 0 1 1.092-1.092C15.9 14 16.6 14 18 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.092 1.092C22 15.9 22 16.6 22 18s0 2.1-.273 2.635a2.5 2.5 0 0 1-1.092 1.092C20.1 22 19.4 22 18 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.092-1.092C14 20.1 14 19.4 14 18 colorcurrentColor/> /svg> /button>button classbtn-icon btn-primary onclicktoggleMetadataEditor() data-custom-tooltipSite Metadata>svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24>g fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 colorcurrentColor>path dM7.5 4.945H16a1.5 1.5 0 0 1 1.5 1.5v1.5m-2.5 5H9m3 4H9/>path dM18.497 2H6.307c-.496 0-1.005.073-1.406.368c-1.274.935-2.256 3.02-.273 4.903c.556.528 1.334.72 2.099.72h11.557c.793 0 2.216.113 2.216 2.536v7.454c0 2.22-1.79 4.019-3.997 4.019h-9.03c-2.204 0-3.807-1.557-3.933-3.929L3.506 5.166/>/g>/svg>/button>button idlogoutButton classbtn-icon btn-primary onclicklogout() data-custom-tooltipLog Out styledisplay: none;> svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24 24> path fillnone strokecurrentColor stroke-linecapround stroke-linejoinround stroke-width1.5 dm11 3l-.663.234c-2.578.91-3.868 1.365-4.602 2.403S5 8.043 5 10.777v2.445c0 2.735 0 4.102.735 5.14c.734 1.039 2.024 1.494 4.602 2.404L11 21m10-9H11m10 0c0-.7-1.994-2.008-2.5-2.5M21 12c0 .7-1.994 2.008-2.5 2.5 colorcurrentColor/> /svg> /button> script>// Initialize mobile menu functionalityfunction initMobileMenu() { const hamburgerBtn document.getElementById(hamburgerBtn); const sidebar document.querySelector(.sidebar); const mobileBackdrop document.getElementById(mobileBackdrop); // --- ADDED CHECK: Only update header if layout is NOT horizontal --- let currentLayout sidebar; // Default if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; } // --- END ADDED CHECK --- // Ensure backdrop is properly hidden for horizontal layout on page load if (mobileBackdrop && document.body.classList.contains(menu-layout-horizontal)) { mobileBackdrop.classList.remove(mobile-open); mobileBackdrop.classList.remove(active); // Completely hide the backdrop for horizontal layout mobileBackdrop.style.display none; mobileBackdrop.style.pointerEvents none; mobileBackdrop.style.opacity 0; mobileBackdrop.style.visibility hidden; } // Toggle mobile menu when hamburger is clicked hamburgerBtn.addEventListener(click, function(e) { e.stopPropagation(); toggleMobileMenu(); // Assumes this function exists }); // Close mobile menu when backdrop is clicked if (mobileBackdrop) { mobileBackdrop.addEventListener(click, function() { closeMobileMenu(); // Assumes this function exists }); } // Handle window resize events to ensure proper state window.addEventListener(resize, function() { // If were not on mobile anymore, remove mobile-open class if (window.innerWidth > 768 && sidebar.classList.contains(mobile-open)) { closeMobileMenu(); // Assumes this function exists } });}document.addEventListener(keydown, function(e) { // Check for caret (Shift+6) if (e.key ^ || (e.key 6 && e.shiftKey)) { // Only work in edit mode if (window.isInEditMode && window.isInEditMode()) { console.log(Import shortcut detected); toggleImportClassicForm(); e.preventDefault(); } }});// Enhanced tooltip management with automatic timeouts// Store active tooltip timeouts by IDwindow.tooltipTimeouts {};function showTooltip(trigger) { const tooltipText trigger.getAttribute(data-custom-tooltip); if (!tooltipText) return; // Get or create the main tooltip container let container document.getElementById(tooltip-container); if (!container) { console.log(Tooltip container not found, creating one.); container document.createElement(div); // Assign to container container.id tooltip-container; // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc. document.body.appendChild(container); } const tooltipId `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`; // Remove any existing tooltip for this trigger const existingTooltip document.getElementById(tooltipId); if (existingTooltip) { existingTooltip.remove(); } // Clear any existing timeout for this tooltip if (!window.tooltipTimeouts) { window.tooltipTimeouts {}; // Initialize if it doesnt exist } if (window.tooltipTimeoutstooltipId) { clearTimeout(window.tooltipTimeoutstooltipId); delete window.tooltipTimeoutstooltipId; } // Create tooltip element const tooltip document.createElement(div); tooltip.className tooltip; // Ensure your CSS styles .tooltip tooltip.id tooltipId; tooltip.textContent tooltipText; // Add to container (now container is guaranteed to be an element) container.appendChild(tooltip); // Position tooltip if (typeof positionTooltip function) { positionTooltip(tooltip, trigger); } else { console.error(positionTooltip function is not defined.); tooltip.remove(); // Clean up if we cant position return; } // Show with animation (slight delay for CSS transition) setTimeout(() > { tooltip.style.opacity 1; // The transform for animation should be handled by CSS transitions // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); } // Or ensure positionTooltip sets final transform if it needs to override CSS. // For now, assuming CSS handles the animated transform from its initial state. }, 10); trigger.setAttribute(data-active-tooltip, tooltipId); // Set automatic timeout to hide after 3 seconds window.tooltipTimeoutstooltipId setTimeout(() > { hideTooltip(trigger); // Ensure hideTooltip function exists }, 3000); // Also set up a global timer to ensure all tooltips are dismissed if (window.globalTooltipTimer) { clearTimeout(window.globalTooltipTimer); } window.globalTooltipTimer setTimeout(() > { // Force hide all visible tooltips after 3 seconds const allTooltips document.querySelectorAll(.tooltip); allTooltips.forEach(tooltip > { if (tooltip.style.opacity ! 0) { tooltip.style.opacity 0; setTimeout(() > { if (tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } }, 200); } }); // Clear all timeout references if (window.tooltipTimeouts) { for (const id in window.tooltipTimeouts) { clearTimeout(window.tooltipTimeoutsid); } window.tooltipTimeouts {}; } }, 3000);}// Function to hide a tooltipfunction hideTooltip(trigger) { const tooltipId trigger.getAttribute(data-active-tooltip); if (!tooltipId) return; const tooltip document.getElementById(tooltipId); if (tooltip) { // Fade out tooltip.style.opacity 0; tooltip.style.transform translateX(-50%) translateY(0); // Remove after animation setTimeout(() > { if (tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } }, 200); } // Clear any active timeout for this tooltip if (window.tooltipTimeouts && window.tooltipTimeoutstooltipId) { clearTimeout(window.tooltipTimeoutstooltipId); delete window.tooltipTimeoutstooltipId; } // Clear reference trigger.removeAttribute(data-active-tooltip); // If this was the last tooltip, clear the global timer const remainingTooltips document.querySelectorAll(.tooltip); if (remainingTooltips.length 0 && window.globalTooltipTimer) { clearTimeout(window.globalTooltipTimer); window.globalTooltipTimer null; }}// Function to initialize tooltips across the documentfunction initTooltipSystem() { // Check if container already exists let tooltipContainer document.getElementById(tooltip-container); // Create it if it doesnt exist if (!tooltipContainer) { tooltipContainer document.createElement(div); tooltipContainer.id tooltip-container; document.body.appendChild(tooltipContainer); } // Find all elements with data-custom-tooltip attribute const tooltipTriggers document.querySelectorAll(data-custom-tooltip); // Add event listeners to each trigger tooltipTriggers.forEach(trigger > { // Skip if already initialized if (trigger.hasAttribute(data-custom-tooltip-initialized)) return; // Mark as initialized trigger.setAttribute(data-custom-tooltip-initialized, true); // MouseEnter: Show tooltip trigger.addEventListener(mouseenter, () > { showTooltip(trigger); }); // MouseLeave: Hide tooltip trigger.addEventListener(mouseleave, () > { hideTooltip(trigger); }); // Click: Hide tooltip immediately trigger.addEventListener(click, () > { hideTooltip(trigger); }); // Also handle focus/blur for accessibility trigger.addEventListener(focus, () > { showTooltip(trigger); }); trigger.addEventListener(blur, () > { hideTooltip(trigger); }); }); // Set up a failsafe function to clear any orphaned tooltips window.clearAllTooltips function() { const container document.getElementById(tooltip-container); if (container) { container.innerHTML ; } // Clear all timeouts if (window.tooltipTimeouts) { for (const id in window.tooltipTimeouts) { clearTimeout(window.tooltipTimeoutsid); } window.tooltipTimeouts {}; } // Clear global timer if (window.globalTooltipTimer) { clearTimeout(window.globalTooltipTimer); window.globalTooltipTimer null; } // Clear all data-active-tooltip attributes document.querySelectorAll(data-active-tooltip).forEach(el > { el.removeAttribute(data-active-tooltip); }); }; // Initialize new tooltips when DOM changes setupMutationObserver(); // Add a global function to manually dismiss all tooltips (for testing) window.dismissAllTooltips function() { const allTooltips document.querySelectorAll(.tooltip); allTooltips.forEach(tooltip > { if (tooltip.style.opacity ! 0) { tooltip.style.opacity 0; setTimeout(() > { if (tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } }, 200); } }); // Clear all timeout references if (window.tooltipTimeouts) { for (const id in window.tooltipTimeouts) { clearTimeout(window.tooltipTimeoutsid); } window.tooltipTimeouts {}; } // Clear global timer if (window.globalTooltipTimer) { clearTimeout(window.globalTooltipTimer); window.globalTooltipTimer null; } // Clear all data-active-tooltip attributes document.querySelectorAll(data-active-tooltip).forEach(el > { el.removeAttribute(data-active-tooltip); }); };}function positionTooltip(tooltip, trigger) { const triggerRect trigger.getBoundingClientRect(); // Viewport-relative coordinates // To accurately get tooltip dimensions, temporarily make it visible but off-screen. // The #tooltip-container is position:fixed, so the tooltips absolute positioning // will be relative to the viewport. tooltip.style.visibility hidden; tooltip.style.display block; // Or inline-block if thats its intended final display // No need to change position to absolute here if its CSS is already position: absolute; // within the fixed #tooltip-container. const tooltipHeight tooltip.offsetHeight; const tooltipWidth tooltip.offsetWidth; // Restore visibility; opacity transition will handle the fade-in. tooltip.style.visibility ; // tooltip.style.display ; // Let CSS manage display if opacity is used for showing/hiding // Calculate desired horizontal center point of the trigger const triggerCenter triggerRect.left + triggerRect.width / 2; // Calculate tooltips left position to center it under/over triggers center let left triggerCenter - tooltipWidth / 2; // Default position: above the trigger // Arrow height (8px) + desired gap (e.g., 2px) 10px total offset let top triggerRect.top - tooltipHeight - 10; tooltip.classList.remove(tooltip-bottom); // Manages arrow direction via CSS // Check if it goes off-screen at the top (add a small buffer, e.g., 5px) if (top 5) { // Position below the trigger top triggerRect.bottom + 10; // 10px gap (arrow height + small space) tooltip.classList.add(tooltip-bottom); } // Boundary checks for left position to prevent going off-screen horizontally const viewportWidth window.innerWidth; if (left 5) { // 5px buffer from left edge left 5; } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge left viewportWidth - tooltipWidth - 5; } tooltip.style.left `${left}px`; tooltip.style.top `${top}px`; // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering. // Since JS now calculates the exact left for centering, // we can clear any such transform that might interfere. // However, if your CSS animation relies on a transform, adjust accordingly. // If your CSS only uses opacity for transition, this is fine: tooltip.style.transform ; }// Watch for new tooltip triggers added to the DOMfunction setupMutationObserver() { // Check if MutationObserver is available if (!window.MutationObserver) return; // Create observer instance const observer new MutationObserver((mutations) > { let shouldInit false; // Check if relevant nodes were added mutations.forEach(mutation > { if (mutation.type childList) { mutation.addedNodes.forEach(node > { // Check if its an Element and has data-custom-tooltip or contains elements with data-custom-tooltip if (node.nodeType 1) { // ELEMENT_NODE if (node.hasAttribute && node.hasAttribute(data-custom-tooltip)) { shouldInit true; } else if (node.querySelectorAll) { const tooltips node.querySelectorAll(data-custom-tooltip); if (tooltips.length) shouldInit true; } } }); } }); // If relevant nodes found, reinitialize tooltips if (shouldInit) { initTooltipSystem(); } }); // Start observing the entire document with the configured parameters observer.observe(document.body, { childList: true, subtree: true });}// Initialize when DOM is readydocument.addEventListener(DOMContentLoaded, initTooltipSystem);// Also initialize immediately if document already loadedif (document.readyState complete || document.readyState interactive) { initTooltipSystem();}// Add a global safety function that runs every minute to clear any tooltips that might have gotten stucksetInterval(() > { // Get all visible tooltips const visibleTooltips document.querySelectorAll(.tooltipstyle*opacity: 1); // If any are found, check how long theyve been visible if (visibleTooltips.length > 0) { console.log(Found visible tooltips during cleanup check, clearing them); window.clearAllTooltips(); }}, 5000); // // Toggle mobile menu statefunction toggleMobileMenu() {const sidebar document.querySelector(.sidebar);const hamburgerBtn document.getElementById(hamburgerBtn);const mobileBackdrop document.getElementById(mobileBackdrop);// Check if were in horizontal layout modeconst isHorizontalLayout document.body.classList.contains(menu-layout-horizontal);sidebar.classList.toggle(mobile-open);hamburgerBtn.classList.toggle(active);if (mobileBackdrop) { if (isHorizontalLayout) { // For horizontal layout, completely hide the backdrop mobileBackdrop.style.display none; mobileBackdrop.style.pointerEvents none; mobileBackdrop.style.opacity 0; mobileBackdrop.style.visibility hidden; mobileBackdrop.classList.remove(mobile-open); mobileBackdrop.classList.remove(active); } else { // For other layouts, use normal backdrop behavior mobileBackdrop.classList.toggle(mobile-open); // Slight delay to show opacity transition setTimeout(() > { mobileBackdrop.classList.toggle(active, sidebar.classList.contains(mobile-open)); }, 10); }}// Prevent body scrolling when menu is opendocument.body.style.overflow sidebar.classList.contains(mobile-open) ? hidden : ;}// Close the mobile menufunction closeMobileMenu() {const sidebar document.querySelector(.sidebar);const hamburgerBtn document.getElementById(hamburgerBtn);const mobileBackdrop document.getElementById(mobileBackdrop);// Check if were in horizontal layout modeconst isHorizontalLayout document.body.classList.contains(menu-layout-horizontal);sidebar.classList.remove(mobile-open);hamburgerBtn.classList.remove(active);if (mobileBackdrop) { if (isHorizontalLayout) { // For horizontal layout, ensure backdrop is completely hidden mobileBackdrop.style.display none; mobileBackdrop.style.pointerEvents none; mobileBackdrop.style.opacity 0; mobileBackdrop.style.visibility hidden; mobileBackdrop.classList.remove(mobile-open); mobileBackdrop.classList.remove(active); } else { // For other layouts, use normal backdrop behavior mobileBackdrop.classList.remove(active); setTimeout(() > { mobileBackdrop.classList.remove(mobile-open); }, 300); // Match transition duration }}// Re-enable body scrollingdocument.body.style.overflow ;}function updateMobileHeaderContent() { // Determine current layout let currentLayout sidebar; // Default if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout window.MenuStyleCustomizer.settings.menuLayout || sidebar; } const isMobileView window.innerWidth 768; // Assuming 768px is your mobile breakpoint const mobileHeaderContent document.querySelector(.mobile-header-content); if (!mobileHeaderContent) { console.warn(updateMobileHeaderContent: .mobile-header-content element not found.); return; } console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`); // Clear previous generic mobile content (like centered logo/title) const existingMobileImage mobileHeaderContent.querySelector(.mobile-header-image); const existingMobileText mobileHeaderContent.querySelector(.mobile-header-text, .mobile-header-menu-title, .mobile-header-title); if (existingMobileImage) existingMobileImage.remove(); if (existingMobileText) existingMobileText.remove(); if (currentLayout horizontal && !isMobileView) { // Desktop Horizontal Layout: // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo. // The menu items are in .horizontal-menu-container. // This function should not add a generic logo/title here. console.log(updateMobileHeaderContent: Desktop horizontal layout, specific logo/menu handled by renderHorizontalMenu.); return; } // For Horizontal layout on mobile, we need to clear the horizontal menu items // and show only the mobile header logo/title if (currentLayout horizontal && isMobileView) { console.log(updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo.); // Clear horizontal menu items from mobile header const horizontalMenuContainer mobileHeaderContent.querySelector(.horizontal-menu-container); if (horizontalMenuContainer) { console.log(updateMobileHeaderContent: Clearing horizontal menu container); horizontalMenuContainer.innerHTML ; } else { console.log(updateMobileHeaderContent: No horizontal menu container found to clear); } // Clear horizontal header logo from mobile header const horizontalHeaderLogo mobileHeaderContent.querySelector(.horizontal-header-logo); if (horizontalHeaderLogo) { console.log(updateMobileHeaderContent: Clearing horizontal header logo); horizontalHeaderLogo.innerHTML ; } else { console.log(updateMobileHeaderContent: No horizontal header logo found to clear); } // Also clear any existing horizontal menu items that might be directly in mobile-header-content const existingHorizontalMenuItems mobileHeaderContent.querySelectorAll(.horizontal-menu-item); if (existingHorizontalMenuItems.length > 0) { console.log(updateMobileHeaderContent: Removing + existingHorizontalMenuItems.length + existing horizontal menu items); existingHorizontalMenuItems.forEach(item > item.remove()); } } // For Sidebar, Top, OR Horizontal layout on mobile: // Populate .mobile-header-content with the first sidebar element (logo/title). // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout. console.log(updateMobileHeaderContent: Populating with first sidebar element.); // For horizontal layout on mobile, use the header-specific function to get logo/title // For other layouts, use the general function let firstElement; if (currentLayout horizontal && isMobileView) { firstElement getFirstSidebarElementForHeader(); // Gets logo/title specifically console.log(updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout); } else { firstElement getFirstSidebarElement(); // Gets any first element console.log(updateMobileHeaderContent: Using getFirstSidebarElement for other layouts); } if (firstElement) { console.log(updateMobileHeaderContent: First element found:, JSON.stringify(firstElement)); if (firstElement.type image && (firstElement.imageData || firstElement.imageUrl)) { console.log(updateMobileHeaderContent: Creating mobile header image); const img document.createElement(img); img.src firstElement.imageUrl || firstElement.imageData; img.alt firstElement.title || Logo; img.className mobile-header-image; // For specific styling if needed // Handle linking (similar to sidebar-manager.js _renderImageElement) if (firstElement.linkType && firstElement.linkType ! none) { const link document.createElement(a); link.target firstElement.linkTarget || _self; let finalLinkHref #; let isValidLink false; if (firstElement.linkType external && firstElement.linkUrl) { finalLinkHref firstElement.linkUrl; if (link.target _blank) { link.rel noopener noreferrer; } isValidLink true; } else if (firstElement.linkType internal && firstElement.linkPageId) { const allGalleries window.galleries || (typeof galleries ! undefined ? galleries : ); const targetPage allGalleries.find(g > g.id parseInt(firstElement.linkPageId)); if (targetPage) { // Handle preview mode URLs const isPreviewMode window.location.hostname preview.neonsky.app; if (isPreviewMode) { const pathParts window.location.pathname.split(/).filter(Boolean); const siteGuid pathParts0; if (siteGuid && targetPage.slug) { finalLinkHref / + siteGuid + / + targetPage.slug; isValidLink true; } else if (targetPage.url && targetPage.url.startsWith(/)) { finalLinkHref targetPage.url; isValidLink true; } } else { if (targetPage.slug) { finalLinkHref / + targetPage.slug; isValidLink true; } else if (targetPage.url && targetPage.url.startsWith(/)) { finalLinkHref targetPage.url; isValidLink true; } } } } if (isValidLink) { link.href finalLinkHref; // For internal links, add click handler to use site navigation if (firstElement.linkType internal && firstElement.linkPageId) { link.addEventListener(click, (e) > { e.preventDefault(); const allGalleries window.galleries || (typeof galleries ! undefined ? galleries : ); const targetPage allGalleries.find(g > g.id parseInt(firstElement.linkPageId)); if (targetPage) { if (targetPage.isPage && typeof window.loadPage function) { window.loadPage(targetPage.id); } else if (typeof window.loadGallery function) { window.loadGallery(targetPage.id); } else { // Fallback to standard navigation window.location.href finalLinkHref; } } else { // Fallback to standard navigation if page not found window.location.href finalLinkHref; } }); } link.appendChild(img); mobileHeaderContent.appendChild(link); } else { mobileHeaderContent.appendChild(img); } } else { mobileHeaderContent.appendChild(img); } } else if (firstElement.type text && firstElement.textContent) { console.log(updateMobileHeaderContent: Creating mobile header text); const textDiv document.createElement(div); textDiv.className mobile-header-text; // For specific styling textDiv.innerHTML firstElement.textContent; // Use innerHTML if textContent can contain HTML mobileHeaderContent.appendChild(textDiv); } else if (firstElement.type menu && galleries && galleries.length > 0) { console.log(updateMobileHeaderContent: Creating mobile header menu title); // Fallback for menu type, find the first visible gallery title const firstVisibleGallery galleries.find(g > g.visible ! false && !g.isSpacer); const titleText firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || Menu); const titleDiv document.createElement(div); titleDiv.className mobile-header-menu-title; // For specific styling titleDiv.textContent titleText; mobileHeaderContent.appendChild(titleDiv); } else { // Default fallback console.log(updateMobileHeaderContent: Creating mobile header title (fallback)); const titleDiv document.createElement(div); titleDiv.className mobile-header-title; // For specific styling titleDiv.textContent firstElement.title || (galleries && galleries.length > 0 && galleries0.title) || Menu; mobileHeaderContent.appendChild(titleDiv); } } else { // Fallback if no sidebar elements exist const defaultTitle document.createElement(div); defaultTitle.className mobile-header-title; defaultTitle.textContent Menu; mobileHeaderContent.appendChild(defaultTitle); console.log(updateMobileHeaderContent: No first sidebar element found, showing default title.); }}// Helper function to get the first element in the sidebarfunction getFirstSidebarElement() {// Check if window.SidebarManager exists and has elementsif (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {// Filter to only visible elementsconst visibleElements window.SidebarManager.elements.filter(el > el.visible ! false);if (visibleElements.length > 0) { // Sort by position and return the first one return visibleElements.sort((a, b) > a.position - b.position)0;}}// If no sidebar elements, return first gallery as fallbackif (galleries.length > 0) {const firstVisibleGallery galleries.find(g > g.visible ! false);if (firstVisibleGallery) { return { type: menu, title: firstVisibleGallery.title };}}// Default fallbackreturn {type: text,title: Menu};}// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE// Just replace the updateMobileTitle function and add our initialization code// Replace the existing updateMobileTitle function with this onefunction updateMobileTitle() {// Always call our implementation that shows the top sidebar elementupdateMobileHeaderContent();}// Add window resize listener to handle mobile header content updateswindow.addEventListener(resize, () > { // Debounce the resize event to avoid excessive calls clearTimeout(window.resizeTimeout); window.resizeTimeout setTimeout(() > { if (typeof updateMobileHeaderContent function) { updateMobileHeaderContent(); } }, 250);});//separate initialization function that doesnt replace the original initMobileMenufunction initTopElementDisplay() {console.log(Initializing top element display);// Run immediatelyupdateMobileHeaderContent();// Also hook into loadGallery to ensure it runs whenever galleries changeconst originalLoadGallery window.loadGallery;if (typeof originalLoadGallery function) {window.loadGallery function(id, event) { // Call the original first originalLoadGallery(id, event); // Then update our header updateMobileHeaderContent();};}}// Initialize our functionality after a short delay to ensure SidebarManager is readydocument.addEventListener(DOMContentLoaded, function() {// Short delay to ensure SidebarManager is initializedsetTimeout(initTopElementDisplay, 100);// Also run a bit later to catch any late-loading elementssetTimeout(updateMobileHeaderContent, 500);});// Run immediately if document is already loadedif (document.readyState complete || document.readyState interactive) {setTimeout(initTopElementDisplay, 100);}// Add a final fallback to ensure gallery visibility on window loadwindow.addEventListener(load, function() { setTimeout(ensureGalleryVisibility, 2000);});(function() { /** * EnhancedGallerySystem - Comprehensive gallery script and state management */ class EnhancedGallerySystem { constructor() { // Core state tracking this.initialized false; this.initPromise null; this.isLoading false; this.currentGalleryId null; this.version 1.0.0; // Change when gallery script updates // Track loaded resources this.loadedScripts new Set(); this.loadedStyles new Set(); // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues) const galleryScriptsBase window.location.origin + /gallery-scripts/; this.essentialResources { scripts: { id: neon-gallery-main, src: galleryScriptsBase + neon-gallery-main-v260119-004.js }, { id: neon-gallery-hp, src: galleryScriptsBase + neon-gallery-hp-hydra-v260119-004.js } , styles: { id: neon-gallery-css, href: galleryScriptsBase + neon-gallery-hp-hydra-v260119-004.css } }; // Keep track of original initialization functions this.originalInitFunctions {}; // Keep reference to gallery state between changes this.previousGalleryState null; } /** * Setup the correct gallery container structure * @param {HTMLElement} container - The main container element * @returns {boolean} - Success indicator */setupCorrectGalleryContainer(container) { console.log(EnhancedGallerySystem: Setting up correct gallery container structure); // Validate container if (!container) { console.error(EnhancedGallerySystem: Main gallery container is null); return false; } try { // Clear the container container.innerHTML ; // Create the exact container structure expected by the gallery script const neonContainer document.createElement(div); neonContainer.id neon-gallery-container; neonContainer.style.width 100%; neonContainer.style.height 100%; neonContainer.style.position relative; // Add the container to the DOM container.appendChild(neonContainer); // Set visibility container.style.opacity 1; container.style.display block; console.log(EnhancedGallerySystem: Gallery container structure created); return true; } catch (error) { console.error(EnhancedGallerySystem: Error setting up gallery container:, error); return false; }} /** * Initialize the gallery system * @returns {Promise} Resolves when all core dependencies are loaded */ initialize() { // Return existing promise if already initializing if (this.initPromise) { return this.initPromise; } console.log(EnhancedGallerySystem: Initializing gallery framework); this.isLoading true; // Create a promise to track loading completion this.initPromise (async () > { try { // CRITICAL: Initialize neonGalleryState before anything else // This prevents Cannot set properties of undefined errors if (!window.neonGalleryState) { window.neonGalleryState { initialized: false, initializing: false, domInitialized: false, configApplied: false, initializationPromise: null }; console.log(EnhancedGallerySystem: Initialized neonGalleryState); } // 1. First ensure all essential styles are loaded await this.loadStyles(); // 2. Preload all scripts to hint to the browser this.preloadScripts(); // 3. Load all essential scripts await this.loadScripts(); // 4. Initialize the gallery events this.setupEvents(); // Mark as fully initialized this.initialized true; this.isLoading false; console.log(EnhancedGallerySystem: Successfully initialized); return true; } catch (error) { console.error(EnhancedGallerySystem: Initialization failed:, error); this.isLoading false; throw error; } })(); return this.initPromise; } /** * Load all necessary stylesheet resources */ async loadStyles() { console.log(EnhancedGallerySystem: Loading essential stylesheets); const stylePromises this.essentialResources.styles.map(style > { // Skip if already loaded if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) { console.log(`Style already loaded: ${style.id}`); this.loadedStyles.add(style.id); return Promise.resolve(); } return new Promise((resolve, reject) > { const link document.createElement(link); link.id style.id; link.rel stylesheet; // Use proxy URL if flag is set, otherwise use CDN URL let styleUrl `${style.href}?v${this.version}`; if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes(cdn.neonsky.app)) { styleUrl window.convertCdnToProxyUrl(style.href) + ?v + this.version; console.log(🔍 Using proxy URL for style:, style.id, styleUrl); } link.href styleUrl; link.onload () > { console.log(`✅ Style loaded: ${style.id}`); this.loadedStyles.add(style.id); resolve(); }; link.onerror (err) > { console.error(`❌ Failed to load style: ${style.id}`, err); if (style.href.includes(cdn.neonsky.app) && !styleUrl.includes(/cdn-proxy/)) { console.error(🔄 Retrying via proxy:, style.href); const proxyUrl window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); link.href proxyUrl + ?v + this.version; link.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); link.href link.href.replace(cdn.neonsky.app, storage.neonsky.app); // Dont reject - let it try storage }; } else { reject(new Error(`Failed to load ${style.id}`)); } }; document.head.appendChild(link); }); }); // Wait for all styles to load await Promise.all(stylePromises); console.log(EnhancedGallerySystem: All stylesheets loaded); } /** * Preload scripts to improve browser loading priority */ preloadScripts() { this.essentialResources.scripts.forEach(script > { // Enhanced check for already preloaded scripts const scriptName script.src.split(/).pop(); const alreadyPreloaded document.querySelector(`linkrelpreloadhref*${scriptName}`) || document.querySelector(`scriptsrc*${scriptName}`); if (alreadyPreloaded) { console.log(`Script already preloaded or loaded: ${script.id}`); return; } const preload document.createElement(link); preload.rel preload; preload.href `${script.src}?v${this.version}`; preload.as script; document.head.appendChild(preload); console.log(`Script preloaded: ${script.id}`); }); } /** * Load all necessary scripts */ async loadScripts() { console.log(EnhancedGallerySystem: Loading essential scripts); const scriptPromises this.essentialResources.scripts.map(script > { // Enhanced check for already loaded scripts const alreadyLoaded this.loadedScripts.has(script.id) || document.getElementById(script.id) || document.querySelector(`scriptsrc*${script.src.split(/).pop()}`); if (alreadyLoaded) { console.log(`Script already loaded: ${script.id}`); this.loadedScripts.add(script.id); return Promise.resolve(); } return new Promise((resolve, reject) > { const scriptElement document.createElement(script); scriptElement.id script.id; // Use proxy URL if flag is set, otherwise use CDN URL let scriptUrl `${script.src}?v${this.version}`; if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes(cdn.neonsky.app)) { scriptUrl window.convertCdnToProxyUrl(script.src) + ?v + this.version; console.log(🔍 Using proxy URL for script:, script.id, scriptUrl); } scriptElement.src scriptUrl; scriptElement.async true; scriptElement.onload () > { console.log(`✅ Script loaded: ${script.id}`); this.loadedScripts.add(script.id); resolve(); }; scriptElement.onerror (err) > { console.error(`❌ Failed to load script: ${script.id}`, err); // Try proxy fallback if not already using proxy if (script.src.includes(cdn.neonsky.app) && !scriptUrl.includes(/cdn-proxy/)) { console.error(🔄 Retrying script via proxy:, script.id); const proxyUrl window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace(https://cdn.neonsky.app/, window.location.origin + /cdn-proxy/); scriptElement.src proxyUrl + ?v + this.version; scriptElement.onerror function() { console.error(❌ Proxy failed, trying storage:, proxyUrl); scriptElement.src scriptElement.src.replace(cdn.neonsky.app, storage.neonsky.app); // Dont reject - let it try storage }; } else { reject(new Error(`Failed to load ${script.id}`)); } }; document.body.appendChild(scriptElement); }); }); // Wait for all scripts to load await Promise.all(scriptPromises); console.log(EnhancedGallerySystem: All scripts loaded); // Save references to original initialization functions after scripts are loaded if (window.initNeonGallery && typeof window.initNeonGallery function) { this.originalInitFunctions.initNeonGallery window.initNeonGallery; } if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave function) { this.originalInitFunctions.reinitializeGalleryAfterSave window.reinitializeGalleryAfterSave; } } /** * Set up enhanced event listeners */ setupEvents() { // Listen for gallery save events to capture latest state document.addEventListener(gallery-saved, (event) > { if (event.detail && event.detail.settings) { this.captureGalleryState(event.detail.settings); } }); // Override existing functions to capture state changes this.enhanceExistingFunctions(); } /** * Enhance existing gallery functions to work with our system */ enhanceExistingFunctions() { // Save original functions const originalInit window.initNeonGallery; const originalReinit window.reinitializeGalleryAfterSave; // Only enhance if they exist if (typeof originalInit function) { window.initNeonGallery (...args) > { // Call original with all arguments const result originalInit.apply(this, args); // Capture state after initialization if (window.neonGalleryConfig) { this.captureGalleryState(window.neonGalleryConfig); } return result; }; } if (typeof originalReinit function) { window.reinitializeGalleryAfterSave async (savedSettings) > { // Call original function const result await originalReinit(savedSettings); // Capture updated state this.captureGalleryState(savedSettings); return result; }; } } /** * Capture current gallery state for potential reuse */ captureGalleryState(settings) { if (!settings) return; // Store a deep copy of the settings to avoid reference issues try { this.previousGalleryState JSON.parse(JSON.stringify(settings)); console.log(EnhancedGallerySystem: Captured gallery state for possible reuse); } catch (e) { console.error(EnhancedGallerySystem: Failed to capture gallery state:, e); } } /** * Load a specific gallery using the enhanced system * @param {Object} gallery The gallery configuration * @param {HTMLElement} container The container to load the gallery into */ async loadGallery(gallery, container) { if (!gallery) { console.error(EnhancedGallerySystem: Invalid gallery configuration); return false; } stopAutoAdvanceTimer(); // CRITICAL: Verify container exists if (!container) { console.error(EnhancedGallerySystem: Gallery container is null or undefined); return false; } // Make sure the system is initialized try { await this.initialize(); } catch (error) { console.error(EnhancedGallerySystem: Failed to initialize gallery framework:, error); //this.showErrorMessage(container, Failed to initialize gallery system. Please try again later.); return false; } // Update current gallery tracking this.currentGalleryId gallery.id; // Create a unique instance ID for this gallery load const galleryInstanceId Date.now(); console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`); try { // Clean up previous gallery state this.cleanupGalleryState(container); // CRITICAL: Set up the correct gallery container FIRST // This ensures the container is ready before any gallery code runs const containerSetup this.setupCorrectGalleryContainer(container); if (!containerSetup) { throw new Error(Failed to set up gallery container); } // Set up Parameters needed by the gallery this.setupGalleryParameters(gallery, galleryInstanceId); // Determine which initialization method to use if (this.canUseReinitForGallery(gallery)) { return await this.reinitializeGallery(gallery, container); } else { return await this.initializeNewGallery(gallery, container, galleryInstanceId); } } catch (error) { console.error(EnhancedGallerySystem: Error loading gallery:, error); if (container) { console.error(EnhancedGallerySystem: Error loading gallery:, error); //this.showErrorMessage(container, There was a problem loading the gallery. Please try again later.); } return false; }} /** * Determine if we can use reinitialization for this gallery */ canUseReinitForGallery(gallery) { // Check if reinitializeGalleryAfterSave function exists if (typeof window.reinitializeGalleryAfterSave ! function) { return false; } // Check if we have a previous state to reuse if (!this.previousGalleryState) { return false; } // Check if the gallery has the necessary properties for reinit if (!gallery.pageId || !gallery.galleryOptions) { return false; } // CRITICAL: Check if the gallery wrapper actually exists in the DOM // Reinitialization requires an existing gallery structure const galleryWrapper document.querySelector(.gallery-wrapper); const neonContainer document.getElementById(neon-gallery-container); if (!galleryWrapper || !neonContainer) { console.log(EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found); return false; } return true; } /** * Reinitialize a gallery using the reinitialization process */async reinitializeGallery(gallery, container) { console.log(EnhancedGallerySystem: Using reinitialization process for gallery, gallery.id); try { // CRITICAL: Ensure CSS is loaded before attempting reinitialization ensureGalleryStylesLoaded(); // We should already have the proper container, but just in case: if (!document.getElementById(neon-gallery-container)) { this.setupCorrectGalleryContainer(container); } // CRITICAL: Ensure config is set up with all required fields before reinitialization const galleryOptions gallery.galleryOptions || {}; window.neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: galleryOptions.manualCollectionName || mod, layoutType: galleryOptions.layoutType || grid, ...galleryOptions, siteId: window.siteId || gallery.siteId || , galleryId: gallery.id, // Explicitly include all permanent storage fields isPerma: galleryOptions.isPerma, permaURL: galleryOptions.permaURL, loadTxId: galleryOptions.loadTxId, loadPermaURL: galleryOptions.loadPermaURL, containerSelector: #neon-gallery-container }; // Prepare settings by merging previous state with new gallery options const settings { ...this.previousGalleryState, ...gallery.galleryOptions, pageId: gallery.pageId, forceReload: true, containerSelector: #neon-gallery-container // Add this explicitly }; // Call reinitializeGalleryAfterSave with the merged settings const result await window.reinitializeGalleryAfterSave(settings); if (result) { console.log(EnhancedGallerySystem: Gallery reinitialization completed successfully); // Fade in the container container.style.opacity 1; return true; } else { throw new Error(Reinitialization did not complete successfully); } } catch (error) { console.error(EnhancedGallerySystem: Error reinitializing gallery:, error); // Fall back to standard initialization - ensure config is set up properly const galleryInstanceId Date.now(); // Re-setup parameters and config for the fallback this.setupGalleryParameters(gallery, galleryInstanceId); return this.initializeNewGallery(gallery, container, galleryInstanceId); }} /** * Initialize a gallery from scratch (fallback method) */ async initializeNewGallery(gallery, container, galleryInstanceId) { console.log(EnhancedGallerySystem: Using standard initialization for gallery, gallery.id); try { // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements ensureGalleryStylesLoaded(); // Clean up container if (container) { container.innerHTML ; } else { console.error(EnhancedGallerySystem: Container is null in initializeNewGallery); return false; } // Create gallery container with unique ID const galleryContainerId `gallery-container-${gallery.id}-${galleryInstanceId}`; const neonGalleryContainer document.createElement(div); neonGalleryContainer.id neon-gallery-container; neonGalleryContainer.className gallery-direct-content; neonGalleryContainer.setAttribute(data-gallery-id, gallery.id.toString()); neonGalleryContainer.setAttribute(data-instance-id, galleryInstanceId.toString()); neonGalleryContainer.setAttribute(data-unique-id, galleryContainerId); neonGalleryContainer.style.width 100%; neonGalleryContainer.style.height 100%; container.appendChild(neonGalleryContainer); // CRITICAL: Initialize neonGalleryState if it doesnt exist if (!window.neonGalleryState) { window.neonGalleryState { initialized: false, initializing: false, domInitialized: false, configApplied: false, initializationPromise: null }; } // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields // Dont rely on conditional logic - always set it correctly here const galleryOptions gallery.galleryOptions || {}; window.neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: galleryOptions.manualCollectionName || mod, layoutType: galleryOptions.layoutType || grid, ...galleryOptions, siteId: window.siteId || gallery.siteId || , galleryId: gallery.id, galleryInstanceId: galleryInstanceId, containerSelector: #neon-gallery-container, // Explicitly include all permanent storage fields (critical for proper initialization) isPerma: galleryOptions.isPerma, permaURL: galleryOptions.permaURL, loadTxId: galleryOptions.loadTxId, loadPermaURL: galleryOptions.loadPermaURL }; // Call the initialization function directly if available if (typeof window.initNeonGallery function) { console.log(EnhancedGallerySystem: Calling direct initialization method); try { // Wait a tiny bit to ensure DOM updates have propagated await new Promise(resolve > setTimeout(resolve, 10)); const result await window.initNeonGallery(); console.log(EnhancedGallerySystem: Direct initialization successful); // Save state for future loads if (window.neonGalleryConfig) { this.captureGalleryState(window.neonGalleryConfig); } // Fade in the container container.style.opacity 1; return true; } catch (e) { console.error(EnhancedGallerySystem: Direct initialization failed:, e); throw e; } } else { // If initNeonGallery isnt available, use event-based initialization console.log(EnhancedGallerySystem: Using event-based initialization fallback); // Create an event to trigger initialization const initEvent new CustomEvent(neon-gallery-init, { detail: { galleryId: gallery.id, instanceId: galleryInstanceId, containerId: galleryContainerId } }); document.dispatchEvent(initEvent); // Wait a short time to see if initialization completes await new Promise(resolve > setTimeout(resolve, 500)); // Fade in regardless of result - gallery script should handle displaying errors container.style.opacity 1; return true; } } catch (error) { console.error(EnhancedGallerySystem: Error in standard initialization:, error); if (container) { this.showErrorMessage(container, Failed to load gallery content. Please try again later.); } return false; } } /** * Clean up gallery state without removing loaded scripts */ cleanupGalleryState(container) { console.log(EnhancedGallerySystem: Cleaning up gallery state); // Stop any ongoing singles autoplay from the previous gallery if (typeof window.stopSinglesAutoplay function) { try { window.stopSinglesAutoplay(); console.log(EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.); } catch (error) { console.warn(EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:, error); } } else { console.warn(EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.); } // 1. Reset global gallery state variables const resetGlobals neonGalleryInitComplete, neonGalleryInitInProgress, neonGalleryLoaded, neonGalleryConfig, neonGalleryCache, neonGalleryImages, neonGallerySettings, neonLightbox, neonGalleryEventListeners, neonGalleryInstance, currentGalleryId, galleryData, imageCache, thumbnailCache ; resetGlobals.forEach(prop > { if (windowprop ! undefined) { windowprop null; } }); // 2. Reset neonGalleryState if its a proper object with properties if (window.neonGalleryState && typeof window.neonGalleryState object) { window.neonGalleryState { initialized: false, initializing: false, domInitialized: false, configApplied: false, initializationPromise: null }; } // 3. Remove lightbox elements and overlays document.querySelectorAll(#neon-lightbox, .neon-lightbox, .lightbox-container).forEach(el > { if (el && el.parentNode) { el.parentNode.removeChild(el); } }); // 4. Remove any gallery-specific DOM elements document.querySelectorAll( .neon-gallery-wrapper, .fullscreen-overlay, + .neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, + .neon-thumbnails, .neon-image, .neon-caption, + .neon-controls, .neon-pagination ).forEach(el > { if (el && el.parentNode) { el.parentNode.removeChild(el); } }); // 5. Clear container if provided and valid if (container) { container.innerHTML ; container.style.opacity 0; } // 6. Clear keyboard listeners to prevent multiple listeners from different galleries if (window.currentLightboxKeyboardListener) { document.removeEventListener(keydown, window.currentLightboxKeyboardListener); window.currentLightboxKeyboardListener null; console.log(EnhancedGallerySystem: Cleared keyboard listeners during cleanup); } window.lightboxKeyboardListenerActive false; } /** * Prepare container for gallery loading */ prepareContainer(container, gallery, instanceId) { // CRITICAL: Add null check for container if (!container) { console.error(EnhancedGallerySystem: Container is null in prepareContainer); return; } try { // Add attributes and data container.setAttribute(data-gallery-id, gallery.id.toString()); container.setAttribute(data-instance-id, instanceId.toString()); // Add loading indicator const loadingIndicator document.createElement(div); loadingIndicator.className gallery-loading; loadingIndicator.innerHTML ` div classspinner>/div> div classloading-text>Loading Gallery.../div> `; container.appendChild(loadingIndicator); } catch (error) { console.error(EnhancedGallerySystem: Error preparing container:, error); } } setupGalleryParameters(gallery, galleryInstanceId) { // Set up gallery parameters with enhanced uniqueness window.Parameters window.Parameters || {}; // Clone and extend existing Parameters to avoid losing important values const existingParams { ...window.Parameters }; window.Parameters { ...existingParams, SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || this.slugify(gallery.title), // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active isInEditor: window.isEditing || localStorage.getItem(hydra_is_admin) true || window.isAdmin true || document.body.classList.contains(hydra-admin), siteId: window.siteId || gallery.siteId || , isHydra: true, // CRITICAL: These properties help the gallery script find the container galleryContainerId: neon-gallery-container, containerSelector: #neon-gallery-container, // Additional properties galleryInstanceId: galleryInstanceId, galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`, loadTimestamp: galleryInstanceId, loadedGalleryId: gallery.id }; // Set up gallery config - CRITICAL: Include all permanent storage fields const galleryOptions gallery.galleryOptions || {}; window.neonGalleryConfig { useData: true, useCDN: true, version: live, manualCollectionName: galleryOptions.manualCollectionName || mod, layoutType: galleryOptions.layoutType || grid, ...galleryOptions, siteId: window.siteId || gallery.siteId || , galleryId: gallery.id, galleryInstanceId: galleryInstanceId, // Add container selector here too containerSelector: #neon-gallery-container, // CRITICAL: Explicitly include all permanent storage fields to ensure theyre available // These are needed for proper gallery initialization and data loading isPerma: galleryOptions.isPerma, permaURL: galleryOptions.permaURL, loadTxId: galleryOptions.loadTxId, loadPermaURL: galleryOptions.loadPermaURL };} /** * Show error message in container */ showErrorMessage(container, message) { // CRITICAL: Add null check for container if (!container) { console.error(EnhancedGallerySystem: Container is null in showErrorMessage); return; } try { // Clear container container.innerHTML ; /* // Create error message element const errorElement document.createElement(div); errorElement.className gallery-error; errorElement.innerHTML ` h3>Error Loading Gallery/h3> p>${message}/p> `; */ container.appendChild(errorElement); container.style.opacity 1; } catch (error) { console.error(EnhancedGallerySystem: Error showing error message:, error); } } /** * Utility function to convert title to slug */ slugify(text) { return text .toLowerCase() .replace(/s+/g, -) .replace(/^w-+/g, ) .replace(/--+/g, -) .trim(); } } // Create a singleton instance window.EnhancedGallerySystem window.EnhancedGallerySystem || new EnhancedGallerySystem(); /** * Enhanced loadGallery function using the EnhancedGallerySystem * This replaces the existing loadGallery function */const enhancedLoadGallery async function(id, event) { const originalLoadGallery window.originalLoadGallery; stopAutoAdvanceTimer(); if (event) { event.preventDefault(); event.stopPropagation(); } let galleryContainer document.querySelector(.gallery-container); if (!galleryContainer) { galleryContainer document.getElementById(galleryFrame)?.parentElement || document.getElementById(main-content) || document.getElementById(content); if (!galleryContainer) { console.error(Gallery container not found, falling back to original function); if (typeof originalLoadGallery function) { return originalLoadGallery(id, event); } else { console.error(Gallery container not found and no fallback available); return; } } } if (galleryContainer) { galleryContainer.style.opacity 0; } console.log(Loading gallery with ID (enhancedLoadGallery):, id); const gallery findGalleryById(galleries, id); if (!gallery) { console.warn(No gallery found with ID:, id); if (galleryContainer) { galleryContainer.innerHTML ` div classgallery-error> h3>Gallery Not Found/h3> p>The requested gallery could not be found./p> /div> `; galleryContainer.style.opacity 1; } return; } if (gallery.isPage true) { return typeof window.loadPage function ? window.loadPage(id, event) : loadPage(id, event); } if (gallery.isSubmenu) { console.log(Gallery is submenu, skipping URL update); toggleSubmenu(id, event || { stopPropagation: () > {} }); if (galleryContainer){ galleryContainer.style.opacity 1; } return; } console.log(Found gallery:, gallery.title); // Check for gallery-level password protection (only for visitors, not in edit mode) if (!isInEditMode() && gallery.passwordProtected) { // Use PageManagers gallery password protection methods if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess function && typeof window.PageManager.createGalleryPasswordOverlay function) { if (!window.PageManager.checkGalleryPasswordAccess(gallery)) { console.log(Gallery is password protected, showing overlay); window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) > { if (accessGranted) { // Reload the gallery now that access is granted enhancedLoadGallery(id, event); } }); return; // Exit early, will reload after password is entered } } } window.activeGalleryId id; activeGalleryId id; const bodyEl document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { bodyEl.classList.add(menu-hidden-on-page); } else { bodyEl.classList.remove(menu-hidden-on-page); } try { if (typeof updateActiveStates function) updateActiveStates(); if (typeof updateMobileTitle function) updateMobileTitle(); if (typeof closeMobileMenu function) closeMobileMenu(); // Close the gallery options panel when navigating to a different gallery if (typeof window.closeOptionsPanel function) { window.closeOptionsPanel(); } if (typeof updateURLWithGallerySlug function) { updateURLWithGallerySlug(gallery); } } catch (uiError) { console.error(Error updating UI state:, uiError); } try { // Add detailed logging about EnhancedGallerySystem availability console.log(🔍 Checking EnhancedGallerySystem availability:, { exists: !!window.EnhancedGallerySystem, hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery function), initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized), isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading) }); if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery function) { console.log(✅ EnhancedGallerySystem available, calling loadGallery); const result await window.EnhancedGallerySystem.loadGallery(gallery, galleryContainer); console.log(📊 EnhancedGallerySystem.loadGallery result:, result); if (!result && typeof originalLoadGallery function) { console.log(⚠️ Enhanced gallery loading failed, falling back to original function); return originalLoadGallery(id, event); } } else { console.error(❌ EnhancedGallerySystem or its loadGallery method is not available., { EnhancedGallerySystem: !!window.EnhancedGallerySystem, loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery function), useCdnProxy: window.useCdnProxy || false }); // Fallback: Try using createGalleryContext directly console.log(🔄 Attempting fallback: createGalleryContext); if (typeof createGalleryContext function) { createGalleryContext(gallery, galleryContainer); if (galleryContainer) { galleryContainer.style.opacity 1; } return; } if (galleryContainer) { galleryContainer.innerHTML `div classgallery-error>Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery function)}/div>`; galleryContainer.style.opacity 1; } return; } } catch (error) { console.error(Error loading gallery:, error); if (galleryContainer) { try { galleryContainer.innerHTML ` div classgallery-error> h3>Error Loading Gallery/h3> p>There was a problem loading the gallery. Please try again later./p> p classerror-details>${error.message}/p> /div> `; galleryContainer.style.opacity 1; } catch (containerError) { console.error(Error showing error message in container:, containerError); } } if (typeof originalLoadGallery function) { console.log(Falling back to original loadGallery after error); return originalLoadGallery(id, event); } } }; /** * Initialization function to set up the gallery system */ function initializeGallerySystem() { console.log(Initializing enhanced gallery system); // Store reference to original function before replacing if (typeof window.loadGallery function) { window.originalLoadGallery window.loadGallery; } // Replace the existing loadGallery function with our optimized version window.loadGallery enhancedLoadGallery; // Add this CSS for loading/error states addGallerySystemStyles(); // Start preloading the gallery system window.EnhancedGallerySystem.initialize().catch(error > { console.warn(Gallery system preloading failed:, error); }); } /** * Add CSS styles for loading indicators and error messages */ function addGallerySystemStyles() { const styleId enhanced-gallery-system-styles; // Skip if already added if (document.getElementById(styleId)) { return; } const style document.createElement(style); style.id styleId; style.textContent ` /* Loading indicator */ .gallery-loading { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; min-height: 200px; opacity: 0 !important; } .gallery-loading .spinner { width: 50px; height: 50px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #4682B4; animation: spin 1s ease-in-out infinite; margin-bottom: 20px; opacity: 0 !important; } @keyframes spin { to { transform: rotate(360deg); } } .gallery-loading .loading-text { font-family: Arial, sans-serif; font-size: 16px; color: #555; opacity: 0; } /* Error message */ .gallery-error { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; min-height: 200px; padding: 30px; text-align: center; } .gallery-error h3 { font-family: Arial, sans-serif; font-size: 24px; color:rgba(211, 47, 47, 0.25); margin-bottom: 20px; } .gallery-error p { font-family: Arial, sans-serif; font-size: 16px; color: #555; max-width: 600px; margin-bottom: 10px; } .gallery-error .error-details { font-size: 14px; color: #888; font-style: italic; } `; document.head.appendChild(style); } // Initialize gallery system immediately if possible initializeGallerySystem();})();/** * Enhanced Gallery Link Interceptor * Handles clicks on gallery links to prevent full page reloads */(function() { // Store any loaded gallery data for quick reference const GALLERY_DATA_CACHE { initialized: false, galleries: , lastUpdate: 0 }; // Initialize when DOM is ready to catch early clicks document.addEventListener(DOMContentLoaded, initGalleryLinkInterceptor); // Also initialize when window loads to ensure complete gallery data window.addEventListener(load, function() { // Use a small delay to ensure galleries have fully initialized setTimeout(initGalleryLinkInterceptor, 200); }); // Additional init on first user interaction document.addEventListener(click, function initOnFirstClick() { initGalleryLinkInterceptor(); // Remove this listener after first click document.removeEventListener(click, initOnFirstClick); }, { once: true }); // Main initialization function function initGalleryLinkInterceptor() { // Prevent multiple initializations if (window._galleryLinkInterceptorInitialized) { // If already initialized, just refresh the gallery data refreshGalleryCache(); return; } window._galleryLinkInterceptorInitialized true; console.log(Initializing enhanced gallery link interceptor); // Update our cache with current galleries data refreshGalleryCache(); // Get primary containers that might contain links const galleryContainer document.querySelector(.gallery-container); const mainContainer document.querySelector(.container) || document.body; if (!galleryContainer && !mainContainer) { console.warn(No gallery containers found, link interceptor partially initialized); } // Add global click handler to catch all link clicks document.addEventListener(click, handleGlobalLinkClick, true); // Try to intercept links from iframes - critical for gallery content interceptAllIframeLinks(); // Set up a mutation observer to handle dynamically added content setupMutationObserver(); // Log initialization with site details const isPreviewMode window.location.hostname preview.neonsky.app; console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`); // Create global helper for debugging window.debugGalleryLinks function() { return { inPreviewMode: isPreviewMode, interceptorInitialized: window._galleryLinkInterceptorInitialized, cachedGalleries: GALLERY_DATA_CACHE.galleries.length, urlPath: window.location.pathname, siteGuid: getSiteGuidFromPath() }; }; } // Update the gallery cache when gallery data changesfunction refreshGalleryCache() { // Initialize GALLERY_DATA_CACHE if it doesnt exist if (!window.GALLERY_DATA_CACHE) { window.GALLERY_DATA_CACHE { initialized: false, galleries: , lastUpdate: 0 }; } // Check for global galleries variable first if (typeof galleries ! undefined && Array.isArray(galleries)) { // Use the global galleries array window.GALLERY_DATA_CACHE.galleries galleries; window.GALLERY_DATA_CACHE.initialized true; window.GALLERY_DATA_CACHE.lastUpdate Date.now(); console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`); return; } // Debug log console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`); } // Global click handler - catches all link clicks function handleGlobalLinkClick(e) { // Find if a link was clicked const link e.target.closest(a); if (!link || !link.href) return; // Skip if the link uses special targets or modifiers if (link.target _blank || e.ctrlKey || e.metaKey || e.shiftKey) return; // Skip for hash links or javascript: links if (link.href.includes(#) || link.href.startsWith(javascript:)) return; try { // Parse the URL to work with it const url new URL(link.href); // Only intercept links to the same domain if (url.hostname ! window.location.hostname) return; // Skip API and resource URLs if (url.pathname.includes(/api/) || isResourceUrl(url.pathname)) return; console.log(Link intercepted:, url.pathname); // For preview URLs, we need to handle the path specially const isPreviewMode window.location.hostname preview.neonsky.app; const siteGuid getSiteGuidFromPath(); console.log(Link interceptor - isPreviewMode:, isPreviewMode, siteGuid:, siteGuid); // Check if this is internal navigation first let matchingItem findGalleryByPath(url.pathname, isPreviewMode, siteGuid); console.log(Link interceptor - matchingItem found:, !!matchingItem, matchingItem ? matchingItem.title : none); if (matchingItem) { // Prevent default navigation (full page reload) e.preventDefault(); e.stopPropagation(); console.log(Found matching gallery for link:, matchingItem.title); // Use the existing navigation functions if (matchingItem.isPage && window.loadPage) { window.loadPage(matchingItem.id); } else if (window.loadGallery) { window.loadGallery(matchingItem.id); } // Construct proper URL for preview mode let targetUrl url.href; if (isPreviewMode && siteGuid) { // Ensure we keep the GUID in the URL path const pathname url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith(/) ? url.pathname : / + url.pathname}`; const newUrl new URL(url); newUrl.pathname pathname; targetUrl newUrl.href; } // Update URL without reload window.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update page title document.title `${matchingItem.title || Gallery} - ${window.location.hostname}`; // Scroll to top window.scrollTo(0, 0); return; } // Special case for preview mode - even if no matching gallery found else if (isPreviewMode && siteGuid) { // Were in preview mode with a GUID - intercept the navigation // to maintain the GUID in the URL console.log(No direct match but in preview mode - handling navigation); // Prevent default navigation e.preventDefault(); e.stopPropagation(); // Construct a URL that preserves the GUID let newPathname url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { // Add the GUID to the path if not already there newPathname `/${siteGuid}${newPathname.startsWith(/) ? newPathname : / + newPathname}`; } // Create the preview-friendly URL for browser history const browserUrl new URL(window.location.origin); browserUrl.pathname newPathname; console.log(Constructing preview URL with GUID:, browserUrl.href); // Get the gallery frame const frame document.getElementById(galleryFrame); if (frame) { // IMPORTANT: For gallery navigation, we need to create a special URL // that tells the gallery system to load the content directly console.log(Loading preview path in iframe:, newPathname); // First update browser URL to show the correct path window.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Create direct path to the gallery content const cleanPath newPathname.replace(`/${siteGuid}/`, /); // For direct gallery paths, we need to load the gallery with special parameters // This tells the gallery system to load content from this path const galleryUrl `https://storage.neonsky.app/sites:site_${siteGuid}.json`; // Two approaches to loading the content: // 1. Use the gallery wrapper approach const wrapperUrl `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId${siteGuid}` + `&path${encodeURIComponent(cleanPath)}` + `&directPathtrue` + `&t${Date.now()}`; // Cache buster console.log(Loading gallery via wrapper URL:, wrapperUrl); frame.src wrapperUrl; // Clear any active gallery ID since were navigating directly if (window.activeGalleryId) { console.log(Clearing active gallery ID during direct navigation); window.activeGalleryId null; } return; // Critical: prevent further navigation } else { console.warn(Gallery frame not found - cannot load content); } } console.log(No matching gallery or special handling - allow normal navigation); } catch (error) { console.error(Error processing link click:, error); } } // Handle all iframes on the page function interceptAllIframeLinks() { // Look for any iframe that might contain gallery content const iframes document.querySelectorAll(iframe); iframes.forEach(iframe > { if (iframe.id galleryFrame || iframe.classList.contains(gallery-iframe)) { // Primary gallery iframe - high priority setupIframeIntercept(iframe, true); } else { // Any other iframe - still try to intercept setupIframeIntercept(iframe, false); } }); console.log(`Attempted to intercept links in ${iframes.length} iframes`); } // Setup link interception for a specific iframe function setupIframeIntercept(iframe, isPrimary) { try { // Skip if iframe is not fully loaded yet if (!iframe.contentWindow || !iframe.contentDocument) { // Add load event to try again when loaded iframe.addEventListener(load, function() { setupIframeIntercept(iframe, isPrimary); }); return; } // Try to access the iframe content - might fail due to same-origin policy try { const iframeDoc iframe.contentDocument; const iframeWin iframe.contentWindow; // Add event listener for all link clicks in the iframe iframeDoc.addEventListener(click, function(e) { const link e.target.closest(a); if (!link || !link.href) return; // Skip special cases if (link.target _blank || e.ctrlKey || e.metaKey || e.shiftKey) return; if (link.href.includes(#) || link.href.startsWith(javascript:)) return; try { const url new URL(link.href); // Check if same domain or relative URL const isSameDomain url.hostname window.location.hostname || url.hostname iframe.src.hostname || !url.hostname; if (isSameDomain) { const isPreviewMode window.location.hostname preview.neonsky.app; const siteGuid getSiteGuidFromPath(); // First try to find a matching gallery/page const matchingItem parent.findGalleryByPath ? parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : findGalleryByPath(url.pathname, isPreviewMode, siteGuid); if (matchingItem) { // We found a match - prevent default and navigate e.preventDefault(); e.stopPropagation(); console.log(Iframe: Found matching content for link:, matchingItem.title); // Access parent window functions if (matchingItem.isPage && parent.loadPage) { parent.loadPage(matchingItem.id); } else if (parent.loadGallery) { parent.loadGallery(matchingItem.id); } // Update URL in parent window let targetUrl url.href; if (isPreviewMode && siteGuid) { // Ensure GUID is preserved in URL const pathname url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith(/) ? url.pathname : / + url.pathname}`; const newUrl new URL(window.location.origin + pathname); targetUrl newUrl.href; } // Update browser URL parent.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update parent window title parent.document.title `${matchingItem.title || Gallery} - ${parent.location.hostname}`; // Scroll parent to top parent.scrollTo(0, 0); return; } // Special handling for preview mode - critical fix here! else if (isPreviewMode && siteGuid) { // Special handling for preview URLs e.preventDefault(); e.stopPropagation(); console.log(Iframe: No direct match but in preview mode - handling navigation); // Construct a URL that preserves the GUID let newPathname url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { newPathname `/${siteGuid}${newPathname.startsWith(/) ? newPathname : / + newPathname}`; } // Create a URL for the browser history const browserUrl new URL(window.location.origin); browserUrl.pathname newPathname; console.log(Iframe: Constructing preview URL with GUID:, browserUrl.href); // Update browser URL in parent parent.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Handle the navigation in the parent window if (isPrimary && parent.document.getElementById(galleryFrame)) { const galleryFrame parent.document.getElementById(galleryFrame); // Create clean path for the gallery system const cleanPath newPathname.replace(`/${siteGuid}/`, /); // Create wrapper URL for gallery content const wrapperUrl `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId${siteGuid}` + `&path${encodeURIComponent(cleanPath)}` + `&directPathtrue` + `&t${Date.now()}`; // Cache buster console.log(Iframe: Loading gallery via wrapper URL:, wrapperUrl); galleryFrame.src wrapperUrl; // Clear any active gallery ID since were navigating directly if (parent.activeGalleryId) { console.log(Iframe: Clearing active gallery ID during direct navigation); parent.activeGalleryId null; } } else { // Fallback - navigate normally in case we cant handle it console.log(Iframe: Gallery frame not found or not primary, navigating normally); window.location.href browserUrl.href; } return; } } } catch (iframeError) { console.error(Error handling iframe link:, iframeError); } }); if (isPrimary) { console.log(Successfully intercepted links in primary gallery iframe); } } catch (sameOriginError) { // This is expected for cross-origin iframes if (isPrimary) { console.warn(Cannot access iframe content due to same-origin policy. Using fallback method.); // Apply a fallback method - add postMessage listener setupPostMessageListener(iframe); } } } catch (error) { console.warn(Error setting up iframe link interception:, error); } } // Fallback: use postMessage for cross-origin iframes function setupPostMessageListener(iframe) { // Listen for messages from the iframe window.addEventListener(message, function(event) { try { // Verify the message is from our iframe if (event.source ! iframe.contentWindow) return; const data event.data; // Check if this is a link click event if (data && data.type linkClick && data.href) { console.log(Received link click message from iframe:, data.href); // Create a URL object to work with the link const url new URL(data.href); // Check if its a local navigation if (url.hostname window.location.hostname) { const isPreviewMode window.location.hostname preview.neonsky.app; const siteGuid getSiteGuidFromPath(); // Try to find a matching gallery const matchingItem findGalleryByPath(url.pathname, isPreviewMode, siteGuid); if (matchingItem) { console.log(Found matching gallery for postMessage link:, matchingItem.title); // Navigate using the appropriate function if (matchingItem.isPage && window.loadPage) { window.loadPage(matchingItem.id); } else if (window.loadGallery) { window.loadGallery(matchingItem.id); } // Update URL without reload let targetUrl url.href; if (isPreviewMode && siteGuid) { // Ensure GUID is preserved const pathname url.pathname.startsWith(`/${siteGuid}/`) ? url.pathname : `/${siteGuid}${url.pathname.startsWith(/) ? url.pathname : / + url.pathname}`; const newUrl new URL(window.location.origin + pathname); targetUrl newUrl.href; } window.history.pushState( { galleryId: matchingItem.id }, matchingItem.title || document.title, targetUrl ); // Update page title document.title `${matchingItem.title || Gallery} - ${window.location.hostname}`; return; } // Special handling for preview mode - Fixed! else if (isPreviewMode && siteGuid) { console.log(No direct match but in preview mode - handling navigation via postMessage); // Construct URL that preserves the GUID let newPathname url.pathname; if (!newPathname.startsWith(`/${siteGuid}/`)) { newPathname `/${siteGuid}${newPathname.startsWith(/) ? newPathname : / + newPathname}`; } // Create browser URL const browserUrl new URL(window.location.origin); browserUrl.pathname newPathname; // Update browser URL window.history.pushState( { customPath: newPathname }, document.title, browserUrl.href ); // Get the main gallery frame - NOT the iframe that sent the message const galleryFrame document.getElementById(galleryFrame); if (galleryFrame) { // Create clean path without GUID const cleanPath newPathname.replace(`/${siteGuid}/`, /); // Create wrapper URL const wrapperUrl `https://storage.neonsky.app/standalone/gallery-wrapper.html` + `?siteId${siteGuid}` + `&path${encodeURIComponent(cleanPath)}` + `&directPathtrue` + `&t${Date.now()}`; // Cache buster console.log(Loading gallery via wrapper URL (postMessage):, wrapperUrl); galleryFrame.src wrapperUrl; // Clear any active gallery ID if (window.activeGalleryId) { window.activeGalleryId null; } return; } else { console.warn(Gallery frame not found - cannot load content via postMessage); } } } } } catch (error) { console.error(Error processing postMessage from iframe:, error); } }); // Inject a script into the iframe if possible to send messages back try { // We might be able to inject a script tag even in cross-origin scenarios // if the iframe is not yet loaded or using document.write iframe.addEventListener(load, function() { try { const script iframe.contentDocument.createElement(script); script.textContent ` // Link interception script injected from parent document.addEventListener(click, function(e) { const link e.target.closest(a); if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) { // Send message to parent window window.parent.postMessage({ type: linkClick, href: link.href, pathname: link.pathname }, *); // Prevent default navigation e.preventDefault(); } }); console.log(Gallery link interceptor injected from parent); `; iframe.contentDocument.head.appendChild(script); } catch (e) { // This will likely fail for cross-origin iframes console.log(Could not inject script into iframe - expected for cross-origin frames); } }); } catch (error) { console.warn(Error trying to inject script into iframe:, error); } } // Observer for new content/iframes function setupMutationObserver() { if (!window.MutationObserver) return; const observer new MutationObserver(function(mutations) { let newIframesFound false; mutations.forEach(function(mutation) { if (mutation.type childList && mutation.addedNodes.length) { // Check if any new iframes were added mutation.addedNodes.forEach(function(node) { // Check if the node is an iframe or contains iframes if (node.nodeName IFRAME) { newIframesFound true; setupIframeIntercept(node, node.id galleryFrame); } else if (node.querySelectorAll) { const iframes node.querySelectorAll(iframe); if (iframes.length > 0) { newIframesFound true; iframes.forEach(function(iframe) { setupIframeIntercept(iframe, iframe.id galleryFrame); }); } } }); } }); // If new iframes were found, update our cache if (newIframesFound) { refreshGalleryCache(); } }); // Observe the entire document observer.observe(document.body, { childList: true, subtree: true }); } // Helper to check if a URL is a resource rather than a page function isResourceUrl(path) { const resourceExtensions .jpg, .jpeg, .png, .gif, .webp, .svg, .css, .js, .pdf, .ico, .ttf, .woff, .woff2 ; return resourceExtensions.some(ext > path.toLowerCase().endsWith(ext)); } // Get the site GUID from the path in preview mode function getSiteGuidFromPath() { if (window.location.hostname preview.neonsky.app) { const pathParts window.location.pathname.split(/).filter(Boolean); if (pathParts.length > 0) { return pathParts0; } } return null; } /** * Enhanced gallery path matching with improved accuracy: * * This function finds the appropriate gallery based on URL path with better * prioritization and more precise matching criteria. * * @param {string} path - The URL path to match * @param {boolean} isPreviewMode - Whether were in preview mode * @param {string} siteGuid - The site GUID (for preview mode) * @returns {Object|null} The matching gallery or null if none found */function findGalleryByPath(path, isPreviewMode, siteGuid) { // Ensure path is a string if (typeof path ! string) { console.warn(Invalid path provided to findGalleryByPath:, path); return null; } // Determine which galleries array to use - check global variable first let galleriesData; let galleryCount 0; if (typeof galleries ! undefined && Array.isArray(galleries)) { galleriesData galleries; galleryCount galleries.length; } else if (typeof window.galleries ! undefined && Array.isArray(window.galleries)) { galleriesData window.galleries; galleryCount window.galleries.length; } else if (window.GALLERY_DATA_CACHE && window.GALLERY_DATA_CACHE.initialized && Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { galleriesData window.GALLERY_DATA_CACHE.galleries; galleryCount window.GALLERY_DATA_CACHE.galleries.length; } else { console.warn(No galleries array available); return null; } console.log(`Searching for match among ${galleryCount} galleries`); // Normalize the path for comparison (handle trailing/leading slashes) let normalizedPath path.replace(/^/|/$/g, ).toLowerCase(); // Remove leading/trailing slashes and lowercase const originalNormalizedPath normalizedPath; // Keep original for logging // If this is a preview URL, we need to handle the path differently if (isPreviewMode && siteGuid) { // Check if the path starts with the GUID if (normalizedPath.startsWith(siteGuid.toLowerCase() + /)) { // Remove the GUID prefix normalizedPath normalizedPath.substring(siteGuid.length + 1); } else if (normalizedPath siteGuid.toLowerCase()) { // This is just the GUID with no additional path normalizedPath ; } } // Create a dash-normalized version (all sequences of dashes become single dashes) // This handles both double-dash patterns and any other dash inconsistencies const normalizedPathWithSingleDashes normalizedPath.replace(/-+/g, -); console.log(`Normalized path: ${normalizedPath} (dash-normalized: ${normalizedPathWithSingleDashes})`); // For empty paths, try to find the homepage if (!normalizedPath) { // Look for a gallery marked as home page const homePage galleriesData.find(g > g.isHomePage true); if (homePage) { console.log(Empty path - returning homepage:, homePage.title); return homePage; } } // Array to store potential matches with their confidence levels const potentialMatches ; // Process each gallery to find potential matches with confidence levels galleriesData.forEach(gallery > { // Skip invalid galleries if (!gallery || !gallery.id) return; // Store matching details for debugging const matchInfo { gallery, confidence: 0, matchType: null, usedNormalizedDashes: false }; // 1. EXACT SLUG MATCH (highest confidence) if (gallery.slug) { const gallerySlug gallery.slug.replace(/^/|/$/g, ).toLowerCase(); // Try original path if (gallerySlug normalizedPath) { matchInfo.confidence 100; // Max confidence matchInfo.matchType exact_slug; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const gallerySlugWithSingleDashes gallerySlug.replace(/-+/g, -); if (normalizedPathWithSingleDashes gallerySlugWithSingleDashes) { matchInfo.confidence 95; // Very high confidence matchInfo.matchType exact_slug_normalized_dashes; matchInfo.usedNormalizedDashes true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 2. EXACT URL PATH MATCH (very high confidence) if (gallery.url) { let galleryPath gallery.url.toLowerCase(); if (galleryPath.startsWith(http)) { try { const urlObj new URL(galleryPath); galleryPath urlObj.pathname; } catch (e) { // If URL parsing fails, use the string as is } } galleryPath galleryPath.replace(/^/|/$/g, ); // Try original path if (galleryPath normalizedPath) { matchInfo.confidence 90; // Very high confidence matchInfo.matchType exact_url_path; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const galleryPathWithSingleDashes galleryPath.replace(/-+/g, -); if (normalizedPathWithSingleDashes galleryPathWithSingleDashes) { matchInfo.confidence 85; // High confidence matchInfo.matchType exact_url_path_normalized_dashes; matchInfo.usedNormalizedDashes true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 3. TITLE SLUG MATCH (high confidence) if (gallery.title) { const titleSlug gallery.title.toLowerCase() .replace(/s+/g, -) .replace(/^w-+/g, ) .replace(/--+/g, -) .trim(); // Try original path if (titleSlug normalizedPath) { matchInfo.confidence 80; // High confidence matchInfo.matchType title_slug_match; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } // Try dash-normalized path const titleSlugWithSingleDashes titleSlug.replace(/-+/g, -); if (normalizedPathWithSingleDashes titleSlugWithSingleDashes) { matchInfo.confidence 75; // Good confidence matchInfo.matchType title_slug_match_normalized_dashes; matchInfo.usedNormalizedDashes true; potentialMatches.push({...matchInfo}); return; // Continue to next gallery } } // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA if (gallery.slug && normalizedPath.length > 5) { const gallerySlug gallery.slug.replace(/^/|/$/g, ).toLowerCase(); const gallerySlugWithSingleDashes gallerySlug.replace(/-+/g, -); // Only if paths are substantial (to avoid false positives) if (normalizedPath.length > 5 && gallerySlug.length > 5) { // Try matching significant portions with dash normalization // If the normalized paths contain each other with substantial overlap if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) { // Calculate overlap ratio based on the shorter string const overlapRatio Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length); // Only consider matches with substantial overlap (over 70%) if (overlapRatio > 0.7) { const score Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap matchInfo.confidence score; matchInfo.matchType partial_match_normalized; matchInfo.usedNormalizedDashes true; matchInfo.overlapRatio overlapRatio; potentialMatches.push({...matchInfo}); } } } } }); // No potential matches found if (potentialMatches.length 0) { console.log(No gallery match found for path:, path); return null; } // Sort potential matches by confidence (highest first) potentialMatches.sort((a, b) > b.confidence - a.confidence); // Get the best match const bestMatch potentialMatches0; // Log detailed debugging info about the match console.log(`Best match for ${normalizedPath}:`, { title: bestMatch.gallery.title, id: bestMatch.gallery.id, matchType: bestMatch.matchType, confidence: bestMatch.confidence, usedNormalizedDashes: bestMatch.usedNormalizedDashes, totalMatches: potentialMatches.length }); // If we have multiple close matches, log them for debugging if (potentialMatches.length > 1 && potentialMatches1.confidence > bestMatch.confidence - 10) { console.log(Close alternative matches:, potentialMatches.slice(1, 3).map(m > ({ title: m.gallery.title, id: m.gallery.id, matchType: m.matchType, confidence: m.confidence }))); } // Return the best matching gallery return bestMatch.gallery;} // Make helpful functions available globally window.findGalleryByPath findGalleryByPath; window.refreshGalleryCache refreshGalleryCache; window.getSiteGuidFromPath getSiteGuidFromPath;})(); /script> /div> !-- Sidebar element form for adding new elements -->div classsidebar-element-form idsidebarElementForm styledisplay: none;>h3>Add Sidebar Element/h3>p>Select the type of element to add:/p>div classradio-group> label> input typeradio namesidebarElementType valuetext checked> Text Block /label> label> input typeradio namesidebarElementType valueimage> Image /label> label> input typeradio namesidebarElementType valuesocial> Social Icons /label> !-- Languages element disabled - use contentLanguage in Site Metadata instead --> !-- label> input typeradio namesidebarElementType valuelanguages> Languages /label> -->/div>div classform-actions>button classbtn btn-primary onclickaddSidebarElement()>Add Element/button>button classbtn onclicktoggleSidebarElementForm()>Cancel/button>/div>/div> div classadd-form idaddForm> input typetext idgalleryTitle placeholderGallery Title> div classradio-group> label> input typeradio namegalleryType valueintegrated> Gallery /label> label> input typeradio namegalleryType valuepage> Page /label> label> input typeradio namegalleryType valuefolder> Folder /label> label> input typeradio namegalleryType valuespacer> Spacer /label> label> input typeradio namegalleryType valueexternal checked> External URL /label>/div> div idurlInputContainer> input typetext idgalleryUrl placeholderGallery URL> /div> div idgalleryParentContainer> label forgalleryParent>Parent Menu:/label> select idgalleryParent> option value>None (Top Level)/option> /select> /div> button classbtn btn-primary onclickaddGallery()>Save/button> script> // Add event listeners to the radio buttons document.addEventListener(DOMContentLoaded, () > { const radioButtons document.querySelectorAll(inputnamegalleryType); radioButtons.forEach(radio > { radio.addEventListener(change, function() { const urlContainer document.getElementById(urlInputContainer); if (this.value external) { urlContainer.style.display block; } else { urlContainer.style.display none; } }); }); }); /script> /div>div classmetadata-editor idmetadataEditor styledisplay: none; max-height: 90vh; overflow-y: auto;> h3>Site Metadata/h3> div classform-group> label formetaTitle>Page Title (Search Results & Browsers)/label> input typetext idmetaTitle classform-control placeholderEnter site title for search results> small>Recommended length: 50-60 characters/small> /div> div classform-group> label formetaDescription>Meta Description/label> textarea idmetaDescription classform-control rows3 placeholderEnter site description for search results>/textarea> small>Recommended length: 150-160 characters/small> /div> div classform-group> label forgoogleAnalytics>Google Analytics Code/label> input typetext idgoogleAnalytics classform-control placeholderEnter GA-XXXXXXXXXX> small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X/small> /div> div classform-group> label forcontentLanguage>Content Language/label> select idcontentLanguage classform-control> option valueen>English (en)/option> option valuees>Spanish / Español (es)/option> option valuefr>French / Français (fr)/option> option valuene>Nepali / नेपाली (ne)/option> option valuede>German / Deutsch (de)/option> option valueit>Italian / Italiano (it)/option> option valuept>Portuguese / Português (pt)/option> option valueru>Russian / Русский (ru)/option> option valuezh>Chinese / 中文 (zh)/option> option valueja>Japanese / 日本語 (ja)/option> option valueko>Korean / 한국어 (ko)/option> option valuear>Arabic / العربية (ar)/option> option valuehi>Hindi / हिन्दी (hi)/option> /select> small>Set the primary language of your site content. Chrome will offer to translate if visitors language is different./small> /div> div classform-group> label> input typecheckbox idnoIndexToggle> Hide site from search engines (noindex) /label> small>Use this if the site is under development or shouldnt appear in search results/small> /div> div classform-group> label> input typecheckbox idmetadataCopyrightEnabled> Enable sidebar copyright footer /label> small>When enabled, the copyright text shows at the bottom of the sidebar./small> /div> div classform-group idmetadataCopyrightTextGroup> label formetadataCopyrightText>Copyright Text/label> input typetext idmetadataCopyrightText classform-control placeholder© 2025 Your Name> small>This text appears in the sidebar footer and inside the copyright overlay./small> /div> div classform-group> label forfaviconUrl>Favicon/label> input typehidden idfaviconUrl classform-control> button typebutton iduploadFaviconBtn classbtn stylebackground-color: #4682B4; color: white; border: none; padding: 10px 20px; border-radius: 0px; cursor: pointer; display: flex; align-items: center; gap: 8px;> svg stylewidth: 16px; height: 16px; viewBox0 0 24 24 fillnone strokecurrentColor stroke-width2> path dM21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4>/path> polyline points17 8 12 3 7 8>/polyline> line x112 y13 x212 y215>/line> /svg> span iduploadFaviconBtnText>Upload Favicon/span> /button> div idfaviconPreviewContainer>/div> small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)/small> /div> div classform-actions> button classbtn btn-primary onclicksaveMetadata()>Save Metadata/button> button classbtn onclicktoggleMetadataEditor()>Cancel/button> /div>/div>script>// Handle favicon upload button clickdocument.addEventListener(DOMContentLoaded, function() { const uploadBtn document.getElementById(uploadFaviconBtn); if (uploadBtn) { uploadBtn.addEventListener(click, function() { uploadFavicon(); }); } // Handle content language dropdown change const contentLanguageSelect document.getElementById(contentLanguage); if (contentLanguageSelect) { contentLanguageSelect.addEventListener(change, function() { console.warn(Content language changed to:, this.value); // Update global variable immediately window.siteMetadata.contentLanguage this.value; console.warn(Updated window.siteMetadata.contentLanguage:, window.siteMetadata.contentLanguage); }); } const copyrightToggle document.getElementById(metadataCopyrightEnabled); if (copyrightToggle) { copyrightToggle.addEventListener(change, updateMetadataCopyrightFieldState); } updateMetadataCopyrightFieldState();});// Function to handle favicon upload using ImageUploaderfunction uploadFavicon() { console.log(Starting favicon upload...); // Check if ImageUploader is available if (!window.ImageUploader || typeof window.ImageUploader.open ! function) { alert(Image uploader not available. Please enter a favicon URL manually.); return; } try { // Create options for the ImageUploader const options { title: Upload Favicon, containerType: favicon, maxWidth: 64, // Favicons are typically small maxHeight: 64, quality: 0.9 }; // Call the ImageUploader window.ImageUploader.open(options, function(imageResult, imageInfo) { console.log(Favicon upload callback received:, imageInfo); // Get the favicon URL input field let faviconInput document.getElementById(faviconUrl); console.warn( UPLOAD CALLBACK DEBUG ); console.warn(Favicon input element found:, !!faviconInput); console.warn(Favicon input element type:, faviconInput ? faviconInput.type : N/A); console.warn(Favicon input element id:, faviconInput ? faviconInput.id : N/A); // If element not found, wait a bit and try again (metadata editor might not be open yet) if (!faviconInput) { console.warn(Favicon input not found, waiting 100ms and trying again...); setTimeout(() > { faviconInput document.getElementById(faviconUrl); console.warn(Retry - Favicon input element found:, !!faviconInput); if (faviconInput) { console.warn(Retry - Setting favicon input value to:, imageInfo.url); faviconInput.value imageInfo.url; console.warn(Retry - Favicon input value after setting:, faviconInput.value); } else { console.error(Retry - Favicon input field still not found); } }, 100); return; } // Set the uploaded image URL if (imageInfo && imageInfo.type r2 && imageInfo.url) { // R2 storage - use the URL console.warn(Setting favicon input value to:, imageInfo.url); faviconInput.value imageInfo.url; console.warn(Favicon input value after setting:, faviconInput.value); // Also store in global variable as backup window.uploadedFaviconUrl imageInfo.url; console.warn(Stored favicon URL in global variable:, window.uploadedFaviconUrl); console.log(Favicon URL set to:, imageInfo.url); // Update button text const btnText document.getElementById(uploadFaviconBtnText); if (btnText) { btnText.textContent Change Favicon; } // Show preview showFaviconPreview(imageInfo.url); } else if (imageResult) { // Legacy base64 storage faviconInput.value imageResult; console.log(Favicon set to base64 data); // Update button text const btnText document.getElementById(uploadFaviconBtnText); if (btnText) { btnText.textContent Change Favicon; } // Show preview showFaviconPreview(imageResult); } else { console.error(No image data received from uploader); alert(Upload failed. Please try again.); return; } // Show success message console.log(Favicon uploaded successfully); }); } catch (error) { console.error(Error opening favicon uploader:, error); alert(Error opening uploader: + (error.message || Unknown error)); }}// Function to show a preview of the uploaded faviconfunction showFaviconPreview(imageUrl) { const container document.getElementById(faviconPreviewContainer); if (!container) { console.error(Favicon preview container not found); return; } container.innerHTML div stylemargin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;> + img src + imageUrl + altFavicon Preview stylewidth: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;> + div styleflex: 1;> + div stylefont-size: 14px; color: #333; font-weight: 500;>Current Favicon/div> + div stylefont-size: 12px; color: #666; margin-top: 2px;>Preview (32x32px)/div> + /div> + button typebutton onclickremoveFavicon() stylebackground: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;> + Remove + /button> + /div>;}// Function to remove the faviconfunction removeFavicon() { const faviconInput document.getElementById(faviconUrl); if (faviconInput) { faviconInput.value ; } const container document.getElementById(faviconPreviewContainer); if (container) { container.innerHTML ; } const btnText document.getElementById(uploadFaviconBtnText); if (btnText) { btnText.textContent Upload Favicon; } console.log(Favicon removed);}/script> !-- Import Classic Form -->div classimport-classic-form idimportClassicForm styledisplay: none;> h3>Import Galleries from JSON or Classic GUID/h3> p>Paste the full site JSON into the text area below, strong>OR/strong> enter the GUID of a classic gallery collection./p> div classform-group> label forpastedJson>Pasted Site JSON:/label> textarea idpastedJson rows6 placeholderPaste the entire site JSON here...>/textarea> /div> div classimport-separator>OR/div> div classform-group> label forclassicGuid>Classic GUID or Import ID:/label> input typetext idclassicGuid placeholderEnter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)> small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage./small> /div> div classform-group> label> input typecheckbox idreplaceMenuCheckbox onchangedocument.getElementById(importParent).disabled this.checked; document.getElementById(createSubmenu).disabled this.checked;> Replace existing menu structure /label> small>If checked, the current menu will be deleted and replaced with the imported items. If unchecked, imported items will be added to the current menu./small> /div> div classform-group> label forimportParent>Import under:/label> select idimportParent> option value>Top Level (No Parent)/option> /select> /div> div classform-group> label> input typecheckbox idcreateSubmenu checked> Create submenu for all imported galleries /label> /div> div classform-group idsubmenuTitleContainer> label forsubmenuTitle>Submenu title:/label> input typetext idsubmenuTitle placeholdere.g., Classic Galleries> /div> div idimportStatus classimport-status styledisplay: none;> div classimport-progress>div classprogress-bar>/div>/div> div classstatus-message>Processing.../div> /div> div classform-actions> button classbtn btn-primary onclickimportClassicGalleries()>Import from Paste/GUID/button> button classbtn onclickdownloadCurrentSiteJSON()>Download Current Site JSON/button> button classbtn onclicktoggleImportClassicForm()>Cancel/button> /div>/div> div classtree-container> div idgalleryTree>/div> /div> !-- Sidebar Elements Container will be inserted here by the sidebar-manager.js --> div classsidebar-elements-container>/div>/div>div classgallery-container> iframe classgallery-iframe idgalleryFrame>/iframe>/div>/div>!-- Cookie Notice -->script>// Reveal content once layout is applied and CSS is loaded(function() { function revealContent() { document.body.classList.add(hydra-ready); } // Wait for CSS to load if (document.readyState complete) { revealContent(); } else { window.addEventListener(load, revealContent); }})();/script>script>// // LANGUAGE TRANSLATION SYSTEM// (function() { // CRITICAL: Only initialize translation system if a languages element exists // This ensures sites without languages element have ZERO overhead const checkForLanguagesElement function() { // Wait for SidebarManager to be initialized if (!window.SidebarManager || !window.SidebarManager.elements) { return false; } // Check if any languages element exists const hasLanguagesElement window.SidebarManager.elements.some(el > el.type languages); if (!hasLanguagesElement) { console.warn(No languages element found - translation system disabled); return false; } return true; }; // Check for languages element, retry if SidebarManager not ready yet let initAttempts 0; const maxAttempts 10; function tryInitialize() { if (checkForLanguagesElement()) { console.warn(✅ Languages element found - initializing translation system); initializeTranslationSystem(); } else { initAttempts++; if (initAttempts maxAttempts) { // Retry in 500ms (total max wait: 5 seconds) setTimeout(tryInitialize, 500); } else { console.warn(Translation system not needed for this site - no languages element found after 5 seconds); } } } // Start checking after initial delay setTimeout(tryInitialize, 500); function initializeTranslationSystem() { console.warn(✅ Translation system initialized successfully); let currentLanguage localStorage.getItem(hydra_language) || en; const originalTexts new Map(); // Expose test function for debugging window.testTranslation function(langCode) { console.warn(🧪 Manual translation test triggered for:, langCode); console.warn(🔍 Dispatching test event...); window.dispatchEvent(new CustomEvent(hydra:language-change, { detail: { code: langCode || es, name: Test } })); console.warn(✅ Test event dispatched - check if listener caught it above); }; console.warn(✅ Event listener for hydra:language-change is now active); // Watch for edit mode changes and restore default language const editModeObserver new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.attributeName class) { const isEditMode document.body.classList.contains(edit-mode-active); if (isEditMode) { console.warn(Edit mode activated - restoring default language); // Restore original text restoreOriginalText(); // Update language buttons to show default language const languagesElement window.SidebarManager?.elements?.find(el > el.type languages); const defaultLang languagesElement?.languages?.find(l > l.isDefault)?.code || en; document.querySelectorAll(.language-button-live).forEach(btn > { if (btn.getAttribute(data-lang-code) defaultLang) { btn.style.background var(--menu-active-color, #4682B4); } else { btn.style.background transparent; } }); } } }); }); // Start observing body for class changes if (document.body) { editModeObserver.observe(document.body, { attributes: true, attributeFilter: class }); } // Watch for dynamically loaded content (galleries, pages, etc.) and translate it const contentObserver new MutationObserver(function(mutations) { // Only process if were not in default language and not in edit mode const languagesElement window.SidebarManager?.elements?.find(el > el.type languages); const defaultLang languagesElement?.languages?.find(l > l.isDefault)?.code || en; const isEditMode document.body.classList.contains(edit-mode-active); if (currentLanguage defaultLang || isEditMode) { return; // Dont translate if in default language or edit mode } // Check if new text content was added let hasNewTextContent false; mutations.forEach(function(mutation) { if (mutation.type childList && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(function(node) { if (node.nodeType Node.ELEMENT_NODE) { // Check if the added element contains text we should translate const textElements node.querySelectorAll(p, h1, h2, h3, .page-text); if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) { hasNewTextContent true; } } }); } }); if (hasNewTextContent) { console.warn(New content detected - translating to:, currentLanguage); // Debounce to avoid translating multiple times rapidly clearTimeout(window._translationDebounce); window._translationDebounce setTimeout(function() { translatePage(currentLanguage, defaultLang); }, 300); } }); // Observe the gallery container and page content areas for new content const galleryContainer document.querySelector(.gallery-container); if (galleryContainer) { contentObserver.observe(galleryContainer, { childList: true, subtree: true }); } // Also observe the main content area const mainContent document.querySelector(.container); if (mainContent) { contentObserver.observe(mainContent, { childList: true, subtree: true }); } // Listen for language change events window.addEventListener(hydra:language-change, async function(e) { const { code, name } e.detail; console.warn(🌐 Language changed to:, code, name); // CRITICAL: Ignore language changes in edit mode if (document.body.classList.contains(edit-mode-active)) { console.warn(⚠️ Edit mode active - ignoring language change); // Reset to default language const languagesElement window.SidebarManager?.elements?.find(el > el.type languages); const defaultLang languagesElement?.languages?.find(l > l.isDefault)?.code || en; // Update button states to show default language document.querySelectorAll(.language-button-live).forEach(btn > { if (btn.getAttribute(data-lang-code) defaultLang) { btn.style.background var(--menu-active-color, #4682B4); } else { btn.style.background transparent; } }); // Show message to user console.warn(Language switching is disabled in edit mode); return; } // Update all language buttons document.querySelectorAll(.language-button-live).forEach(btn > { if (btn.getAttribute(data-lang-code) code) { btn.style.background var(--menu-active-color, #4682B4); } else { btn.style.background transparent; } }); // Save preference localStorage.setItem(hydra_language, code); currentLanguage code; // Get default language from languages element const languagesElement window.SidebarManager?.elements?.find(el > el.type languages); const defaultLang languagesElement?.languages?.find(l > l.isDefault)?.code || en; if (code defaultLang) { // Restore original language restoreOriginalText(); } else { // Translate page await translatePage(code, defaultLang); } }); async function translatePage(targetLang, sourceLang) { console.warn(`🔄 Translating page from ${sourceLang} to ${targetLang}`); // Elements to translate const selectors .sidebar li a:not(.edit-mode-active .sidebar li a), // Menu items (not in edit mode) .page-text:not(.edit-mode-active .page-text), // Page content (not in edit mode) h1:not(.edit-mode-active h1), h2:not(.edit-mode-active h2), h3:not(.edit-mode-active h3), p:not(.edit-mode-active p) ; const elements document.querySelectorAll(selectors.join(, )); for (const element of elements) { // Skip if already translated to this language if (element.dataset.translatedTo targetLang) continue; // Skip if element is empty const text element.textContent.trim(); if (!text || text.length 0) continue; // Store original text if (!originalTexts.has(element)) { originalTexts.set(element, text); } // Check translation cache const cacheKey `translate_${text}_${targetLang}`; let translated getFromCache(cacheKey); if (translated) { element.textContent translated; element.dataset.translatedTo targetLang; } else { // Translate try { translated await translate(text, sourceLang, targetLang); if (translated && translated ! text) { element.textContent translated; element.dataset.translatedTo targetLang; saveToCache(cacheKey, translated); } } catch (error) { console.error(Translation error:, error); } } } } function restoreOriginalText() { originalTexts.forEach((originalText, element) > { if (element && element.isConnected) { element.textContent originalText; delete element.dataset.translatedTo; } }); } async function translate(text, sourceLang, targetLang) { if (!text || text.trim().length 0) return text; // Try Browser Translation API (Free, Chrome 130+) if (translation in self && createTranslator in self.translation) { try { console.warn(🔄 Using Browser Translation API); const translator await self.translation.createTranslator({ sourceLanguage: sourceLang, targetLanguage: targetLang }); const result await translator.translate(text); console.warn(✅ Translation successful via Browser API); return result; } catch (error) { console.warn(❌ Browser translation failed:, error); } } else { console.warn(⚠️ Browser Translation API not available); console.warn( To enable in Chrome: chrome://flags/#translation-api); console.warn( OR add Google Translate API key to use paid translation); } // TODO: Add Google Translate API fallback here // For now, return original text console.warn(⚠️ No translation provider configured - returning original text); return text; } // Cache management function getFromCache(key) { try { const item localStorage.getItem(`hydra_${key}`); if (!item) return null; const { value, expiry } JSON.parse(item); if (expiry && Date.now() > expiry) { localStorage.removeItem(`hydra_${key}`); return null; } return value; } catch { return null; } } function saveToCache(key, value) { try { const item { value, expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days }; localStorage.setItem(`hydra_${key}`, JSON.stringify(item)); } catch (error) { console.warn(Cache save failed:, error); } } } // End of initializeTranslationSystem})();/script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]