Montag, 25 August 2025

Top 5 diese Woche

Ähnliche Tutorials

Ultimate Airbnb Klon

Mit unserem umfangreichen Airbnb Klon Script erhältst Du die perfekte Grundlage für Dein eigenes Online-Buchungsportal. Das System ist komplett in PHP, MySQL, JavaScript und AJAX entwickelt und kommt mit einer modernen, professionellen Oberfläche.

Das Script ist sofort einsatzbereit und bringt alle wichtigen Funktionen mit, die Du von einer großen Plattform wie Airbnb kennst. Dazu zählen Nutzerkonten mit sicherem OAuth-Login (Google/Facebook), E-Mail-Verifikation per PHPMailer, reCAPTCHA-Schutz gegen Spam sowie ein intelligentes Kalender-System für die Verwaltung von Buchungen und Verfügbarkeiten.

Vermieter können Inserate mit Bildern, Beschreibungen, Preisen und Kalenderdaten einstellen. Gäste können Unterkünfte suchen, filtern und direkt buchen. Ein Administrations-Panel ermöglicht die Moderation, Verwaltung von Nutzern, Inseraten und Buchungen sowie die Kontrolle über Benachrichtigungen und Reports.

Highlights:

  • Vollständig in PHP & MySQL entwickelt
  • OAuth-Login (Google/Facebook)
  • PHPMailer-Integration mit sicherer SMTP-Unterstützung
  • reCAPTCHA-Schutz gegen Bots & Spam
  • Umfangreiche Kalender-Logik für flexible Verfügbarkeiten
  • Inserate mit Bildern, Preisen, Beschreibung & Standortdaten
  • Benutzerprofile mit High-End-Sicherheit
  • Admin-Panel für Moderation & Verwaltung
  • Responsive Design für Desktop & Mobile
  • Erweiterbar & anpassbar für Deine eigene Marke

Ideal für:

  • Entwickler, die ein eigenes Buchungsportal aufbauen möchten
  • Startups mit Fokus auf Sharing Economy
  • Unternehmen, die eine Airbnb-ähnliche Plattform in kurzer Zeit starten wollen
<?php
session_start();
error_reporting(E_ALL);
ini_set('display_errors',1);

// === Konfiguration ===
$dbHost = 'localhost';
$dbUser = 'root';
$dbPass = '';
$dbName = 'airclone';
$uploadDir = __DIR__ . '/uploads';
if(!is_dir($uploadDir)) mkdir($uploadDir,0755,true);

// reCAPTCHA Keys
$recaptcha_site_key = 'DEINE_RECAPTCHA_SITEKEY';
$recaptcha_secret_key = 'DEINE_RECAPTCHA_SECRETKEY';

// Google OAuth Konfiguration
$google_client_id = 'DEINE_GOOGLE_CLIENT_ID';
$google_client_secret = 'DEIN_GOOGLE_CLIENT_SECRET';
$google_redirect_uri = (isset($_SERVER['HTTPS'])? 'https':'http').'://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'?action=oauth_callback&provider=google';

// PHPMailer konfiguration fallback
$smtp_host = 'smtp.example.com';
$smtp_user = 'user@example.com';
$smtp_pass = 'password';
$smtp_port = 587;
$smtp_from = 'noreply@'.($_SERVER['HTTP_HOST'] ?? 'example.com');
$smtp_from_name = 'AirbnbClone';

// Rate limit config
$register_cooldown_seconds = 30;

