Compare commits
2 Commits
3453746fc0
...
074d314897
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
074d314897 | ||
|
|
b039aceb21 |
@@ -1,11 +1,17 @@
|
|||||||
---
|
---
|
||||||
import type { SiteConfig, UIComponent } from '../../utils/configLoader';
|
import type { SiteConfig, UIComponent } from '../../utils/configLoader';
|
||||||
|
import { resolveDynamicBinding } from '../../utils/dynamicDataBinding';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config: SiteConfig['hero'];
|
config: SiteConfig['hero'];
|
||||||
|
contextData?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config } = Astro.props;
|
const { config, contextData = {} } = Astro.props;
|
||||||
|
|
||||||
|
const heroTitle = resolveDynamicBinding(config.title, contextData);
|
||||||
|
const heroSubtitle = resolveDynamicBinding(config.subtitle, contextData);
|
||||||
|
const heroBadge = resolveDynamicBinding(config.badge, contextData);
|
||||||
|
|
||||||
const elements = config.elements || [];
|
const elements = config.elements || [];
|
||||||
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');
|
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');
|
||||||
@@ -21,41 +27,41 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
|
|||||||
|
|
||||||
<!-- Slot 1: Badge Slot -->
|
<!-- Slot 1: Badge Slot -->
|
||||||
<div id="hero-badge-slot" class="inline-flex flex-wrap items-center justify-center gap-2">
|
<div id="hero-badge-slot" class="inline-flex flex-wrap items-center justify-center gap-2">
|
||||||
{config.badge && (
|
{heroBadge && (
|
||||||
<div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/20 bg-sky-400/10 text-sky-400 text-xs font-mono tracking-wide backdrop-blur-md">
|
<div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/20 bg-sky-400/10 text-sky-400 text-xs font-mono tracking-wide backdrop-blur-md">
|
||||||
<span>{config.badge}</span>
|
<span>{heroBadge}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{badgeElements.map(el => (
|
{badgeElements.map(el => (
|
||||||
<span class={`inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/30 bg-sky-400/20 text-sky-300 text-xs font-mono ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
|
<span class={`inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/30 bg-sky-400/20 text-sky-300 text-xs font-mono ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
|
||||||
{el.content}
|
{resolveDynamicBinding(el.content, contextData)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-white leading-tight">
|
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-white leading-tight">
|
||||||
{config.title}
|
{heroTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="text-slate-400 text-xs sm:text-lg max-w-2xl mx-auto leading-relaxed font-normal">
|
<p class="text-slate-400 text-xs sm:text-lg max-w-2xl mx-auto leading-relaxed font-normal">
|
||||||
{config.subtitle}
|
{heroSubtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Slot 2: Action Buttons Slot -->
|
<!-- Slot 2: Action Buttons Slot -->
|
||||||
<div id="hero-actions-slot" class="flex flex-wrap items-center justify-center gap-4 pt-4">
|
<div id="hero-actions-slot" class="flex flex-wrap items-center justify-center gap-4 pt-4">
|
||||||
{config.ctaPrimaryText && (
|
{config.ctaPrimaryText && (
|
||||||
<a href={config.ctaPrimaryLink} class="px-8 py-3.5 rounded-xl bg-sky-400 text-slate-950 font-bold text-xs hover:bg-white transition-all duration-300 shadow-lg shadow-sky-400/20 active:scale-95">
|
<a href={config.ctaPrimaryLink} class="px-8 py-3.5 rounded-xl bg-sky-400 text-slate-950 font-bold text-xs hover:bg-white transition-all duration-300 shadow-lg shadow-sky-400/20 active:scale-95">
|
||||||
{config.ctaPrimaryText}
|
{resolveDynamicBinding(config.ctaPrimaryText, contextData)}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{config.ctaSecondaryText && (
|
{config.ctaSecondaryText && (
|
||||||
<a href={config.ctaSecondaryLink} class="px-8 py-3.5 rounded-xl glass-panel text-white font-semibold text-xs glass-panel-hover active:scale-95">
|
<a href={config.ctaSecondaryLink} class="px-8 py-3.5 rounded-xl glass-panel text-white font-semibold text-xs glass-panel-hover active:scale-95">
|
||||||
{config.ctaSecondaryText}
|
{resolveDynamicBinding(config.ctaSecondaryText, contextData)}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{actionElements.map(el => (
|
{actionElements.map(el => (
|
||||||
<a href={el.link || '#'} class={`px-8 py-3.5 rounded-xl font-bold text-xs transition-all active:scale-95 ${el.style === 'glass' ? 'glass-panel text-white' : 'bg-sky-400 text-slate-950 hover:bg-white'} ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
|
<a href={el.link || '#'} class={`px-8 py-3.5 rounded-xl font-bold text-xs transition-all active:scale-95 ${el.style === 'glass' ? 'glass-panel text-white' : 'bg-sky-400 text-slate-950 hover:bg-white'} ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
|
||||||
{el.content}
|
{resolveDynamicBinding(el.content, contextData)}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +72,7 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
|
|||||||
{customElements.map(el => (
|
{customElements.map(el => (
|
||||||
<div class="p-4 rounded-xl glass-panel text-xs text-slate-300">
|
<div class="p-4 rounded-xl glass-panel text-xs text-slate-300">
|
||||||
<span class="font-bold text-sky-400 block mb-1 uppercase text-[10px]">{el.type} [ID: {el.id}]</span>
|
<span class="font-bold text-sky-400 block mb-1 uppercase text-[10px]">{el.type} [ID: {el.id}]</span>
|
||||||
{el.content}
|
{resolveDynamicBinding(el.content, contextData)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="theme-conditions-btn" class="px-3.5 py-1.5 rounded-lg border border-sky-400/30 bg-sky-400/10 text-sky-400 font-semibold text-xs hover:bg-sky-400 hover:text-slate-950 transition-all">
|
||||||
|
Display Conditions
|
||||||
|
</button>
|
||||||
<a href="/admin" class="px-3.5 py-1.5 rounded-lg glass-panel text-xs hover:border-sky-400 transition-all">
|
<a href="/admin" class="px-3.5 py-1.5 rounded-lg glass-panel text-xs hover:border-sky-400 transition-all">
|
||||||
Classic Admin
|
Classic Admin
|
||||||
</a>
|
</a>
|
||||||
@@ -220,8 +223,22 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label</label>
|
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Dynamische Daten-Verknüpfung (Dynamic Tag)</label>
|
||||||
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
|
<select id="new-el-dynamic-tag" onchange="if(this.value){ document.getElementById('new-el-content').value = '{{' + this.value + '}}'; }" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-sky-400 focus:outline-none focus:border-sky-400 font-mono mb-2">
|
||||||
|
<option value="">-- Statischer Text (Kein Tag) --</option>
|
||||||
|
<option value="site.siteName">site.siteName (Website Name)</option>
|
||||||
|
<option value="site.metaDescription">site.metaDescription (Beschreibung)</option>
|
||||||
|
<option value="site.contactEmail">site.contactEmail (Kontakt Email)</option>
|
||||||
|
<option value="site.phone">site.phone (Telefonnummer)</option>
|
||||||
|
<option value="post.title">post.title (Beitragstitel)</option>
|
||||||
|
<option value="post.excerpt">post.excerpt (Beitragsauszug)</option>
|
||||||
|
<option value="author.name">author.name (Autor Name)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label / Tag</label>
|
||||||
|
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen oder {{site.siteName}}" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400 font-mono" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" id="add-el-btn" class="w-full py-2.5 rounded-lg bg-sky-400/20 text-sky-400 border border-sky-400/30 font-bold text-xs uppercase tracking-wider hover:bg-sky-400 hover:text-slate-950 transition-all">
|
<button type="button" id="add-el-btn" class="w-full py-2.5 rounded-lg bg-sky-400/20 text-sky-400 border border-sky-400/30 font-bold text-xs uppercase tracking-wider hover:bg-sky-400 hover:text-slate-950 transition-all">
|
||||||
@@ -383,6 +400,33 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme Builder Conditions Modal Overlay -->
|
||||||
|
<div id="conditions-modal" class="hidden fixed inset-0 bg-slate-950/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-slate-900 border border-white/10 rounded-2xl p-6 w-full max-w-lg space-y-5 shadow-2xl text-xs">
|
||||||
|
<div class="flex items-center justify-between border-b border-white/10 pb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold text-sky-400 uppercase tracking-wider">Theme Builder Display Conditions</h3>
|
||||||
|
<p class="text-slate-400 text-[11px] mt-0.5">Bestimme, wo dieses Template auf der Website erscheinen soll.</p>
|
||||||
|
</div>
|
||||||
|
<button id="close-conditions-modal" class="text-slate-400 hover:text-white font-bold text-sm">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="conditions-list" class="space-y-3 max-h-60 overflow-y-auto pr-1">
|
||||||
|
<!-- Dynamic Condition Rows -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="add-condition-btn" class="text-xs text-sky-400 font-bold hover:underline flex items-center gap-1">
|
||||||
|
+ Bedingung hinzufügen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-3 border-t border-white/10">
|
||||||
|
<button id="cancel-conditions-btn" class="px-4 py-2 rounded-lg glass-panel text-slate-300 font-semibold hover:text-white">Abbrechen</button>
|
||||||
|
<button id="save-conditions-btn" class="px-5 py-2 rounded-lg bg-sky-400 text-slate-950 font-bold uppercase hover:bg-white transition-all">Bedingungen Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@@ -721,4 +765,78 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Theme Builder Display Conditions Modal Management ---
|
||||||
|
let themeConditionsState = [
|
||||||
|
{ type: 'include', scope: 'entire_site' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const conditionsModal = document.getElementById('conditions-modal');
|
||||||
|
const conditionsBtn = document.getElementById('theme-conditions-btn');
|
||||||
|
const closeConditionsBtn = document.getElementById('close-conditions-modal');
|
||||||
|
const cancelConditionsBtn = document.getElementById('cancel-conditions-btn');
|
||||||
|
const addConditionBtn = document.getElementById('add-condition-btn');
|
||||||
|
const saveConditionsBtn = document.getElementById('save-conditions-btn');
|
||||||
|
|
||||||
|
function openConditionsModal() {
|
||||||
|
renderConditionsList();
|
||||||
|
conditionsModal?.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeConditionsModal() {
|
||||||
|
conditionsModal?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConditionsList() {
|
||||||
|
const listContainer = document.getElementById('conditions-list');
|
||||||
|
if (!listContainer) return;
|
||||||
|
|
||||||
|
listContainer.innerHTML = themeConditionsState.map((cond, idx) => (
|
||||||
|
'<div class="flex gap-2 items-center bg-slate-950 p-2.5 rounded-xl border border-white/5 text-xs">' +
|
||||||
|
'<select onchange="updateCondition(' + idx + ', \'type\', this.value)" class="bg-slate-900 text-xs text-white p-2 rounded-lg border border-white/10 focus:border-sky-400">' +
|
||||||
|
'<option value="include"' + (cond.type === 'include' ? ' selected' : '') + '>INCLUDE</option>' +
|
||||||
|
'<option value="exclude"' + (cond.type === 'exclude' ? ' selected' : '') + '>EXCLUDE</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'<select onchange="updateCondition(' + idx + ', \'scope\', this.value)" class="flex-1 bg-slate-900 text-xs text-white p-2 rounded-lg border border-white/10 focus:border-sky-400">' +
|
||||||
|
'<option value="entire_site"' + (cond.scope === 'entire_site' ? ' selected' : '') + '>Entire Site (Ganze Website)</option>' +
|
||||||
|
'<option value="singular"' + (cond.scope === 'singular' ? ' selected' : '') + '>Singular (Einzelseiten)</option>' +
|
||||||
|
'<option value="page_404"' + (cond.scope === 'page_404' ? ' selected' : '') + '>404 Error Page</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'<button type="button" onclick="removeCondition(' + idx + ')" class="text-red-400 hover:text-red-300 font-bold px-2 py-1">✕</button>' +
|
||||||
|
'</div>'
|
||||||
|
)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.updateCondition = function(idx, field, value) {
|
||||||
|
if (themeConditionsState[idx]) {
|
||||||
|
themeConditionsState[idx][field] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeCondition = function(idx) {
|
||||||
|
themeConditionsState.splice(idx, 1);
|
||||||
|
renderConditionsList();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addConditionBtn) {
|
||||||
|
addConditionBtn.addEventListener('click', () => {
|
||||||
|
themeConditionsState.push({ type: 'include', scope: 'entire_site' });
|
||||||
|
renderConditionsList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
conditionsBtn?.addEventListener('click', openConditionsModal);
|
||||||
|
closeConditionsBtn?.addEventListener('click', closeConditionsModal);
|
||||||
|
cancelConditionsBtn?.addEventListener('click', closeConditionsModal);
|
||||||
|
|
||||||
|
if (saveConditionsBtn) {
|
||||||
|
saveConditionsBtn.addEventListener('click', () => {
|
||||||
|
closeConditionsModal();
|
||||||
|
if (alertBox) {
|
||||||
|
alertBox.classList.remove('hidden');
|
||||||
|
alertBox.textContent = 'Display Conditions erfolgreich gespeichert!';
|
||||||
|
setTimeout(() => alertBox.classList.add('hidden'), 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
67
src/utils/conditionResolver.ts
Normal file
67
src/utils/conditionResolver.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
export interface DisplayCondition {
|
||||||
|
type: 'include' | 'exclude';
|
||||||
|
scope: 'entire_site' | 'singular' | 'archive' | 'page_404';
|
||||||
|
entity?: 'page' | 'post' | 'category';
|
||||||
|
entityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateMeta {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'header' | 'footer' | 'single' | 'archive' | '404';
|
||||||
|
status: 'draft' | 'published';
|
||||||
|
priority: number;
|
||||||
|
conditions: DisplayCondition[];
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves which template to use based on target type and request context conditions.
|
||||||
|
* Specific matches (singular/page_404) take precedence over general 'entire_site' matches.
|
||||||
|
*/
|
||||||
|
export function resolveTemplate(
|
||||||
|
templates: TemplateMeta[],
|
||||||
|
targetType: TemplateMeta['type'],
|
||||||
|
context: { url: string; routeType: 'singular' | 'archive' | '404' | 'home'; entityId?: string }
|
||||||
|
): TemplateMeta | null {
|
||||||
|
const candidates = templates.filter(t => t.type === targetType && t.status === 'published');
|
||||||
|
let bestMatch: TemplateMeta | null = null;
|
||||||
|
let highestScore = -1;
|
||||||
|
|
||||||
|
for (const tpl of candidates) {
|
||||||
|
let isIncluded = false;
|
||||||
|
let isExcluded = false;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (const cond of tpl.conditions) {
|
||||||
|
if (cond.type === 'exclude') {
|
||||||
|
if (cond.scope === 'entire_site') isExcluded = true;
|
||||||
|
if (cond.scope === 'singular' && context.routeType === 'singular' && (!cond.entityId || cond.entityId === context.entityId)) {
|
||||||
|
isExcluded = true;
|
||||||
|
}
|
||||||
|
if (cond.scope === 'page_404' && context.routeType === '404') isExcluded = true;
|
||||||
|
} else if (cond.type === 'include') {
|
||||||
|
if (cond.scope === 'entire_site') {
|
||||||
|
isIncluded = true;
|
||||||
|
score = Math.max(score, tpl.priority || 1);
|
||||||
|
} else if (cond.scope === 'singular' && context.routeType === 'singular') {
|
||||||
|
if (!cond.entityId || cond.entityId === context.entityId) {
|
||||||
|
isIncluded = true;
|
||||||
|
score = Math.max(score, (tpl.priority || 1) + 10);
|
||||||
|
}
|
||||||
|
} else if (cond.scope === 'page_404' && context.routeType === '404') {
|
||||||
|
isIncluded = true;
|
||||||
|
score = Math.max(score, (tpl.priority || 1) + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIncluded && !isExcluded && score > highestScore) {
|
||||||
|
highestScore = score;
|
||||||
|
bestMatch = tpl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
49
src/utils/dynamicDataBinding.ts
Normal file
49
src/utils/dynamicDataBinding.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export interface DynamicFieldDefinition {
|
||||||
|
key: string; // e.g. 'site.siteName', 'post.title', 'custom.clientName'
|
||||||
|
label: string;
|
||||||
|
category: 'site' | 'post' | 'author' | 'custom';
|
||||||
|
defaultValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DYNAMIC_FIELDS_REGISTRY: DynamicFieldDefinition[] = [
|
||||||
|
{ key: 'site.siteName', label: 'Website Name', category: 'site', defaultValue: 'N&D IT Solutions' },
|
||||||
|
{ key: 'site.metaDescription', label: 'Website Beschreibung', category: 'site', defaultValue: 'High-Performance Webdesign & Digital Solutions' },
|
||||||
|
{ key: 'site.contactEmail', label: 'Kontakt E-Mail', category: 'site', defaultValue: 'kontakt@ndsolutions.de' },
|
||||||
|
{ key: 'site.phone', label: 'Telefonnummer', category: 'site', defaultValue: '+49 (0) 123 456789' },
|
||||||
|
{ key: 'post.title', label: 'Beitragstitel (Dynamisch)', category: 'post', defaultValue: 'Beispiel Artikel' },
|
||||||
|
{ key: 'post.excerpt', label: 'Beitrags Auszug', category: 'post', defaultValue: 'Ein kurzer Auszug des Artikels...' },
|
||||||
|
{ key: 'author.name', label: 'Autor Name', category: 'author', defaultValue: 'N&D Team' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces dynamic binding tags like `{{site.siteName}}` or evaluates binding keys against a data context.
|
||||||
|
*/
|
||||||
|
export function resolveDynamicBinding(
|
||||||
|
content: string,
|
||||||
|
contextData: Record<string, any>
|
||||||
|
): string {
|
||||||
|
if (!content) return content;
|
||||||
|
|
||||||
|
return content.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
||||||
|
const trimmedKey = key.trim();
|
||||||
|
const keys = trimmedKey.split('.');
|
||||||
|
let val: any = contextData;
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (val && typeof val === 'object' && k in val) {
|
||||||
|
val = val[k];
|
||||||
|
} else {
|
||||||
|
val = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val !== undefined && val !== null) {
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to registry default if available
|
||||||
|
const regItem = DYNAMIC_FIELDS_REGISTRY.find(f => f.key === trimmedKey);
|
||||||
|
return regItem ? regItem.defaultValue : `{{${trimmedKey}}}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -66,4 +66,49 @@ describe('WaaS Engine Core Unit Tests', () => {
|
|||||||
const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||||
expect(desktopConfig.siteConfig.siteName).toBe('Desktop Site');
|
expect(desktopConfig.siteConfig.siteName).toBe('Desktop Site');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('6. Should correctly resolve Theme Builder template based on Display Conditions', async () => {
|
||||||
|
const { resolveTemplate } = await import('../src/utils/conditionResolver');
|
||||||
|
const templates = [
|
||||||
|
{
|
||||||
|
id: 'header-global',
|
||||||
|
name: 'Global Header',
|
||||||
|
type: 'header' as const,
|
||||||
|
status: 'published' as const,
|
||||||
|
priority: 1,
|
||||||
|
conditions: [{ type: 'include' as const, scope: 'entire_site' as const }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'header-custom-page',
|
||||||
|
name: 'Custom Page Header',
|
||||||
|
type: 'header' as const,
|
||||||
|
status: 'published' as const,
|
||||||
|
priority: 1,
|
||||||
|
conditions: [{ type: 'include' as const, scope: 'singular' as const, entityId: 'landing_1' }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Global matching
|
||||||
|
const matchGlobal = resolveTemplate(templates, 'header', { url: '/about', routeType: 'singular', entityId: 'about_page' });
|
||||||
|
expect(matchGlobal?.id).toBe('header-global');
|
||||||
|
|
||||||
|
// Specific page matching
|
||||||
|
const matchSpecific = resolveTemplate(templates, 'header', { url: '/landing', routeType: 'singular', entityId: 'landing_1' });
|
||||||
|
expect(matchSpecific?.id).toBe('header-custom-page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('7. Should correctly resolve dynamic data tags like {{site.siteName}}', async () => {
|
||||||
|
const { resolveDynamicBinding } = await import('../src/utils/dynamicDataBinding');
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
site: { siteName: 'Test Agency Pro', contactEmail: 'info@test.de' },
|
||||||
|
post: { title: 'Mein erster Post' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvedTitle = resolveDynamicBinding('Willkommen bei {{site.siteName}}', context);
|
||||||
|
expect(resolvedTitle).toBe('Willkommen bei Test Agency Pro');
|
||||||
|
|
||||||
|
const resolvedPost = resolveDynamicBinding('Artikel: {{post.title}}', context);
|
||||||
|
expect(resolvedPost).toBe('Artikel: Mein erster Post');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user