300 lines
13 KiB
Markdown
300 lines
13 KiB
Markdown
# Modul: Website-CMS (Hero · Über uns · Galerie · Kontakt · Passwort)
|
||
|
||
> Vollständiges CMS für eine öffentliche Website: Hero-Sektion (Texte, Badges, Hintergrundbild, Logo, Favicon), Über-uns-Sektion (Texte, Statistiken, Sektionsbild), Bildergalerie (Upload, Sortierung, animiertes Grid), Kontakt-Daten (Info, Öffnungszeiten, Social Media) und Admin-Passwort-Änderung. Alle Sektionen lesen direkt aus Supabase; bei DB-Fehler greift ein statischer Fallback.
|
||
|
||
> **Anfrageformular** (Kontaktformular mit E-Mail-Versand) → Modul 07
|
||
|
||
---
|
||
|
||
## Enthaltene Dateien
|
||
|
||
| Ziel im neuen Projekt | Inhalt |
|
||
|---|---|
|
||
| `app/api/admin/hero/route.ts` | GET + PATCH: hero_content (UPSERT) + hero_badges (lesen) |
|
||
| `app/api/admin/hero/bild/route.ts` | POST/DELETE: Hintergrundbild (`hero-bilder` Bucket) |
|
||
| `app/api/admin/hero/logo/route.ts` | POST/DELETE: Logo (`site-assets` Bucket) |
|
||
| `app/api/admin/hero/favicon/route.ts` | POST/DELETE: Favicon (`site-assets` Bucket) |
|
||
| `app/api/admin/hero/badges/route.ts` | POST: Badge hinzufügen |
|
||
| `app/api/admin/hero/badges/id/route.ts` | PATCH/DELETE: Badge bearbeiten/löschen |
|
||
| `app/api/admin/galerie/route.ts` | GET/POST/PATCH/DELETE: Galerie-Bilder + Storage |
|
||
| `app/api/admin/galerie/sort/route.ts` | POST: Reihenfolge speichern |
|
||
| `app/api/admin/ueber-uns/route.ts` | GET + PATCH: ueber_uns_content (UPSERT) + Stats (lesen) |
|
||
| `app/api/admin/ueber-uns/upload/route.ts` | POST: Sektionsbild hochladen (`ueber-uns-bilder` Bucket) |
|
||
| `app/api/admin/ueber-uns/stats/route.ts` | POST: Statistik hinzufügen |
|
||
| `app/api/admin/ueber-uns/stats/id/route.ts` | PATCH/DELETE: Statistik bearbeiten/löschen |
|
||
| `app/api/admin/kontakt/route.ts` | GET + PATCH: kontakt_info (UPSERT) + Listen lesen |
|
||
| `app/api/admin/kontakt/oeffnungszeiten/route.ts` | POST: Öffnungszeit hinzufügen |
|
||
| `app/api/admin/kontakt/oeffnungszeiten/id/route.ts` | PATCH/DELETE |
|
||
| `app/api/admin/kontakt/social/route.ts` | POST: Social-Link hinzufügen |
|
||
| `app/api/admin/kontakt/social/id/route.ts` | PATCH/DELETE |
|
||
| `app/api/admin/passwort/route.ts` | POST: Admin-Passwort ändern (bcrypt) |
|
||
| `app/admin/hero/page.tsx` | Admin-Page Hero |
|
||
| `app/admin/galerie/page.tsx` | Admin-Page Galerie |
|
||
| `app/admin/ueber-uns/page.tsx` | Admin-Page Über uns |
|
||
| `app/admin/kontakt/page.tsx` | Admin-Page Kontakt |
|
||
| `app/admin/passwort/page.tsx` | Admin-Page Passwort |
|
||
| `components/GalerieAnzeige.tsx` | Öffentliche Galerie mit 3D-Flip-Animation |
|
||
| `components/admin/HeroVerwaltung.tsx` | Admin-UI: Hero |
|
||
| `components/admin/GalerieVerwaltung.tsx` | Admin-UI: Galerie |
|
||
| `components/admin/UeberUnsVerwaltung.tsx` | Admin-UI: Über uns |
|
||
| `components/admin/KontaktVerwaltung.tsx` | Admin-UI: Kontakt |
|
||
| `components/admin/PasswortAendern.tsx` | Admin-UI: Passwort ändern |
|
||
|
||
**Hinweis:** Ordner `id/` in `files/` entsprechen Next.js Dynamic-Route-Ordnern `[id]`.
|
||
|
||
---
|
||
|
||
## Voraussetzungen
|
||
|
||
Benötigt:
|
||
- `lib/supabase.ts` mit `createPublicClient()` und `createServiceClient()`
|
||
- `lib/admin-auth.ts` (Modul 02): `requireAdmin()` und `getAdminSession()`
|
||
- Tabelle `admins` mit Spalten `id`, `email`, `password_hash` (Modul 02)
|
||
|
||
```bash
|
||
npm install bcryptjs
|
||
npm install -D @types/bcryptjs
|
||
```
|
||
|
||
---
|
||
|
||
## Umgebungsvariablen
|
||
|
||
```env
|
||
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co # Für Storage-URL-Konstruktion im Client
|
||
SUPABASE_INTERNAL_URL=http://supabase-kong:8000 # Nur Docker-intern; API-Routen verwenden diese
|
||
```
|
||
|
||
---
|
||
|
||
## Supabase Storage Buckets anlegen
|
||
|
||
Im Supabase Dashboard → Storage → New Bucket für jeden Bucket:
|
||
|
||
| Bucket-Name | Public | Verwendung |
|
||
|---|---|---|
|
||
| `hero-bilder` | ✓ | Hero-Hintergrundbild |
|
||
| `site-assets` | ✓ | Logo + Favicon |
|
||
| `ueber-uns-bilder` | ✓ | Über-uns-Sektionsbild |
|
||
| `galerie-bilder` | ✓ | Galerie-Fotos |
|
||
|
||
---
|
||
|
||
## Datenbank-Migrationen (Supabase)
|
||
|
||
Die vollständige SQL-Datei liegt unter `migrations/MIGRATIONS_WEBSITE_CMS.sql`.
|
||
|
||
Im Supabase SQL-Editor einmalig ausführen. Sie enthält alle Tabellen, Spalten und RLS-Policies.
|
||
|
||
Erstellt werden:
|
||
- `hero_content` — Singleton (Seitenname, Tagline, Überschriften, Buttons, Bildpfade)
|
||
- `hero_badges` — Liste der Trust-Badges
|
||
- `ueber_uns_content` — Singleton (Eyebrow, Absätze, Bildpfad)
|
||
- `ueber_uns_stats` — Liste der Kennzahlen/Statistiken
|
||
- `galerie_bilder` — Liste der Galeriefotos (storage_path, alt_text, reihenfolge)
|
||
- `kontakt_info` — Singleton (Telefon, E-Mail, Adresse, Formular-Empfänger)
|
||
- `kontakt_oeffnungszeiten` — Liste der Öffnungszeiten
|
||
- `kontakt_social` — Liste der Social-Media-Links
|
||
|
||
Alle Tabellen erhalten `public read` RLS-Policy (anonymer Lesezugriff).
|
||
|
||
### Supabase-Typen (`lib/supabase.ts`) ergänzen
|
||
|
||
In `Database.public.Tables` hinzufügen:
|
||
|
||
```ts
|
||
hero_content: { Row: {
|
||
id: string; site_name: string; site_tagline: string;
|
||
eyebrow_text: string; headline1: string; headline2: string;
|
||
subtext1: string; subtext2: string;
|
||
cta1_text: string; cta1_href: string; cta2_text: string; cta2_href: string;
|
||
bg_image_path: string | null; logo_path: string | null; favicon_path: string | null;
|
||
updated_at: string;
|
||
} }
|
||
hero_badges: { Row: { id: string; text: string; reihenfolge: number } }
|
||
ueber_uns_content: { Row: {
|
||
id: string; eyebrow_text: string; absatz1: string; absatz2: string;
|
||
bild_url: string | null; updated_at: string;
|
||
} }
|
||
ueber_uns_stats: { Row: { id: string; wert: string; label: string; reihenfolge: number } }
|
||
galerie_bilder: { Row: {
|
||
id: string; storage_path: string; alt_text: string; reihenfolge: number;
|
||
} }
|
||
kontakt_info: { Row: {
|
||
id: string; telefon: string; email: string;
|
||
adresse_zeile1: string; adresse_zeile2: string;
|
||
formular_empfaenger: string; updated_at: string;
|
||
} }
|
||
kontakt_oeffnungszeiten: { Row: { id: string; tag: string; von: string; bis: string; reihenfolge: number } }
|
||
kontakt_social: { Row: { id: string; platform: string; url: string; reihenfolge: number } }
|
||
```
|
||
|
||
---
|
||
|
||
## Architektur-Muster
|
||
|
||
### UPSERT-Singleton (Einzel-Zeilen-Tabellen)
|
||
`hero_content`, `ueber_uns_content`, `kontakt_info` haben immer genau eine Zeile.
|
||
API-Route: erst `select('id').limit(1).single()` → bei Treffer `update`, sonst `insert`.
|
||
|
||
### CRUD-Liste (Tabellen mit `reihenfolge`)
|
||
`hero_badges`, `ueber_uns_stats`, `galerie_bilder`, `kontakt_oeffnungszeiten`, `kontakt_social` — klassisches INSERT/PATCH/DELETE. Reihenfolge wird als Integer gespeichert.
|
||
|
||
### Server Component + Fallback
|
||
Öffentliche Website-Sektionen sind async Server Components. Alle Supabase-Aufrufe in `try/catch`; bei Fehler werden hartcodierte Fallback-Daten gerendert.
|
||
|
||
### force-dynamic
|
||
Alle Admin-Pages müssen `export const dynamic = 'force-dynamic'` haben (verhindert statisches Pre-Rendering beim Docker-Build ohne Env-Variablen).
|
||
|
||
### Galerie: Hydration-Mismatch vermeiden
|
||
`GalerieAnzeige` initialisiert `slots` ohne Shuffle im `useState()`-Initializer (Server und Client rendern identisch). Der Shuffle + Intervall-Start läuft nur im `useEffect([])` nach dem Mount.
|
||
|
||
---
|
||
|
||
## Einbindung Schritt für Schritt
|
||
|
||
### 1. Dateien kopieren (Dynamic-Route-Ordner umbenennen)
|
||
```
|
||
files/app/api/admin/hero/badges/id/ → app/api/admin/hero/badges/[id]/
|
||
files/app/api/admin/ueber-uns/stats/id/ → app/api/admin/ueber-uns/stats/[id]/
|
||
files/app/api/admin/kontakt/oeffnungszeiten/id/ → app/api/admin/kontakt/oeffnungszeiten/[id]/
|
||
files/app/api/admin/kontakt/social/id/ → app/api/admin/kontakt/social/[id]/
|
||
```
|
||
|
||
### 2. Standardwerte anpassen
|
||
In `components/admin/HeroVerwaltung.tsx`:
|
||
```tsx
|
||
const DEFAULTS: HeroContent = {
|
||
site_name: 'Musterfirma', // ← eigenen Firmennamen setzen
|
||
site_tagline: '',
|
||
eyebrow_text: 'Ihr Slogan hier',
|
||
headline1: 'Wir lieben Qualität.',
|
||
headline2: 'Ihre Kunden auch.',
|
||
// ...
|
||
}
|
||
```
|
||
|
||
In `app/api/admin/hero/route.ts` PATCH-Handler dieselben Defaults setzen.
|
||
|
||
### 3. Fallback-Bild konfigurieren (`components/admin/HeroVerwaltung.tsx`)
|
||
```tsx
|
||
src={bgPreview ?? bgUrl ?? '/hero.jpg'}
|
||
```
|
||
Das Fallback-Bild `/hero.jpg` muss im `public/`-Ordner liegen (oder durch eigenen Pfad ersetzen).
|
||
|
||
### 4. Öffentliche Website-Sektionen einbinden
|
||
Die Galerie-Anzeige-Komponente auf der Hauptseite:
|
||
```tsx
|
||
// In app/page.tsx (async Server Component):
|
||
import { GalerieAnzeige } from '@/components/GalerieAnzeige'
|
||
|
||
const { data: galerie } = await supabase.from('galerie_bilder').select('*').order('reihenfolge')
|
||
const bilder = (galerie ?? []).map(b => ({
|
||
id: b.id,
|
||
src: `${supabaseUrl}/storage/v1/object/public/galerie-bilder/${b.storage_path}`,
|
||
alt: b.alt_text,
|
||
}))
|
||
|
||
<GalerieAnzeige bilder={bilder} />
|
||
```
|
||
|
||
### 5. Admin-Navigation ergänzen
|
||
```tsx
|
||
<a href="/admin/hero">Hero</a>
|
||
<a href="/admin/ueber-uns">Über uns</a>
|
||
<a href="/admin/galerie">Galerie</a>
|
||
<a href="/admin/kontakt">Kontakt</a>
|
||
<a href="/admin/passwort">Passwort</a>
|
||
```
|
||
|
||
---
|
||
|
||
## Anpassungspunkte
|
||
|
||
| Was | Wo |
|
||
|---|---|
|
||
| Firmenname / Standardtexte | `components/admin/HeroVerwaltung.tsx` → `DEFAULTS` + `api/admin/hero/route.ts` |
|
||
| Fallback-Hero-Bild | `components/admin/HeroVerwaltung.tsx` → `bgPreview ?? bgUrl ?? '/hero.jpg'` |
|
||
| Galerie-Flip-Geschwindigkeit | `components/GalerieAnzeige.tsx` → `INTERVAL_MS` (Standard: 1500 ms) |
|
||
| Galerie-Grid-Layout | `components/GalerieAnzeige.tsx` → `VISIBLE`, `ROW_H`, `numGroups`-Berechnung |
|
||
| Bucket-Namen | Alle `app/api/admin/*/route.ts` → `const BUCKET = '...'` |
|
||
| Max. Upload-Größe | `app/api/admin/galerie/route.ts` → `MAX_SIZE` |
|
||
|
||
---
|
||
|
||
## Integrations-Prompt
|
||
|
||
Kopiere diesen Prompt in eine neue KI-Konversation, nachdem du die `files/` in dein Projekt kopiert und die SQL-Migration ausgeführt hast. Ersetze alle `[PLATZHALTER]`.
|
||
|
||
```
|
||
Ich integriere das Website-CMS-Modul (Hero, Über uns, Galerie, Kontakt, Passwort) in mein Next.js/Supabase-Projekt.
|
||
|
||
PROJEKT-KONTEXT:
|
||
- Firmenname: [FIRMENNAME]
|
||
- Tagline (optional): [TAGLINE oder leer lassen]
|
||
- Hero-Eyebrow-Text: [z. B. "Ihr Ansprechpartner seit 2010"]
|
||
- Hero-Überschrift 1 (weiß): [z. B. "Wir lieben Qualität."]
|
||
- Hero-Überschrift 2 (akzentfarbe): [z. B. "Ihre Kunden auch."]
|
||
- Modul 02 (Admin-Auth) ist bereits integriert (requireAdmin + getAdminSession verfügbar)
|
||
- Tabelle `admins` mit password_hash-Spalte existiert bereits
|
||
|
||
BEREITS KOPIERTE DATEIEN (aus modules/06-website-cms/files/):
|
||
Wichtig: Ordner "id/" umbenennen zu "[id]" — betrifft:
|
||
- app/api/admin/hero/badges/id/ → [id]/
|
||
- app/api/admin/ueber-uns/stats/id/ → [id]/
|
||
- app/api/admin/kontakt/oeffnungszeiten/id/ → [id]/
|
||
- app/api/admin/kontakt/social/id/ → [id]/
|
||
|
||
AUFGABEN – führe sie der Reihe nach aus:
|
||
|
||
1. SQL-MIGRATION: Führe modules/06-website-cms/migrations/MIGRATIONS_WEBSITE_CMS.sql
|
||
im Supabase SQL-Editor aus (enthält alle Tabellen + RLS-Policies).
|
||
|
||
2. SUPABASE STORAGE BUCKETS anlegen (alle Public):
|
||
- hero-bilder
|
||
- site-assets
|
||
- ueber-uns-bilder
|
||
- galerie-bilder
|
||
|
||
3. BCRYPTJS installieren (für Passwort-Änderung):
|
||
npm install bcryptjs && npm install -D @types/bcryptjs
|
||
|
||
4. SUPABASE-TYPEN: Lies lib/supabase.ts.
|
||
Ergänze in Database.public.Tables die Typen für:
|
||
hero_content, hero_badges, ueber_uns_content, ueber_uns_stats,
|
||
galerie_bilder, kontakt_info, kontakt_oeffnungszeiten, kontakt_social
|
||
(Typen aus modules/06-website-cms/TEMPLATE.md kopieren)
|
||
|
||
5. STANDARDWERTE anpassen:
|
||
a) Lies components/admin/HeroVerwaltung.tsx.
|
||
Setze in DEFAULTS: site_name: '[FIRMENNAME]', site_tagline: '[TAGLINE]',
|
||
eyebrow_text: '[EYEBROW]', headline1: '[H1]', headline2: '[H2]'
|
||
b) Lies app/api/admin/hero/route.ts.
|
||
Setze dieselben Werte als Fallback in den PATCH-Handler-fields.
|
||
|
||
6. FALLBACK-BILD: Lege eine Datei hero.jpg (oder eigenen Namen) in public/ ab.
|
||
Lies components/admin/HeroVerwaltung.tsx und ersetze '/hero.jpg' durch deinen Pfad.
|
||
|
||
7. ÖFFENTLICHE SEITE einbinden:
|
||
a) Lies app/page.tsx (oder deine Hauptseite).
|
||
Importiere und verwende die Sektionskomponenten (Hero, UeberUns, GalerieAnzeige).
|
||
Für GalerieAnzeige: Daten aus galerie_bilder laden, Storage-URLs konstruieren,
|
||
als `bilder: GalerieBild[]`-Array übergeben.
|
||
b) Kontaktsektion mit Formular → Modul 07 einbinden.
|
||
|
||
8. ADMIN-NAVIGATION ergänzen:
|
||
Lies app/admin/AdminNav.tsx (oder deine Admin-Nav).
|
||
Füge Links zu /admin/hero, /admin/ueber-uns, /admin/galerie, /admin/kontakt, /admin/passwort hinzu.
|
||
Import-Icons: Home, Info, Grid2x2, Phone, KeyRound (aus lucide-react).
|
||
|
||
9. TEST:
|
||
a) /admin/hero → Seitenname + Texte + Hintergrundbild + Logo + Favicon speichern
|
||
b) /admin/ueber-uns → Texte + Statistik hinzufügen
|
||
c) /admin/galerie → Bild hochladen, Reihenfolge ändern
|
||
d) /admin/kontakt → Öffnungszeiten + Social-Links anlegen, Formular-Empfänger eintragen
|
||
e) /admin/passwort → Passwort ändern (aktuelles Passwort + neues Passwort)
|
||
f) Öffentliche Hauptseite → alle Sektionen zeigen Daten aus der DB
|
||
|
||
Lies jede Datei vor dem Bearbeiten. Melde wenn alle Schritte abgeschlossen sind.
|
||
```
|