mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
Merge branch 'develop'
This commit is contained in:
commit
0f29fd3ced
9 changed files with 5531 additions and 18 deletions
417
docs/c2s/C2S_MIGRATION_ANALYSIS.md
Normal file
417
docs/c2s/C2S_MIGRATION_ANALYSIS.md
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
# complexcaresolutions.de → Payload CMS Migration Analysis
|
||||||
|
|
||||||
|
**Source:** WordPress (Divi Builder) on Hetzner 1
|
||||||
|
**Target:** Payload CMS Tenant ID 10
|
||||||
|
**Date:** 2026-02-28
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Site Overview
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Domain | complexcaresolutions.de |
|
||||||
|
| WordPress URL | https://www.complexcaresolutions.de |
|
||||||
|
| Theme | Divi + Divi-c2s-child |
|
||||||
|
| Brand Colors | Navy `#004166`, Gold `#B3AF09` |
|
||||||
|
| Phone | 0800 80 44 100 |
|
||||||
|
| Fax | 0800 80 44 190 |
|
||||||
|
| Email | kontakt@complexcaresolutions.de |
|
||||||
|
| Company | complex care solutions GmbH |
|
||||||
|
| Address | Hans-Böckler-Str. 19, 46236 Bottrop |
|
||||||
|
| Geschäftsführer | Martin Porwoll |
|
||||||
|
| HRB | 15753 (AG Gelsenkirchen) |
|
||||||
|
| USt-IdNr | DE334815479 |
|
||||||
|
| Social Media | LinkedIn, Facebook, Instagram, Twitter, YouTube |
|
||||||
|
| Datenschutz | alfright.eu (key: `f60250224d4a459a90dbeeb289cd47f9`, tenant: `alfright_schutzteam`) |
|
||||||
|
| Media Files | 55 images/uploads |
|
||||||
|
| Status | WordPress returning HTTP 500 (broken) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Navigation Structure
|
||||||
|
|
||||||
|
### Hauptmenü (7 items)
|
||||||
|
| Order | Label | Slug | Parent |
|
||||||
|
|-------|-------|------|--------|
|
||||||
|
| 1 | Patienten | patienten | — |
|
||||||
|
| 2 | Zweitmeinung | zweitmeinung | Patienten (submenu) |
|
||||||
|
| 3 | Ärzte | aerzte | — |
|
||||||
|
| 4 | Versicherungen | versicherungen | — |
|
||||||
|
| 5 | Über uns | ueber-uns | — |
|
||||||
|
| 6 | Motivation | motivation | — |
|
||||||
|
| 7 | Kontakt | kontakt | — |
|
||||||
|
|
||||||
|
### Footer Menu (2 items)
|
||||||
|
| Order | Label | Slug |
|
||||||
|
|-------|-------|------|
|
||||||
|
| 1 | Datenschutz | datenschutzerklaerung |
|
||||||
|
| 2 | Impressum | impressum |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Page Inventory & Content Mapping
|
||||||
|
|
||||||
|
### Pages TO MIGRATE (10 substantial pages)
|
||||||
|
|
||||||
|
#### 3.1 Startseite (ID: 11, slug: `startseite`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Willkommen bei complex care solutions" + subline about healthcare company
|
||||||
|
2. **Image-Text pairs** — Schilddrüsen-Zweitmeinung + Gallenblasen-Zweitmeinung CTAs
|
||||||
|
3. **Card Grid** — "Die besten Lösungen" (3 cards: Expertenmeinungen, Spezialisierte Unterstützung, Smarte Technologie)
|
||||||
|
4. **Text Section** — "Wir schaffen Mehrwert" (3 paragraphs about quality, cost control, quality assurance)
|
||||||
|
5. **Quote/Text** — "Unser Versprechen" (company values statement)
|
||||||
|
6. **Contact Footer** — Phone, email, social media, contact form
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block` (headline, subline, CTA)
|
||||||
|
- `image-text-block` × 2 (Schilddrüsen + Gallenblasen sections)
|
||||||
|
- `card-grid-block` (3 cards: "Die besten Lösungen")
|
||||||
|
- `text-block` ("Wir schaffen Mehrwert")
|
||||||
|
- `quote-block` ("Unser Versprechen")
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2 Patienten (ID: 987511082, slug: `patienten`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Die beste Versorgung für unsere Patienten"
|
||||||
|
2. **Text** — "Umfassende medizinische Lösungen" (overview)
|
||||||
|
3. **Card Grid** — 3 service cards (Zweitmeinung, Intensivpflegeberatung, Medikationsanalyse) with descriptions
|
||||||
|
4. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `text-block` (intro)
|
||||||
|
- `card-grid-block` (3 service cards linking to subpages)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.3 Zweitmeinung (ID: 32, slug: `zweitmeinung`, LARGEST PAGE 48KB)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Zweitmeinung rettet Leben" + CTA
|
||||||
|
2. **Section: Kardiologie** — Headline + description + image + 2 blurbs (Qualifizierte Kardiologen, Schnelle Zweitmeinungen)
|
||||||
|
3. **Section: Onkologie** — Headline + description + image + 2 blurbs
|
||||||
|
4. **Section: Chirurgie** — Headline + description + image
|
||||||
|
5. **Section: Intensivmedizin** — Headline + description + image + 2 blurbs
|
||||||
|
6. **Section: Vorteile** — "Eine zweite Meinung für Ihre Gesundheit" + 3 benefit blurbs
|
||||||
|
7. **CTA** — "Holen Sie sich eine Zweitmeinung"
|
||||||
|
8. **Testimonials** — 3 patient testimonials (Basalzellkarzinom, Radiologie, Herzstolpern)
|
||||||
|
9. **Contact Footer** + integrated Divi contact form
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `image-text-block` × 4 (Kardiologie, Onkologie, Chirurgie, Intensivmedizin) — each with sub-features
|
||||||
|
- `card-grid-block` (3 benefit cards)
|
||||||
|
- `cta-block`
|
||||||
|
- `testimonials-block` (3 testimonials)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
**Testimonials to seed (Collection):**
|
||||||
|
1. "Nach einer Gewebeprobe wurde ein Basalzellkarzinom diagnostiziert..." — anonymous patient
|
||||||
|
2. "Die radiologische Zweitmeinung, die ich erhielt, war ein echter Lebensretter!..." — anonymous patient
|
||||||
|
3. "Seit einiger Zeit bemerkte ich ein Herzstolpern in meiner Brust..." — anonymous patient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.4 Ärzte (ID: 20, slug: `aerzte`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Kompetente Unterstützung für Sie und Ihre Patienten" (subtitle: "Für Ärzte")
|
||||||
|
2. **Text** — "Die beste Unterstützung für Ihre Patienten" (intro question)
|
||||||
|
3. **Card Grid** — 6 feature cards (Qualitätsdiagnose, Lokale Kapazitäten, Leitlinien, Expertennetzwerk, Über-/Fehlversorgung minimieren, Schnelle Prozesse + Transparenz, Beitrag zum Patientenwohl)
|
||||||
|
4. **CTA** — "Werden Sie Teil unseres Expertennetzwerks!"
|
||||||
|
5. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block` (with "Für Ärzte" subtitle)
|
||||||
|
- `text-block` (intro)
|
||||||
|
- `card-grid-block` (features)
|
||||||
|
- `cta-block`
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.5 Versicherungen (ID: 22, slug: `versicherungen`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Für höchste Versorgungsqualität, intelligent und wirtschaftlich" (subtitle: "Für Krankenkassen")
|
||||||
|
2. **Text** — "Hochwertige Lösungen für Ihre Versicherten"
|
||||||
|
3. **Card Grid** — 3 service areas (Datenbasierte Forschung + §25b SGB V, Innovationen §68 SGB V, Strategie)
|
||||||
|
4. **Card Grid** — "Ihre Vorteile" (3 benefit cards: Kostenersparnis, Bessere Versorgung, Wettbewerbsvorteil)
|
||||||
|
5. **Image-Text: Data Science** — "Data Science und KI intelligent nutzen" + 3 blurbs
|
||||||
|
6. **Image-Text: Expertise** — "Spezialisierte Expertise" + description
|
||||||
|
7. **Image-Text: Strategie** — "Strategie- und Organisationsentwicklung" + 2 blurbs
|
||||||
|
8. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `text-block` (intro)
|
||||||
|
- `card-grid-block` (service areas)
|
||||||
|
- `card-grid-block` (Vorteile)
|
||||||
|
- `image-text-block` (Data Science)
|
||||||
|
- `image-text-block` (Expertise)
|
||||||
|
- `image-text-block` (Strategie)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.6 Über uns (ID: 28, slug: `ueber-uns`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Gesundheit in den besten Händen" + company description
|
||||||
|
2. **Text** — "Die Geschichte hinter der Gründung" (Martin Porwoll / Whistleblower story)
|
||||||
|
3. **Card Grid** — "Woran wir glauben" + 3 paragraphs about values
|
||||||
|
4. **Text** — "Was wir mitbringen" + 3 competency items (Expertise, Netzwerk, Ethische Grundhaltung)
|
||||||
|
5. **Timeline** — Company history timeline (Divi custom CSS timeline)
|
||||||
|
6. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `text-block` (Gründung)
|
||||||
|
- `text-block` (Woran wir glauben)
|
||||||
|
- `card-grid-block` (Was wir mitbringen - 3 competency cards)
|
||||||
|
- `timeline-block` (company history) — NOTE: Timeline had extensive custom CSS in WordPress; may simplify to text or card layout
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.7 Motivation (ID: 30, slug: `motivation`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Patientenwohl im Mittelpunkt" (subtitle: "Unsere Motivation")
|
||||||
|
2. **Text** — "Unser Fokus" + 3 paragraphs (patientenzentriert, Transparenz, Expertennetzwerk)
|
||||||
|
3. **Text** — "Motivation und Geschichte" (Martin Porwoll / Zytoskandal founding story)
|
||||||
|
4. **Quote** — Martin Porwoll quote about Zytoskandal and his mission
|
||||||
|
5. **Text** — "Der Zytoskandal Bottrop" (detailed scandal description)
|
||||||
|
6. **Text** — "Martin Porwolls Weg zum Streiter für das Patientenwohl"
|
||||||
|
7. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `card-grid-block` (3 focus areas)
|
||||||
|
- `text-block` (Motivation und Geschichte)
|
||||||
|
- `quote-block` (Martin Porwoll quote)
|
||||||
|
- `text-block` (Zytoskandal)
|
||||||
|
- `text-block` (Porwolls Weg)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.8 Kontakt (ID: 26, slug: `kontakt`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Contact Section** — Phone, email, social media + Divi contact form (Name, Email, Betreff, Nachricht)
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `contact-form-block` (linked to form)
|
||||||
|
|
||||||
|
**Note:** The WordPress Kontakt page is essentially just the contact footer section that appears on every page. The form has 4 fields: Name, Email, Betreff (Subject), Nachricht (Message).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.9 Impressum (ID: 14, slug: `impressum`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **Hero** — "Impressum"
|
||||||
|
2. **Text** — Full legal text (company info, Handelsregister, Kontakt, USt-ID, EU-Streitschlichtung, Verbraucherschlichtung)
|
||||||
|
3. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block`
|
||||||
|
- `text-block` (legal text, width: narrow)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.10 Datenschutzerklärung (ID: 16, slug: `datenschutzerklaerung`)
|
||||||
|
**Content Structure:**
|
||||||
|
1. **alfright.eu widget** — Privacy policy generated dynamically
|
||||||
|
2. **Contact Footer**
|
||||||
|
|
||||||
|
**Payload Blocks:**
|
||||||
|
- `hero-block` (headline: "Datenschutzerklärung")
|
||||||
|
- `html-embed-block` (alfright.eu `<div>` widget with key `f60250224d4a459a90dbeeb289cd47f9`)
|
||||||
|
- `contact-form-block`
|
||||||
|
|
||||||
|
**alfright embed code:**
|
||||||
|
```html
|
||||||
|
<div data-alfidcl-type="dps" data-alfidcl-tenant="alfright_schutzteam" data-alfidcl-lang="de-de" data-alfidcl-key="f60250224d4a459a90dbeeb289cd47f9"></div>
|
||||||
|
<script src="https://app.alfright.eu/js/alcl/alcl.js" async></script>
|
||||||
|
```
|
||||||
|
**Note:** c2s uses the alfright.eu `<div>` widget (not iframe like zweitmeinu.ng). This requires both the div AND the script tag to be embedded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Pages TO SKIP (4 placeholder/empty pages)
|
||||||
|
|
||||||
|
| Page | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| Blog (ID: 24) | Empty — only 29 chars of content (empty Divi placeholder) |
|
||||||
|
| Medikationsanalyse (ID: 34) | Only contains contact footer section, no real content |
|
||||||
|
| Intensivpflegeberatung (ID: 38) | Only contains contact footer section, no real content |
|
||||||
|
| Klinik (ID: 987510888) | Lorem ipsum placeholder content |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Blog Post TO MIGRATE (1)
|
||||||
|
|
||||||
|
#### "Geheimnis der guten Fürsorge" (ID: 987511216)
|
||||||
|
**Slug:** `geheimnis-der-guten-fuersorge-warum-uebertherapie-ein-gesellschaftliches-problem-ist`
|
||||||
|
**Content:** Long-form article (~7800 chars) about Übertherapie (over-treatment) in healthcare
|
||||||
|
**Structure:** Pure text (Gutenberg paragraphs), no Divi shortcodes
|
||||||
|
**Category:** Blog/News
|
||||||
|
|
||||||
|
**Payload Collection:** `posts` (with tenant_id = 10)
|
||||||
|
**Content:** Rich text (Lexical paragraphs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Collections to Seed
|
||||||
|
|
||||||
|
### 4.1 Site Settings (1 record)
|
||||||
|
```
|
||||||
|
site_name: "complex care solutions GmbH"
|
||||||
|
site_url: "https://complexcaresolutions.de"
|
||||||
|
company_name: "complex care solutions GmbH"
|
||||||
|
address: "Hans-Böckler-Str. 19"
|
||||||
|
zip: "46236"
|
||||||
|
city: "Bottrop"
|
||||||
|
phone: "0800 80 44 100"
|
||||||
|
fax: "0800 80 44 190"
|
||||||
|
email: "kontakt@complexcaresolutions.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Social Links (2 confirmed + 3 placeholder)
|
||||||
|
| Platform | URL | Status |
|
||||||
|
|----------|-----|--------|
|
||||||
|
| LinkedIn | https://de.linkedin.com/company/complex-care-solutions-gmbh | Confirmed |
|
||||||
|
| Facebook | https://www.facebook.com/complex-care-solutions-GmbH | Confirmed |
|
||||||
|
| Instagram | — | Listed in nav but no URL set in WordPress |
|
||||||
|
| Twitter/X | — | Listed in nav but no URL set in WordPress |
|
||||||
|
| YouTube | — | Listed in nav but no URL set in WordPress |
|
||||||
|
|
||||||
|
**Seed only LinkedIn + Facebook** (the others have no URLs configured).
|
||||||
|
|
||||||
|
### 4.3 Navigation (1 record)
|
||||||
|
**mainMenu:**
|
||||||
|
1. Patienten → `/patienten`
|
||||||
|
- Zweitmeinung → `/zweitmeinung` (submenu)
|
||||||
|
2. Ärzte → `/aerzte`
|
||||||
|
3. Versicherungen → `/versicherungen`
|
||||||
|
4. Über uns → `/ueber-uns`
|
||||||
|
5. Motivation → `/motivation`
|
||||||
|
6. Kontakt → `/kontakt`
|
||||||
|
|
||||||
|
**footerMenu:**
|
||||||
|
1. Datenschutz → `/datenschutzerklaerung`
|
||||||
|
2. Impressum → `/impressum`
|
||||||
|
|
||||||
|
### 4.4 Contact Form (1 form, 4 fields)
|
||||||
|
| Field | Type | Required |
|
||||||
|
|-------|------|----------|
|
||||||
|
| Name | text | yes |
|
||||||
|
| Email | email | yes |
|
||||||
|
| Betreff | text | yes |
|
||||||
|
| Nachricht | textarea | yes |
|
||||||
|
|
||||||
|
Email notification → `kontakt@complexcaresolutions.de`
|
||||||
|
|
||||||
|
### 4.5 Testimonials (3 records)
|
||||||
|
1. Basalzellkarzinom patient — quote about finding right therapy
|
||||||
|
2. Radiology second opinion patient — "Lebensretter" quote
|
||||||
|
3. Heart patient — Herzstolpern/stress diagnosis
|
||||||
|
|
||||||
|
### 4.6 Pages (10 pages)
|
||||||
|
See Section 3 for full content mapping.
|
||||||
|
|
||||||
|
### 4.7 Posts (1 blog post)
|
||||||
|
"Geheimnis der guten Fürsorge – warum Übertherapie ein gesellschaftliches Problem ist"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Media Files to Migrate
|
||||||
|
|
||||||
|
55 total WordPress media files. Key images:
|
||||||
|
|
||||||
|
| Image | Used On |
|
||||||
|
|-------|---------|
|
||||||
|
| c2s_master_logo_400x130.png | Site logo |
|
||||||
|
| Favicon-c2s.png | Favicon |
|
||||||
|
| c2s_startseite.jpg | Homepage hero |
|
||||||
|
| c2s_startseite_2.jpg | Homepage section |
|
||||||
|
| c2s_startseite_loesungen.jpg | Homepage solutions |
|
||||||
|
| c2s_startseite_versprechen.jpg | Homepage promise |
|
||||||
|
| zweitmeinung.jpg | Zweitmeinung hero |
|
||||||
|
| zweitmeinung_desktop.jpg | Zweitmeinung wide |
|
||||||
|
| zweitmeinung_herz.jpg | Kardiologie section |
|
||||||
|
| zweitmeinung_onkologie.jpg | Onkologie section |
|
||||||
|
| zweitmeinung_chirurgie.jpg | Chirurgie section |
|
||||||
|
| zweitmeinung_intensiv.jpg | Intensivmedizin section |
|
||||||
|
| patienten.jpg | Patienten hero |
|
||||||
|
| aerzte.jpg | Ärzte hero |
|
||||||
|
| krankenversicherung.jpg | Versicherungen hero |
|
||||||
|
| medikationsanalyse.jpg | Medikationsanalyse hero |
|
||||||
|
| tbvs-image-person-1024x1024-1.png | Martin Porwoll portrait |
|
||||||
|
| tbvs-image-2560x1440-1.png | Hero background |
|
||||||
|
| post-4.jpg | Blog post image |
|
||||||
|
| Various AdobeStock_*.jpg | Section backgrounds |
|
||||||
|
|
||||||
|
**Strategy:** Upload to Payload media collection with `tenant_id: 10`. Download from WordPress `wp-content/uploads/` on Hetzner 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Key Differences from zweitmeinu.ng
|
||||||
|
|
||||||
|
| Aspect | zweitmeinu.ng (Tenant 12) | complexcaresolutions.de (Tenant 10) |
|
||||||
|
|--------|--------------------------|-------------------------------------|
|
||||||
|
| Pages | 9 seeded | 10 needed |
|
||||||
|
| Services | 6 (dedicated collection) | Embedded in page content (no service collection needed) |
|
||||||
|
| FAQs | 24 items | None |
|
||||||
|
| Blog Posts | 0 | 1 |
|
||||||
|
| Testimonials | 0 | 3 |
|
||||||
|
| Contact Form | 4 fields | 4 fields (same: Name, Email, Betreff, Nachricht) |
|
||||||
|
| Datenschutz | iframe (alfright.eu) | div widget (alfright.eu, different key) |
|
||||||
|
| Target Audience Pages | None | 3 (Patienten, Ärzte, Versicherungen) |
|
||||||
|
| Content Volume | Medium | Large (Zweitmeinung page alone is 48KB) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Seed Script (`scripts/seed-c2s.ts`)
|
||||||
|
1. Create site settings for tenant 10
|
||||||
|
2. Create 5 social links
|
||||||
|
3. Create navigation (mainMenu with Patienten submenu + footerMenu)
|
||||||
|
4. Create contact form (4 fields)
|
||||||
|
5. Create 3 testimonials
|
||||||
|
6. Create 10 pages with block-based layout
|
||||||
|
7. Create 1 blog post
|
||||||
|
|
||||||
|
### Phase 2: Media Migration
|
||||||
|
1. Download all 55 media files from WordPress on Hetzner 1
|
||||||
|
2. Upload to Payload via Local API
|
||||||
|
3. Link media IDs to pages/blocks (hero images, image-text sections)
|
||||||
|
|
||||||
|
### Phase 3: Frontend (complexcaresolutions.de)
|
||||||
|
- Either create dedicated Next.js frontend OR
|
||||||
|
- Reuse existing frontend framework (like zweitmeinu.ng's BlockRenderer pattern)
|
||||||
|
- Same design system: Navy #004166, Gold #B3AF09
|
||||||
|
|
||||||
|
### Phase 4: DNS/Hosting Cutover
|
||||||
|
- Replace WordPress on Hetzner 1 with Next.js frontend
|
||||||
|
- Point domain to new frontend
|
||||||
|
- Verify all pages, SEO, redirects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Existing Payload Data (Tenant 10)
|
||||||
|
|
||||||
|
| Collection | Count |
|
||||||
|
|------------|-------|
|
||||||
|
| Pages | 0 |
|
||||||
|
| Site Settings | 0 |
|
||||||
|
| Navigations | 0 |
|
||||||
|
| Services | 0 |
|
||||||
|
| Forms | 0 |
|
||||||
|
| Posts | 0 |
|
||||||
|
| FAQs | 0 |
|
||||||
|
| Social Links | 0 |
|
||||||
|
| Testimonials | 0 |
|
||||||
|
|
||||||
|
**Tenant 10 is completely empty — clean slate for seeding.**
|
||||||
1155
docs/plans/2026-03-01-telegram-media-bot.md
Normal file
1155
docs/plans/2026-03-01-telegram-media-bot.md
Normal file
File diff suppressed because it is too large
Load diff
477
docs/sensualmoments/farbschema.html
Normal file
477
docs/sensualmoments/farbschema.html
Normal file
|
|
@ -0,0 +1,477 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Finales Farbschema – Bordeaux-Dominant</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=Lato:wght@300;400;700&display=swap');
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
background: #131313;
|
||||||
|
color: #e8e4e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 900px; margin: 0 auto; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #a09890;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge span {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
padding: 5px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #8B3A4A;
|
||||||
|
color: #F8F4F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main palette strip */
|
||||||
|
.palette-strip {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100px;
|
||||||
|
box-shadow: 0 6px 30px rgba(0,0,0,0.5);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-swatch {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: flex 0.3s;
|
||||||
|
}
|
||||||
|
.strip-swatch:hover { flex: 1.6; }
|
||||||
|
|
||||||
|
.strip-label {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.strip-swatch:hover .strip-label { opacity: 1; }
|
||||||
|
|
||||||
|
/* Color cards */
|
||||||
|
.color-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-card {
|
||||||
|
background: #1c1c1e;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 1px solid #2a2a2c;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.color-card:hover { border-color: #444; }
|
||||||
|
|
||||||
|
.color-card .dot {
|
||||||
|
width: 44px; height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-card .info { flex: 1; }
|
||||||
|
|
||||||
|
.color-card .name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: #e0dcd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-card .role-tag {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-card .hex-val {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #D4A9A0;
|
||||||
|
background: rgba(212,169,160,0.08);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-card .usage {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Usage rules */
|
||||||
|
.rules-section {
|
||||||
|
background: #1c1c1e;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 32px;
|
||||||
|
border: 1px solid #2a2a2c;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-section h2 {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #D4A9A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2c;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.rule-row:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.rule-label {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-value { color: #ddd; line-height: 1.5; }
|
||||||
|
|
||||||
|
.swatch-inline {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px; height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 3px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contrast check */
|
||||||
|
.contrast-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card .combo-label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card .combo-sample {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card .combo-body {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS vars output */
|
||||||
|
.code-block {
|
||||||
|
background: #0d0d0f;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #bbb;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #2a2a2c;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block .comment { color: #666; }
|
||||||
|
.code-block .prop { color: #D4A9A0; }
|
||||||
|
.code-block .val { color: #8B3A4A; }
|
||||||
|
|
||||||
|
.copy-all-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border: none;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.copy-all-btn:hover { background: rgba(255,255,255,0.14); color: #fff; }
|
||||||
|
|
||||||
|
.copy-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
background: #444;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s, transform 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.copy-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.color-grid { grid-template-columns: 1fr; }
|
||||||
|
.contrast-grid { grid-template-columns: 1fr; }
|
||||||
|
.rule-row { grid-template-columns: 1fr; gap: 4px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Finales Farbschema</h1>
|
||||||
|
<p class="subtitle">Bordeaux-Dominant · Warm & Sinnlich</p>
|
||||||
|
<div class="badge"><span>Freigegeben</span></div>
|
||||||
|
|
||||||
|
<!-- Strip -->
|
||||||
|
<div class="palette-strip">
|
||||||
|
<div class="strip-swatch" style="background:#2A1520" onclick="copy('#2A1520')"><span class="strip-label" style="color:#D4A9A0">Dark Wine<br>#2A1520</span></div>
|
||||||
|
<div class="strip-swatch" style="background:#8B3A4A" onclick="copy('#8B3A4A')"><span class="strip-label" style="color:#F8F4F0">Bordeaux<br>#8B3A4A</span></div>
|
||||||
|
<div class="strip-swatch" style="background:#D4A9A0" onclick="copy('#D4A9A0')"><span class="strip-label" style="color:#2A1520">Blush Nude<br>#D4A9A0</span></div>
|
||||||
|
<div class="strip-swatch" style="background:#151B2B" onclick="copy('#151B2B')"><span class="strip-label" style="color:#D4A9A0">Deep Navy<br>#151B2B</span></div>
|
||||||
|
<div class="strip-swatch" style="background:#F8F4F0" onclick="copy('#F8F4F0')"><span class="strip-label" style="color:#3D2F30">Crème<br>#F8F4F0</span></div>
|
||||||
|
<div class="strip-swatch" style="background:#3D2F30" onclick="copy('#3D2F30')"><span class="strip-label" style="color:#D4A9A0">Espresso<br>#3D2F30</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Color cards -->
|
||||||
|
<div class="color-grid">
|
||||||
|
<div class="color-card" onclick="copy('#2A1520')">
|
||||||
|
<div class="dot" style="background:#2A1520"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Dark Wine</div>
|
||||||
|
<div class="role-tag">Basis · Haupthintergrund</div>
|
||||||
|
<div class="hex-val">#2A1520</div>
|
||||||
|
<div class="usage">Hero, Header, dunkle Sektionen, Haupthintergrund der Seite</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-card" onclick="copy('#D4A9A0')">
|
||||||
|
<div class="dot" style="background:#D4A9A0"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Blush Nude</div>
|
||||||
|
<div class="role-tag">Akzent 1 · Interaktion</div>
|
||||||
|
<div class="hex-val">#D4A9A0</div>
|
||||||
|
<div class="usage">Buttons, Hover-Effekte, Links, Icons, aktive Elemente</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-card" onclick="copy('#8B3A4A')">
|
||||||
|
<div class="dot" style="background:#8B3A4A"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Bordeaux</div>
|
||||||
|
<div class="role-tag">Akzent 2 · Headlines auf Hell</div>
|
||||||
|
<div class="hex-val">#8B3A4A</div>
|
||||||
|
<div class="usage">Überschriften auf hellen Flächen, Trennlinien, sekundäre Buttons</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-card" onclick="copy('#151B2B')">
|
||||||
|
<div class="dot" style="background:#151B2B"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Deep Navy</div>
|
||||||
|
<div class="role-tag">Kontrast · Sektionswechsel</div>
|
||||||
|
<div class="hex-val">#151B2B</div>
|
||||||
|
<div class="usage">Testimonial-Bereich, Footer, Sektionswechsel für Tiefe</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-card" onclick="copy('#F8F4F0')">
|
||||||
|
<div class="dot" style="background:#F8F4F0"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Crème</div>
|
||||||
|
<div class="role-tag">Neutral · Helle Flächen</div>
|
||||||
|
<div class="hex-val">#F8F4F0</div>
|
||||||
|
<div class="usage">Textblöcke, Formulare, helle Sektionen, Kartenhintrergründe</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-card" onclick="copy('#3D2F30')">
|
||||||
|
<div class="dot" style="background:#3D2F30"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">Espresso</div>
|
||||||
|
<div class="role-tag">Text · Fließtext auf Hell</div>
|
||||||
|
<div class="hex-val">#3D2F30</div>
|
||||||
|
<div class="usage">Fließtext auf Crème-Hintergrund (statt reinem Schwarz)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contrast samples -->
|
||||||
|
<div class="contrast-grid">
|
||||||
|
<div class="contrast-card" style="background:#2A1520; color:#F8F4F0">
|
||||||
|
<div class="combo-label" style="color:#D4A9A0">Hero / Dunkle Sektionen</div>
|
||||||
|
<div class="combo-sample" style="color:#F8F4F0">Headline auf Dark Wine</div>
|
||||||
|
<div class="combo-body">Fließtext in Crème auf dem Haupthintergrund der Website.</div>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-card" style="background:#F8F4F0; color:#3D2F30">
|
||||||
|
<div class="combo-label" style="color:#8B3A4A">Helle Sektionen</div>
|
||||||
|
<div class="combo-sample" style="color:#8B3A4A">Headline in Bordeaux</div>
|
||||||
|
<div class="combo-body">Espresso-Fließtext auf Crème-Hintergrund – warm und gut lesbar.</div>
|
||||||
|
</div>
|
||||||
|
<div class="contrast-card" style="background:#151B2B; color:#F8F4F0">
|
||||||
|
<div class="combo-label" style="color:#D4A9A0">Navy-Kontrast</div>
|
||||||
|
<div class="combo-sample" style="color:#D4A9A0">Blush auf Navy</div>
|
||||||
|
<div class="combo-body">Testimonials und Footer – kühler Gegenpol zu den warmen Tönen.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Usage rules -->
|
||||||
|
<div class="rules-section">
|
||||||
|
<h2>Anwendungsregeln</h2>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Hintergrund</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#2A1520"></span> Dark Wine als Standard, <span class="swatch-inline" style="background:#151B2B"></span> Navy für Sektionswechsel</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Buttons primär</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#D4A9A0"></span> Blush Nude mit <span class="swatch-inline" style="background:#2A1520"></span> Dark Wine Text</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Buttons sekundär</div>
|
||||||
|
<div class="rule-value">Outline in <span class="swatch-inline" style="background:#D4A9A0"></span> Blush oder <span class="swatch-inline" style="background:#8B3A4A"></span> Bordeaux</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Headlines dunkel</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#F8F4F0"></span> Crème auf dunklem Grund</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Headlines hell</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#8B3A4A"></span> Bordeaux auf <span class="swatch-inline" style="background:#F8F4F0"></span> Crème</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Fließtext dunkel</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#F8F4F0"></span> Crème auf dunklem Grund (Opacity 80–85 %)</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Fließtext hell</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#3D2F30"></span> Espresso auf <span class="swatch-inline" style="background:#F8F4F0"></span> Crème</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Links / Hover</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#D4A9A0"></span> Blush Nude, Hover: leicht aufgehellt</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Trennlinien</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#8B3A4A"></span> Bordeaux mit 20–30 % Opacity</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-row">
|
||||||
|
<div class="rule-label">Formulare</div>
|
||||||
|
<div class="rule-value"><span class="swatch-inline" style="background:#F8F4F0"></span> Crème-Hintergrund, <span class="swatch-inline" style="background:#3D2F30"></span> Espresso-Text, <span class="swatch-inline" style="background:#D4A9A0"></span> Blush-Fokusrand</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CSS Variables -->
|
||||||
|
<div class="code-block">
|
||||||
|
<button class="copy-all-btn" onclick="copyCSS()">CSS kopieren</button>
|
||||||
|
<span class="comment">/* Farbschema – Bordeaux-Dominant · Boudoir Website */</span>
|
||||||
|
:root {
|
||||||
|
<span class="prop">--color-base</span>: <span class="val">#2A1520</span>; <span class="comment">/* Dark Wine – Haupthintergrund */</span>
|
||||||
|
<span class="prop">--color-accent-1</span>: <span class="val">#D4A9A0</span>; <span class="comment">/* Blush Nude – Buttons, Links, Hover */</span>
|
||||||
|
<span class="prop">--color-accent-2</span>: <span class="val">#8B3A4A</span>; <span class="comment">/* Bordeaux – Headlines auf Hell */</span>
|
||||||
|
<span class="prop">--color-contrast</span>: <span class="val">#151B2B</span>; <span class="comment">/* Deep Navy – Sektionswechsel */</span>
|
||||||
|
<span class="prop">--color-neutral</span>: <span class="val">#F8F4F0</span>; <span class="comment">/* Crème – Helle Flächen */</span>
|
||||||
|
<span class="prop">--color-text</span>: <span class="val">#3D2F30</span>; <span class="comment">/* Espresso – Fließtext auf Hell */</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="copy-toast" id="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copy(hex) {
|
||||||
|
navigator.clipboard.writeText(hex);
|
||||||
|
const t = document.getElementById('toast');
|
||||||
|
t.textContent = hex + ' kopiert!';
|
||||||
|
t.classList.add('show');
|
||||||
|
setTimeout(() => t.classList.remove('show'), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCSS() {
|
||||||
|
const css = `/* Farbschema – Bordeaux-Dominant · Boudoir Website */
|
||||||
|
:root {
|
||||||
|
--color-base: #2A1520; /* Dark Wine – Haupthintergrund */
|
||||||
|
--color-accent-1: #D4A9A0; /* Blush Nude – Buttons, Links, Hover */
|
||||||
|
--color-accent-2: #8B3A4A; /* Bordeaux – Headlines auf Hell */
|
||||||
|
--color-contrast: #151B2B; /* Deep Navy – Sektionswechsel */
|
||||||
|
--color-neutral: #F8F4F0; /* Crème – Helle Flächen */
|
||||||
|
--color-text: #3D2F30; /* Espresso – Fließtext auf Hell */
|
||||||
|
}`;
|
||||||
|
navigator.clipboard.writeText(css);
|
||||||
|
const t = document.getElementById('toast');
|
||||||
|
t.textContent = 'CSS Variables kopiert!';
|
||||||
|
t.classList.add('show');
|
||||||
|
setTimeout(() => t.classList.remove('show'), 1500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
928
docs/sensualmoments/prototype.html
Normal file
928
docs/sensualmoments/prototype.html
Normal file
|
|
@ -0,0 +1,928 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sensual Moment Photography</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Josefin+Sans:wght@200;300;400&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--dark-wine: #2A1520;
|
||||||
|
--blush: #D4A9A0;
|
||||||
|
--bordeaux: #8B3A4A;
|
||||||
|
--navy: #151B2B;
|
||||||
|
--creme: #F8F4F0;
|
||||||
|
--espresso: #3D2F30;
|
||||||
|
--blush-soft: rgba(212,169,160,0.15);
|
||||||
|
--blush-medium: rgba(212,169,160,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
color: var(--espresso);
|
||||||
|
background: var(--creme);
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── NAV ─── */
|
||||||
|
.nav {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||||
|
padding: 24px 48px;
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.nav.scrolled {
|
||||||
|
background: rgba(42,21,32,0.95);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
padding: 16px 48px;
|
||||||
|
box-shadow: 0 2px 40px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.nav-logo {
|
||||||
|
display: flex; flex-direction: column; align-items: flex-start;
|
||||||
|
text-decoration: none; color: var(--blush);
|
||||||
|
}
|
||||||
|
.nav-logo .logo-main {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.6rem; font-weight: 400;
|
||||||
|
letter-spacing: 0.02em; line-height: 1;
|
||||||
|
}
|
||||||
|
.nav-logo .logo-sub {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-style: italic; font-size: 0.95rem;
|
||||||
|
font-weight: 400; margin-top: -2px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
.nav-logo .logo-tag {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.55rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.35em; text-transform: uppercase;
|
||||||
|
margin-top: 4px; align-self: flex-end;
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
display: flex; gap: 36px; list-style: none;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.72rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.18em; text-transform: uppercase;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
position: relative; transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.nav-links a::after {
|
||||||
|
content: ''; position: absolute;
|
||||||
|
bottom: -4px; left: 0; width: 0; height: 1px;
|
||||||
|
background: var(--blush); transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.nav-links a:hover::after { width: 100%; }
|
||||||
|
.nav-links a:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
/* ─── HERO ─── */
|
||||||
|
.hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--dark-wine);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 20% 50%, rgba(139,58,74,0.15) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse at 80% 30%, rgba(212,169,160,0.08) 0%, transparent 50%);
|
||||||
|
}
|
||||||
|
.hero-content {
|
||||||
|
text-align: center; position: relative; z-index: 2;
|
||||||
|
padding: 0 24px;
|
||||||
|
animation: fadeUp 1.2s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(40px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.hero-ornament {
|
||||||
|
width: 60px; height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--blush), transparent);
|
||||||
|
margin: 0 auto 32px;
|
||||||
|
}
|
||||||
|
.hero-title {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: clamp(3.5rem, 8vw, 7rem);
|
||||||
|
font-weight: 400; color: var(--blush);
|
||||||
|
line-height: 0.95; margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.hero-title em {
|
||||||
|
font-style: italic; font-weight: 400;
|
||||||
|
font-size: 0.65em; display: block;
|
||||||
|
text-align: right; margin-top: -8px;
|
||||||
|
margin-right: 5%;
|
||||||
|
}
|
||||||
|
.hero-tagline {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.7rem; font-weight: 200;
|
||||||
|
letter-spacing: 0.4em; text-transform: uppercase;
|
||||||
|
color: var(--blush); opacity: 0.6;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.hero-cta {
|
||||||
|
display: inline-block; margin-top: 48px;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.68rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.25em; text-transform: uppercase;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
padding: 16px 48px;
|
||||||
|
border: 1px solid rgba(212,169,160,0.3);
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
.hero-cta:hover {
|
||||||
|
background: rgba(212,169,160,0.1);
|
||||||
|
border-color: var(--blush);
|
||||||
|
}
|
||||||
|
.scroll-indicator {
|
||||||
|
position: absolute; bottom: 40px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
gap: 8px; color: var(--blush); opacity: 0.4;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.4; transform: translateX(-50%) translateY(0); }
|
||||||
|
50% { opacity: 0.7; transform: translateX(-50%) translateY(6px); }
|
||||||
|
}
|
||||||
|
.scroll-indicator span {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.55rem; letter-spacing: 0.2em; text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.scroll-indicator svg { width: 16px; height: 16px; }
|
||||||
|
|
||||||
|
/* ─── SECTION COMMON ─── */
|
||||||
|
.section {
|
||||||
|
padding: 120px 48px;
|
||||||
|
}
|
||||||
|
.section-label {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.6rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.4em; text-transform: uppercase;
|
||||||
|
margin-bottom: 16px; opacity: 0.5;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: clamp(2rem, 4vw, 3.2rem);
|
||||||
|
font-weight: 400; line-height: 1.15;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.section-title em { font-style: italic; }
|
||||||
|
.section-divider {
|
||||||
|
width: 48px; height: 1px;
|
||||||
|
background: var(--blush); margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── INTRO / ÜBER MICH ─── */
|
||||||
|
.intro {
|
||||||
|
background: var(--creme);
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 80px; align-items: center;
|
||||||
|
max-width: 1280px; margin: 0 auto;
|
||||||
|
}
|
||||||
|
.intro-image {
|
||||||
|
aspect-ratio: 3/4;
|
||||||
|
background: var(--blush-soft);
|
||||||
|
border-radius: 0 120px 0 0;
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.intro-image::after {
|
||||||
|
content: 'Dein Foto hier';
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.7rem; letter-spacing: 0.15em;
|
||||||
|
color: var(--bordeaux); opacity: 0.3;
|
||||||
|
}
|
||||||
|
.intro-text p {
|
||||||
|
font-size: 1.2rem; line-height: 1.8;
|
||||||
|
color: var(--espresso); margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.intro-signature {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-style: italic; font-size: 1.4rem;
|
||||||
|
color: var(--bordeaux); margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── GALERIE ─── */
|
||||||
|
.galerie-section {
|
||||||
|
background: var(--dark-wine);
|
||||||
|
color: var(--blush);
|
||||||
|
padding: 120px 0;
|
||||||
|
}
|
||||||
|
.galerie-header {
|
||||||
|
text-align: center; padding: 0 48px;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
.galerie-header .section-label { color: var(--blush); }
|
||||||
|
.galerie-header .section-title { color: var(--blush); }
|
||||||
|
.galerie-header .section-divider {
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.galerie-header p {
|
||||||
|
font-size: 1.1rem; font-weight: 300;
|
||||||
|
max-width: 560px; margin: 0 auto;
|
||||||
|
line-height: 1.7; opacity: 0.7;
|
||||||
|
}
|
||||||
|
.galerie-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.galerie-item {
|
||||||
|
aspect-ratio: 3/4;
|
||||||
|
background: rgba(212,169,160,0.08);
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
cursor: pointer; transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
.galerie-item:nth-child(3) { grid-row: span 2; }
|
||||||
|
.galerie-item:nth-child(6) { grid-column: span 2; aspect-ratio: 6/4; }
|
||||||
|
.galerie-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(180deg, transparent 50%, rgba(42,21,32,0.6));
|
||||||
|
opacity: 0; transition: opacity 0.4s;
|
||||||
|
}
|
||||||
|
.galerie-item:hover::after { opacity: 1; }
|
||||||
|
.galerie-item-label {
|
||||||
|
position: absolute; bottom: 20px; left: 20px;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.6rem; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; color: var(--blush);
|
||||||
|
z-index: 2; opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.4s ease 0.1s;
|
||||||
|
}
|
||||||
|
.galerie-item:hover .galerie-item-label {
|
||||||
|
opacity: 1; transform: translateY(0);
|
||||||
|
}
|
||||||
|
.galerie-more {
|
||||||
|
text-align: center; margin-top: 48px;
|
||||||
|
}
|
||||||
|
.galerie-more a {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.68rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.25em; text-transform: uppercase;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
padding: 16px 48px;
|
||||||
|
border: 1px solid rgba(212,169,160,0.25);
|
||||||
|
transition: all 0.4s;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.galerie-more a:hover {
|
||||||
|
background: rgba(212,169,160,0.1);
|
||||||
|
border-color: var(--blush);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── ERFAHRUNG / TESTIMONIALS ─── */
|
||||||
|
.testimonials-section {
|
||||||
|
background: var(--creme);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.testimonials-section .section-divider { margin: 24px auto; }
|
||||||
|
.testimonial-cards {
|
||||||
|
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 40px; max-width: 1100px; margin: 48px auto 0;
|
||||||
|
}
|
||||||
|
.testimonial-card {
|
||||||
|
padding: 40px 32px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid rgba(212,169,160,0.2);
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.4s;
|
||||||
|
}
|
||||||
|
.testimonial-card:hover {
|
||||||
|
box-shadow: 0 20px 60px rgba(42,21,32,0.08);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
.testimonial-quote {
|
||||||
|
font-size: 2rem; color: var(--blush);
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
line-height: 1; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.testimonial-text {
|
||||||
|
font-size: 1.05rem; line-height: 1.75;
|
||||||
|
font-weight: 300; font-style: italic;
|
||||||
|
color: var(--espresso); margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.testimonial-author {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.65rem; letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase; color: var(--bordeaux);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── PAKETE / PREISE ─── */
|
||||||
|
.pakete-section {
|
||||||
|
background: var(--navy);
|
||||||
|
color: var(--creme);
|
||||||
|
}
|
||||||
|
.pakete-section .section-label { color: var(--blush); opacity: 0.5; }
|
||||||
|
.pakete-section .section-title { color: var(--blush); }
|
||||||
|
.pakete-section .section-divider { margin: 24px auto; }
|
||||||
|
.pakete-header { text-align: center; margin-bottom: 64px; }
|
||||||
|
.pakete-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px; max-width: 1100px; margin: 0 auto;
|
||||||
|
}
|
||||||
|
.paket-card {
|
||||||
|
padding: 48px 36px;
|
||||||
|
border: 1px solid rgba(212,169,160,0.12);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.4s;
|
||||||
|
}
|
||||||
|
.paket-card.featured {
|
||||||
|
border-color: var(--blush);
|
||||||
|
background: rgba(212,169,160,0.05);
|
||||||
|
}
|
||||||
|
.paket-card.featured::before {
|
||||||
|
content: 'Beliebtestes Paket';
|
||||||
|
position: absolute; top: -12px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.55rem; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; color: var(--dark-wine);
|
||||||
|
background: var(--blush); padding: 4px 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.paket-card:hover {
|
||||||
|
border-color: rgba(212,169,160,0.4);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
.paket-name {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.4rem; font-weight: 400;
|
||||||
|
color: var(--blush); margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.paket-price {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.65rem; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; color: var(--blush);
|
||||||
|
opacity: 0.5; margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.paket-features {
|
||||||
|
list-style: none; margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.paket-features li {
|
||||||
|
font-size: 1rem; font-weight: 300;
|
||||||
|
color: rgba(248,244,240,0.7);
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(212,169,160,0.06);
|
||||||
|
}
|
||||||
|
.paket-cta {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.62rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.2em; text-transform: uppercase;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
padding: 14px 36px;
|
||||||
|
border: 1px solid rgba(212,169,160,0.3);
|
||||||
|
transition: all 0.4s;
|
||||||
|
}
|
||||||
|
.paket-cta:hover {
|
||||||
|
background: var(--blush);
|
||||||
|
color: var(--dark-wine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── BLOG PREVIEW ─── */
|
||||||
|
.blog-section { background: var(--creme); }
|
||||||
|
.blog-section .section-divider { margin: 24px auto; }
|
||||||
|
.blog-header { text-align: center; margin-bottom: 64px; }
|
||||||
|
.blog-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 32px; max-width: 1100px; margin: 0 auto;
|
||||||
|
}
|
||||||
|
.blog-card { cursor: pointer; }
|
||||||
|
.blog-card-image {
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
background: var(--blush-soft);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.blog-card-image::after {
|
||||||
|
content: 'Blog-Bild';
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.65rem; letter-spacing: 0.15em;
|
||||||
|
color: var(--bordeaux); opacity: 0.25;
|
||||||
|
}
|
||||||
|
.blog-card-date {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.58rem; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; color: var(--bordeaux);
|
||||||
|
opacity: 0.5; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.blog-card-title {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.2rem; font-weight: 400;
|
||||||
|
color: var(--espresso); line-height: 1.35;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
.blog-card:hover .blog-card-title { color: var(--bordeaux); }
|
||||||
|
.blog-card-excerpt {
|
||||||
|
font-size: 0.95rem; font-weight: 300;
|
||||||
|
line-height: 1.7; color: var(--espresso);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── KONTAKT ─── */
|
||||||
|
.kontakt-section {
|
||||||
|
background: var(--dark-wine);
|
||||||
|
color: var(--blush);
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.kontakt-left {
|
||||||
|
padding: 120px 80px;
|
||||||
|
}
|
||||||
|
.kontakt-left .section-label { color: var(--blush); }
|
||||||
|
.kontakt-left .section-title { color: var(--blush); }
|
||||||
|
.kontakt-info {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
.kontakt-info-item {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.kontakt-info-label {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.58rem; letter-spacing: 0.25em;
|
||||||
|
text-transform: uppercase; opacity: 0.4;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.kontakt-info-value {
|
||||||
|
font-size: 1.1rem; font-weight: 300;
|
||||||
|
color: var(--blush);
|
||||||
|
}
|
||||||
|
.kontakt-info-value a {
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
border-bottom: 1px solid rgba(212,169,160,0.3);
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.kontakt-info-value a:hover { border-color: var(--blush); }
|
||||||
|
.kontakt-right {
|
||||||
|
background: rgba(212,169,160,0.04);
|
||||||
|
padding: 120px 80px;
|
||||||
|
border-left: 1px solid rgba(212,169,160,0.08);
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 28px; }
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.6rem; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; color: var(--blush);
|
||||||
|
opacity: 0.5; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%; padding: 14px 0;
|
||||||
|
background: transparent; border: none;
|
||||||
|
border-bottom: 1px solid rgba(212,169,160,0.2);
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-size: 1.05rem; color: var(--blush);
|
||||||
|
outline: none; transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.form-input:focus { border-color: var(--blush); }
|
||||||
|
.form-input::placeholder { color: rgba(212,169,160,0.3); }
|
||||||
|
textarea.form-input {
|
||||||
|
resize: vertical; min-height: 100px;
|
||||||
|
border: 1px solid rgba(212,169,160,0.2);
|
||||||
|
padding: 14px 16px; margin-top: 4px;
|
||||||
|
}
|
||||||
|
.form-submit {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.65rem; font-weight: 300;
|
||||||
|
letter-spacing: 0.25em; text-transform: uppercase;
|
||||||
|
color: var(--dark-wine); background: var(--blush);
|
||||||
|
border: none; padding: 18px 56px;
|
||||||
|
cursor: pointer; transition: all 0.4s;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.form-submit:hover {
|
||||||
|
background: #e0b8b0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 30px rgba(212,169,160,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── FOOTER ─── */
|
||||||
|
.footer {
|
||||||
|
background: var(--navy);
|
||||||
|
color: var(--blush);
|
||||||
|
padding: 80px 48px 40px;
|
||||||
|
}
|
||||||
|
.footer-top {
|
||||||
|
display: grid; grid-template-columns: 2fr 1fr 1fr 1fr;
|
||||||
|
gap: 48px; max-width: 1200px; margin: 0 auto;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
border-bottom: 1px solid rgba(212,169,160,0.08);
|
||||||
|
}
|
||||||
|
.footer-logo .logo-main {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-size: 1.4rem; font-weight: 400;
|
||||||
|
color: var(--blush);
|
||||||
|
}
|
||||||
|
.footer-logo .logo-sub {
|
||||||
|
font-family: 'Playfair Display', serif;
|
||||||
|
font-style: italic; font-size: 0.85rem;
|
||||||
|
color: var(--blush); opacity: 0.7;
|
||||||
|
}
|
||||||
|
.footer-logo p {
|
||||||
|
margin-top: 16px; font-size: 0.9rem;
|
||||||
|
font-weight: 300; line-height: 1.7;
|
||||||
|
opacity: 0.4; max-width: 280px;
|
||||||
|
}
|
||||||
|
.footer-col h4 {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.58rem; letter-spacing: 0.25em;
|
||||||
|
text-transform: uppercase; opacity: 0.4;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.footer-col ul { list-style: none; }
|
||||||
|
.footer-col li { margin-bottom: 12px; }
|
||||||
|
.footer-col a {
|
||||||
|
font-size: 0.9rem; font-weight: 300;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
opacity: 0.6; transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.footer-col a:hover { opacity: 1; }
|
||||||
|
.footer-bottom {
|
||||||
|
max-width: 1200px; margin: 0 auto;
|
||||||
|
padding-top: 32px;
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.footer-copy {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.58rem; letter-spacing: 0.1em;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.footer-legal {
|
||||||
|
display: flex; gap: 24px;
|
||||||
|
}
|
||||||
|
.footer-legal a {
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.58rem; letter-spacing: 0.1em;
|
||||||
|
color: var(--blush); text-decoration: none;
|
||||||
|
opacity: 0.3; transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.footer-legal a:hover { opacity: 0.7; }
|
||||||
|
|
||||||
|
/* ─── PAGE LABELS (for prototype) ─── */
|
||||||
|
.page-label {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.page-label::before {
|
||||||
|
content: attr(data-page);
|
||||||
|
position: absolute; top: 16px; right: 16px;
|
||||||
|
font-family: 'Josefin Sans', sans-serif;
|
||||||
|
font-size: 0.5rem; letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: var(--bordeaux); color: var(--creme);
|
||||||
|
padding: 4px 12px; z-index: 10;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── RESPONSIVE ─── */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.nav { padding: 16px 24px; }
|
||||||
|
.nav-links { display: none; }
|
||||||
|
.section { padding: 80px 24px; }
|
||||||
|
.intro { grid-template-columns: 1fr; gap: 48px; }
|
||||||
|
.galerie-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.galerie-item:nth-child(3) { grid-row: auto; }
|
||||||
|
.galerie-item:nth-child(6) { grid-column: auto; aspect-ratio: 3/4; }
|
||||||
|
.testimonial-cards { grid-template-columns: 1fr; }
|
||||||
|
.pakete-grid { grid-template-columns: 1fr; }
|
||||||
|
.blog-grid { grid-template-columns: 1fr; }
|
||||||
|
.kontakt-section { grid-template-columns: 1fr; }
|
||||||
|
.kontakt-left, .kontakt-right { padding: 64px 24px; }
|
||||||
|
.kontakt-right { border-left: none; border-top: 1px solid rgba(212,169,160,0.08); }
|
||||||
|
.footer-top { grid-template-columns: 1fr 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── ANIMATIONS ─── */
|
||||||
|
.reveal {
|
||||||
|
opacity: 0; transform: translateY(30px);
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
}
|
||||||
|
.reveal.visible {
|
||||||
|
opacity: 1; transform: translateY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ═══ NAVIGATION ═══ -->
|
||||||
|
<nav class="nav" id="nav">
|
||||||
|
<a href="#" class="nav-logo">
|
||||||
|
<span class="logo-main">Sensual</span>
|
||||||
|
<span class="logo-sub">Moment</span>
|
||||||
|
<span class="logo-tag">Photography</span>
|
||||||
|
</a>
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li><a href="#ueber">Über mich</a></li>
|
||||||
|
<li><a href="#galerie">Galerie</a></li>
|
||||||
|
<li><a href="#pakete">Pakete</a></li>
|
||||||
|
<li><a href="#blog">Journal</a></li>
|
||||||
|
<li><a href="#kontakt">Kontakt</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ═══ HERO (Startseite) ═══ -->
|
||||||
|
<section class="hero page-label" data-page="Startseite">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="hero-ornament"></div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
Sensual
|
||||||
|
<em>Moment</em>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-tagline">Boudoir Photography · Dein Moment der Selbstliebe</p>
|
||||||
|
<a href="#kontakt" class="hero-cta">Dein Shooting buchen</a>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-indicator">
|
||||||
|
<span>Entdecken</span>
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M8 2v10M4 9l4 4 4-4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ ÜBER MICH ═══ -->
|
||||||
|
<section class="section page-label" data-page="Über mich" id="ueber">
|
||||||
|
<div class="intro">
|
||||||
|
<div class="intro-image reveal"></div>
|
||||||
|
<div class="intro-text reveal">
|
||||||
|
<span class="section-label">Über mich</span>
|
||||||
|
<h2 class="section-title">Jede Frau verdient es, <em>sich selbst zu feiern</em></h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p>Ich glaube daran, dass wahre Schönheit nicht inszeniert werden muss – sie muss nur sichtbar gemacht werden. In meinem Studio schaffe ich einen geschützten Raum, in dem du dich fallen lassen kannst.</p>
|
||||||
|
<p>Mit einfühlsamer Anleitung und einem Blick für das Besondere entstehen Bilder, die deine Stärke, Sinnlichkeit und Einzigartigkeit einfangen – authentisch und mit Respekt.</p>
|
||||||
|
<p>Kein Shooting gleicht dem anderen, denn keine Frau gleicht der anderen.</p>
|
||||||
|
<div class="intro-signature">— Dein Name</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ GALERIE ═══ -->
|
||||||
|
<section class="galerie-section page-label" data-page="Galerie" id="galerie">
|
||||||
|
<div class="galerie-header reveal">
|
||||||
|
<span class="section-label">Portfolio</span>
|
||||||
|
<h2 class="section-title">Momente der <em>Selbstliebe</em></h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p>Jedes Bild erzählt eine Geschichte von Mut, Verletzlichkeit und Stärke. Mit Einverständnis meiner Kundinnen teile ich hier ausgewählte Arbeiten.</p>
|
||||||
|
</div>
|
||||||
|
<div class="galerie-grid">
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Klassisch</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Artistisch</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Elegant</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Natürlich</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Dramatisch</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Sinnlich</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Intim</span></div>
|
||||||
|
<div class="galerie-item"><span class="galerie-item-label">Mutig</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="galerie-more reveal">
|
||||||
|
<a href="#">Alle Arbeiten ansehen</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ TESTIMONIALS ═══ -->
|
||||||
|
<section class="section testimonials-section page-label reveal" data-page="Testimonials">
|
||||||
|
<span class="section-label">Erfahrungen</span>
|
||||||
|
<h2 class="section-title">Was meine Kundinnen <em>sagen</em></h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<div class="testimonial-cards">
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<div class="testimonial-quote">"</div>
|
||||||
|
<p class="testimonial-text">Ich habe mich noch nie so schön gefühlt. Die Atmosphäre war so vertrauensvoll, dass ich komplett loslassen konnte. Die Bilder sind unfassbar.</p>
|
||||||
|
<span class="testimonial-author">Sandra, 42</span>
|
||||||
|
</div>
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<div class="testimonial-quote">"</div>
|
||||||
|
<p class="testimonial-text">Anfangs war ich nervös, aber nach fünf Minuten fühlte es sich an wie ein Abend mit einer guten Freundin. Die Ergebnisse haben mich zu Tränen gerührt.</p>
|
||||||
|
<span class="testimonial-author">Katrin, 38</span>
|
||||||
|
</div>
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<div class="testimonial-quote">"</div>
|
||||||
|
<p class="testimonial-text">Ein Geschenk an mich selbst, das ich jeder Frau empfehlen würde. Professionell, einfühlsam und mit unglaublichem Blick für Details.</p>
|
||||||
|
<span class="testimonial-author">Maria, 51</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ PAKETE / PREISE ═══ -->
|
||||||
|
<section class="section pakete-section page-label" data-page="Preise & Pakete" id="pakete">
|
||||||
|
<div class="pakete-header reveal">
|
||||||
|
<span class="section-label">Investition in dich</span>
|
||||||
|
<h2 class="section-title">Pakete & <em>Preise</em></h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pakete-grid">
|
||||||
|
<div class="paket-card reveal">
|
||||||
|
<div class="paket-name">Entdecken</div>
|
||||||
|
<div class="paket-price">Ab 299 €</div>
|
||||||
|
<ul class="paket-features">
|
||||||
|
<li>60 Min. Shooting</li>
|
||||||
|
<li>Styling-Beratung vorab</li>
|
||||||
|
<li>10 bearbeitete Bilder</li>
|
||||||
|
<li>Private Online-Galerie</li>
|
||||||
|
<li>5 Feinabzüge 13×18</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#kontakt" class="paket-cta">Anfragen</a>
|
||||||
|
</div>
|
||||||
|
<div class="paket-card featured reveal">
|
||||||
|
<div class="paket-name">Erleben</div>
|
||||||
|
<div class="paket-price">Ab 499 €</div>
|
||||||
|
<ul class="paket-features">
|
||||||
|
<li>120 Min. Shooting</li>
|
||||||
|
<li>Professionelles Styling</li>
|
||||||
|
<li>25 bearbeitete Bilder</li>
|
||||||
|
<li>Private Online-Galerie</li>
|
||||||
|
<li>Hochwertiges Fotobuch</li>
|
||||||
|
<li>Alle Digitale Dateien</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#kontakt" class="paket-cta">Anfragen</a>
|
||||||
|
</div>
|
||||||
|
<div class="paket-card reveal">
|
||||||
|
<div class="paket-name">Zelebrieren</div>
|
||||||
|
<div class="paket-price">Ab 799 €</div>
|
||||||
|
<ul class="paket-features">
|
||||||
|
<li>Halber Tag (4 Std.)</li>
|
||||||
|
<li>Styling + Visagistin</li>
|
||||||
|
<li>Alle bearbeiteten Bilder</li>
|
||||||
|
<li>Luxus-Leinenalbum</li>
|
||||||
|
<li>3 Wandbilder nach Wahl</li>
|
||||||
|
<li>Behind-the-Scenes Video</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#kontakt" class="paket-cta">Anfragen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ BLOG PREVIEW ═══ -->
|
||||||
|
<section class="section blog-section page-label" data-page="Blog / Journal" id="blog">
|
||||||
|
<div class="blog-header reveal">
|
||||||
|
<span class="section-label">Journal</span>
|
||||||
|
<h2 class="section-title">Gedanken & <em>Geschichten</em></h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
</div>
|
||||||
|
<div class="blog-grid">
|
||||||
|
<div class="blog-card reveal">
|
||||||
|
<div class="blog-card-image"></div>
|
||||||
|
<div class="blog-card-date">Februar 2026</div>
|
||||||
|
<h3 class="blog-card-title">Warum sich jede Frau ein Boudoir-Shooting gönnen sollte</h3>
|
||||||
|
<p class="blog-card-excerpt">Es geht nicht um perfekte Posen – es geht darum, sich selbst mit neuen Augen zu sehen…</p>
|
||||||
|
</div>
|
||||||
|
<div class="blog-card reveal">
|
||||||
|
<div class="blog-card-image"></div>
|
||||||
|
<div class="blog-card-date">Januar 2026</div>
|
||||||
|
<h3 class="blog-card-title">Was ziehe ich bloß an? Dein Style-Guide fürs Shooting</h3>
|
||||||
|
<p class="blog-card-excerpt">Die richtige Garderobe kann den Unterschied machen. Hier sind meine besten Tipps…</p>
|
||||||
|
</div>
|
||||||
|
<div class="blog-card reveal">
|
||||||
|
<div class="blog-card-image"></div>
|
||||||
|
<div class="blog-card-date">Dezember 2025</div>
|
||||||
|
<h3 class="blog-card-title">Behind the Scenes: So entsteht dein persönliches Fotobuch</h3>
|
||||||
|
<p class="blog-card-excerpt">Vom ersten Klick bis zum fertigen Album – ein Blick hinter die Kulissen…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ KONTAKT ═══ -->
|
||||||
|
<section class="kontakt-section page-label" data-page="Kontakt" id="kontakt">
|
||||||
|
<div class="kontakt-left reveal">
|
||||||
|
<span class="section-label">Kontakt</span>
|
||||||
|
<h2 class="section-title">Bereit für deinen <em>Moment</em>?</h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p style="font-size: 1.05rem; font-weight: 300; line-height: 1.7; opacity: 0.7; margin-top: 24px;">
|
||||||
|
Schreib mir unverbindlich – ich melde mich innerhalb von 24 Stunden bei dir.
|
||||||
|
Gemeinsam besprechen wir in einem kostenlosen Vorgespräch, wie dein perfektes Shooting aussehen kann.
|
||||||
|
</p>
|
||||||
|
<div class="kontakt-info">
|
||||||
|
<div class="kontakt-info-item">
|
||||||
|
<div class="kontakt-info-label">E-Mail</div>
|
||||||
|
<div class="kontakt-info-value"><a href="mailto:hello@sensualmoment.de">hello@sensualmoment.de</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="kontakt-info-item">
|
||||||
|
<div class="kontakt-info-label">Telefon</div>
|
||||||
|
<div class="kontakt-info-value"><a href="tel:+491234567890">+49 123 456 78 90</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="kontakt-info-item">
|
||||||
|
<div class="kontakt-info-label">Studio</div>
|
||||||
|
<div class="kontakt-info-value">Musterstraße 12, 12345 Stadt</div>
|
||||||
|
</div>
|
||||||
|
<div class="kontakt-info-item">
|
||||||
|
<div class="kontakt-info-label">Social</div>
|
||||||
|
<div class="kontakt-info-value">
|
||||||
|
<a href="#">Instagram</a> · <a href="#">Pinterest</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kontakt-right reveal">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input class="form-input" type="text" placeholder="Dein Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">E-Mail</label>
|
||||||
|
<input class="form-input" type="email" placeholder="deine@email.de">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Telefon (optional)</label>
|
||||||
|
<input class="form-input" type="tel" placeholder="+49 ...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Welches Paket interessiert dich?</label>
|
||||||
|
<input class="form-input" type="text" placeholder="Entdecken / Erleben / Zelebrieren">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Deine Nachricht</label>
|
||||||
|
<textarea class="form-input" placeholder="Erzähl mir von deinem Wunsch-Shooting…"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="form-submit">Nachricht senden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ FOOTER ═══ -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-top">
|
||||||
|
<div class="footer-logo">
|
||||||
|
<div class="logo-main">Sensual</div>
|
||||||
|
<div class="logo-sub">Moment</div>
|
||||||
|
<p>Boudoir Photography für Frauen, die sich selbst feiern wollen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>Navigation</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#ueber">Über mich</a></li>
|
||||||
|
<li><a href="#galerie">Galerie</a></li>
|
||||||
|
<li><a href="#pakete">Pakete</a></li>
|
||||||
|
<li><a href="#blog">Journal</a></li>
|
||||||
|
<li><a href="#kontakt">Kontakt</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>Für dich</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#">FAQ</a></li>
|
||||||
|
<li><a href="#">Vorbereitung</a></li>
|
||||||
|
<li><a href="#">Geschenkgutschein</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>Social</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#">Instagram</a></li>
|
||||||
|
<li><a href="#">Pinterest</a></li>
|
||||||
|
<li><a href="#">Facebook</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<span class="footer-copy">© 2026 Sensual Moment Photography. Alle Rechte vorbehalten.</span>
|
||||||
|
<div class="footer-legal">
|
||||||
|
<a href="#">Impressum</a>
|
||||||
|
<a href="#">Datenschutz</a>
|
||||||
|
<a href="#">AGB</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Nav scroll effect
|
||||||
|
const nav = document.getElementById('nav');
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
nav.classList.toggle('scrolled', window.scrollY > 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reveal on scroll
|
||||||
|
const reveals = document.querySelectorAll('.reveal');
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.15 });
|
||||||
|
reveals.forEach(el => observer.observe(el));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
467
docs/sensualmoments/sensualmomentsdesign.md
Normal file
467
docs/sensualmoments/sensualmomentsdesign.md
Normal file
|
|
@ -0,0 +1,467 @@
|
||||||
|
# Design-Briefing: sensualmoment.de
|
||||||
|
|
||||||
|
## Projekt-Übersicht
|
||||||
|
|
||||||
|
**Website:** sensualmoment.de
|
||||||
|
**Branche:** Boudoir-Fotografie
|
||||||
|
**Zielgruppe:** Frauen, 35–55 Jahre, die sich ein hochwertiges, intimes Fotoshooting als Akt der Selbstliebe gönnen möchten
|
||||||
|
**Tonalität:** Warm, einladend, luxuriös aber nicht einschüchternd. Empowerment statt Sexualisierung. Die Kundin soll sich sicher, wertgeschätzt und inspiriert fühlen.
|
||||||
|
**CMS:** Payload CMS (bereits vorhanden)
|
||||||
|
**Design-Referenz:** Die beigefügte Datei `sensualmoment-prototype.html` enthält den vollständigen visuellen Prototyp mit allen Sektionen, Farben, Typografie und Layoutstrukturen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Seitenstruktur & Payload-Collections
|
||||||
|
|
||||||
|
### 1.1 Globale Elemente
|
||||||
|
|
||||||
|
#### Navigation (Payload Global: `navigation`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `logo` | Upload (SVG/PNG) | Logo-Wordmark in Blush-Variante für dunklen Hintergrund |
|
||||||
|
| `menuItems` | Array | Menüpunkte mit `label` (Text) und `link` (Relationship oder URL) |
|
||||||
|
|
||||||
|
Verhalten: Fixiert am oberen Rand, transparent über dem Hero. Nach 80px Scroll wechselt der Hintergrund zu Dark Wine (#2A1520) mit 95% Deckkraft und Backdrop-Blur. Menüpunkte sind in Josefin Sans, Uppercase, gesperrt.
|
||||||
|
|
||||||
|
#### Footer (Payload Global: `footer`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `tagline` | Text | Kurzbeschreibung unter dem Logo |
|
||||||
|
| `navigationColumns` | Array of Groups | Spalten mit `title` + Array aus `label`/`link` |
|
||||||
|
| `socialLinks` | Array | Plattform + URL |
|
||||||
|
| `legalLinks` | Array | Impressum, Datenschutz, AGB als `label`/`link` |
|
||||||
|
|
||||||
|
Hintergrund: Deep Navy (#151B2B). Vierspaltig: Logo-Spalte (breit), drei schmale Navigationsspalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Einzelne Seiten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Startseite (Homepage)
|
||||||
|
|
||||||
|
**Route:** `/`
|
||||||
|
**Payload:** Page-Collection mit Slug `home`, Layout-Builder oder feste Felder
|
||||||
|
|
||||||
|
##### Sektion 1 — Hero
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `headline` | Text | Hauptüberschrift (Standard: „Sensual Moment") |
|
||||||
|
| `subline` | Text | Untertitel (Standard: „Boudoir Photography · Dein Moment der Selbstliebe") |
|
||||||
|
| `ctaLabel` | Text | Button-Text (Standard: „Dein Shooting buchen") |
|
||||||
|
| `ctaLink` | Relationship/URL | Ziel des Buttons |
|
||||||
|
| `backgroundImage` | Upload (optional) | Hintergrundbild, wird mit Dark-Wine-Overlay versehen |
|
||||||
|
|
||||||
|
Layout: Fullscreen (100vh), zentrierter Inhalt. Hintergrund ist Dark Wine mit zwei subtilen radialen Gradienten (Bordeaux + Blush, je sehr niedrige Deckkraft). Scroll-Indikator am unteren Rand mit Puls-Animation. Einblendung des Inhalts mit fadeUp-Animation.
|
||||||
|
|
||||||
|
##### Sektion 2 — Über mich (Kurzversion)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `portrait` | Upload | Fotografen-Portrait, hochformat (3:4) |
|
||||||
|
| `sectionLabel` | Text | „Über mich" |
|
||||||
|
| `headline` | Rich Text | z.B. „Jede Frau verdient es, *sich selbst zu feiern*" |
|
||||||
|
| `bodyText` | Rich Text | 2–3 Absätze persönliche Vorstellung |
|
||||||
|
| `signature` | Text | Name der Fotografin |
|
||||||
|
| `linkToFullPage` | URL | Link zur vollständigen Über-mich-Seite |
|
||||||
|
|
||||||
|
Layout: Zweispaltiges Grid (1:1) auf Crème-Hintergrund. Bild links mit abgerundeter Ecke oben rechts (border-radius: 0 120px 0 0). Text rechts. Max-Width 1280px, zentriert.
|
||||||
|
|
||||||
|
##### Sektion 3 — Galerie-Vorschau
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `sectionLabel` | Text | „Portfolio" |
|
||||||
|
| `headline` | Rich Text | z.B. „Momente der *Selbstliebe*" |
|
||||||
|
| `description` | Text | Kurzbeschreibung |
|
||||||
|
| `images` | Array of Uploads | 8 Galerie-Bilder mit optionalem `category`-Label |
|
||||||
|
| `linkToGallery` | URL | „Alle Arbeiten ansehen" |
|
||||||
|
|
||||||
|
Layout: Dark Wine Hintergrund, volle Breite. Asymmetrisches 4-Spalten-Grid mit 4px Gap. Das 3. Bild spannt 2 Zeilen, das 6. Bild spannt 2 Spalten. Hover-Effekt: dunkler Gradient von unten mit Category-Label das einblendet.
|
||||||
|
|
||||||
|
##### Sektion 4 — Testimonials
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `sectionLabel` | Text | „Erfahrungen" |
|
||||||
|
| `headline` | Rich Text | z.B. „Was meine Kundinnen *sagen*" |
|
||||||
|
| `testimonials` | Relationship zu Testimonials-Collection | 3 Stück auf der Startseite |
|
||||||
|
|
||||||
|
Layout: Crème-Hintergrund, zentrierter Header. Drei Karten im Grid mit weißem Hintergrund, feinem Blush-Border. Große Anführungszeichen in Playfair Display als Dekorelement.
|
||||||
|
|
||||||
|
##### Sektion 5 — Pakete-Vorschau
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `sectionLabel` | Text | „Investition in dich" |
|
||||||
|
| `headline` | Rich Text | „Pakete & *Preise*" |
|
||||||
|
| `packages` | Relationship zu Pakete-Collection | 3 Pakete |
|
||||||
|
|
||||||
|
Layout: Deep Navy Hintergrund. Drei Karten nebeneinander. Mittlere Karte ist „featured" (Blush-Border + Label „Beliebtestes Paket" als Pill oben). Hover: Border heller, translateY(-4px).
|
||||||
|
|
||||||
|
##### Sektion 6 — Blog/Journal-Vorschau
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `sectionLabel` | Text | „Journal" |
|
||||||
|
| `headline` | Rich Text | „Gedanken & *Geschichten*" |
|
||||||
|
| `posts` | Relationship zu Blog-Collection | 3 neueste Beiträge |
|
||||||
|
|
||||||
|
Layout: Crème-Hintergrund. Drei Karten mit Vorschaubild (4:3), Datum, Titel, Excerpt. Hover: Titel wechselt zu Bordeaux.
|
||||||
|
|
||||||
|
##### Sektion 7 — Kontakt-CTA
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `headline` | Rich Text | „Bereit für deinen *Moment*?" |
|
||||||
|
| `description` | Text | Einladungstext |
|
||||||
|
| `contactInfo` | Group | E-Mail, Telefon, Adresse, Social-Links |
|
||||||
|
| `formFields` | Definiert im Code | Name, E-Mail, Telefon, Paket-Interesse, Nachricht |
|
||||||
|
|
||||||
|
Layout: Dark Wine, zweispaltig. Links: Text + Kontaktinfos. Rechts: Formular mit Underline-Inputs (kein Border, nur border-bottom). Submit-Button in Blush mit Dark-Wine-Text.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Über mich
|
||||||
|
|
||||||
|
**Route:** `/ueber-mich`
|
||||||
|
**Payload:** Page-Collection mit Slug `ueber-mich`
|
||||||
|
|
||||||
|
| Sektion | Felder | Layout |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| Hero-Bereich | Großes Portrait + Headline + Kurztext | Zweispaltig, Dark Wine BG |
|
||||||
|
| Meine Geschichte | Rich Text mit Zwischenüberschriften | Schmale Spalte (max 720px), Crème BG |
|
||||||
|
| Meine Werte | 3–4 Werte-Blöcke (Icon/Titel/Text) | Grid auf Crème |
|
||||||
|
| Persönliches | Lockerer Text, ggf. mit persönlichen Fotos | Crème, freies Layout |
|
||||||
|
| CTA | „Lass uns kennenlernen" → Kontaktseite | Dark Wine Band |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Galerie
|
||||||
|
|
||||||
|
**Route:** `/galerie`
|
||||||
|
**Payload Collection:** `gallery-images`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `image` | Upload | Das Bild selbst |
|
||||||
|
| `title` | Text (optional) | Bildtitel |
|
||||||
|
| `category` | Select | Klassisch / Artistisch / Elegant / Natürlich / Dramatisch / Sinnlich |
|
||||||
|
| `featured` | Boolean | Auf Startseite zeigen |
|
||||||
|
| `order` | Number | Sortierreihenfolge |
|
||||||
|
|
||||||
|
Layout: Masonry- oder asymmetrisches Grid. Filtermöglichkeit nach Kategorie (animierte Tabs). Dark Wine Hintergrund. Lightbox bei Klick.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Pakete & Preise
|
||||||
|
|
||||||
|
**Route:** `/pakete`
|
||||||
|
**Payload Collection:** `packages`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `name` | Text | Paketname (Entdecken / Erleben / Zelebrieren) |
|
||||||
|
| `priceLabel` | Text | z.B. „Ab 499 €" |
|
||||||
|
| `features` | Array of Text | Leistungsliste |
|
||||||
|
| `featured` | Boolean | Hervorgehobenes Paket |
|
||||||
|
| `ctaLabel` | Text | Button-Text |
|
||||||
|
| `order` | Number | Reihenfolge |
|
||||||
|
|
||||||
|
Zusätzliche Sektionen auf der Seite: Einleitungstext oben, FAQ-Accordion unten (Payload Collection `faqs` mit `question`/`answer`), CTA-Band zum Kontaktformular.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Blog / Journal
|
||||||
|
|
||||||
|
**Route:** `/journal`
|
||||||
|
**Payload Collection:** `blog-posts`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `title` | Text | Beitragstitel |
|
||||||
|
| `slug` | Text (auto) | URL-Pfad |
|
||||||
|
| `publishDate` | Date | Veröffentlichungsdatum |
|
||||||
|
| `coverImage` | Upload | Vorschaubild (4:3) |
|
||||||
|
| `excerpt` | Textarea | Kurztext für Vorschau |
|
||||||
|
| `content` | Rich Text / Blocks | Voller Artikelinhalt |
|
||||||
|
| `category` | Select | Tipps / Behind the Scenes / Persönlich |
|
||||||
|
|
||||||
|
Übersichtsseite: Grid mit Karten (wie Startseiten-Vorschau, aber mehr Beiträge, paginiert). Einzelseite: Schmale Lesespalte (max 720px), großes Header-Bild, elegant typografiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: Kontakt
|
||||||
|
|
||||||
|
**Route:** `/kontakt`
|
||||||
|
**Payload:** Page mit eingebettetem Formular oder Payload Forms Plugin
|
||||||
|
|
||||||
|
Identisch mit der Kontakt-Sektion auf der Startseite, aber als eigenständige Seite mit optionaler Karte/Anfahrt und erweitertem Text.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SEITE: FAQ
|
||||||
|
|
||||||
|
**Route:** `/faq`
|
||||||
|
**Payload Collection:** `faqs`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `question` | Text | Frage |
|
||||||
|
| `answer` | Rich Text | Antwort |
|
||||||
|
| `category` | Select | Vor dem Shooting / Während / Nachher / Allgemein |
|
||||||
|
| `order` | Number | Reihenfolge |
|
||||||
|
|
||||||
|
Layout: Accordion-Elemente, gruppiert nach Kategorie. Crème-Hintergrund.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Pflichtseiten (Rechtlich)
|
||||||
|
|
||||||
|
| Seite | Route | Payload |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| Impressum | `/impressum` | Page mit Rich-Text-Feld |
|
||||||
|
| Datenschutz | `/datenschutz` | Page mit Rich-Text-Feld |
|
||||||
|
| AGB | `/agb` | Page mit Rich-Text-Feld |
|
||||||
|
|
||||||
|
Einfaches Layout: Schmale Textspalte auf Crème, sachliche Typografie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 Payload Collections – Zusammenfassung
|
||||||
|
|
||||||
|
| Collection | Slug | Verwendung |
|
||||||
|
|------------|------|------------|
|
||||||
|
| Pages | `pages` | Alle statischen Seiten (Home, Über mich, Kontakt, Legal) |
|
||||||
|
| Gallery Images | `gallery-images` | Galerie-Bilder mit Kategorien |
|
||||||
|
| Packages | `packages` | Shooting-Pakete mit Preisen |
|
||||||
|
| Testimonials | `testimonials` | Kundinnenstimmen (`quote`, `authorName`, `authorAge`, `featured`) |
|
||||||
|
| Blog Posts | `blog-posts` | Journal-Beiträge |
|
||||||
|
| FAQs | `faqs` | Häufige Fragen |
|
||||||
|
| Media | `media` | Standard Payload Media-Collection |
|
||||||
|
|
||||||
|
| Global | Slug | Verwendung |
|
||||||
|
|--------|------|------------|
|
||||||
|
| Navigation | `navigation` | Logo + Menüpunkte |
|
||||||
|
| Footer | `footer` | Footer-Inhalte |
|
||||||
|
| Site Settings | `site-settings` | SEO-Defaults, Social-Media-Links, Kontaktdaten |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Farbschema
|
||||||
|
|
||||||
|
### 2.1 Die sechs Kernfarben
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┬───────────┬────────────────────────────────────────────┐
|
||||||
|
│ Name │ Hex │ Verwendung │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Dark Wine │ #2A1520 │ Haupthintergrund dunkler Sektionen, │
|
||||||
|
│ │ │ Hero, Kontakt, scrolled Navigation │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Blush Nude │ #D4A9A0 │ Primäre Akzentfarbe: Buttons, Links, │
|
||||||
|
│ │ │ Hover-States, Logo auf dunklem Grund, │
|
||||||
|
│ │ │ Ornamente, Trennlinien │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Bordeaux │ #8B3A4A │ Headlines auf hellem Hintergrund, │
|
||||||
|
│ │ │ Hover-Farbe für Blog-Titel, Akzente │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Deep Navy │ #151B2B │ Sektionswechsel (Pakete-Bereich), Footer │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Crème │ #F8F4F0 │ Helle Flächen, Formulare, Fließtext- │
|
||||||
|
│ │ │ Hintergrund, Cards │
|
||||||
|
├─────────────────┼───────────┼────────────────────────────────────────────┤
|
||||||
|
│ Espresso │ #3D2F30 │ Fließtext auf hellem Hintergrund │
|
||||||
|
└─────────────────┴───────────┴────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Abgeleitete Werte (CSS Custom Properties)
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--dark-wine: #2A1520;
|
||||||
|
--blush: #D4A9A0;
|
||||||
|
--bordeaux: #8B3A4A;
|
||||||
|
--navy: #151B2B;
|
||||||
|
--creme: #F8F4F0;
|
||||||
|
--espresso: #3D2F30;
|
||||||
|
|
||||||
|
/* Transparenzvarianten für Overlays, Borders, Hintergründe */
|
||||||
|
--blush-soft: rgba(212, 169, 160, 0.15);
|
||||||
|
--blush-medium: rgba(212, 169, 160, 0.30);
|
||||||
|
--blush-border: rgba(212, 169, 160, 0.20);
|
||||||
|
--blush-hover-bg: rgba(212, 169, 160, 0.10);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Farbzuordnung nach Sektion
|
||||||
|
|
||||||
|
| Sektion | Hintergrund | Text | Akzente |
|
||||||
|
|---------|------------|------|---------|
|
||||||
|
| Navigation (gescrollt) | Dark Wine 95% | Blush | Blush Underline-Hover |
|
||||||
|
| Hero | Dark Wine + radiale Gradienten | Blush | Blush (CTA-Border) |
|
||||||
|
| Über mich | Crème | Espresso | Bordeaux (Headline), Blush (Divider) |
|
||||||
|
| Galerie | Dark Wine | Blush | Blush-soft (Bild-Platzhalter) |
|
||||||
|
| Testimonials | Crème | Espresso | Blush (Anführungszeichen), Bordeaux (Autorin) |
|
||||||
|
| Pakete | Deep Navy | Crème/Blush | Blush (Borders, CTA, Featured-Badge) |
|
||||||
|
| Blog | Crème | Espresso | Bordeaux (Hover), Blush (Divider) |
|
||||||
|
| Kontakt | Dark Wine | Blush | Blush (Submit-Button Hintergrund) |
|
||||||
|
| Footer | Deep Navy | Blush | Blush bei 30–60% Opacity |
|
||||||
|
|
||||||
|
### 2.4 Interaktionszustände
|
||||||
|
|
||||||
|
| Element | Default | Hover | Active/Focus |
|
||||||
|
|---------|---------|-------|-------------|
|
||||||
|
| CTA-Buttons (dunkel) | Transparent + Blush-Border 30% | Blush-BG 10% + Border 100% | — |
|
||||||
|
| CTA-Buttons (Pakete) | Transparent + Blush-Border 30% | Blush-BG solid + Dark-Wine-Text | — |
|
||||||
|
| Submit-Button | Blush-BG + Dark-Wine-Text | Helleres Blush (#E0B8B0) + Schatten + translateY(-2px) | — |
|
||||||
|
| Nav-Links | Blush | Opacity 0.8 + Underline von links (width 0→100%) | — |
|
||||||
|
| Blog-Titel | Espresso | Bordeaux | — |
|
||||||
|
| Karten | Ohne Schatten | Box-Shadow + translateY(-4px) | — |
|
||||||
|
| Form-Inputs | Border-bottom Blush 20% | — | Border-bottom Blush 100% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Typografie
|
||||||
|
|
||||||
|
### 3.1 Schriftfamilien
|
||||||
|
|
||||||
|
| Rolle | Font | Gewichte | Quelle |
|
||||||
|
|-------|------|----------|--------|
|
||||||
|
| **Display / Headlines** | Playfair Display | 400, 500, 600, 700 + Italic | Google Fonts |
|
||||||
|
| **Fließtext / Body** | Cormorant Garamond | 300, 400, 500 + Italic | Google Fonts |
|
||||||
|
| **UI / Labels / Navigation** | Josefin Sans | 200, 300, 400 | Google Fonts |
|
||||||
|
|
||||||
|
### 3.2 Typografie-Hierarchie
|
||||||
|
|
||||||
|
| Element | Font | Größe | Gewicht | Sonstiges |
|
||||||
|
|---------|------|-------|---------|-----------|
|
||||||
|
| Hero-Headline | Playfair Display | clamp(3.5rem, 8vw, 7rem) | 400 | Kursiv für zweite Zeile |
|
||||||
|
| Sektion-Titel (h2) | Playfair Display | clamp(2rem, 4vw, 3.2rem) | 400 | `<em>` = Italic |
|
||||||
|
| Sektion-Label | Josefin Sans | 0.6rem | 300 | Uppercase, letter-spacing: 0.4em, opacity: 0.5 |
|
||||||
|
| Fließtext | Cormorant Garamond | 1.2rem | 300 | line-height: 1.8 |
|
||||||
|
| Navigation | Josefin Sans | 0.72rem | 300 | Uppercase, letter-spacing: 0.18em |
|
||||||
|
| Buttons/CTAs | Josefin Sans | 0.62–0.68rem | 300 | Uppercase, letter-spacing: 0.2–0.25em |
|
||||||
|
| Testimonial-Text | Cormorant Garamond | 1.05rem | 300 Italic | line-height: 1.75 |
|
||||||
|
| Karten-Titel | Playfair Display | 1.2–1.4rem | 400 | — |
|
||||||
|
| Footer-Links | Cormorant Garamond | 0.9rem | 300 | Opacity: 0.6, Hover: 1.0 |
|
||||||
|
| Rechtliches/Meta | Josefin Sans | 0.55–0.58rem | 300 | letter-spacing: 0.1–0.15em |
|
||||||
|
|
||||||
|
### 3.3 Typografie-Prinzipien
|
||||||
|
|
||||||
|
Die drei Schriften haben klar getrennte Rollen: Playfair Display erzeugt visuelle Spannung und Eleganz in Headlines, wird aber nie für kleine Texte eingesetzt. Cormorant Garamond ist die „Stimme" der Seite und transportiert den Fließtext mit Leichtigkeit (Gewicht 300). Josefin Sans fungiert als funktionale Schrift für UI-Elemente und wird ausschließlich in Uppercase mit großzügiger Sperrung eingesetzt, um einen Kontrast zur organischen Serif-Welt zu schaffen.
|
||||||
|
|
||||||
|
Headlines nutzen häufig eine Kombination aus Regular und Italic, wobei ein Wort oder eine Phrase kursiv gesetzt wird, um einen visuellen Rhythmus zu erzeugen (z.B. „Momente der *Selbstliebe*"). Das Pattern zieht sich durch die gesamte Seite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Designsprache & Gestaltungsprinzipien
|
||||||
|
|
||||||
|
### 4.1 Gesamteindruck
|
||||||
|
|
||||||
|
Die Seite vermittelt **luxuriöse Intimität** — sie fühlt sich an wie ein warmer, geschützter Raum. Dunkel genug für Atmosphäre, aber nie düster. Die Farbwelt ist bewusst Bordeaux-dominant mit warmen Hauttönen, nicht kühl oder technisch. Alles strahlt Vertrauen und Wertschätzung aus.
|
||||||
|
|
||||||
|
### 4.2 Layout-Prinzipien
|
||||||
|
|
||||||
|
**Großzügiger Weißraum:** Sektionen haben 120px Padding vertikal (80px auf Mobile). Nichts fühlt sich gedrängt an.
|
||||||
|
|
||||||
|
**Rhythmischer Wechsel zwischen Hell und Dunkel:** Die Seite alterniert konsequent:
|
||||||
|
Hero (dunkel) → Über mich (hell) → Galerie (dunkel) → Testimonials (hell) → Pakete (dunkel/navy) → Blog (hell) → Kontakt (dunkel) → Footer (dunkel/navy)
|
||||||
|
|
||||||
|
**Asymmetrie mit System:** Das Galerie-Grid bricht bewusst aus dem gleichmäßigen Raster aus (ein Bild spannt 2 Zeilen, eines 2 Spalten). Das erzeugt visuelles Interesse ohne Chaos.
|
||||||
|
|
||||||
|
**Max-Width für Inhalte:** Textsektionen begrenzen sich auf 1100–1280px. Die Galerie darf volle Breite nutzen.
|
||||||
|
|
||||||
|
### 4.3 Dekorative Elemente
|
||||||
|
|
||||||
|
| Element | Beschreibung |
|
||||||
|
|---------|-------------|
|
||||||
|
| Trennlinien | 48px breit, 1px hoch, Farbe Blush. Unter Sektion-Titeln. |
|
||||||
|
| Hero-Ornament | 60px Linie mit Gradient (transparent → Blush → transparent), über dem Titel |
|
||||||
|
| Anführungszeichen | Großes „ in Playfair Display (2rem), Farbe Blush, in Testimonial-Karten |
|
||||||
|
| Featured-Badge | Pill-Shape, Blush-Hintergrund, Dark-Wine-Text, über der Paketkarte positioniert |
|
||||||
|
| Bild-Ecke | Das Über-mich-Portrait hat einen border-radius nur oben rechts (0 120px 0 0) |
|
||||||
|
|
||||||
|
### 4.4 Animationen & Transitions
|
||||||
|
|
||||||
|
| Effekt | Beschreibung | Dauer |
|
||||||
|
|--------|-------------|-------|
|
||||||
|
| Scroll-Reveal | Elemente mit Klasse `.reveal` faden von unten ein (translateY 30px → 0, opacity 0 → 1) | 0.8s ease |
|
||||||
|
| Hero-Einblendung | Gesamter Hero-Content fährt von unten ein | 1.2s ease-out |
|
||||||
|
| Nav-Transition | Padding, Hintergrund, Schatten ändern sich beim Scrollen | 0.5s ease |
|
||||||
|
| Scroll-Indikator | Puls-Animation (Opacity + translateY) | 2s infinite |
|
||||||
|
| Hover-Underline | Linie unter Nav-Links wächst von links (width 0 → 100%) | 0.3s ease |
|
||||||
|
| Karten-Hover | Box-Shadow einblenden + translateY(-4px) | 0.4s |
|
||||||
|
| Galerie-Hover | Dunkler Gradient von unten einblenden + Label einblenden (delay 0.1s) | 0.4s |
|
||||||
|
| Button-Hover | Background-Change + ggf. Schatten + translateY(-2px) | 0.4s |
|
||||||
|
|
||||||
|
Alle Animationen sind subtil und unterstützend. Keine springenden, aufmerksamkeitsheischenden Effekte. Die IntersectionObserver-Schwelle liegt bei 15%.
|
||||||
|
|
||||||
|
### 4.5 Responsive-Verhalten
|
||||||
|
|
||||||
|
| Breakpoint | Änderungen |
|
||||||
|
|------------|------------|
|
||||||
|
| ≤ 900px | Navigation: Hamburger-Menü (Mobile-Menu noch zu implementieren). Alle Grids fallen auf 1 Spalte. Galerie wird 2-spaltig ohne Span-Varianten. Padding reduziert auf 80px/24px. Kontakt-Sektion wird einspaltig gestapelt. Footer wird 2-spaltig. |
|
||||||
|
|
||||||
|
### 4.6 Bildsprache (Hinweis für Content)
|
||||||
|
|
||||||
|
Alle Platzhalter im Prototyp sind als dezente Blush-soft-Flächen angelegt. Die echten Bilder sollten folgenden Charakter haben: warme Töne, weiches Licht, natürliche Posen, Fokus auf Stärke und Anmut. Keine kühlen oder harten Kontraste. Die Bildwelt sollte die Farbwelt der Seite widerspiegeln — warme Hauttöne, dunkle Hintergründe, gelegentliche Stoffe in Bordeaux oder Crème.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Logo-Spezifikation
|
||||||
|
|
||||||
|
### 5.1 Aufbau
|
||||||
|
|
||||||
|
Das Logo ist ein reines Wordmark (keine Bildmarke, kein Symbol):
|
||||||
|
|
||||||
|
```
|
||||||
|
Sensual ← Playfair Display, Regular, groß
|
||||||
|
Moment ← Playfair Display, Italic, kleiner, rechtsbündig unter "Sensual"
|
||||||
|
PHOTOGRAPHY ← Josefin Sans, Light, sehr klein, Uppercase, stark gesperrt, rechtsbündig
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Farbvarianten
|
||||||
|
|
||||||
|
| Variante | Text | Hintergrund | Einsatz |
|
||||||
|
|----------|------|-------------|---------|
|
||||||
|
| **Primary** | Blush (#D4A9A0) | Dark Wine (#2A1520) | Navigation, Hero, Kontakt |
|
||||||
|
| Navy | Blush (#D4A9A0) | Deep Navy (#151B2B) | Footer, Pakete-Bereich |
|
||||||
|
| Hell | Bordeaux (#8B3A4A) | Crème (#F8F4F0) | Helle Sektionen, Drucksachen |
|
||||||
|
| Invertiert | Crème (#F8F4F0) | Bordeaux (#8B3A4A) | Spezielle Akzente, Social Media |
|
||||||
|
|
||||||
|
Das Logo existiert als Canva-Design mit allen 4 Varianten (Design-ID: DAHCgZTIQkg). Für die Website sollte es als SVG exportiert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Technische Hinweise für Payload
|
||||||
|
|
||||||
|
### 6.1 Empfohlene Payload-Konfiguration
|
||||||
|
|
||||||
|
Die Seite hat einen klaren Wechsel zwischen dunklen und hellen Sektionen. Im Payload-Admin sollte es für Page-Layouts einen Block-Builder geben, bei dem jeder Block ein Feld `theme` hat (`light` | `dark` | `navy`), das automatisch die richtige Farbkombination anwendet.
|
||||||
|
|
||||||
|
### 6.2 Rich-Text-Kursiv-Konvention
|
||||||
|
|
||||||
|
Überall auf der Seite werden Kursivierungen in Headlines als Stilmittel eingesetzt. Das Payload-Rich-Text-Feld muss Italic unterstützen und im Frontend als `<em>` mit Playfair Display Italic gerendert werden.
|
||||||
|
|
||||||
|
### 6.3 SEO-Felder
|
||||||
|
|
||||||
|
Jede Page und jeder Blog-Post sollte folgende Felder haben: `metaTitle`, `metaDescription`, `ogImage` (Upload). Payload bietet dafür ein SEO-Plugin an.
|
||||||
|
|
||||||
|
### 6.4 Formular-Handling
|
||||||
|
|
||||||
|
Das Kontaktformular sollte über Payload Forms oder eine eigene Collection (`form-submissions`) verarbeitet werden. Felder: Name, E-Mail, Telefon (optional), Paket-Interesse (Select), Nachricht. E-Mail-Benachrichtigung an die Fotografin konfigurieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dateireferenzen
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|-------------|
|
||||||
|
| `sensualmoment-prototype.html` | Vollständiger visueller Prototyp mit allen Sektionen und CSS |
|
||||||
|
| `farbschema-final.html` | Interaktive Farbschema-Dokumentation |
|
||||||
|
| Canva Design DAHCgZTIQkg | Logo in allen 4 Farbvarianten |
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
"unrs-resolver"
|
"unrs-resolver"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"minimatch": ">=10.2.1",
|
"minimatch": ">=10.2.3",
|
||||||
"esbuild": ">=0.25.0",
|
"esbuild": ">=0.25.0",
|
||||||
"ajv": ">=8.18.0",
|
"ajv": ">=8.18.0",
|
||||||
"ioredis": "5.9.3"
|
"ioredis": "5.9.3"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
minimatch: '>=10.2.1'
|
minimatch: '>=10.2.3'
|
||||||
esbuild: '>=0.25.0'
|
esbuild: '>=0.25.0'
|
||||||
ajv: '>=8.18.0'
|
ajv: '>=8.18.0'
|
||||||
ioredis: 5.9.3
|
ioredis: 5.9.3
|
||||||
|
|
@ -3539,8 +3539,8 @@ packages:
|
||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
minimatch@10.2.2:
|
minimatch@10.2.4:
|
||||||
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
|
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
|
|
@ -5326,7 +5326,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.7
|
'@eslint/object-schema': 2.1.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
@ -5347,7 +5347,7 @@ snapshots:
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
import-fresh: 3.3.1
|
import-fresh: 3.3.1
|
||||||
js-yaml: 4.1.1
|
js-yaml: 4.1.1
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
strip-json-comments: 3.1.1
|
strip-json-comments: 3.1.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
@ -6403,7 +6403,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.56.0
|
'@typescript-eslint/types': 8.56.0
|
||||||
'@typescript-eslint/visitor-keys': 8.56.0
|
'@typescript-eslint/visitor-keys': 8.56.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
|
@ -7341,7 +7341,7 @@ snapshots:
|
||||||
eslint: 9.39.3
|
eslint: 9.39.3
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3))(eslint@9.39.3)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3)
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.39.3)
|
eslint-plugin-react: 7.37.5(eslint@9.39.3)
|
||||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3)
|
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3)
|
||||||
|
|
@ -7374,7 +7374,7 @@ snapshots:
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3))(eslint@9.39.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
@ -7389,7 +7389,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3))(eslint@9.39.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
|
|
@ -7404,7 +7404,7 @@ snapshots:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
object.groupby: 1.0.3
|
object.groupby: 1.0.3
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
|
|
@ -7432,7 +7432,7 @@ snapshots:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
language-tags: 1.0.9
|
language-tags: 1.0.9
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
string.prototype.includes: 2.0.1
|
||||||
|
|
@ -7460,7 +7460,7 @@ snapshots:
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.entries: 1.1.9
|
object.entries: 1.1.9
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
|
|
@ -7514,7 +7514,7 @@ snapshots:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
json-stable-stringify-without-jsonify: 1.0.1
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
optionator: 0.9.4
|
optionator: 0.9.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -7759,7 +7759,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
jackspeak: 3.4.3
|
jackspeak: 3.4.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 1.11.1
|
path-scurry: 1.11.1
|
||||||
|
|
@ -7769,7 +7769,7 @@ snapshots:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
inflight: 1.0.6
|
inflight: 1.0.6
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
|
|
||||||
|
|
@ -8591,7 +8591,7 @@ snapshots:
|
||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
minimatch@10.2.2:
|
minimatch@10.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.2
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
|
|
@ -9143,7 +9143,7 @@ snapshots:
|
||||||
|
|
||||||
readdir-glob@1.1.3:
|
readdir-glob@1.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
749
prompts/PROMPT_TELEGRAM_MEDIA_BOT.md
Normal file
749
prompts/PROMPT_TELEGRAM_MEDIA_BOT.md
Normal file
|
|
@ -0,0 +1,749 @@
|
||||||
|
# Telegram Media Upload Bot – Payload CMS Integration
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
- **Projektverzeichnis:** `/home/payload/telegram-media-bot` (auf sv-payload, LXC 700, IP: 10.10.181.100)
|
||||||
|
- **Alternative:** Neues GitHub Repository `complexcaresolutions/telegram-media-bot`
|
||||||
|
- **Tech-Stack:** Node.js 22 LTS, TypeScript, grammy (Telegram Bot Framework), PM2
|
||||||
|
- **Ziel-System:** Payload CMS Multi-Tenant (Production: `https://cms.c2sgmbh.de`, Staging: `https://pl.porwoll.tech`)
|
||||||
|
- **Betriebssystem:** Debian/Ubuntu (LXC Container im Proxmox-Cluster)
|
||||||
|
- **Package Manager:** pnpm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projektbeschreibung
|
||||||
|
|
||||||
|
Erstelle einen Telegram Bot, der es autorisierten Benutzern ermöglicht, Bilder direkt aus dem Telegram-Chat in die Media Collection des Payload CMS hochzuladen. Der Bot authentifiziert sich gegen die Payload REST-API, empfängt Bilder über die Telegram Bot API, lädt sie von den Telegram-Servern herunter und leitet sie per `multipart/form-data` an den Payload Media-Endpoint weiter. Dabei wird die Multi-Tenant-Isolation strikt eingehalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technische Referenzen
|
||||||
|
|
||||||
|
### Payload CMS REST-API
|
||||||
|
|
||||||
|
**Base URLs:**
|
||||||
|
- Production: `https://cms.c2sgmbh.de/api`
|
||||||
|
- Staging: `https://pl.porwoll.tech/api`
|
||||||
|
- Swagger UI: `https://cms.c2sgmbh.de/api/docs`
|
||||||
|
|
||||||
|
**Authentifizierung – Login:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://cms.c2sgmbh.de/api/users/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"password": "your-password"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Auth Passed",
|
||||||
|
"user": { "id": 1, "email": "admin@example.com", "isSuperAdmin": true },
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token verwenden:**
|
||||||
|
```bash
|
||||||
|
curl "https://cms.c2sgmbh.de/api/media" \
|
||||||
|
-H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Media Upload Endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://cms.c2sgmbh.de/api/media" \
|
||||||
|
-H "Authorization: JWT <token>" \
|
||||||
|
-F "file=@/path/to/image.jpg" \
|
||||||
|
-F "alt=Beschreibungstext" \
|
||||||
|
-F "tenant=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**WICHTIG:** Das `tenant`-Feld ist **Pflicht** bei jedem Upload. Ohne korrekte Tenant-Zuordnung gibt die API 403 Forbidden zurück. Die Tenant-Isolation ist ein Kernprinzip des gesamten Systems.
|
||||||
|
|
||||||
|
### Verfügbare Tenants
|
||||||
|
|
||||||
|
| ID | Name | Slug | Domain |
|
||||||
|
|----|------|------|--------|
|
||||||
|
| 1 | porwoll.de | porwoll | porwoll.de |
|
||||||
|
| 4 | Complex Care Solutions GmbH | c2s | complexcaresolutions.de |
|
||||||
|
| 5 | Gunshin | gunshin | gunshin.de |
|
||||||
|
| (weitere Tenants ggf. dynamisch über API abrufen) |
|
||||||
|
|
||||||
|
### Media Collection – Automatische Bildverarbeitung
|
||||||
|
|
||||||
|
Payload generiert automatisch folgende responsive Größen beim Upload:
|
||||||
|
|
||||||
|
| Size | Auflösung | Format |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| thumbnail | 150×150 | Original + AVIF |
|
||||||
|
| small | 300×300 | Original + AVIF |
|
||||||
|
| medium | 600×600 | Original + AVIF |
|
||||||
|
| large | 1200×1200 | Original + AVIF |
|
||||||
|
| xlarge | 1920×1920 | Original + AVIF |
|
||||||
|
| 2k | 2560×2560 | Original + AVIF |
|
||||||
|
| og | 1200×630 | Original (Social Media) |
|
||||||
|
|
||||||
|
→ Der Bot muss sich NICHT um Bildgrößen kümmern. Payload erledigt das serverseitig.
|
||||||
|
|
||||||
|
### Rate Limiting (Payload API)
|
||||||
|
|
||||||
|
| Limiter | Limit | Fenster |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| publicApiLimiter | 60 Requests | 1 Minute |
|
||||||
|
| authLimiter | 5 Requests | 15 Minuten |
|
||||||
|
|
||||||
|
→ Der Bot sollte Token cachen und nicht bei jedem Upload neu einloggen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aufgaben
|
||||||
|
|
||||||
|
### 1. Projekt-Setup
|
||||||
|
|
||||||
|
#### 1.1 Projektstruktur erstellen
|
||||||
|
|
||||||
|
```
|
||||||
|
telegram-media-bot/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Entry Point, Bot-Start
|
||||||
|
│ ├── bot.ts # Grammy Bot-Instanz + Handler
|
||||||
|
│ ├── config.ts # Environment-Konfiguration (typisiert)
|
||||||
|
│ ├── payload/
|
||||||
|
│ │ ├── client.ts # Payload API Client (Login, Token-Management)
|
||||||
|
│ │ └── media.ts # Media Upload Logik
|
||||||
|
│ ├── telegram/
|
||||||
|
│ │ ├── handlers.ts # Message Handler (Photo, Document, Commands)
|
||||||
|
│ │ └── keyboards.ts # Inline Keyboards (Tenant-Auswahl etc.)
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ └── auth.ts # User-Whitelist Middleware
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── logger.ts # Logging Utility
|
||||||
|
│ └── download.ts # Telegram File Download Helper
|
||||||
|
├── .env.example # Template für Environment Variables
|
||||||
|
├── .env # (gitignored) Actual Config
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── ecosystem.config.cjs # PM2 Konfiguration
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] `pnpm init` ausgeführt
|
||||||
|
- [ ] TypeScript konfiguriert (strict mode)
|
||||||
|
- [ ] Alle Dependencies installiert
|
||||||
|
- [ ] `.gitignore` enthält `node_modules`, `.env`, `dist`
|
||||||
|
|
||||||
|
#### 1.2 Dependencies installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add grammy dotenv
|
||||||
|
pnpm add -D typescript @types/node tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** `grammy` ist das bevorzugte Telegram Bot Framework (modern, TypeScript-first, aktiv maintained). Alternativ wäre `node-telegram-bot-api` möglich, aber `grammy` hat bessere TypeScript-Unterstützung.
|
||||||
|
|
||||||
|
#### 1.3 TypeScript Konfiguration
|
||||||
|
|
||||||
|
**Datei:** `tsconfig.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 Package.json Scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "telegram-media-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Konfiguration
|
||||||
|
|
||||||
|
#### 2.1 Environment Variables
|
||||||
|
|
||||||
|
**Datei:** `.env.example`
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Telegram Bot
|
||||||
|
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
||||||
|
|
||||||
|
# Zugelassene Telegram User-IDs (kommasepariert)
|
||||||
|
ALLOWED_USER_IDS=123456789,987654321
|
||||||
|
|
||||||
|
# Payload CMS
|
||||||
|
PAYLOAD_API_URL=https://cms.c2sgmbh.de/api
|
||||||
|
PAYLOAD_ADMIN_EMAIL=admin@example.com
|
||||||
|
PAYLOAD_ADMIN_PASSWORD=your-secure-password
|
||||||
|
|
||||||
|
# Standard-Tenant (wird verwendet wenn kein Tenant gewählt)
|
||||||
|
DEFAULT_TENANT_ID=1
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Node Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Typisierte Config
|
||||||
|
|
||||||
|
**Datei:** `src/config.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Alle Environment Variables typisiert laden und validieren.
|
||||||
|
// Bei fehlenden Pflicht-Variablen: Prozess mit Error beenden.
|
||||||
|
// ALLOWED_USER_IDS als number[] parsen (kommasepariert).
|
||||||
|
// Validation beim Import, nicht lazy.
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
telegram: {
|
||||||
|
botToken: string;
|
||||||
|
allowedUserIds: number[];
|
||||||
|
};
|
||||||
|
payload: {
|
||||||
|
apiUrl: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
defaultTenantId: number;
|
||||||
|
logLevel: string;
|
||||||
|
nodeEnv: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] Config wird beim Start validiert
|
||||||
|
- [ ] Fehlende Pflicht-Variablen = sofortiger Exit mit klarer Fehlermeldung
|
||||||
|
- [ ] `ALLOWED_USER_IDS` wird als `number[]` geparst
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Payload API Client
|
||||||
|
|
||||||
|
#### 3.1 Authentifizierung mit Token-Caching
|
||||||
|
|
||||||
|
**Datei:** `src/payload/client.ts`
|
||||||
|
|
||||||
|
Implementiere einen Payload API Client mit folgender Logik:
|
||||||
|
|
||||||
|
1. **Login:** `POST /api/users/login` mit Email/Password
|
||||||
|
2. **Token speichern** (In-Memory, NICHT auf Disk)
|
||||||
|
3. **Token-Expiry tracken:** JWT decodieren (ohne Verifikation, nur Payload lesen), `exp` Feld prüfen
|
||||||
|
4. **Auto-Refresh:** Vor jedem API-Call prüfen ob Token noch gültig (mit 5 Min. Buffer). Falls abgelaufen → neu einloggen.
|
||||||
|
5. **Retry-Logik:** Bei 401-Response einmal neu einloggen und Request wiederholen.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class PayloadClient {
|
||||||
|
private token: string | null = null;
|
||||||
|
private tokenExpiry: number = 0;
|
||||||
|
|
||||||
|
async getToken(): Promise<string> { /* ... */ }
|
||||||
|
async login(): Promise<void> { /* ... */ }
|
||||||
|
async uploadMedia(file: Buffer, filename: string, options: MediaUploadOptions): Promise<MediaResponse> { /* ... */ }
|
||||||
|
async listMedia(tenantId: number, limit?: number): Promise<MediaListResponse> { /* ... */ }
|
||||||
|
async deleteMedia(mediaId: number): Promise<void> { /* ... */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaUploadOptions {
|
||||||
|
alt: string;
|
||||||
|
tenantId: number;
|
||||||
|
caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaResponse {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
filename: string;
|
||||||
|
alt: string;
|
||||||
|
sizes: Record<string, { url: string; width: number; height: number }>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WICHTIG – Multi-Tenant:**
|
||||||
|
- Jeder API-Call der Media betrifft MUSS `tenant` als Feld mitsenden
|
||||||
|
- Bei Reads: `?where[tenant][equals]=<ID>` als Query-Parameter
|
||||||
|
- Bei Writes: `tenant` im Request-Body
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] Login funktioniert, Token wird gecacht
|
||||||
|
- [ ] Token wird vor Ablauf automatisch erneuert
|
||||||
|
- [ ] 401 Response triggert Re-Login + Retry
|
||||||
|
- [ ] Alle API-Calls enthalten korrekte Tenant-Filterung
|
||||||
|
|
||||||
|
#### 3.2 Media Upload Funktion
|
||||||
|
|
||||||
|
**Datei:** `src/payload/media.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Upload einer Bilddatei an POST /api/media
|
||||||
|
// Content-Type: multipart/form-data
|
||||||
|
//
|
||||||
|
// Felder:
|
||||||
|
// file: Die Bilddatei (Buffer) mit korrektem filename + mimetype
|
||||||
|
// alt: Alt-Text (string)
|
||||||
|
// tenant: Tenant-ID (number)
|
||||||
|
//
|
||||||
|
// Die Payload API generiert automatisch alle responsiven Größen.
|
||||||
|
// Response enthält die vollständige Media-Resource inkl. aller Size-URLs.
|
||||||
|
```
|
||||||
|
|
||||||
|
Für den multipart Upload verwende die native `FormData` API (ab Node.js 18+ verfügbar) oder `form-data` Package. **Kein axios nötig** – nutze native `fetch`.
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] Upload funktioniert mit JPG, PNG, WebP, AVIF
|
||||||
|
- [ ] Alt-Text wird korrekt gesetzt
|
||||||
|
- [ ] Tenant-Zuordnung funktioniert
|
||||||
|
- [ ] Response wird korrekt geparst
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Telegram Bot
|
||||||
|
|
||||||
|
#### 4.1 Bot-Instanz und Middleware
|
||||||
|
|
||||||
|
**Datei:** `src/bot.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Bot, Context, session } from 'grammy';
|
||||||
|
|
||||||
|
interface SessionData {
|
||||||
|
selectedTenantId: number;
|
||||||
|
selectedTenantName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BotContext = Context & { session: SessionData };
|
||||||
|
|
||||||
|
// Bot erstellen mit Grammy
|
||||||
|
// Session-Middleware für Tenant-Auswahl pro User
|
||||||
|
// Auth-Middleware für User-Whitelist
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Auth Middleware (User-Whitelist)
|
||||||
|
|
||||||
|
**Datei:** `src/middleware/auth.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Middleware die prüft ob ctx.from.id in ALLOWED_USER_IDS enthalten ist.
|
||||||
|
// Falls nicht: Antwort "⛔ Du bist nicht autorisiert, diesen Bot zu verwenden."
|
||||||
|
// und ctx.next() NICHT aufrufen.
|
||||||
|
//
|
||||||
|
// WICHTIG: Auch in Gruppen-Chats nur auf autorisierte User reagieren.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] Nicht-autorisierte User erhalten Fehlermeldung
|
||||||
|
- [ ] Autorisierte User können alle Funktionen nutzen
|
||||||
|
- [ ] Middleware blockiert alle Handler, nicht nur einzelne
|
||||||
|
|
||||||
|
#### 4.3 Command Handler
|
||||||
|
|
||||||
|
**Datei:** `src/telegram/handlers.ts`
|
||||||
|
|
||||||
|
Implementiere folgende Befehle:
|
||||||
|
|
||||||
|
**`/start`**
|
||||||
|
- Begrüßungsnachricht mit Kurzanleitung
|
||||||
|
- Zeige aktuell gewählten Tenant
|
||||||
|
- Text:
|
||||||
|
```
|
||||||
|
🤖 Payload Media Upload Bot
|
||||||
|
|
||||||
|
Schicke mir ein Bild und ich lade es in die Payload CMS Media-Bibliothek hoch.
|
||||||
|
|
||||||
|
📌 Aktueller Tenant: [Tenant-Name]
|
||||||
|
📋 Befehle:
|
||||||
|
/tenant - Tenant wechseln
|
||||||
|
/list - Letzte 5 Uploads anzeigen
|
||||||
|
/status - Bot- und API-Status
|
||||||
|
/help - Hilfe anzeigen
|
||||||
|
```
|
||||||
|
|
||||||
|
**`/tenant`**
|
||||||
|
- Zeige Inline-Keyboard mit allen verfügbaren Tenants
|
||||||
|
- Tenants dynamisch von der API laden: `GET /api/tenants` (Auth erforderlich)
|
||||||
|
- Nach Auswahl: Tenant in Session speichern
|
||||||
|
- Bestätigung: `✅ Tenant gewechselt zu: [Name] (ID: [ID])`
|
||||||
|
|
||||||
|
**`/list`**
|
||||||
|
- Zeige die letzten 5 hochgeladenen Medien des aktuellen Tenants
|
||||||
|
- API: `GET /api/media?where[tenant][equals]=<ID>&sort=-createdAt&limit=5`
|
||||||
|
- Ausgabe als Liste mit Thumbnail-URL, Dateiname, Datum
|
||||||
|
|
||||||
|
**`/status`**
|
||||||
|
- Zeige:
|
||||||
|
- Bot-Uptime
|
||||||
|
- Payload API erreichbar? (Quick-Check: `GET /api/users/me`)
|
||||||
|
- Aktueller Tenant
|
||||||
|
- Token-Status (gültig bis...)
|
||||||
|
|
||||||
|
**`/help`**
|
||||||
|
- Ausführliche Hilfe mit allen Befehlen und Nutzungshinweisen
|
||||||
|
|
||||||
|
#### 4.4 Photo Handler (Kern-Funktionalität)
|
||||||
|
|
||||||
|
**Datei:** `src/telegram/handlers.ts` (fortgesetzt)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Handler für bot.on('message:photo')
|
||||||
|
//
|
||||||
|
// Ablauf:
|
||||||
|
// 1. Höchste verfügbare Auflösung wählen:
|
||||||
|
// ctx.message.photo ist ein Array von PhotoSize-Objekten,
|
||||||
|
// sortiert nach Größe. Letztes Element = höchste Auflösung.
|
||||||
|
//
|
||||||
|
// 2. File-Info abrufen:
|
||||||
|
// const file = await ctx.api.getFile(photo.file_id)
|
||||||
|
// Download-URL: https://api.telegram.org/file/bot<TOKEN>/<file.file_path>
|
||||||
|
//
|
||||||
|
// 3. Bild herunterladen (als Buffer):
|
||||||
|
// fetch() auf die Download-URL
|
||||||
|
//
|
||||||
|
// 4. Alt-Text bestimmen:
|
||||||
|
// - Falls Caption vorhanden (ctx.message.caption) → als Alt-Text verwenden
|
||||||
|
// - Falls nicht → Generiere: "Upload via Telegram – [Datum] [Uhrzeit]"
|
||||||
|
//
|
||||||
|
// 5. Statusmeldung senden:
|
||||||
|
// "⏳ Bild wird hochgeladen..."
|
||||||
|
//
|
||||||
|
// 6. An Payload API hochladen:
|
||||||
|
// payloadClient.uploadMedia(buffer, filename, { alt, tenantId })
|
||||||
|
//
|
||||||
|
// 7. Erfolgsmeldung:
|
||||||
|
// "✅ Upload erfolgreich!
|
||||||
|
// 📎 ID: [id]
|
||||||
|
// 📁 Dateiname: [filename]
|
||||||
|
// 🔗 URL: [url]
|
||||||
|
// 🏷️ Tenant: [tenant-name]
|
||||||
|
// 📐 Größen: thumbnail, small, medium, large, xlarge, 2k, og"
|
||||||
|
//
|
||||||
|
// 8. Bei Fehler:
|
||||||
|
// "❌ Upload fehlgeschlagen: [Fehlermeldung]"
|
||||||
|
// Logge den vollständigen Error serverseitig.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
- [ ] Bilder werden in höchster Auflösung heruntergeladen
|
||||||
|
- [ ] Caption wird als Alt-Text verwendet (falls vorhanden)
|
||||||
|
- [ ] Statusmeldung wird gesendet BEVOR der Upload startet
|
||||||
|
- [ ] Erfolgsmeldung enthält Media-ID und URL
|
||||||
|
- [ ] Fehler werden sauber abgefangen und dem User angezeigt
|
||||||
|
- [ ] Tenant-Zuordnung ist korrekt
|
||||||
|
|
||||||
|
#### 4.5 Document Handler (Erweitert)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Handler für bot.on('message:document')
|
||||||
|
//
|
||||||
|
// Akzeptiere nur Bildformate: jpg, jpeg, png, webp, avif, gif, svg
|
||||||
|
// Bei nicht unterstütztem Format:
|
||||||
|
// "⚠️ Format nicht unterstützt. Erlaubt: JPG, PNG, WebP, AVIF, GIF, SVG"
|
||||||
|
//
|
||||||
|
// Vorteil von Document-Upload gegenüber Photo:
|
||||||
|
// Telegram komprimiert Bilder die als Foto gesendet werden.
|
||||||
|
// Als Dokument gesendet bleibt die Originalqualität erhalten.
|
||||||
|
// → Dem User diesen Tipp in /help erklären.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.6 Album/Bulk Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Handler für mehrere Bilder gleichzeitig (Media Group / Album)
|
||||||
|
//
|
||||||
|
// Telegram sendet Alben als einzelne Messages mit gleicher media_group_id.
|
||||||
|
// Sammle alle Messages mit gleicher media_group_id über ein kurzes Zeitfenster
|
||||||
|
// (500ms Debounce), dann lade alle Bilder sequentiell hoch.
|
||||||
|
//
|
||||||
|
// Status: "⏳ Album erkannt: [N] Bilder werden hochgeladen..."
|
||||||
|
// Pro Bild: Fortschritt melden: "📤 [X]/[N] hochgeladen..."
|
||||||
|
// Am Ende: Zusammenfassung aller Upload-IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.7 Inline Keyboards
|
||||||
|
|
||||||
|
**Datei:** `src/telegram/keyboards.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Tenant-Auswahl Keyboard
|
||||||
|
// Dynamisch aus API geladen (GET /api/tenants)
|
||||||
|
// Format: 2 Buttons pro Reihe
|
||||||
|
// Jeder Button: callback_data = "tenant:<ID>"
|
||||||
|
//
|
||||||
|
// Beispiel:
|
||||||
|
// [ [porwoll.de] [C2S] ]
|
||||||
|
// [ [Gunshin] [BlogWoman] ]
|
||||||
|
|
||||||
|
// Callback Query Handler:
|
||||||
|
// Bei "tenant:<ID>" → Session updaten, Bestätigung senden
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Utilities
|
||||||
|
|
||||||
|
#### 5.1 Logger
|
||||||
|
|
||||||
|
**Datei:** `src/utils/logger.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Einfacher Logger mit Levels: debug, info, warn, error
|
||||||
|
// Format: [TIMESTAMP] [LEVEL] [MODULE] Message
|
||||||
|
// Beispiel: [2026-03-01 14:30:00] [INFO] [PayloadClient] Login erfolgreich
|
||||||
|
//
|
||||||
|
// Kein externes Logging-Framework nötig – console.log basiert reicht.
|
||||||
|
// LOG_LEVEL aus config bestimmt Mindest-Level.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 Download Helper
|
||||||
|
|
||||||
|
**Datei:** `src/utils/download.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Funktion zum Herunterladen einer Datei von einer URL als Buffer.
|
||||||
|
// Nutze native fetch().
|
||||||
|
// Timeout: 30 Sekunden
|
||||||
|
// Max. Dateigröße: 20 MB (Telegram-Limit)
|
||||||
|
// Bei Fehler: Spezifische Error-Messages (Timeout, Too Large, Network Error)
|
||||||
|
|
||||||
|
async function downloadFile(url: string): Promise<{ buffer: Buffer; mimeType: string }> { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. PM2 Konfiguration & Deployment
|
||||||
|
|
||||||
|
#### 6.1 PM2 Ecosystem File
|
||||||
|
|
||||||
|
**Datei:** `ecosystem.config.cjs`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'telegram-media-bot',
|
||||||
|
script: './dist/index.js',
|
||||||
|
instances: 1, // NUR 1 Instanz! Telegram Long-Polling verträgt kein Clustering
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '256M',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
},
|
||||||
|
error_file: './logs/error.log',
|
||||||
|
out_file: './logs/out.log',
|
||||||
|
merge_logs: true,
|
||||||
|
time: true
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**WICHTIG:** Der Bot nutzt Long-Polling (kein Webhook). Deshalb darf nur EINE Instanz laufen. Mehrere Instanzen führen zu Konflikten bei der Telegram API.
|
||||||
|
|
||||||
|
#### 6.2 Deployment auf sv-payload
|
||||||
|
|
||||||
|
Der Bot läuft als zusätzlicher PM2-Prozess auf **sv-payload (LXC 700, 10.10.181.100)**, wo bereits das Payload CMS per PM2 managed wird.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf sv-payload (als user 'payload')
|
||||||
|
cd /home/payload
|
||||||
|
git clone git@github.com:complexcaresolutions/telegram-media-bot.git
|
||||||
|
cd telegram-media-bot
|
||||||
|
pnpm install
|
||||||
|
cp .env.example .env
|
||||||
|
# → .env mit echten Werten befüllen
|
||||||
|
|
||||||
|
# Build und Start
|
||||||
|
pnpm build
|
||||||
|
pm2 start ecosystem.config.cjs
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3 GitHub Actions Workflow (Optional)
|
||||||
|
|
||||||
|
**Datei:** `.github/workflows/deploy.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy Telegram Bot
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: 37.24.237.181 # Externe IP (UDM Pro SE)
|
||||||
|
username: payload
|
||||||
|
key: ${{ secrets.STAGING_SSH_KEY }}
|
||||||
|
port: 22122 # SSH-Port über Port-Forwarding
|
||||||
|
script: |
|
||||||
|
cd /home/payload/telegram-media-bot
|
||||||
|
git pull origin main
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm build
|
||||||
|
pm2 restart telegram-media-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSH-Zugang:** Externer Zugang über UDM Pro SE Port-Forwarding (Port 22122 → sv-payload:22). Der SSH-Key `STAGING_SSH_KEY` ist bereits als GitHub Secret konfiguriert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Sicherheit
|
||||||
|
|
||||||
|
#### 7.1 Maßnahmen
|
||||||
|
|
||||||
|
1. **User-Whitelist:** Nur explizit erlaubte Telegram User-IDs dürfen den Bot nutzen
|
||||||
|
2. **Token-Sicherheit:** Payload JWT wird nur im Memory gehalten, nie auf Disk geschrieben
|
||||||
|
3. **Environment Variables:** Alle Secrets in `.env`, nie im Code
|
||||||
|
4. **Rate Limiting:** Maximal 10 Uploads pro Minute pro User (Bot-seitig implementiert)
|
||||||
|
5. **Dateigrößen-Limit:** Telegram begrenzt auf 20 MB, zusätzlich Bot-seitiges Limit von 20 MB
|
||||||
|
6. **Dateityp-Validierung:** Nur erlaubte MIME-Types akzeptieren (image/jpeg, image/png, image/webp, image/avif, image/gif, image/svg+xml)
|
||||||
|
7. **Kein Webhook-Modus:** Long-Polling vermeidet die Notwendigkeit eines öffentlich erreichbaren Endpoints
|
||||||
|
|
||||||
|
#### 7.2 Telegram Bot erstellen
|
||||||
|
|
||||||
|
1. Öffne Telegram und suche `@BotFather`
|
||||||
|
2. Sende `/newbot`
|
||||||
|
3. Name: `CCS Media Upload Bot` (oder ähnlich)
|
||||||
|
4. Username: `ccs_media_upload_bot` (muss eindeutig sein und auf `bot` enden)
|
||||||
|
5. Token sichern → in `.env` als `TELEGRAM_BOT_TOKEN` eintragen
|
||||||
|
6. Optional via BotFather:
|
||||||
|
- `/setdescription` – Bot-Beschreibung setzen
|
||||||
|
- `/setcommands` – Bot-Befehle registrieren:
|
||||||
|
```
|
||||||
|
start - Bot starten und Hilfe anzeigen
|
||||||
|
tenant - Ziel-Tenant wechseln
|
||||||
|
list - Letzte Uploads anzeigen
|
||||||
|
status - Bot- und API-Status prüfen
|
||||||
|
help - Ausführliche Hilfe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Error Handling & Edge Cases
|
||||||
|
|
||||||
|
#### 8.1 Zu behandelnde Szenarien
|
||||||
|
|
||||||
|
| Szenario | Verhalten |
|
||||||
|
|----------|-----------|
|
||||||
|
| Payload API nicht erreichbar | User informieren, Retry nach 30s, Log Error |
|
||||||
|
| JWT abgelaufen während Upload | Auto-Relogin + Retry (max. 1x) |
|
||||||
|
| Telegram File Download fehlschlägt | User informieren mit spezifischem Error |
|
||||||
|
| Bild zu groß (>20 MB) | `⚠️ Datei zu groß. Maximum: 20 MB` |
|
||||||
|
| Nicht unterstütztes Format | `⚠️ Format nicht unterstützt. Erlaubt: JPG, PNG, WebP, AVIF, GIF, SVG` |
|
||||||
|
| Kein Tenant gewählt | Default-Tenant verwenden, User informieren |
|
||||||
|
| Payload 403 (Tenant-Problem) | `❌ Zugriff verweigert. Prüfe die Tenant-Zuordnung.` |
|
||||||
|
| Payload 429 (Rate Limit) | `⏳ Zu viele Anfragen. Bitte warte [X] Sekunden.` |
|
||||||
|
| Bot-Start ohne gültige Config | Sofortiger Exit mit klarem Fehlertext |
|
||||||
|
| Album mit >10 Bildern | Hinweis dass max. 10 gleichzeitig verarbeitet werden |
|
||||||
|
|
||||||
|
#### 8.2 Graceful Shutdown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bei SIGINT/SIGTERM:
|
||||||
|
// 1. Bot-Polling stoppen
|
||||||
|
// 2. Laufende Uploads abwarten (max. 60s Timeout)
|
||||||
|
// 3. Prozess beenden
|
||||||
|
// PM2 sendet SIGINT, dann nach Timeout SIGKILL.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erfolgskriterien (Gesamt)
|
||||||
|
|
||||||
|
- [ ] `pnpm lint` (tsc --noEmit) ohne Errors
|
||||||
|
- [ ] `pnpm build` erfolgreich
|
||||||
|
- [ ] Bot startet und verbindet sich mit Telegram
|
||||||
|
- [ ] `/start` zeigt Begrüßung
|
||||||
|
- [ ] `/tenant` zeigt Inline-Keyboard mit Tenants aus der API
|
||||||
|
- [ ] Tenant-Wechsel funktioniert und wird in Session gespeichert
|
||||||
|
- [ ] Bild-Upload (als Foto) → Bild erscheint in Payload CMS Media Collection mit korrektem Tenant
|
||||||
|
- [ ] Bild-Upload (als Dokument) → Bild in Originalqualität hochgeladen
|
||||||
|
- [ ] Caption wird als Alt-Text übernommen
|
||||||
|
- [ ] Album-Upload funktioniert (mehrere Bilder)
|
||||||
|
- [ ] `/list` zeigt letzte 5 Uploads
|
||||||
|
- [ ] `/status` zeigt API-Status und Token-Validität
|
||||||
|
- [ ] Nicht-autorisierte User werden blockiert
|
||||||
|
- [ ] Fehler werden dem User als verständliche Meldungen angezeigt
|
||||||
|
- [ ] PM2 managed den Prozess mit Auto-Restart
|
||||||
|
- [ ] Logs werden in `./logs/` geschrieben
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Selbst-Prüfung
|
||||||
|
|
||||||
|
Nach jeder Iteration:
|
||||||
|
1. `pnpm lint` (tsc --noEmit)
|
||||||
|
2. `pnpm build`
|
||||||
|
3. Bei Fehler: korrigieren und wiederholen
|
||||||
|
4. Manueller Test-Flow:
|
||||||
|
- Bot starten mit `pnpm dev`
|
||||||
|
- `/start` senden
|
||||||
|
- `/tenant` → Tenant wählen
|
||||||
|
- Bild senden → Upload prüfen
|
||||||
|
- `/list` → Upload in Liste sichtbar
|
||||||
|
5. Fortschritt in README.md dokumentieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Escape Hatch
|
||||||
|
|
||||||
|
Nach 15 Iterationen ohne Fortschritt:
|
||||||
|
- Dokumentiere was blockiert in BLOCKERS.md
|
||||||
|
- Liste alle versuchten Ansätze auf
|
||||||
|
- Schlage 3 alternative Lösungswege vor
|
||||||
|
- Output <promise>BLOCKED</promise>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hinweise für die Implementierung
|
||||||
|
|
||||||
|
1. **Telegram Bot Token** muss vor dem Start via @BotFather erstellt und in `.env` eingetragen werden.
|
||||||
|
2. **Payload Admin Credentials** müssen einen User mit SuperAdmin-Rechten referenzieren (für Tenant-übergreifenden Zugriff).
|
||||||
|
3. **Grammy statt node-telegram-bot-api** – Grammy ist moderner, hat bessere TypeScript-Unterstützung und aktive Wartung.
|
||||||
|
4. **Native fetch statt axios** – Node.js 22 hat native fetch/FormData. Keine zusätzliche HTTP-Library nötig.
|
||||||
|
5. **Long-Polling statt Webhooks** – Einfacher zu deployen (kein öffentlicher Endpoint nötig), perfekt für den Use Case.
|
||||||
|
6. **Kein separater LXC-Container** nötig – der Bot läuft als zusätzlicher PM2-Prozess auf sv-payload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fertig?
|
||||||
|
|
||||||
|
Wenn ALLE Aufgaben erledigt sind UND alle Erfolgskriterien erfüllt sind:
|
||||||
|
|
||||||
|
<promise>TELEGRAM_BOT_COMPLETE</promise>
|
||||||
1320
scripts/seed-c2s.ts
Normal file
1320
scripts/seed-c2s.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue