Raiju/includes/footer.php
touche 3c591a287a
Some checks are pending
Raiju CI / PHP ${{ matrix.php }} (8.1) (push) Waiting to run
Raiju CI / PHP ${{ matrix.php }} (8.4) (push) Waiting to run
Raiju CI / PHP ${{ matrix.php }} (8.5) (push) Waiting to run
Raiju CI / Apache routing / PHP-FPM (push) Waiting to run
Raiju CI / Chromium (push) Waiting to run
Brand: capitalize Raiju in content, UI strings and prompts
- Starter content and admin language files: Raiju, not raiju
- Test email, backup messages, mailer header and AI prompts use Raiju
- Meta generator and footer fallback use Raiju
- Technical identifiers (paths, CSS classes, user agents, temp prefixes) unchanged
2026-10-25 06:30:17 +03:00

793 lines
38 KiB
PHP

<?php
// Footer Template
$basePath = $basePath ?? '';
$currentLang = $currentLang ?? (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en');
$defaultLang = $defaultLang ?? (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en');
$PAGE_MAPPING = $PAGE_MAPPING ?? [];
$langLinks = $langLinks ?? [];
require_once __DIR__ . '/page-path.php';
// Check if admin is logged in
require_once __DIR__ . '/access-guard.php';
$isAdminLoggedIn = raijuAccessIsLoggedIn();
$csrfToken = $isAdminLoggedIn ? ($_SESSION['csrf_token'] ?? '') : '';
if ($isAdminLoggedIn && !function_exists('getBlockTypes')) {
require_once __DIR__ . '/content-loader.php';
}
// Load footer content from JSON
$footerData = [];
$footerJsonPath = __DIR__ . '/../content/pages/footer.json';
if (file_exists($footerJsonPath)) {
$footerData = json_decode(file_get_contents($footerJsonPath), true) ?: [];
}
// Language-dependent texts
$tagline = $footerData['tagline'][$currentLang] ?? '';
$services = $footerData['services'][$currentLang] ?? '';
$claim = $footerData['claim'][$currentLang] ?? '';
$phone = $footerData['contact']['phone'] ?? '';
$email = $footerData['contact']['email'] ?? 'info@example.com';
$creditText = $footerData['credit']['text'] ?? '';
$creditLink = $footerData['credit']['link'] ?? '';
$creditLinkText = $footerData['credit']['linkText'] ?? '';
$contactHeading = $footerData['contactHeading'][$currentLang] ?? 'Contact';
$siteName = $_settings['branding']['name'] ?? (defined('SITE_NAME') ? SITE_NAME : '');
$copyrightVal = $footerData['copyright'] ?? '&copy; [id="adminAccess"]' . date('Y') . '[/id]';
$copyrightRaw = is_array($copyrightVal) ? ($copyrightVal[$currentLang] ?? $copyrightVal[array_key_first($copyrightVal)] ?? '') : $copyrightVal;
/**
* Parse shortcode-like syntax: [id="foo"]content[/id] → <span id="foo">content</span>
* Supports: [id="..."], [class="..."], combined [id="..." class="..."]
*/
if (!function_exists('parseFooterShortcodes')) {
function parseFooterShortcodes($text) {
// [id="value" class="value"]content[/id] or [/class]
return preg_replace_callback(
'/\[([^\]]+)\](.*?)\[\/\w+\]/s',
function ($m) {
$attrs = $m[1];
$content = $m[2];
$htmlAttrs = '';
if (preg_match('/id="([^"]*)"/', $attrs, $id)) {
$htmlAttrs .= ' id="' . htmlspecialchars($id[1]) . '"';
}
if (preg_match('/class="([^"]*)"/', $attrs, $cls)) {
$htmlAttrs .= ' class="' . htmlspecialchars($cls[1]) . '"';
}
return '<span' . $htmlAttrs . '>' . $content . '</span>';
},
$text
);
}
}
$copyrightHtml = parseFooterShortcodes($copyrightRaw);
$_adminAccessBase = ($basePath === '' ? '/admin/' : rtrim($basePath, '/') . '/admin/');
$_adminAccessBaseJson = json_encode($_adminAccessBase, JSON_UNESCAPED_SLASHES);
$_footerLabels = [
'docs' => ['en' => 'Docs', 'de' => 'Dokumentation', 'es' => 'Docs'],
'privacy' => ['en' => 'Privacy', 'de' => 'Datenschutz', 'es' => 'Privacidad'],
'imprint' => ['en' => 'Imprint', 'de' => 'Impressum', 'es' => 'Aviso legal'],
'language' => ['en' => 'Language selection', 'de' => 'Sprachauswahl', 'es' => 'Selección de idioma'],
];
$_footerPageHref = function (string $page) use ($basePath, $currentLang, $PAGE_MAPPING, $defaultLang): string {
if (isset($PAGE_MAPPING[$page][$currentLang])) {
return $basePath . $PAGE_MAPPING[$page][$currentLang];
}
if (isset($PAGE_MAPPING[$page][$defaultLang])) {
return $basePath . $PAGE_MAPPING[$page][$defaultLang];
}
return $basePath . $page;
};
$_footerSummary = [
'en' => 'Raiju is a flat-file CMS written in PHP.<br>Lightning fast. Lightweight. Elusive.',
'de' => 'Raiju ist ein Flat-File-CMS in PHP.<br>Blitzschnell. Leichtgewichtig. Flüchtig.',
'es' => 'Raiju es un CMS flat-file escrito en PHP.<br>Rápido como un rayo. Ligero. Esquivo.',
];
?>
<!-- Footer -->
<footer class="footer"<?php if ($isAdminLoggedIn): ?> data-content-page="footer"<?php endif; ?>>
<div class="footer-accent"></div>
<div class="footer-inner footer-product-bar">
<div class="footer-brand-block">
<a href="<?php echo $basePath; ?>." class="footer-wordmark" aria-label="Home"><?php echo htmlspecialchars($siteName ?: 'Raiju'); ?></a>
<p class="footer-copyright<?php echo $isAdminLoggedIn ? ' editable-footer-field' : ''; ?>"<?php if ($isAdminLoggedIn): ?> data-field="copyright"<?php endif; ?>><?php echo $copyrightHtml; ?></p>
<p class="footer-credit">
<a href="https://1studi.ru" target="_blank" rel="noopener">Raiju. Lightning fast.</a>
</p>
</div>
<div class="footer-summary">
<p><?php echo $_footerSummary[$currentLang] ?? $_footerSummary['en']; ?></p>
</div>
<div class="footer-actions">
<nav class="footer-links" aria-label="Footer">
<a href="<?php echo htmlspecialchars($_footerPageHref('docs')); ?>"><?php echo htmlspecialchars($_footerLabels['docs'][$currentLang] ?? $_footerLabels['docs']['en']); ?></a>
<a href="<?php echo htmlspecialchars($_footerPageHref('privacy')); ?>"><?php echo htmlspecialchars($_footerLabels['privacy'][$currentLang] ?? $_footerLabels['privacy']['en']); ?></a>
<a href="<?php echo htmlspecialchars($_footerPageHref('legal-notice')); ?>"><?php echo htmlspecialchars($_footerLabels['imprint'][$currentLang] ?? $_footerLabels['imprint']['en']); ?></a>
</nav>
<?php if (!empty($langLinks) && count($langLinks) > 1): ?>
<div class="footer-lang" aria-label="<?php echo htmlspecialchars($_footerLabels['language'][$currentLang] ?? $_footerLabels['language']['en']); ?>">
<span class="footer-lang-globe" aria-hidden="true"></span>
<?php $footerLangCodes = array_keys($langLinks); ?>
<?php foreach ($footerLangCodes as $i => $code): ?>
<?php if ($i > 0): ?><span class="footer-lang-separator">|</span><?php endif; ?>
<a href="<?php echo htmlspecialchars($langLinks[$code]); ?>" class="<?php echo ($code === $currentLang) ? 'active' : ''; ?>"<?php echo ($code === $currentLang) ? ' aria-current="true"' : ''; ?>><?php echo htmlspecialchars(strtoupper($code)); ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</footer>
<!-- Main JavaScript -->
<script>
(function() {
'use strict';
const mainEl = document.querySelector('main');
if (mainEl && !mainEl.id) {
mainEl.id = 'main-content';
}
// ============================================================
// HEADER SCROLL BEHAVIOR
// ============================================================
const header = document.getElementById('siteHeader');
let lastScrollY = 0;
let ticking = false;
const scrollThreshold = 150;
function updateHeader() {
const currentScrollY = window.scrollY;
if (currentScrollY > 50) {
header.classList.add('header-scrolled');
} else {
header.classList.remove('header-scrolled');
}
if (currentScrollY > scrollThreshold) {
if (currentScrollY > lastScrollY) {
header.classList.add('header-hidden');
} else {
header.classList.remove('header-hidden');
}
} else {
header.classList.remove('header-hidden');
}
lastScrollY = currentScrollY;
ticking = false;
}
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(updateHeader);
ticking = true;
}
}, { passive: true });
// ============================================================
// MOBILE NAVIGATION
// ============================================================
const hamburger = document.getElementById('hamburger');
const mobileNavOverlay = document.getElementById('mobileNavOverlay');
if (hamburger && mobileNavOverlay) {
hamburger.addEventListener('click', function() {
const isOpen = mobileNavOverlay.classList.contains('active');
if (isOpen) {
mobileNavOverlay.classList.remove('active');
mobileNavOverlay.setAttribute('aria-hidden', 'true');
hamburger.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
hamburger.setAttribute('aria-label', 'Open menu');
document.body.style.overflow = '';
} else {
mobileNavOverlay.classList.add('active');
mobileNavOverlay.setAttribute('aria-hidden', 'false');
hamburger.classList.add('active');
hamburger.setAttribute('aria-expanded', 'true');
hamburger.setAttribute('aria-label', 'Close menu');
document.body.style.overflow = 'hidden';
var firstMobileLink = mobileNavOverlay.querySelector('a, button');
if (firstMobileLink) firstMobileLink.focus();
}
});
mobileNavOverlay.querySelectorAll('a').forEach(function(link) {
link.addEventListener('click', function() {
mobileNavOverlay.classList.remove('active');
mobileNavOverlay.setAttribute('aria-hidden', 'true');
hamburger.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
hamburger.setAttribute('aria-label', 'Open menu');
document.body.style.overflow = '';
});
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && mobileNavOverlay.classList.contains('active')) {
mobileNavOverlay.classList.remove('active');
mobileNavOverlay.setAttribute('aria-hidden', 'true');
hamburger.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
hamburger.setAttribute('aria-label', 'Open menu');
document.body.style.overflow = '';
hamburger.focus();
}
});
}
// ============================================================
// SMOOTH SCROLL FOR ANCHOR LINKS
// ============================================================
document.querySelectorAll('a[href^="#"], a[data-nav-hash]').forEach(function(anchor) {
anchor.addEventListener('click', function(e) {
var targetId = this.getAttribute('href');
if (this.dataset.navHash) {
try {
var url = new URL(this.href, window.location.href);
if (url.pathname !== window.location.pathname) return;
targetId = '#' + this.dataset.navHash;
} catch(e) {}
}
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
e.preventDefault();
const headerHeight = header ? header.offsetHeight : 0;
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
});
});
// One-page menu active state: hash links become active only while
// their target section is actually visible on the current page.
(function initOnePageNavActiveState() {
var links = Array.prototype.slice.call(document.querySelectorAll('a[data-nav-hash]'));
if (!links.length || !('IntersectionObserver' in window)) return;
var sectionMap = new Map();
links.forEach(function(link) {
var id = link.dataset.navHash;
if (!id || sectionMap.has(id)) return;
var section = document.getElementById(id);
if (section) sectionMap.set(id, section);
});
if (!sectionMap.size) return;
function setActive(id) {
links.forEach(function(link) {
link.classList.toggle('active', link.dataset.navHash === id);
});
}
var visible = new Map();
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
var id = entry.target.id;
if (entry.isIntersecting) {
visible.set(id, entry.intersectionRatio);
} else {
visible.delete(id);
}
});
var best = null;
var bestRatio = 0;
visible.forEach(function(ratio, id) {
if (ratio > bestRatio) {
best = id;
bestRatio = ratio;
}
});
if (best) setActive(best);
}, {
rootMargin: '-25% 0px -55% 0px',
threshold: [0.1, 0.25, 0.5, 0.75]
});
sectionMap.forEach(function(section) {
observer.observe(section);
});
})();
// ============================================================
// SCROLL REVEAL ANIMATIONS
// ============================================================
(function initRevealAnimations() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelectorAll('.reveal').forEach(function(el) {
el.classList.remove('reveal');
});
document.querySelectorAll('.stagger-reveal').forEach(function(el) {
el.classList.remove('stagger-reveal');
});
return;
}
var revealElements = document.querySelectorAll('.reveal');
if (revealElements.length) {
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.15,
rootMargin: '0px 0px -40px 0px'
});
revealElements.forEach(function(el) {
observer.observe(el);
});
}
function staggerChildren(container, baseDelay) {
var children = container.children;
for (var i = 0; i < children.length; i++) {
(function(child, delay) {
child.style.animationDelay = delay + 'ms';
setTimeout(function() {
child.classList.add('stagger-visible');
}, 10);
})(children[i], baseDelay + (i * 120));
}
}
var staggerContainers = document.querySelectorAll('.stagger-reveal');
if (staggerContainers.length) {
staggerContainers.forEach(function(el) {
var revealParent = el.closest('.reveal');
if (revealParent) {
var mo = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.target.classList.contains('revealed')) {
setTimeout(function() {
staggerChildren(el, 100);
}, 500);
mo.disconnect();
}
});
});
mo.observe(revealParent, { attributes: true, attributeFilter: ['class'] });
} else {
var staggerObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
staggerChildren(entry.target, 150);
staggerObserver.unobserve(entry.target);
}
});
}, {
threshold: 0.05,
rootMargin: '0px 0px -20px 0px'
});
staggerObserver.observe(el);
}
});
}
})();
// ============================================================
// THEME TOGGLE (Dark / Light)
// ============================================================
(function initThemeToggle() {
var REMEMBER_PUBLIC_THEME = <?php echo (!isset($_settings['privacy']['rememberPublicTheme']) || !empty($_settings['privacy']['rememberPublicTheme'])) ? 'true' : 'false'; ?>;
var PUBLIC_THEME_DEFAULT = <?php
$_footerPublicThemeDefault = $_settings['theme']['publicDefault'] ?? 'system';
if (!in_array($_footerPublicThemeDefault, ['light', 'dark', 'system'], true)) {
$_footerPublicThemeDefault = 'system';
}
echo json_encode($_footerPublicThemeDefault);
?>;
var STORAGE_KEY = 'site-theme';
var CYCLE = ['dark', 'light'];
var THEME_FAVICON_COLORS = { light: '#0a0a0a', dark: '#e5e5e5' };
var faviconSvgCache = null;
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getDefaultTheme() {
return CYCLE.indexOf(PUBLIC_THEME_DEFAULT) !== -1 ? PUBLIC_THEME_DEFAULT : getSystemTheme();
}
function getInitialTheme() {
if (!REMEMBER_PUBLIC_THEME) {
return getDefaultTheme();
}
try {
var stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'system') stored = null;
if (CYCLE.indexOf(stored) !== -1) {
return stored;
}
} catch(e) {}
return getDefaultTheme();
}
function updateBrowserFavicon(theme) {
var link = document.querySelector('link[rel="icon"]');
if (!link) return;
var href = link.getAttribute('data-original-href') || link.getAttribute('href');
if (!href || !/\.svg(\?|#|$)/i.test(href)) return;
if (!link.getAttribute('data-original-href')) link.setAttribute('data-original-href', href);
if (faviconSvgCache === null) {
fetch(href).then(function(r){ return r.ok ? r.text() : null; }).then(function(svg){
if (!svg) return;
faviconSvgCache = svg;
applyFavicon(theme);
}).catch(function(){});
} else {
applyFavicon(theme);
}
}
function applyFavicon(theme) {
if (!faviconSvgCache) return;
var color = THEME_FAVICON_COLORS[theme] || THEME_FAVICON_COLORS.light;
var patched = faviconSvgCache
.replace(/<svg\b/, '<svg data-theme="' + theme + '"')
.replace(/currentColor/g, color);
var dataUrl = 'data:image/svg+xml;utf8,' + encodeURIComponent(patched);
document.querySelectorAll('link[rel="icon"], link[rel="alternate icon"]').forEach(function(l){
if (/\.svg(\?|#|$)/i.test(l.getAttribute('data-original-href') || l.getAttribute('href') || '')) {
l.setAttribute('href', dataUrl);
}
});
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
if (REMEMBER_PUBLIC_THEME) {
try { localStorage.setItem(STORAGE_KEY, theme); } catch(e) {}
}
updateMobileButtons(theme);
updateBrowserFavicon(theme);
}
function updateMobileButtons(theme) {
document.querySelectorAll('.theme-toggle-mobile').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.themeChoice === theme);
});
}
setTheme(getInitialTheme());
// Desktop toggle: cycles dark → light → dark
var desktopToggle = document.getElementById('themeToggle');
if (desktopToggle) {
desktopToggle.addEventListener('click', function() {
var current = document.documentElement.getAttribute('data-theme') || 'dark';
var next = current === 'dark' ? 'light' : 'dark';
setTheme(next);
});
}
// Mobile toggles: direct selection
document.querySelectorAll('.theme-toggle-mobile').forEach(function(btn) {
btn.addEventListener('click', function() {
setTheme(btn.dataset.themeChoice);
});
});
// Initial state for mobile buttons + favicon
var initial = document.documentElement.getAttribute('data-theme') || 'dark';
updateMobileButtons(initial);
updateBrowserFavicon(initial);
})();
// ============================================================
// HIDDEN ADMIN ACCESS (double-click on year)
// ============================================================
let lastAdminAccessClick = 0;
const openAdminAccess = function(e) {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
const adminUrl = new URL(<?php echo $_adminAccessBaseJson; ?>, window.location.origin);
adminUrl.searchParams.set('redirect', window.location.pathname + window.location.search + window.location.hash);
window.location.assign(adminUrl.toString());
};
document.addEventListener('click', function(e) {
const adminAccess = e.target && e.target.closest ? e.target.closest('#adminAccess') : null;
if (!adminAccess) return;
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
const now = Date.now();
if (e.detail >= 2 || now - lastAdminAccessClick < 700) {
openAdminAccess(e);
return;
}
lastAdminAccessClick = now;
}, true);
document.addEventListener('dblclick', function(e) {
const adminAccess = e.target && e.target.closest ? e.target.closest('#adminAccess') : null;
if (adminAccess) openAdminAccess(e);
}, true);
// ============================================================
// CONTACT FORM AJAX SUBMISSION
// ============================================================
const bindContactForm = function(contactForm) {
if (!contactForm || contactForm.dataset.raijuContactBound === 'true') return;
const submitBtn = contactForm.querySelector('[type="submit"]');
if (!submitBtn) return;
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
const feedback = contactForm.querySelector('.form-feedback');
if (!btnText || !btnLoading || !feedback) return;
contactForm.dataset.raijuContactBound = 'true';
const updateFormToken = function(token) {
const tokenField = contactForm.querySelector('input[name="form_token"]');
if (!tokenField || !token) return;
tokenField.value = token;
tokenField.defaultValue = token;
};
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
submitBtn.disabled = true;
feedback.className = 'form-feedback';
feedback.textContent = '';
const formData = new FormData(contactForm);
fetch(contactForm.action, {
method: 'POST',
body: formData
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.success) {
feedback.className = 'form-feedback success';
feedback.textContent = feedback.dataset.success;
contactForm.reset();
updateFormToken(data.formToken);
} else {
feedback.className = 'form-feedback error';
feedback.textContent = data.message || feedback.dataset.error;
updateFormToken(data.formToken);
}
})
.catch(function() {
feedback.className = 'form-feedback error';
feedback.textContent = feedback.dataset.error;
})
.finally(function() {
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
submitBtn.disabled = false;
});
});
};
const runInsertedScripts = function(container) {
container.querySelectorAll('script').forEach(function(oldScript) {
const script = document.createElement('script');
Array.prototype.forEach.call(oldScript.attributes, function(attr) {
script.setAttribute(attr.name, attr.value);
});
script.textContent = oldScript.textContent;
oldScript.replaceWith(script);
});
};
const loadLazyForm = function(container) {
if (!container || container.dataset.loaded === 'true' || container.dataset.loading === 'true') return;
container.dataset.loading = 'true';
const params = new URLSearchParams();
params.set('form', container.dataset.raijuLazyForm || '');
Array.prototype.forEach.call(container.attributes, function(attr) {
if (attr.name.indexOf('data-param-') !== 0) return;
params.set(attr.name.replace('data-param-', ''), attr.value);
});
fetch((container.dataset.endpoint || 'api/form.php') + '?' + params.toString(), {
method: 'GET',
credentials: 'same-origin'
})
.then(function(response) {
if (!response.ok) throw new Error('Form request failed');
return response.text();
})
.then(function(html) {
container.innerHTML = html;
runInsertedScripts(container);
container.querySelectorAll('form.contact-form').forEach(bindContactForm);
container.dataset.loaded = 'true';
container.dispatchEvent(new CustomEvent('raiju:lazy-form-loaded', { bubbles: true }));
})
.catch(function() {
container.innerHTML = '<p class="form-feedback error">This form could not be loaded. Please try again later.</p>';
})
.finally(function() {
delete container.dataset.loading;
});
};
document.querySelectorAll('[data-raiju-lazy-form]').forEach(function(container) {
window.setTimeout(function() {
loadLazyForm(container);
}, parseInt(container.dataset.delay || '3500', 10));
});
document.querySelectorAll('form.contact-form').forEach(bindContactForm);
})();
</script>
<?php if (!empty($pageExternalScripts)): ?>
<?php foreach ($pageExternalScripts as $_extScript): ?>
<script src="<?php echo htmlspecialchars($_extScript); ?>"></script>
<?php endforeach; ?>
<?php endif; ?>
<!-- Custom Audio Player -->
<script src="<?php echo $basePath; ?>js/audio-player.js"></script>
<?php if (isset($pageClass) && strpos($pageClass, 'page-landing') !== false && file_exists(__DIR__ . '/../js/landing-effects.js')): ?>
<script src="<?php echo $basePath; ?>js/landing-effects.js"></script>
<?php endif; ?>
<?php if (file_exists(__DIR__ . '/../js/faq-accordion.js')): ?>
<script src="<?php echo $basePath; ?>js/faq-accordion.js"></script>
<?php endif; ?>
<?php if (function_exists('raijuAiLabelsRendered') && raijuAiLabelsRendered() && file_exists(__DIR__ . '/../js/ai-labels.js')): ?>
<script src="<?php echo $basePath; ?>js/ai-labels.js" defer></script>
<?php endif; ?>
<?php if (!$isAdminLoggedIn && !empty($_settings['privacy']['emailObfuscation']) && file_exists(__DIR__ . '/../js/email-obfuscator.js')): ?>
<script src="<?php echo $basePath; ?>js/email-obfuscator.js"></script>
<?php endif; ?>
<?php if ($isAdminLoggedIn && empty($GLOBALS['raijuAgentPreview']['active'])): ?>
<!-- Inline Editor for logged-in admins (not in an AI change set preview: nothing staged may be saved) -->
<meta name="csrf-token" content="<?php echo htmlspecialchars($csrfToken); ?>">
<?php if (isset($contentPage)): ?>
<meta name="content-page" content="<?php echo htmlspecialchars($contentPage); ?>">
<?php endif; ?>
<meta name="site-languages" content="<?php echo htmlspecialchars(json_encode($SITE_LANGUAGES ?? ['en' => 'English'])); ?>">
<meta name="site-lang-default" content="<?php echo htmlspecialchars(defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en'); ?>">
<?php
// Cache-busting for editor assets: query string with file mtime forces
// a fresh fetch as soon as the file changes on the server. Without
// this, browsers serve the previous JS/CSS from disk cache and admins
// continue to see old bugs after a hotfix.
$_assetsDir = __DIR__ . '/..';
$_v = function($relPath) use ($basePath, $_assetsDir) {
$full = $_assetsDir . '/' . ltrim($relPath, '/');
clearstatcache(true, $full);
$mtime = is_file($full) ? filemtime($full) : 0;
return $basePath . $relPath . ($mtime ? '?v=' . $mtime : '');
};
$_footerModules = is_array($_settings['modules'] ?? null) ? $_settings['modules'] : [];
$_aiModuleEnabled = !array_key_exists('ai', $_footerModules) || !empty($_footerModules['ai']);
$_aiCopilotAvailable = false;
if ($_aiModuleEnabled && is_file(__DIR__ . '/ai/ai-helper.php')) {
require_once __DIR__ . '/ai/ai-helper.php';
$_aiPublicSettings = function_exists('raijuAiLoadSettings') ? raijuAiLoadSettings(true) : [];
$_aiCopilotAvailable = !empty($_aiPublicSettings['enabled'])
&& !empty($_aiPublicSettings['hasApiKey'])
&& !empty($_aiPublicSettings['features']['backendAssistant'])
&& (!isset($_aiPublicSettings['assistantSurfaces']['visualEditor']) || !empty($_aiPublicSettings['assistantSurfaces']['visualEditor']));
}
// The agent (js/ai-agent.js) is offered only when it is switched on, the provider can call tools and the user may chat with it.
$_aiAgentConfig = null;
if ($_aiCopilotAvailable && is_file(__DIR__ . '/ai/agent/agent-config.php')) {
require_once __DIR__ . '/ai/agent/agent-config.php';
$_aiAgentReason = '';
if (raijuAgentAvailable($_aiAgentReason) && raijuAgentCan('chat')) {
$_aiAgentConfig = [
'available' => true,
'rights' => array_combine(RAIJU_AGENT_RIGHTS, array_map('raijuAgentCan', RAIJU_AGENT_RIGHTS)),
'classic' => raijuAgentSettings()['classicMode'] !== false,
'maxUploadFiles' => 6,
'maxUploadBytes' => function_exists('getMediaConfig') ? (int)(getMediaConfig('image')['maxSize'] ?? 10485760) : 10485760,
];
}
}
?>
<link rel="stylesheet" href="<?php echo $basePath; ?>css/raiju-admin-tokens.css">
<link rel="stylesheet" href="<?php echo $_v('css/image-manager.css'); ?>">
<link rel="stylesheet" href="<?php echo $_v('css/inline-editor.css'); ?>">
<link rel="stylesheet" href="<?php echo $_v('css/rj-select.css'); ?>">
<?php if ($_aiCopilotAvailable && file_exists(__DIR__ . '/../css/ai-copilot.css')): ?>
<link rel="stylesheet" href="<?php echo $_v('css/ai-copilot.css'); ?>">
<?php endif; ?>
<?php if (!empty($_editorVars)): ?>
<style>:root{<?php echo implode(';', $_editorVars); ?>}</style>
<?php endif; ?>
<script>
window.BlockTypeRegistry = <?php echo json_encode(getBlockTypes(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); ?>;
<?php
// Inject editor translations for inline-editor.js
require_once __DIR__ . '/../admin/lang/i18n.php';
?>
window.RJ_LANG = <?php echo json_encode(array_merge(tAll(), tEditorAll()), JSON_UNESCAPED_UNICODE); ?>;
window.RJ_MENUS = <?php echo json_encode(getMenuRegistry()['menus'] ?? [], JSON_UNESCAPED_UNICODE); ?>;
window.RJ_AVAILABLE_ICONS = <?php echo json_encode(function_exists('getAvailableIcons') ? getAvailableIcons() : [], JSON_UNESCAPED_UNICODE); ?>;
window.RJ_SEO_HEALTH = <?php echo json_encode($_seoHealth ?? ['status' => 'yellow', 'score' => 0, 'label' => 'SEO prüfen', 'issues' => ['SEO-Daten konnten nicht berechnet werden.']], JSON_UNESCAPED_UNICODE); ?>;
window.RJ_AI_FEATURES_ENABLED = <?php echo json_encode($_aiModuleEnabled); ?>;
window.RJ_AI_COPILOT_AVAILABLE = <?php echo json_encode($_aiCopilotAvailable); ?>;
<?php if ($_aiAgentConfig !== null): ?>
window.RJ_AI_AGENT = <?php echo json_encode($_aiAgentConfig, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG); ?>;
<?php endif; ?>
window.RJ_AI_ASSISTANT_LANGUAGE = <?php echo json_encode(function_exists('_nbAdminLang') ? _nbAdminLang() : ($currentLang ?? (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en'))); ?>;
window.RJ_ADMIN_API_URL = <?php echo json_encode($basePath . 'admin/api.php', JSON_UNESCAPED_SLASHES); ?>;
window.RJ_ADMIN_BASE_URL = <?php echo $_adminAccessBaseJson; ?>;
window.RJ_AI_LABELS = <?php echo json_encode(raijuAiLabelEditorConfig($currentLang ?? null), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG); ?>;
<?php
// Build lightweight page list for link picker (slug → title for current language)
$_linkPages = [];
$_linkLang = $currentLang ?? (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en');
$_linkDefaultLang = defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en';
foreach (glob(__DIR__ . '/../content/pages/' . $_linkLang . '_*.json') as $_pf) {
$_page = raijuPageParseContentKey(basename($_pf, '.json'));
if ($_page === null || $_page['lang'] !== $_linkLang) continue;
$_slug = $_page['path'];
$_pd = json_decode(file_get_contents($_pf), true);
$_title = $_pd['title'] ?? ucfirst(str_replace('-', ' ', $_slug));
$_href = raijuPageUrlPath($_linkLang, $_slug, $_linkDefaultLang);
$_linkPages[] = ['slug' => $_slug, 'title' => $_title, 'href' => $_href];
}
usort($_linkPages, function($a, $b) { return strcasecmp($a['title'], $b['title']); });
?>
window.RJ_PAGES = <?php echo json_encode($_linkPages, JSON_UNESCAPED_UNICODE); ?>;
<?php
// Surface auto-generated fields so the editor can show a toast
$autoGenFields = function_exists('autoGeneratedFields') ? autoGeneratedFields() : [];
if ($autoGenFields):
?>
window.RJ_AUTO_GENERATED = <?php echo json_encode($autoGenFields, JSON_UNESCAPED_UNICODE); ?>;
<?php endif; ?>
function t(key, params) {
let s = (window.RJ_LANG && window.RJ_LANG[key]) || key;
if (params) { for (const [k, v] of Object.entries(params)) { s = s.replace('{' + k + '}', v); } }
return s;
}
</script>
<script src="<?php echo $_v('js/rj-select.js'); ?>"></script>
<script src="<?php echo $_v('js/image-manager.js'); ?>"></script>
<link rel="stylesheet" href="<?php echo $_v('css/revision-client.css'); ?>">
<script src="<?php echo $_v('js/revision-client.js'); ?>"></script>
<script src="<?php echo $_v('js/inline-editor.js'); ?>"></script>
<?php if ($_aiCopilotAvailable && file_exists(__DIR__ . '/../js/ai-copilot.js')): ?>
<script src="<?php echo $_v('js/ai-copilot.js'); ?>"></script>
<?php if ($_aiAgentConfig !== null && file_exists(__DIR__ . '/../js/ai-agent.js')): ?>
<script src="<?php echo $_v('js/ai-agent.js'); ?>"></script>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
</body>
</html>