Contact Form Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a public contact form (contact.php) that saves submissions to DB, plus a read-only Anfragen tab in the dashboard.
Architecture: Single-file PRG pattern for contact.php (GET shows form, POST validates + saves + redirects). Dashboard uses URL-based tab switching (?tab=mandanten / ?tab=anfragen). All DB access via existing Db::pdo(). CSRF via existing Csrf::field() / Csrf::verify().
Tech Stack: PHP 8+, PDO/MySQL, Tailwind CSS (landing.css for public pages, app.css for dashboard), existing Csrf.php + Db.php
File Map
| File | Action | What changes |
|---|---|---|
init.sql |
Modify | Append contact_messages table |
contact.php |
Create | Public form + POST handler |
dashboard.php |
Modify | Add $tab + $anfragen data loading |
templates/dashboard.php |
Modify | Tab nav + Anfragen tab content |
index.php |
Modify | Add Kontakt link to nav |
service.php |
Modify | Add Kontakt link to nav |
Task 1: DB Schema — contact_messages Tabelle
Files:
- Modify: init.sql
- [ ] Step 1: Tabelle an init.sql anhängen
Append this block at the end of /home/branko/www.wptransformer.org/init.sql:
CREATE TABLE IF NOT EXISTS `contact_messages` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL,
`email` VARCHAR(200) NOT NULL,
`paket` ENUM('starter','professional','agentur') NOT NULL,
`nachricht` TEXT NOT NULL,
`ip_adresse` VARCHAR(45) NOT NULL,
`gelesen` TINYINT(1) DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- [ ] Step 2: Syntax prüfen
grep -c "contact_messages" /home/branko/www.wptransformer.org/init.sql
Expected: 2 (CREATE TABLE und Table-Name in der Definition)
- [ ] Step 3: Commit
git add init.sql
git commit -m "feat: add contact_messages table to schema"
Task 2: contact.php erstellen
Files:
- Create: contact.php
- [ ] Step 1: Datei erstellen
Create /home/branko/www.wptransformer.org/contact.php with this exact content:
<?php
require __DIR__ . '/config.php';
require __DIR__ . '/classes/Csrf.php';
if (session_status() === PHP_SESSION_NONE) {
session_name(SESSION_NAME);
session_start();
}
$errors = [];
$values = ['name' => '', 'email' => '', 'paket' => '', 'nachricht' => ''];
$sent = isset($_GET['sent']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Csrf::verify();
$values['name'] = trim($_POST['name'] ?? '');
$values['email'] = trim($_POST['email'] ?? '');
$values['paket'] = trim($_POST['paket'] ?? '');
$values['nachricht'] = trim($_POST['nachricht'] ?? '');
if ($values['name'] === '' || mb_strlen($values['name']) > 100) {
$errors['name'] = 'Bitte gib deinen Namen ein (max. 100 Zeichen).';
}
if (!filter_var($values['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Bitte gib eine gültige E-Mail-Adresse ein.';
}
if (!in_array($values['paket'], ['starter', 'professional', 'agentur'], true)) {
$errors['paket'] = 'Bitte wähle ein Paket aus.';
}
if ($values['nachricht'] === '' || mb_strlen($values['nachricht']) > 2000) {
$errors['nachricht'] = 'Bitte gib eine Nachricht ein (max. 2000 Zeichen).';
}
if (empty($errors)) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
Db::pdo()->prepare(
'INSERT INTO contact_messages (name, email, paket, nachricht, ip_adresse) VALUES (?, ?, ?, ?, ?)'
)->execute([$values['name'], $values['email'], $values['paket'], $values['nachricht'], $ip]);
header('Location: /contact.php?sent=1');
exit;
}
}
?><!doctype html>
<html lang="de" class="scroll-smooth">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Kontakt · WP Transformer</title>
<meta name="description" content="Kontaktiere das WP Transformer Team — wir beraten dich zu deinem WordPress-Migrationsprojekt.">
<link rel="stylesheet" href="/assets/css/landing.css">
<style>
body { background: #030b1a; }
.glow-sm { text-shadow: 0 0 12px rgba(21, 209, 105, 0.3); }
</style>
</head>
<body class="font-sans text-ink antialiased min-h-screen flex flex-col">
<!-- NAV -->
<nav class="fixed top-0 inset-x-0 z-50 border-b border-white/5 bg-surface/90 backdrop-blur-md">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<a href="/" class="font-mono font-semibold text-signal glow-sm tracking-tight text-sm">
WP<span class="text-ink-muted">→</span>TRANSFORMER
</a>
<div class="flex items-center gap-6 text-sm">
<a href="/service.php" class="text-ink-muted text-xs font-mono hover:text-ink">Leistungen</a>
<a href="/contact.php" class="text-signal text-xs font-mono">Kontakt</a>
<a href="/login.php" class="inline-flex items-center gap-1.5 px-4 py-1.5 rounded text-sm font-medium
bg-signal/10 text-signal border border-signal/30 hover:bg-signal hover:text-navy
transition-all duration-200 cursor-pointer">
Dashboard
</a>
</div>
</div>
</nav>
<main class="flex-1 pt-20 pb-20">
<!-- HEADER -->
<section class="px-6 py-16 text-center">
<div class="max-w-2xl mx-auto">
<p class="inline-block px-3 py-1 rounded-full text-xs font-mono font-medium
border border-signal/20 text-signal mb-6 tracking-wide uppercase"
style="background:rgba(21,209,105,0.08)">
Kontakt
</p>
<h1 class="text-3xl sm:text-4xl font-bold mb-4 leading-tight">
Projekt anfragen.
</h1>
<p class="text-ink-muted leading-relaxed">
Wähle dein Paket und beschreib dein Projekt — wir melden uns innerhalb von 24 Stunden.
</p>
</div>
</section>
<!-- FORM / SUCCESS -->
<section class="px-6 pb-16">
<div class="max-w-xl mx-auto">
<?php if ($sent): ?>
<div class="rounded-xl border border-signal/30 p-8 text-center"
style="background:rgba(21,209,105,0.06)">
<div class="text-3xl mb-4">✓</div>
<h2 class="text-xl font-bold mb-2 text-signal">Nachricht eingegangen.</h2>
<p class="text-ink-muted text-sm">Wir melden uns innerhalb von 24 Stunden bei dir.</p>
<a href="/" class="inline-block mt-6 text-xs font-mono text-ink-muted hover:text-ink">← Zurück zur Startseite</a>
</div>
<?php else: ?>
<form method="post" action="/contact.php" class="space-y-5"
style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:0.75rem;padding:2rem">
<?= Csrf::field() ?>
<!-- Name -->
<div>
<label class="block text-xs font-mono text-ink-muted mb-1.5" for="name">
Name <span class="text-red-400">*</span>
</label>
<input type="text" id="name" name="name" required maxlength="100"
value="<?= htmlspecialchars($values['name']) ?>"
class="w-full px-3 py-2 rounded text-sm bg-surface border text-ink
focus:outline-none focus:border-signal/50 transition-colors
<?= isset($errors['name']) ? 'border-red-500' : 'border-surface-muted' ?>">
<?php if (isset($errors['name'])): ?>
<p class="mt-1 text-xs text-red-400"><?= htmlspecialchars($errors['name']) ?></p>
<?php endif; ?>
</div>
<!-- E-Mail -->
<div>
<label class="block text-xs font-mono text-ink-muted mb-1.5" for="email">
E-Mail <span class="text-red-400">*</span>
</label>
<input type="email" id="email" name="email" required
value="<?= htmlspecialchars($values['email']) ?>"
class="w-full px-3 py-2 rounded text-sm bg-surface border text-ink
focus:outline-none focus:border-signal/50 transition-colors
<?= isset($errors['email']) ? 'border-red-500' : 'border-surface-muted' ?>">
<?php if (isset($errors['email'])): ?>
<p class="mt-1 text-xs text-red-400"><?= htmlspecialchars($errors['email']) ?></p>
<?php endif; ?>
</div>
<!-- Paket -->
<div>
<label class="block text-xs font-mono text-ink-muted mb-1.5" for="paket">
Paket <span class="text-red-400">*</span>
</label>
<select id="paket" name="paket" required
class="w-full px-3 py-2 rounded text-sm bg-surface border text-ink
focus:outline-none focus:border-signal/50 transition-colors
<?= isset($errors['paket']) ? 'border-red-500' : 'border-surface-muted' ?>">
<option value="">— Bitte wählen —</option>
<option value="starter" <?= $values['paket']==='starter' ? 'selected' : '' ?>>Starter</option>
<option value="professional" <?= $values['paket']==='professional' ? 'selected' : '' ?>>Professional</option>
<option value="agentur" <?= $values['paket']==='agentur' ? 'selected' : '' ?>>Agentur</option>
</select>
<?php if (isset($errors['paket'])): ?>
<p class="mt-1 text-xs text-red-400"><?= htmlspecialchars($errors['paket']) ?></p>
<?php endif; ?>
</div>
<!-- Nachricht -->
<div>
<label class="block text-xs font-mono text-ink-muted mb-1.5" for="nachricht">
Nachricht <span class="text-red-400">*</span>
</label>
<textarea id="nachricht" name="nachricht" required rows="5" maxlength="2000"
class="w-full px-3 py-2 rounded text-sm bg-surface border text-ink
focus:outline-none focus:border-signal/50 transition-colors resize-y
<?= isset($errors['nachricht']) ? 'border-red-500' : 'border-surface-muted' ?>"><?= htmlspecialchars($values['nachricht']) ?></textarea>
<?php if (isset($errors['nachricht'])): ?>
<p class="mt-1 text-xs text-red-400"><?= htmlspecialchars($errors['nachricht']) ?></p>
<?php endif; ?>
</div>
<button type="submit"
class="w-full py-2.5 rounded text-sm font-medium font-mono
bg-signal text-navy hover:bg-signal/90 transition-colors">
Anfrage senden →
</button>
<p class="text-center text-xs text-ink-faint">
Deine Daten werden ausschließlich zur Bearbeitung deiner Anfrage gespeichert.
</p>
</form>
<?php endif; ?>
</div>
</section>
</main>
<footer class="border-t border-white/5 px-6 py-4 text-center text-xs text-ink-muted font-mono">
<a href="/impressum.php" class="hover:text-ink mr-4">Impressum</a>
<a href="/datenschutz.php" class="hover:text-ink">Datenschutz</a>
</footer>
</body>
</html>
- [ ] Step 2: Syntax prüfen
php -l /home/branko/www.wptransformer.org/contact.php
Expected: No syntax errors detected in contact.php
- [ ] Step 3: Commit
git add contact.php
git commit -m "feat: add contact.php with form and DB storage"
Task 3: Dashboard — Anfragen-Tab
Files:
- Modify: dashboard.php (add $tab + $anfragen)
- Modify: templates/dashboard.php (add tab nav + Anfragen content)
- [ ] Step 1:
dashboard.php— Tab-Logik hinzufügen
In /home/branko/www.wptransformer.org/dashboard.php, replace:
$tenants = TenantManager::all();
$statuses = [];
foreach ($tenants as $t) $statuses[$t['slug']] = TenantManager::status($t['slug']);
with:
$tenants = TenantManager::all();
$statuses = [];
foreach ($tenants as $t) $statuses[$t['slug']] = TenantManager::status($t['slug']);
$tab = $_GET['tab'] ?? 'mandanten';
$anfragen = [];
if ($tab === 'anfragen') {
$anfragen = Db::pdo()->query(
'SELECT id, name, email, paket, nachricht, ip_adresse, created_at FROM contact_messages ORDER BY created_at DESC'
)->fetchAll();
}
- [ ] Step 2: Syntax prüfen
php -l /home/branko/www.wptransformer.org/dashboard.php
Expected: No syntax errors detected
- [ ] Step 3:
templates/dashboard.php— Tab-Nav und Anfragen-Inhalt
In /home/branko/www.wptransformer.org/templates/dashboard.php, replace the opening line:
<div class="flex items-end justify-between mb-8">
with:
<!-- TAB NAV -->
<div class="flex gap-1 mb-8 border-b border-surface-muted">
<a href="/dashboard.php?tab=mandanten"
class="px-4 py-2 text-sm font-mono <?= ($tab ?? 'mandanten') === 'mandanten'
? 'text-accent border-b-2 border-accent'
: 'text-ink-muted hover:text-ink' ?>">
Mandanten
</a>
<a href="/dashboard.php?tab=anfragen"
class="px-4 py-2 text-sm font-mono <?= ($tab ?? '') === 'anfragen'
? 'text-accent border-b-2 border-accent'
: 'text-ink-muted hover:text-ink' ?>">
Anfragen
</a>
</div>
<?php if (($tab ?? 'mandanten') === 'anfragen'): ?>
<!-- ANFRAGEN TAB -->
<?php if (empty($anfragen)): ?>
<div class="text-center py-20 text-ink-faint font-mono text-sm border border-dashed border-surface-muted rounded-xl">
Noch keine Anfragen eingegangen.
</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-surface-muted text-left text-xs font-mono text-ink-muted">
<th class="pb-3 pr-4">Datum</th>
<th class="pb-3 pr-4">Name</th>
<th class="pb-3 pr-4">E-Mail</th>
<th class="pb-3 pr-4">Paket</th>
<th class="pb-3">Nachricht</th>
</tr>
</thead>
<tbody>
<?php foreach ($anfragen as $a): ?>
<tr class="border-b border-surface-muted/50 align-top">
<td class="py-3 pr-4 text-xs font-mono text-ink-faint whitespace-nowrap">
<?= htmlspecialchars(substr($a['created_at'], 0, 16)) ?>
</td>
<td class="py-3 pr-4 text-ink"><?= htmlspecialchars($a['name']) ?></td>
<td class="py-3 pr-4 text-ink-muted text-xs font-mono"><?= htmlspecialchars($a['email']) ?></td>
<td class="py-3 pr-4">
<span class="px-2 py-0.5 rounded text-xs font-mono bg-accent/10 text-accent">
<?= htmlspecialchars($a['paket']) ?>
</span>
</td>
<td class="py-3 text-ink-muted text-xs">
<?php if (mb_strlen($a['nachricht']) > 80): ?>
<details>
<summary class="cursor-pointer hover:text-ink"><?= htmlspecialchars(mb_substr($a['nachricht'], 0, 80)) ?>…</summary>
<div class="mt-2 text-ink"><?= htmlspecialchars($a['nachricht']) ?></div>
</details>
<?php else: ?>
<?= htmlspecialchars($a['nachricht']) ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php else: ?>
<!-- MANDANTEN TAB -->
<div class="flex items-end justify-between mb-8">
Then find the very last line of the file (the closing <?php endif; ?> after the tenants grid) and append <?php endif; ?> to close the outer if ($tab === 'mandanten') block:
The last line currently is:
<?php endif; ?>
Replace it with:
<?php endif; ?>
<?php endif; ?>
- [ ] Step 4: Syntax prüfen
php -l /home/branko/www.wptransformer.org/templates/dashboard.php
Expected: No syntax errors detected
- [ ] Step 5: Commit
git add dashboard.php templates/dashboard.php
git commit -m "feat: add Anfragen tab to dashboard"
Task 4: Nav-Links + Final Commit
Files:
- Modify: index.php (Kontakt-Link in Nav)
- Modify: service.php (Kontakt-Link in Nav)
- [ ] Step 1:
index.php— Kontakt-Link ergänzen
In /home/branko/www.wptransformer.org/index.php, replace:
<a href="/login.php"
class="inline-flex items-center gap-1.5 px-4 py-1.5 rounded text-sm font-medium
bg-signal/10 text-signal border border-signal/30 hover:bg-signal hover:text-navy
transition-all duration-200 cursor-pointer">
Dashboard öffnen
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M3 8h10M9 4l4 4-4 4"/></svg>
</a>
with:
<div class="flex items-center gap-6">
<a href="/service.php" class="text-ink-muted text-xs font-mono hover:text-ink">Leistungen</a>
<a href="/contact.php" class="text-ink-muted text-xs font-mono hover:text-ink">Kontakt</a>
<a href="/login.php"
class="inline-flex items-center gap-1.5 px-4 py-1.5 rounded text-sm font-medium
bg-signal/10 text-signal border border-signal/30 hover:bg-signal hover:text-navy
transition-all duration-200 cursor-pointer">
Dashboard öffnen
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M3 8h10M9 4l4 4-4 4"/></svg>
</a>
</div>
- [ ] Step 2:
service.php— Kontakt-Link ergänzen
In /home/branko/www.wptransformer.org/service.php, replace:
<a href="/service.php" class="text-ink text-xs font-mono">Leistungen</a>
with:
<a href="/service.php" class="text-ink text-xs font-mono">Leistungen</a>
<a href="/contact.php" class="text-ink-muted text-xs font-mono hover:text-ink">Kontakt</a>
- [ ] Step 3: Syntax prüfen
php -l /home/branko/www.wptransformer.org/index.php && php -l /home/branko/www.wptransformer.org/service.php
Expected: No syntax errors detected für beide Dateien.
- [ ] Step 4: Final commit
git add index.php service.php
git commit -m "feat: add Kontakt nav link to index.php and service.php"