// === DB Verbindung ===
$mysqli = new mysqli($dbHost,$dbUser,$dbPass);
if($mysqli->connect_errno) {
    http_response_code(500);
    echo "Datenbank Verbindung fehlgeschlagen: ".$mysqli->connect_error;
    exit;
}
$mysqli->query("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$mysqli->select_db($dbName);
$mysqli->set_charset('utf8mb4');

// === Tabellen erzeugen ===
$create = [
"CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) DEFAULT NULL,
  name VARCHAR(150) DEFAULT '',
  avatar VARCHAR(255) DEFAULT '',
  verified TINYINT(1) DEFAULT 0,
  verify_token VARCHAR(255) DEFAULT NULL,
  oauth_provider VARCHAR(50) DEFAULT NULL,
  oauth_id VARCHAR(255) DEFAULT NULL,
  is_admin TINYINT(1) DEFAULT 0,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS listings (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  price DECIMAL(10,2) NOT NULL DEFAULT 0,
  address VARCHAR(255) DEFAULT '',
  city VARCHAR(120) DEFAULT '',
  country VARCHAR(120) DEFAULT '',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS listing_images (
  id INT AUTO_INCREMENT PRIMARY KEY,
  listing_id INT NOT NULL,
  filename VARCHAR(255),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS listing_availability (
  id INT AUTO_INCREMENT PRIMARY KEY,
  listing_id INT NOT NULL,
  start_date DATE NOT NULL,
  end_date DATE NOT NULL,
  type ENUM('available','blocked') NOT NULL DEFAULT 'available',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS bookings (
  id INT AUTO_INCREMENT PRIMARY KEY,
  listing_id INT NOT NULL,
  guest_id INT NOT NULL,
  start_date DATE,
  end_date DATE,
  status ENUM('pending','confirmed','rejected','cancelled') DEFAULT 'pending',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
  FOREIGN KEY (guest_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS reviews (
  id INT AUTO_INCREMENT PRIMARY KEY,
  listing_id INT NOT NULL,
  user_id INT NOT NULL,
  rating TINYINT NOT NULL,
  text TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",

"CREATE TABLE IF NOT EXISTS messages (
  id INT AUTO_INCREMENT PRIMARY KEY,
  from_user INT NOT NULL,
  to_user INT NOT NULL,
  listing_id INT DEFAULT NULL,
  text TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (from_user) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (to_user) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
];

foreach($create as $q) $mysqli->query($q);

// === Helferfunktionen ===
function json_response($data){ header('Content-Type: application/json; charset=utf-8'); echo json_encode($data); exit; }
function is_logged(){ return !empty($_SESSION['user_id']); }
function current_user_id(){ return $_SESSION['user_id'] ?? null; }
function current_is_admin(){ return (!empty($_SESSION['is_admin'])); }

// sichere Ausgabe
function h($s){ return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }

// Send mail helper mit PHPMailer fallback
function send_mail($to, $subject, $body, $smtp_config=null){
    // Wenn PHPMailer per Composer installiert ist nutzen
    if(class_exists('PHPMailer\PHPMailer\PHPMailer')){
        $mail = new PHPMailer\PHPMailer\PHPMailer(true);
        try {
            $mail->isSMTP();
            $mail->Host = $smtp_config['host'];
            $mail->SMTPAuth = true;
            $mail->Username = $smtp_config['user'];
            $mail->Password = $smtp_config['pass'];
            $mail->SMTPSecure = $smtp_config['secure'] ?? 'tls';
            $mail->Port = $smtp_config['port'];
            $mail->setFrom($smtp_config['from'], $smtp_config['from_name'] ?? '');
            $mail->addAddress($to);
            $mail->isHTML(true);
            $mail->Subject = $subject;
            $mail->Body = $body;
            $mail->send();
            return true;
        } catch(Exception $e){
            error_log('PHPMailer error: '.$mail->ErrorInfo);
            return false;
        }
    } else {
        // Fallback einfache mail
        $headers = "From: ".($smtp_config['from'] ?? 'noreply@'.($_SERVER['HTTP_HOST']))."\r\n";
        $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
        return @mail($to, $subject, $body, $headers);
    }
}

// reCAPTCHA prüfen
function verify_recaptcha($token, $secret){
    if(!$token) return false;
    $ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['secret'=>$secret,'response'=>$token]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    $resp = curl_exec($ch);
    curl_close($ch);
    $d = json_decode($resp, true);
    return !empty($d['success']);
}

// Google OAuth quick flow ohne libs
function google_oauth_start($client_id, $redirect_uri, $state){
    $scope = urlencode('openid email profile');
    $url = "https://accounts.google.com/o/oauth2/v2/auth?client_id=".urlencode($client_id).
           "&redirect_uri=".urlencode($redirect_uri).
           "&response_type=code&scope={$scope}&access_type=online&state=".urlencode($state);
    header('Location: '.$url);
    exit;
}
function google_oauth_exchange($code, $client_id, $client_secret, $redirect_uri){
    $post = [
        'code' => $code,
        'client_id' => $client_id,
        'client_secret' => $client_secret,
        'redirect_uri' => $redirect_uri,
        'grant_type' => 'authorization_code'
    ];
    $ch = curl_init('https://oauth2.googleapis.com/token');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
    $resp = curl_exec($ch); curl_close($ch);
    $data = json_decode($resp, true);
    return $data;
}
function google_oauth_userinfo($access_token){
    $ch = curl_init('https://www.googleapis.com/oauth2/v2/userinfo?access_token='.urlencode($access_token));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $resp = curl_exec($ch); curl_close($ch);
    return json_decode($resp, true);
}

// Datum Überlappung prüfen
function ranges_overlap($startA, $endA, $startB, $endB){
    return !($endA < $startB || $endB < $startA);
}

// Rate limit Registrierung
if(!isset($_SESSION['last_register'])) $_SESSION['last_register'] = 0;

// === Aktionen / Endpunkte AJAX ===
$action = $_REQUEST['action'] ?? null;

// Registrierung
if($action === 'register'){
    global $mysqli, $recaptcha_secret_key, $register_cooldown_seconds;
    $token = $_POST['recaptcha'] ?? null;
    if($recaptcha_secret_key && !verify_recaptcha($token, $recaptcha_secret_key)){
        json_response(['ok'=>false,'msg'=>'Captcha fehlgeschlagen']);
    }
    if(time() - ($_SESSION['last_register'] ?? 0) < $register_cooldown_seconds){
        json_response(['ok'=>false,'msg'=>'Bitte kurz warten']);
    }
    $email = $mysqli->real_escape_string(trim($_POST['email'] ?? ''));
    $password = $_POST['password'] ?? '';
    $name = $mysqli->real_escape_string(trim($_POST['name'] ?? ''));
    if(!filter_var($email, FILTER_VALIDATE_EMAIL)) json_response(['ok'=>false,'msg'=>'Ungültige E Mail']);
    if(strlen($password) < 6) json_response(['ok'=>false,'msg'=>'Passwort zu kurz']);
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $token_verify = bin2hex(random_bytes(16));
    $stmt = $mysqli->prepare("INSERT INTO users (email,password,name,verify_token) VALUES (?,?,?,?)");
    if(!$stmt) json_response(['ok'=>false,'msg'=>'DB Fehler']);
    $stmt->bind_param('ssss', $email, $hash, $name, $token_verify);
    if($stmt->execute()){
        $_SESSION['last_register'] = time();
        // Mail senden
        $link = (isset($_SERVER['HTTPS'])? 'https':'http').'://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'?action=verify&token='.$token_verify;
        $body = "<p>Hallo ".h($name)."</p><p>Bitte bestätige deine E Mail: <a href=\"{$link}\">Bestätigen</a></p>";
        send_mail($email, "Bestätige dein Konto", $body, ['host'=>$smtp_host,'user'=>$smtp_user,'pass'=>$smtp_pass,'port'=>$smtp_port,'from'=>$smtp_from,'from_name'=>$smtp_from_name]);
        json_response(['ok'=>true,'msg'=>'Registriert bitte Email prüfen']);
    } else {
        json_response(['ok'=>false,'msg'=>'E Mail bereits registriert']);
    }
}

// E Mail Verifikation (GET)
if(isset($_GET['action']) && $_GET['action'] === 'verify'){
    $token = $_GET['token'] ?? '';
    if(!$token) { echo "Token fehlt"; exit; }
    $stmt = $mysqli->prepare("SELECT id FROM users WHERE verify_token=? LIMIT 1");
    $stmt->bind_param('s', $token);
    $stmt->execute(); $res = $stmt->get_result();
    if($row = $res->fetch_assoc()){
        $id = $row['id'];
        $mysqli->query("UPDATE users SET verified=1, verify_token=NULL WHERE id=".(int)$id);
        echo "Email verifiziert du kannst dich nun anmelden";
        exit;
    } else {
        echo "Token ungültig";
        exit;
    }
}

// Login
if($action === 'login'){
    global $recaptcha_secret_key;
    $token = $_POST['recaptcha'] ?? null;
    if($recaptcha_secret_key && !verify_recaptcha($token, $recaptcha_secret_key)){
        json_response(['ok'=>false,'msg'=>'Captcha fehlgeschlagen']);
    }
    $email = $mysqli->real_escape_string(trim($_POST['email'] ?? ''));
    $password = $_POST['password'] ?? '';
    $stmt = $mysqli->prepare("SELECT id,password,verified,is_admin,name FROM users WHERE email=? LIMIT 1");
    $stmt->bind_param('s', $email);
    $stmt->execute(); $res = $stmt->get_result();
    if($row = $res->fetch_assoc()){
        if(!empty($row['password']) && password_verify($password, $row['password'])){
            if(!$row['verified']) json_response(['ok'=>false,'msg'=>'Email nicht verifiziert']);
            $_SESSION['user_id'] = $row['id'];
            $_SESSION['is_admin'] = (bool)$row['is_admin'];
            json_response(['ok'=>true]);
        } else json_response(['ok'=>false,'msg'=>'Login fehlgeschlagen']);
    } else json_response(['ok'=>false,'msg'=>'Login fehlgeschlagen']);
}

// Logout
if($action === 'logout'){
    session_destroy();
    json_response(['ok'=>true]);
}

// OAuth Start Google
if(isset($_GET['action']) && $_GET['action'] === 'oauth_start' && ($_GET['provider'] ?? '') === 'google'){
    $state = bin2hex(random_bytes(8));
    $_SESSION['oauth_state'] = $state;
    google_oauth_start($google_client_id, $google_redirect_uri, $state);
}

// OAuth Callback
if(isset($_GET['action']) && $_GET['action'] === 'oauth_callback' && ($_GET['provider'] ?? '') === 'google'){
    $code = $_GET['code'] ?? null;
    $state = $_GET['state'] ?? null;
    if(!$code || $state !== ($_SESSION['oauth_state'] ?? '')) { echo "OAuth Fehler"; exit; }
    $tokenResp = google_oauth_exchange($code, $google_client_id, $google_client_secret, $google_redirect_uri);
    if(empty($tokenResp['access_token'])) { echo "Token Fehler"; exit; }
    $userinfo = google_oauth_userinfo($tokenResp['access_token']);
    if(empty($userinfo['id'])) { echo "Userinfo Fehler"; exit; }
    // Nutzer in DB anlegen falls nicht vorhanden
    $googleId = $mysqli->real_escape_string($userinfo['id']);
    $email = $mysqli->real_escape_string($userinfo['email'] ?? '');
    $name = $mysqli->real_escape_string($userinfo['name'] ?? '');
    $avatar = $mysqli->real_escape_string($userinfo['picture'] ?? '');
    $stmt = $mysqli->prepare("SELECT id FROM users WHERE oauth_provider='google' AND oauth_id=? LIMIT 1");
    $stmt->bind_param('s', $googleId); $stmt->execute(); $res = $stmt->get_result();
    if($row = $res->fetch_assoc()){
        $uid = $row['id'];
    } else {
        // Falls email existiert, verknüpfen
        $uid = null;
        if($email){
            $stmt2 = $mysqli->prepare("SELECT id FROM users WHERE email=? LIMIT 1");
            $stmt2->bind_param('s',$email); $stmt2->execute(); $res2 = $stmt2->get_result();
            if($r2 = $res2->fetch_assoc()) $uid = $r2['id'];
        }
        if($uid){
            $mysqli->query("UPDATE users SET oauth_provider='google', oauth_id='".$googleId."', verified=1, avatar='".$avatar."', name='".$name."' WHERE id=".$uid);
        } else {
            $stmt3 = $mysqli->prepare("INSERT INTO users (email,name,avatar,verified,oauth_provider,oauth_id) VALUES (?,?,?,?,?,?)");
            $v1 = $email; $v2 = $name; $v3 = $avatar; $v4 = 1; $v5 = 'google'; $v6 = $googleId;
            $stmt3->bind_param('ssisss', $v1,$v2,$v3,$v4,$v5,$v6);
            $stmt3->execute();
            $uid = $mysqli->insert_id;
        }
    }
    // Login
    $_SESSION['user_id'] = $uid;
    $is_admin_row = $mysqli->query("SELECT is_admin FROM users WHERE id=".((int)$uid))->fetch_assoc();
    $_SESSION['is_admin'] = (bool)($is_admin_row['is_admin'] ?? false);
    header('Location: '.$_SERVER['PHP_SELF']);
    exit;
}

// Passwort Reset anfordern
if($action === 'password_reset_request'){
    $email = $mysqli->real_escape_string(trim($_POST['email'] ?? ''));
    $token = bin2hex(random_bytes(16));
    $stmt = $mysqli->prepare("UPDATE users SET verify_token=? WHERE email=?");
    $stmt->bind_param('ss', $token, $email); $stmt->execute();
    $link = (isset($_SERVER['HTTPS'])? 'https':'http').'://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'?action=reset&token='.$token;
    send_mail($email, "Passwort zurücksetzen", "Reset Link: <a href=\"{$link}\">{$link}</a>", ['host'=>$smtp_host,'user'=>$smtp_user,'pass'=>$smtp_pass,'port'=>$smtp_port,'from'=>$smtp_from,'from_name'=>$smtp_from_name]);
    json_response(['ok'=>true,'msg'=>'Reset Email gesendet falls Konto existiert']);
}

// Passwort Reset durchführen
if($action === 'password_reset'){
    $token = $_POST['token'] ?? '';
    $password = $_POST['password'] ?? '';
    if(strlen($password) < 6) json_response(['ok'=>false,'msg'=>'Passwort zu kurz']);
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $stmt = $mysqli->prepare("UPDATE users SET password=?, verify_token=NULL WHERE verify_token=?");
    $stmt->bind_param('ss', $hash, $token);
    $stmt->execute();
    json_response(['ok'=>true]);
}

// Listing erstellen
if($action === 'create_listing'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $title = $mysqli->real_escape_string(trim($_POST['title'] ?? ''));
    $desc = $mysqli->real_escape_string(trim($_POST['description'] ?? ''));
    $price = floatval($_POST['price'] ?? 0);
    $address = $mysqli->real_escape_string(trim($_POST['address'] ?? ''));
    $city = $mysqli->real_escape_string(trim($_POST['city'] ?? ''));
    $country = $mysqli->real_escape_string(trim($_POST['country'] ?? ''));
    $uid = (int)current_user_id();
    $stmt = $mysqli->prepare("INSERT INTO listings (user_id,title,description,price,address,city,country) VALUES (?,?,?,?,?,?,?)");
    $stmt->bind_param('issdsss',$uid,$title,$desc,$price,$address,$city,$country);
    $stmt->execute();
    $lid = $mysqli->insert_id;
    // Bilder speichern
    if(!empty($_FILES['images'])){
        foreach($_FILES['images']['tmp_name'] as $i => $tmp){
            if(!$tmp) continue;
            $name = uniqid().'_'.preg_replace('/[^a-zA-Z0-9_.]/','_',basename($_FILES['images']['name'][$i]));
            $dest = $uploadDir.'/'.$name;
            if(move_uploaded_file($tmp,$dest)){
                $stmt2 = $mysqli->prepare("INSERT INTO listing_images (listing_id,filename) VALUES (?,?)");
                $stmt2->bind_param('is', $lid, $name);
                $stmt2->execute();
            }
        }
    }
    json_response(['ok'=>true,'listing_id'=>$lid]);
}

// Availability verwalten
if($action === 'add_availability'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $lid = intval($_POST['listing_id']);
    $start = $_POST['start_date'];
    $end = $_POST['end_date'];
    $type = in_array($_POST['type'] ?? '', ['available','blocked']) ? $_POST['type'] : 'available';
    // Prüfen Eigentümer
    $stmt = $mysqli->prepare("SELECT user_id FROM listings WHERE id=?");
    $stmt->bind_param('i',$lid); $stmt->execute(); $res=$stmt->get_result();
    $row = $res->fetch_assoc(); if(!$row || $row['user_id'] != current_user_id()) json_response(['ok'=>false,'msg'=>'Keine Rechte']);
    $stmt2 = $mysqli->prepare("INSERT INTO listing_availability (listing_id,start_date,end_date,type) VALUES (?,?,?,?)");
    $stmt2->bind_param('isss',$lid,$start,$end,$type); $stmt2->execute();
    json_response(['ok'=>true]);
}

// Buchung anfragen
if($action === 'book'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $listing_id = intval($_POST['listing_id']);
    $start = $_POST['start_date'] ?? null;
    $end = $_POST['end_date'] ?? null;
    $guest = current_user_id();

    if(!$start || !$end) json_response(['ok'=>false,'msg'=>'Datum fehlt']);

    // 1 prüfe Konflikte mit bestätigten Buchungen
    $stmt = $mysqli->prepare("SELECT start_date,end_date FROM bookings WHERE listing_id=? AND status IN ('confirmed','pending')");
    $stmt->bind_param('i',$listing_id); $stmt->execute(); $res = $stmt->get_result();
    while($r = $res->fetch_assoc()){
        if(ranges_overlap($start, $end, $r['start_date'], $r['end_date'])){
            json_response(['ok'=>false,'msg'=>'Zeitraum bereits gebucht oder angefragt']);
        }
    }

    // 2 prüfe blocked ranges
    $stmt2 = $mysqli->prepare("SELECT start_date,end_date FROM listing_availability WHERE listing_id=? AND type='blocked'");
    $stmt2->bind_param('i',$listing_id); $stmt2->execute(); $res2 = $stmt2->get_result();
    while($b=$res2->fetch_assoc()){
        if(ranges_overlap($start,$end,$b['start_date'],$b['end_date'])){
            json_response(['ok'=>false,'msg'=>'Unterkunft in diesem Zeitraum blockiert']);
        }
    }

    // 3 wenn es available ranges gibt dann sicherstellen, dass die Anfrage komplett innerhalb einer available range liegt
    $hasAvailable = $mysqli->query("SELECT COUNT(*) as c FROM listing_availability WHERE listing_id=".intval($listing_id)." AND type='available'")->fetch_assoc()['c'];
    if($hasAvailable){
        $okInside = false;
        $res3 = $mysqli->query("SELECT start_date,end_date FROM listing_availability WHERE listing_id=".intval($listing_id)." AND type='available'");
        while($a = $res3->fetch_assoc()){
            if($start >= $a['start_date'] && $end <= $a['end_date']) { $okInside = true; break; }
        }
        if(!$okInside) json_response(['ok'=>false,'msg'=>'Zeitraum liegt außerhalb der verfügbaren Daten']);
    }

    // Wenn alles ok dann Buchung anlegen als pending und Mail an Host
    $stmt3 = $mysqli->prepare("INSERT INTO bookings (listing_id,guest_id,start_date,end_date,status) VALUES (?,?,?,?,?)");
    $status='pending';
    $stmt3->bind_param('iisss',$listing_id,$guest,$start,$end,$status);
    $stmt3->execute();
    $bookingId = $mysqli->insert_id;

    // Mail an Host senden
    $hostRow = $mysqli->query("SELECT u.email,u.name FROM users u JOIN listings l ON l.user_id=u.id WHERE l.id=".intval($listing_id))->fetch_assoc();
    if($hostRow){
        $hostMail = $hostRow['email'];
        $hostName = $hostRow['name'];
        $guestRow = $mysqli->query("SELECT name,email FROM users WHERE id=".intval($guest))->fetch_assoc();
        $body = "<p>Neue Buchungsanfrage von ".h($guestRow['name'])."</p><p>Zeitraum {$start} bis {$end}</p><p><a href='http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']."?admin=bookings'>Zum Dashboard</a></p>";
        send_mail($hostMail, "Neue Buchungsanfrage", $body, ['host'=>$smtp_host,'user'=>$smtp_user,'pass'=>$smtp_pass,'port'=>$smtp_port,'from'=>$smtp_from,'from_name'=>$smtp_from_name]);
    }

    json_response(['ok'=>true,'booking_id'=>$bookingId]);
}

// Buchung verwalten
if($action === 'manage_booking'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $bid = intval($_POST['booking_id']);
    $status = $_POST['status'] ?? 'pending';
    // prüfen ob Nutzer Host oder admin
    $stmt = $mysqli->prepare("SELECT b.listing_id,l.user_id,b.guest_id FROM bookings b JOIN listings l ON b.listing_id=l.id WHERE b.id=?");
    $stmt->bind_param('i',$bid); $stmt->execute(); $res = $stmt->get_result();
    if(!$row = $res->fetch_assoc()) json_response(['ok'=>false,'msg'=>'Nicht gefunden']);
    if($row['user_id'] != current_user_id() && !current_is_admin()) json_response(['ok'=>false,'msg'=>'Keine Rechte']);
    $stmt2 = $mysqli->prepare("UPDATE bookings SET status=? WHERE id=?");
    $stmt2->bind_param('si',$status,$bid); $stmt2->execute();
    // Mail an Gast
    $guest = $row['guest_id'];
    $guestRow = $mysqli->query("SELECT email,name FROM users WHERE id=".intval($guest))->fetch_assoc();
    if($guestRow){
        $body = "<p>Deine Buchung wurde auf {$status} gesetzt</p>";
        send_mail($guestRow['email'], "Buchungsstatus {$status}", $body, ['host'=>$smtp_host,'user'=>$smtp_user,'pass'=>$smtp_pass,'port'=>$smtp_port,'from'=>$smtp_from,'from_name'=>$smtp_from_name]);
    }
    json_response(['ok'=>true]);
}

// Reviews
if($action === 'add_review'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $listing_id = intval($_POST['listing_id']);
    $rating = intval($_POST['rating']);
    $text = $mysqli->real_escape_string(trim($_POST['text'] ?? ''));
    $uid = current_user_id();
    $stmt = $mysqli->prepare("INSERT INTO reviews (listing_id,user_id,rating,text) VALUES (?,?,?,?)");
    $stmt->bind_param('iiis',$listing_id,$uid,$rating,$text);
    $stmt->execute();
    json_response(['ok'=>true]);
}

// Messages
if($action === 'send_message'){
    if(!is_logged()) json_response(['ok'=>false,'msg'=>'Login erforderlich']);
    $to = intval($_POST['to_user']);
    $text = $mysqli->real_escape_string(trim($_POST['text'] ?? ''));
    $from = current_user_id();
    $stmt = $mysqli->prepare("INSERT INTO messages (from_user,to_user,text,listing_id) VALUES (?,?,?,NULL)");
    $stmt->bind_param('iis',$from,$to,$text);
    $stmt->execute();
    json_response(['ok'=>true]);
}

// Listings Liste
if(isset($_GET['action']) && $_GET['action'] === 'listings'){
    $city = $mysqli->real_escape_string(trim($_GET['city'] ?? ''));
    $q = "SELECT l.*, u.name as host_name FROM listings l JOIN users u ON l.user_id = u.id WHERE 1";
    if($city) $q .= " AND city LIKE '%".$city."%'";
    $res = $mysqli->query($q." ORDER BY l.created_at DESC LIMIT 200");
    $out = [];
    while($r = $res->fetch_assoc()){
        $lid = $r['id'];
        $imgs = [];
        $ir = $mysqli->query("SELECT filename FROM listing_images WHERE listing_id=".$lid);
        while($im = $ir->fetch_assoc()) $imgs[] = $im['filename'];
        $r['images'] = $imgs;
        $out[] = $r;
    }
    json_response($out);
}

// Einzel Listing
if(isset($_GET['action']) && $_GET['action'] === 'get_listing'){
    $lid = intval($_GET['listing_id'] ?? 0);
    $stmt = $mysqli->prepare("SELECT l.*, u.name as host_name, u.avatar as host_avatar, u.email as host_email FROM listings l JOIN users u ON l.user_id = u.id WHERE l.id=?");
    $stmt->bind_param('i',$lid); $stmt->execute(); $res = $stmt->get_result();
    $row = $res->fetch_assoc();
    if(!$row) json_response(['ok'=>false]);
    $imgs = []; $ir = $mysqli->query("SELECT filename FROM listing_images WHERE listing_id=".$lid);
    while($im = $ir->fetch_assoc()) $imgs[] = $im['filename'];
    $row['images'] = $imgs;
    // availability
    $av = $mysqli->query("SELECT start_date,end_date,type FROM listing_availability WHERE listing_id=".$lid)->fetch_all(MYSQLI_ASSOC);
    $row['availability'] = $av;
    json_response($row);
}

// Admin: CSV Export bookings
if($action === 'admin_export_bookings'){
    if(!current_is_admin()) json_response(['ok'=>false,'msg'=>'Admin erforderlich']);
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="bookings.csv"');
    $out = fopen('php://output','w');
    fputcsv($out, ['booking_id','listing_id','guest_id','start_date','end_date','status','created_at']);
    $res = $mysqli->query("SELECT * FROM bookings ORDER BY created_at DESC");
    while($r = $res->fetch_assoc()) fputcsv($out, [$r['id'],$r['listing_id'],$r['guest_id'],$r['start_date'],$r['end_date'],$r['status'],$r['created_at']]);
    fclose($out);
    exit;
}

// Admin: list users
if($action === 'admin_list_users'){
    if(!current_is_admin()) json_response(['ok'=>false,'msg'=>'Admin erforderlich']);
    $res = $mysqli->query("SELECT id,email,name,verified,is_admin,created_at FROM users ORDER BY id DESC LIMIT 500");
    $arr=[]; while($r=$res->fetch_assoc()) $arr[]=$r;
    json_response($arr);
}

// Admin delete user
if($action === 'admin_delete_user'){
    if(!current_is_admin()) json_response(['ok'=>false,'msg'=>'Admin erforderlich']);
    $uid = intval($_POST['user_id']);
    $mysqli->query("DELETE FROM users WHERE id=".$uid);
    json_response(['ok'=>true]);
}

// Wenn keine Aktion dann UI rendern
// === UI HTML ===
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dreamcodes - AirbnbClone</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<style>
body { background:#0b0b0f; color:#e6eef6; font-family:Inter, system-ui, Arial, sans-serif; }
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.04); border-radius:12px; padding:16px; }
.listing-img { width:100%; height:220px; object-fit:cover; border-radius:8px; }
.small { font-size:0.85rem; color:#aab9c6 }
.footer-link { color: #9fd8ff; text-decoration: underline; }
</style>
</head>
<body class="antialiased">
<header class="max-w-6xl mx-auto py-6 px-4 flex justify-between items-center">
  <div>
    <h1 class="text-2xl font-bold">AirClone</h1>
    <div class="small">Komplettlösung mit OAuth PHPMailer reCAPTCHA Kalender</div>
  </div>
  <div>
    <?php if(is_logged()): ?>
      <span class="small mr-3">eingeloggt als <?php
        $u = $mysqli->query("SELECT name FROM users WHERE id=".(int)current_user_id())->fetch_assoc();
        echo h($u['name'] ?: 'User');
      ?></span>
      <button id="logoutBtn" class="bg-sky-500 text-black px-3 py-2 rounded">Logout</button>
    <?php else: ?>
      <button id="showLogin" class="bg-sky-500 text-black px-3 py-2 rounded">Login</button>
      <button id="showRegister" class="ml-2 bg-emerald-500 text-black px-3 py-2 rounded">Registrieren</button>
      <a href="?action=oauth_start&provider=google" class="ml-2 inline-block bg-white text-black px-3 py-2 rounded">Login mit Google</a>
    <?php endif; ?>
  </div>
</header>

<main class="max-w-6xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-3 gap-6 pb-12">
  <section class="lg:col-span-2 space-y-6">
    <div class="card">
      <div class="flex justify-between items-center mb-4">
        <h2 class="text-lg font-semibold">Suche Unterkünfte</h2>
        <div>
          <input id="searchCity" placeholder="Stadt" class="px-3 py-2 rounded border bg-transparent small" />
          <button id="searchBtn" class="ml-2 bg-sky-500 text-black px-3 py-2 rounded">Suchen</button>
          <?php if(is_logged()): ?>
            <button id="createListingBtn" class="ml-2 bg-amber-500 text-black px-3 py-2 rounded">Neue Unterkunft</button>
          <?php endif; ?>
        </div>
      </div>
      <div id="listings" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
    </div>

    <div id="listingDetail" class="card hidden"></div>
  </section>

  <aside class="space-y-6">
    <div class="card">
      <h3 class="font-semibold mb-3">Dein Dashboard</h3>
      <?php if(is_logged()): ?>
        <div id="myListings"></div>
        <div id="myBookings" class="mt-4"></div>
      <?php else: ?>
        <p class="small">Bitte anmelden um Dashboard Funktionen zu sehen</p>
      <?php endif; ?>
    </div>

    <div class="card">
      <h3 class="font-semibold mb-2">Top Gastgeber</h3>
      <div id="topHosts" class="space-y-2 small"></div>
    </div>
  </aside>
</main>

<footer class="text-center py-6 small">© <?=date('Y')?> <a href="http://www.dreamcodes.net" target="_blank" class="footer-link">Dreamcodes</a> — AirbnbClone</footer>

<!-- Modal -->
<div id="modal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 hidden z-50">
  <div id="modalContent" class="bg-white text-black rounded-xl p-6 w-full max-w-2xl"></div>
</div>

<script>
const apiUrl = '<?php echo $_SERVER['PHP_SELF']; ?>';
function api(data, cb){
  fetch(apiUrl, { method:'POST', body: new URLSearchParams(data) })
    .then(r=>r.json()).then(cb).catch(e=>{ console.error(e); alert('Netzwerkfehler'); });
}
function showModal(html){ document.getElementById('modalContent').innerHTML = html; document.getElementById('modal').classList.remove('hidden'); }
function closeModal(){ document.getElementById('modal').classList.add('hidden'); }

// Logout
document.getElementById('logoutBtn')?.addEventListener('click', ()=> { api({action:'logout'}, r=> location.reload()); });

// Show login
document.getElementById('showLogin')?.addEventListener('click', ()=> {
  showModal(`<h3 class="text-lg font-semibold mb-3">Login</h3>
    <input id="loginEmail" placeholder="E Mail" class="border p-2 w-full mb-2" />
    <input id="loginPass" type="password" placeholder="Passwort" class="border p-2 w-full mb-2" />
    <div class="flex justify-between items-center">
      <div class="small">
        <a href="#" id="showReset">Passwort vergessen</a>
      </div>
      <div>
        <button id="doLogin" class="bg-sky-500 text-black px-3 py-2 rounded">Login</button>
      </div>
    </div>`);
  document.getElementById('doLogin').addEventListener('click', ()=>{
    const formData = new URLSearchParams();
    formData.append('action','login');
    formData.append('email',document.getElementById('loginEmail').value);
    formData.append('password',document.getElementById('loginPass').value);
    // reCAPTCHA wenn vorhanden
    if(window.grecaptcha){
      grecaptcha.ready(()=>grecaptcha.execute('<?php echo $recaptcha_site_key ?>',{action:'login'}).then(token=>{
        formData.append('recaptcha', token);
        fetch(apiUrl,{method:'POST',body:formData}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.msg||'Fehler'); });
      }));
    } else {
      fetch(apiUrl,{method:'POST',body:formData}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.msg||'Fehler'); });
    }
  });
  document.getElementById('showReset').addEventListener('click', ()=>{
    document.getElementById('modalContent').innerHTML = `<h3 class="text-lg font-semibold mb-3">Passwort zurücksetzen</h3>
      <input id="resetEmail" placeholder="E Mail" class="border p-2 w-full mb-2" />
      <div class="flex justify-end"><button id="doReset" class="bg-amber-500 px-3 py-2 rounded">Senden</button></div>`;
    document.getElementById('doReset').addEventListener('click', ()=>{
      fetch(apiUrl,{method:'POST',body:new URLSearchParams({action:'password_reset_request', email: document.getElementById('resetEmail').value})})
        .then(r=>r.json()).then(d=>{ alert(d.msg||'Gesendet'); closeModal(); });
    });
  });
});

// Show register
document.getElementById('showRegister')?.addEventListener('click', ()=> {
  showModal(`<h3 class="text-lg font-semibold mb-3">Register</h3>
    <input id="regName" placeholder="Name" class="border p-2 w-full mb-2" />
    <input id="regEmail" placeholder="E Mail" class="border p-2 w-full mb-2" />
    <input id="regPass" type="password" placeholder="Passwort" class="border p-2 w-full mb-2" />
    <div class="g-recaptcha" data-sitekey="<?php echo $recaptcha_site_key ?>"></div>
    <div class="flex justify-end">
      <button id="doRegister" class="bg-emerald-500 text-black px-3 py-2 rounded">Registrieren</button>
    </div>`);
  document.getElementById('doRegister').addEventListener('click', ()=>{
    // wenn grecaptcha verfügbar nutze token
    const email = document.getElementById('regEmail').value;
    const name = document.getElementById('regName').value;
    const pass = document.getElementById('regPass').value;
    if(window.grecaptcha){
      grecaptcha.ready(()=>grecaptcha.execute('<?php echo $recaptcha_site_key ?>',{action:'register'}).then(token=>{
        api({action:'register', email: email, password: pass, name: name, recaptcha: token}, r=>{ alert(r.msg||'OK'); if(r.ok) closeModal(); });
      }));
    } else {
      api({action:'register', email: email, password: pass, name: name}, r=>{ alert(r.msg||'OK'); if(r.ok) closeModal(); });
    }
  });
});

// Laden Listings
function loadListings(city=''){
  fetch(apiUrl+'?action=listings&city='+encodeURIComponent(city)).then(r=>r.json()).then(data=>{
    const target = document.getElementById('listings');
    target.innerHTML = '';
    data.forEach(l=>{
      const img = l.images[0] ? '<?php echo basename($uploadDir); ?>/'+l.images[0] : 'https://via.placeholder.com/400x220?text=No+Image';
      const el = document.createElement('div');
      el.className = 'p-3 card';
      el.innerHTML = `<img src="${img}" class="listing-img mb-2" />
        <h3 class="font-semibold">${escapeHtml(l.title)}</h3>
        <div class="small">Host ${escapeHtml(l.host_name)}</div>
        <div class="mt-2">Ab ${l.price} € pro Nacht</div>
        <div class="mt-3 flex gap-2">
          <button class="bg-sky-500 text-black px-3 py-1 rounded" onclick="showListing(${l.id})">Details</button>
          <button class="bg-amber-500 text-black px-3 py-1 rounded" onclick="bookQuick(${l.id})">Anfrage</button>
        </div>`;
      target.appendChild(el);
    });
  });
}
function escapeHtml(s){ if(!s) return ''; return s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
document.getElementById('searchBtn').addEventListener('click', ()=> loadListings(document.getElementById('searchCity').value));
loadListings();

// Listing Detail
function showListing(id){
  fetch(apiUrl+'?action=get_listing&listing_id='+id).then(r=>r.json()).then(l=>{
    let imgs = l.images.length ? l.images.map(i=>`<img src="<?php echo basename($uploadDir); ?>/${i}" class="mb-2 rounded w-full"/>`).join('') : '';
    let html = `<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
      <div class="md:col-span-2">
        ${imgs}
        <h2 class="text-xl font-bold mt-2">${escapeHtml(l.title)}</h2>
        <div class="small">Host ${escapeHtml(l.host_name)}</div>
        <p class="mt-2">${escapeHtml(l.description)}</p>
        <div class="mt-4">Adresse: ${escapeHtml(l.address)} ${escapeHtml(l.city)} ${escapeHtml(l.country)}</div>
      </div>
      <div>
        <div class="card">
          <div class="text-lg font-semibold">Preis ${l.price} € Nacht</div>
          <div class="mt-2">
            <label>Start</label><input id="startDate" type="date" class="border p-2 w-full mb-2" />
            <label>Ende</label><input id="endDate" type="date" class="border p-2 w-full mb-2" />
            <button class="bg-emerald-500 text-black px-3 py-2 rounded w-full" onclick="book(${l.id})">Anfrage senden</button>
          </div>
          <div class="mt-4">
            <h4 class="font-semibold">Verfügbarkeit</h4>
            ${(l.availability||[]).map(a=>`<div class="small">${a.type}: ${a.start_date} bis ${a.end_date}</div>`).join('')}
          </div>
        </div>
      </div>
    </div>`;
    document.getElementById('listingDetail').innerHTML = html;
    document.getElementById('listingDetail').classList.remove('hidden');
    window.scrollTo({top:0,behavior:'smooth'});
  });
}

// Schnell buchen Demo
function bookQuick(id){
  if(!confirm('Buchungsanfrage senden?')) return;
  api({action:'book', listing_id:id, start_date:new Date().toISOString().slice(0,10), end_date:new Date().toISOString().slice(0,10)}, r=>{
    if(r.ok) alert('Anfrage gesendet'); else alert(r.msg || 'Fehler');
  });
}
function book(id){
  const s = document.getElementById('startDate').value;
  const e = document.getElementById('endDate').value;
  if(!s || !e) return alert('Datum wählen');
  api({action:'book', listing_id:id, start_date:s, end_date:e}, r=>{ if(r.ok) alert('Anfrage gesendet'); else alert(r.msg || 'Fehler'); });
}

// Create listing modal
document.getElementById('createListingBtn')?.addEventListener('click', ()=> {
  showModal(`<h3 class="text-lg font-semibold mb-3">Neue Unterkunft erstellen</h3>
    <input id="liTitle" placeholder="Titel" class="border p-2 w-full mb-2" />
    <textarea id="liDesc" placeholder="Beschreibung" class="border p-2 w-full mb-2"></textarea>
    <input id="liPrice" placeholder="Preis pro Nacht" class="border p-2 w-full mb-2" />
    <input id="liAddress" placeholder="Adresse" class="border p-2 w-full mb-2" />
    <input id="liCity" placeholder="Stadt" class="border p-2 w-full mb-2" />
    <input id="liCountry" placeholder="Land" class="border p-2 w-full mb-2" />
    <input id="liImages" type="file" multiple class="mb-2" />
    <div class="flex justify-end">
      <button id="doCreateListing" class="bg-amber-500 text-black px-3 py-2 rounded">Erstellen</button>
    </div>`);
  document.getElementById('doCreateListing').addEventListener('click', async ()=>{
    const fd = new FormData();
    fd.append('action','create_listing');
    fd.append('title',document.getElementById('liTitle').value);
    fd.append('description',document.getElementById('liDesc').value);
    fd.append('price',document.getElementById('liPrice').value);
    fd.append('address',document.getElementById('liAddress').value);
    fd.append('city',document.getElementById('liCity').value);
    fd.append('country',document.getElementById('liCountry').value);
    const files = document.getElementById('liImages').files;
    for(let i=0;i<files.length;i++) fd.append('images[]',files[i]);
    const res = await fetch(apiUrl,{method:'POST',body:fd});
    const j = await res.json();
    if(j.ok){ alert('Erstellt'); closeModal(); loadListings(); } else alert(j.msg||'Fehler');
  });
});

document.getElementById('modal')?.addEventListener('click', (e)=>{ if(e.target.id === 'modal') closeModal(); });
</script>
</body>
</html>
Vorheriges Tutorial
Nächstes Tutorial

Hier etwas für dich dabei?