feat: implement Theme Builder concept and Display Conditions resolver system
This commit is contained in:
@@ -30,6 +30,9 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Classic Admin
|
||||
</a>
|
||||
@@ -383,6 +386,33 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
|
||||
</main>
|
||||
|
||||
</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>
|
||||
</Layout>
|
||||
|
||||
@@ -721,4 +751,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>
|
||||
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;
|
||||
}
|
||||
@@ -66,4 +66,34 @@ describe('WaaS Engine Core Unit Tests', () => {
|
||||
const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user