From b159b061ae0e9532c66b91a83b2fb6e6a3142a26 Mon Sep 17 00:00:00 2001 From: Daniel S Date: Sun, 9 Aug 2026 12:35:46 +0200 Subject: [PATCH] feat: implement device-specific configurations for desktop, tablet, and mobile --- app/data/site.config.json | 10 +++++----- app/data/site.desktop.config.json | 1 + app/data/theme.config.json | 2 +- src/components/Footer.astro | 2 +- src/components/Navbar.astro | 2 +- src/layouts/Layout.astro | 2 +- src/pages/admin/editor.astro | 2 +- src/pages/admin/index.astro | 2 +- src/pages/api/config.ts | 2 +- src/pages/api/contact.ts | 2 +- src/pages/index.astro | 2 +- src/utils/configLoader.ts | 19 ++++++++++++++----- src/utils/deviceDetect.ts | 14 ++++++++++++++ tests/waasEngine.test.ts | 20 ++++++++++++++++++-- 14 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 app/data/site.desktop.config.json create mode 100644 src/utils/deviceDetect.ts diff --git a/app/data/site.config.json b/app/data/site.config.json index 0eab3a3..d07c9a7 100644 --- a/app/data/site.config.json +++ b/app/data/site.config.json @@ -1,10 +1,10 @@ { "siteName": "N&D IT Solutions", - "metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.", + "metaDescription": "Webdesign & Digital Solutions", "hero": { - "badge": "⚡ WaaS Enterprise Baukastensystem", + "badge": "WaaS", "title": "Maßgeschneiderte Webseiten. Digital im Griff.", - "subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis – inklusive Wartung & Hosting.", + "subtitle": "Wir machen Zeugs", "ctaPrimaryText": "Projekt anfragen", "ctaPrimaryLink": "#contact", "ctaSecondaryText": "Mehr erfahren", @@ -71,9 +71,9 @@ "address": "Deutschland" }, "sectionsOrder": [ - "hero", + "contact", "bento", - "contact" + "hero" ], "sectionsDetails": [ { diff --git a/app/data/site.desktop.config.json b/app/data/site.desktop.config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/app/data/site.desktop.config.json @@ -0,0 +1 @@ +{} diff --git a/app/data/theme.config.json b/app/data/theme.config.json index dde2770..f5393cf 100644 --- a/app/data/theme.config.json +++ b/app/data/theme.config.json @@ -1,5 +1,5 @@ { - "activePreset": "sunsetCoral", + "activePreset": "amberGlow", "customColors": { "bg": "#0B0F19", "accent": "#38BDF8" diff --git a/src/components/Footer.astro b/src/components/Footer.astro index e02bd4a..0e3aea8 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -2,7 +2,7 @@ // src/components/Footer.astro import { loadWaasConfigs } from '../utils/configLoader'; -const { siteConfig } = loadWaasConfigs(); +const { siteConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const footer = siteConfig.footer || { brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.', copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.', diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index 576691c..a714c07 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -3,7 +3,7 @@ import { FaBars, FaTimes } from 'react-icons/fa'; import { loadWaasConfigs } from '../utils/configLoader'; -const { siteConfig } = loadWaasConfigs(); +const { siteConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const header = siteConfig.header || { brandName: 'N&D', brandHighlight: 'i-t SOLUTIONS', diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index ac5bd1e..06a8a20 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -17,7 +17,7 @@ interface Props { description?: string; } -const { siteConfig, themeConfig } = loadWaasConfigs(); +const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark; const colors = activePreset.colors; diff --git a/src/pages/admin/editor.astro b/src/pages/admin/editor.astro index ac82aa9..0ebc6fb 100644 --- a/src/pages/admin/editor.astro +++ b/src/pages/admin/editor.astro @@ -3,7 +3,7 @@ import Layout from '../../layouts/Layout.astro'; import { loadWaasConfigs } from '../../utils/configLoader'; import { THEME_PRESETS } from '../../utils/themePresets'; -const { siteConfig, themeConfig } = loadWaasConfigs(); +const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); --- diff --git a/src/pages/admin/index.astro b/src/pages/admin/index.astro index a581724..2214b79 100644 --- a/src/pages/admin/index.astro +++ b/src/pages/admin/index.astro @@ -3,7 +3,7 @@ import Layout from '../../layouts/Layout.astro'; import { loadWaasConfigs } from '../../utils/configLoader'; import { THEME_PRESETS } from '../../utils/themePresets'; -const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs(); +const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const presets = Object.values(THEME_PRESETS); --- diff --git a/src/pages/api/config.ts b/src/pages/api/config.ts index 71b1662..4daeb6a 100644 --- a/src/pages/api/config.ts +++ b/src/pages/api/config.ts @@ -11,7 +11,7 @@ export const POST: APIRoute = async ({ request }) => { fs.mkdirSync(dataDir, { recursive: true }); } - const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs(); + const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); if (body.siteConfig) { const newSite = { diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index 7208579..9889848 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -13,7 +13,7 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ success: false, message: 'Bitte füllen Sie alle Pflichtfelder aus.' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } - const { smtpConfig, siteConfig } = loadWaasConfigs(); + const { smtpConfig, siteConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); if (!smtpConfig.isConfigured || smtpConfig.host === 'smtp.placeholder.local') { console.log('[WaaS Contact API] (Demo Mode) Received Submission:', { name, email, message }); diff --git a/src/pages/index.astro b/src/pages/index.astro index 17a6532..adb4fd8 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -9,7 +9,7 @@ import FallbackSection from '../components/sections/FallbackSection.astro'; import { loadWaasConfigs } from '../utils/configLoader'; import { THEME_PRESETS } from '../utils/themePresets'; -const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs(); +const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark; const colors = activePreset.colors; diff --git a/src/utils/configLoader.ts b/src/utils/configLoader.ts index 6a1de85..3423da4 100644 --- a/src/utils/configLoader.ts +++ b/src/utils/configLoader.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; - +import { detectDeviceType } from './deviceDetect'; export interface UIComponent { id: string; // Eindeutige ID (z.B. uuid / timestamp) slotId: string; // Platz-ID (z.B. 'hero-badge-slot', 'hero-actions-slot', 'bento-slot-1') @@ -206,11 +206,20 @@ function readJsonFile(filePath: string, fallback: T): T { } } -export function loadWaasConfigs() { +export function loadWaasConfigs(userAgent: string = '') { const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data'); - const sitePath = path.join(dataDir, 'site.config.json'); - const themePath = path.join(dataDir, 'theme.config.json'); - const smtpPath = path.join(dataDir, 'smtp.config.json'); + const device = detectDeviceType(userAgent); + const deviceSitePath = path.join(dataDir, `site.${device}.config.json`); + const genericSitePath = path.join(dataDir, 'site.config.json'); + const sitePath = fs.existsSync(deviceSitePath) ? deviceSitePath : genericSitePath; + + const deviceThemePath = path.join(dataDir, `theme.${device}.config.json`); + const genericThemePath = path.join(dataDir, 'theme.config.json'); + const themePath = fs.existsSync(deviceThemePath) ? deviceThemePath : genericThemePath; + + const deviceSmtpPath = path.join(dataDir, `smtp.${device}.config.json`); + const genericSmtpPath = path.join(dataDir, 'smtp.config.json'); + const smtpPath = fs.existsSync(deviceSmtpPath) ? deviceSmtpPath : genericSmtpPath; const rawSite = readJsonFile>(sitePath, FALLBACK_SITE_CONFIG); const siteConfig: SiteConfig = { diff --git a/src/utils/deviceDetect.ts b/src/utils/deviceDetect.ts new file mode 100644 index 0000000..31d4456 --- /dev/null +++ b/src/utils/deviceDetect.ts @@ -0,0 +1,14 @@ +export type DeviceType = 'desktop' | 'tablet' | 'mobile'; + +/** + * Simple User-Agent based device detection. + * Returns 'desktop' by default when detection fails. + */ +export function detectDeviceType(userAgent: string): DeviceType { + const ua = userAgent.toLowerCase(); + // Tablet detection (including iPad which reports as Mac on iOS 13+) + if (/tablet|ipad/.test(ua)) return 'tablet'; + // Mobile detection (phones and Android) + if (/mobile|iphone|android/.test(ua)) return 'mobile'; + return 'desktop'; +} diff --git a/tests/waasEngine.test.ts b/tests/waasEngine.test.ts index 8bfdc16..76f394f 100644 --- a/tests/waasEngine.test.ts +++ b/tests/waasEngine.test.ts @@ -21,7 +21,7 @@ describe('WaaS Engine Core Unit Tests', () => { }); it('1. Should return fallbacks when JSON files do not exist (Zero-Crash Guarantee)', () => { - const configs = loadWaasConfigs(); + const configs = loadWaasConfigs(''); expect(configs.siteConfig.siteName).toBe(FALLBACK_SITE_CONFIG.siteName); expect(configs.themeConfig.activePreset).toBe(FALLBACK_THEME_CONFIG.activePreset); expect(configs.smtpConfig.host).toBe(FALLBACK_SMTP_CONFIG.host); @@ -32,7 +32,7 @@ describe('WaaS Engine Core Unit Tests', () => { const customSite = { siteName: 'Custom Test Agency', hero: { title: 'Custom Title' } }; fs.writeFileSync(path.join(testDataDir, 'site.config.json'), JSON.stringify(customSite), 'utf8'); - const configs = loadWaasConfigs(); + const configs = loadWaasConfigs(''); expect(configs.siteConfig.siteName).toBe('Custom Test Agency'); expect(configs.siteConfig.hero.title).toBe('Custom Title'); expect(configs.siteConfig.hero.badge).toBe(FALLBACK_SITE_CONFIG.hero.badge); @@ -50,4 +50,20 @@ describe('WaaS Engine Core Unit Tests', () => { expect(heroElements.length).toBeGreaterThan(0); expect(heroElements[0].type).toBe('badge'); }); + + it('5. Should load device-specific configs for desktop, mobile, tablet with fallback', () => { + fs.mkdirSync(testDataDir, { recursive: true }); + fs.writeFileSync(path.join(testDataDir, 'site.mobile.config.json'), JSON.stringify({ siteName: 'Mobile Site' }), 'utf8'); + fs.writeFileSync(path.join(testDataDir, 'site.tablet.config.json'), JSON.stringify({ siteName: 'Tablet Site' }), 'utf8'); + fs.writeFileSync(path.join(testDataDir, 'site.desktop.config.json'), JSON.stringify({ siteName: 'Desktop Site' }), 'utf8'); + + const mobileConfig = loadWaasConfigs('Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)'); + expect(mobileConfig.siteConfig.siteName).toBe('Mobile Site'); + + const tabletConfig = loadWaasConfigs('Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)'); + expect(tabletConfig.siteConfig.siteName).toBe('Tablet Site'); + + const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + expect(desktopConfig.siteConfig.siteName).toBe('Desktop Site'); + }); }); \ No newline at end of file