feat(admin): add global site settings API, tests and dashboard UI
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 35s
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 35s
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,4 +29,8 @@ coverage/
|
||||
# draft files
|
||||
*.draft.json
|
||||
|
||||
# test data directories
|
||||
app/test_data*/
|
||||
|
||||
|
||||
|
||||
|
||||
75
src/pages/admin/settings.astro
Normal file
75
src/pages/admin/settings.astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
// src/pages/admin/settings.astro
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig } = loadWaasConfigs(userAgent);
|
||||
const info = (siteConfig as any).site_info || {};
|
||||
---
|
||||
|
||||
<Layout title="Globale Einstellungen | N&D Admin">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<div class="flex items-center justify-between mb-8 border-b border-slate-800 pb-4">
|
||||
<div>
|
||||
<a href="/admin/pages" class="text-xs text-slate-400 hover:text-sky-400 transition-colors">← Zurück zum Seiten-Dashboard</a>
|
||||
<h1 class="text-2xl font-bold text-white mt-1">Globale Stammdaten & SEO</h1>
|
||||
<p class="text-slate-400 text-sm">Diese Daten werden für automatisches Data Binding & Kontakt-Infos genutzt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="form-site-settings" class="space-y-6">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-xl p-6 space-y-4 shadow-xl">
|
||||
<h2 class="text-base font-bold text-sky-400 border-b border-slate-800 pb-2">Unternehmensdaten</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Firmenname / Website-Titel</label>
|
||||
<input type="text" name="title" value={info.title || ''} required class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">E-Mail Adresse</label>
|
||||
<input type="email" name="email" value={info.email || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Telefonnummer</label>
|
||||
<input type="text" name="phone" value={info.phone || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Adresse / Standort</label>
|
||||
<input type="text" name="address" value={info.address || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-6 py-3 rounded-lg text-sm transition-colors shadow-lg shadow-sky-500/20">
|
||||
💾 Stammdaten Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
document.getElementById('form-site-settings')?.addEventListener('submit', async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const site_info = Object.fromEntries(formData.entries());
|
||||
|
||||
const res = await fetch('/api/admin/settings/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ site_info })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Stammdaten erfolgreich aktualisiert!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
41
src/pages/api/admin/settings/update.ts
Normal file
41
src/pages/api/admin/settings/update.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { site_info, navigation } = await request.json();
|
||||
|
||||
if (!site_info || typeof site_info !== 'object') {
|
||||
return new Response(JSON.stringify({ error: 'Gültige site_info Daten erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
let config: any = {};
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(raw);
|
||||
} catch {
|
||||
// Fallback bei neuer Instanz
|
||||
config = { site_info: {}, pages: [], navigation: [] };
|
||||
}
|
||||
|
||||
// Update der globalen Eigenschaften
|
||||
config.site_info = { ...config.site_info, ...site_info };
|
||||
if (Array.isArray(navigation)) {
|
||||
config.navigation = navigation;
|
||||
}
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, site_info: config.site_info }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern der Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
33
tests/siteSettings.test.ts
Normal file
33
tests/siteSettings.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_site_settings');
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
|
||||
describe('Global Site Settings API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Alte Firma', phone: '0123456' },
|
||||
navigation: [{ label: 'Home', page_id: 'page_1' }]
|
||||
};
|
||||
await fs.writeFile(configPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll globale Stammdaten in site.config.json aktualisieren', async () => {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
config.site_info.title = 'N&D IT Solutions GmbH';
|
||||
config.site_info.phone = '+49 7473 123456';
|
||||
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(configPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.site_info.title).toBe('N&D IT Solutions GmbH');
|
||||
expect(updatedConfig.site_info.phone).toBe('+49 7473 123456');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user