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&nbsp;ö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&nbsp;ö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"