Montag, 25 August 2025

Top 5 diese Woche

Ähnliche Tutorials

Change.org Klon

Mit unserem hochwertigen Portal-Script erhalten Sie ein voll funktionsfähiges Petitionssystem im modernen Design. Es ist nicht nur ein Nachbau von Change.org, sondern bietet sogar erweiterte Funktionen, die das Original ergänzen. Perfekt für Organisationen, Communities, NGOs oder private Initiativen, die online Unterschriften sammeln möchten.

Hauptfunktionen:

  • Petitionen erstellen & teilen – Benutzer können eigene Kampagnen starten und veröffentlichen
  • E-Mail-Verifikation – Jede Unterschrift wird durch echte E-Mail-Bestätigung abgesichert
  • CSV-Export – Unterschriften serverseitig exportieren, für Auswertungen und Dokumentation
  • Automatischer Mail-Export – Unterschriftenlisten regelmäßig per Mail zugeschickt bekommen
  • OAuth Login (Google) – Schneller & sicherer Login ohne Passwortchaos
  • Captcha-Schutz (hCaptcha / reCAPTCHA) – Starker Schutz gegen Bots & Spam
  • Moderations-Panel – Verwaltung mit Bulk-Löschung, Spam-Markierung und Kontrolle der Kampagnen
  • MySQL/SQLite Migrationstool – Einfache Installation und flexible Datenbankwahl

Besonderheiten:

  • Modernes, responsives Layout (funktioniert auf Smartphone, Tablet und Desktop)
  • PHPMailer Integration mit TLS – Sichere und zuverlässige Mail-Zustellung
  • Performance-optimierter Code in einer einzigen Datei
  • Erweiterbar und individuell anpassbar

Technische Daten:

  • Sprachen: PHP, AJAX, JavaScript
  • Datenbanken: MySQL oder SQLite
  • Hosting: Läuft auf jedem Standard-Webserver mit PHP-Unterstützung
<?php
session_start();
date_default_timezone_set('UTC');
header('X-Content-Type-Options: nosniff');
/*
  Installation Hinweise
  1. composer require phpmailer/phpmailer
  2. PHP 7.4 oder höher empfohlen
  3. Schreibrechte für data_*.json
  4. SMTP Daten, Captcha Keys in CONFIG setzen
*/

// ===================== CONFIG =====================
define('ROOT_URL', 'https://your-domain.example/petitions.php');

// PHPMailer Nutzung, setze true wenn composer installiert und du PHPMailer nutzen willst
define('SMTP_USE_PHPMailer', true);
// PHPMailer / SMTP Einstellungen
define('SMTP_HOST', 'smtp.example.com');
define('SMTP_PORT', 587);
define('SMTP_USER', 'user@example.com');
define('SMTP_PASS', 'password');
define('SMTP_FROM', 'no-reply@example.com');
define('SMTP_FROM_NAME', 'Petitions Portal');

// Fallback simple mail absender
// Admin Passwort
define('ADMIN_PASSWORD', 'admin123');

// Captcha Config, setze keys nach Wahl
// reCAPTCHA v2 oder v3
define('RECAPTCHA_SECRET', ''); // wenn leer, reCAPTCHA deaktiviert
// hCaptcha
define('HCAPTCHA_SECRET', ''); // wenn leer, hCaptcha deaktiviert

// Spam protection
$SPAM_BLACKLIST = ['spamword1','buy now','viagra'];
define('RATE_LIMIT_SIGNATURES_PER_IP_PER_HOUR', 12);
define('RATE_LIMIT_COMMENTS_PER_IP_PER_HOUR', 36);

// Dateien
define('USERS_FILE','data_users.json');
define('PETITIONS_FILE','data_petitions.json');
define('SIGS_FILE','data_signatures.json');
define('COMMENTS_FILE','data_comments.json');
define('META_FILE','data_meta.json');
define('ADMIN_LOG','admin_actions.log');

// ensure data files
$files = [USERS_FILE,PETITIONS_FILE,SIGS_FILE,COMMENTS_FILE,META_FILE];
foreach($files as $f){ if(!file_exists($f)) file_put_contents($f, json_encode([])); @chmod($f, 0666); }

// PHPMailer autoload
if(SMTP_USE_PHPMailer){
  if(file_exists(__DIR__.'/vendor/autoload.php')) require_once __DIR__.'/vendor/autoload.php';
}

// ===================== Hilfsfunktionen =====================
function readJson($file){ $c=@file_get_contents($file); return $c ? json_decode($c, true) : []; }
function writeJson($file,$data){ $tmp=$file.'.tmp'; file_put_contents($tmp,json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); rename($tmp,$file); }
function sanitize($s){ return trim(htmlspecialchars((string)$s, ENT_QUOTES)); }
function ipAddr(){ return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; }
function randomToken($len=48){ return bin2hex(random_bytes(max(8,intval($len/2)))); }
function logAdmin($msg){ $line = date('c').' '.$_SERVER['REMOTE_ADDR'].' '.$msg.PHP_EOL; @file_put_contents(ADMIN_LOG, $line, FILE_APPEND); }

// Captcha prüfung
function verifyCaptcha($token){
  // prüft reCAPTCHA zuerst, ansonsten hCaptcha, return true wenn ok oder beide keys leer
  if(defined('RECAPTCHA_SECRET') && RECAPTCHA_SECRET !== ''){
    $resp = @file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=".urlencode(RECAPTCHA_SECRET)."&response=".urlencode($token)."&remoteip=".urlencode(ipAddr()));
    $j = $resp ? json_decode($resp, true) : null;
    if($j && !empty($j['success'])) return true;
    return false;
  }
  if(defined('HCAPTCHA_SECRET') && HCAPTCHA_SECRET !== ''){
    $resp = @file_get_contents("https://hcaptcha.com/siteverify", false, stream_context_create(['http'=>['method'=>'POST','header'=>"Content-type: application/x-www-form-urlencoded\r\n",'content'=>http_build_query(['secret'=>HCAPTCHA_SECRET,'response'=>$token,'remoteip'=>ipAddr()])]]));
    $j = $resp ? json_decode($resp, true) : null;
    if($j && !empty($j['success'])) return true;
    return false;
  }
  // keine captcha keys, allow
  return true;
}

// Spam check
function isSpam($text){
  global $SPAM_BLACKLIST;
  $t = mb_strtolower($text);
  foreach($SPAM_BLACKLIST as $b) if($b !== '' && mb_strpos($t, mb_strtolower($b)) !== false) return true;
  return false;
}

// Rate limit
function countRecentByIp($type, $ip, $hours=1){
  $cut = time() - $hours*3600;
  if($type==='sig'){ $sigs = readJson(SIGS_FILE); $count=0; foreach($sigs as $pid=>$arr) foreach($arr as $s) if(isset($s['ip']) && $s['ip']===$ip && ($s['time'] ?? 0) > $cut) $count++; return $count; }
  if($type==='com'){ $coms = readJson(COMMENTS_FILE); $count=0; foreach($coms as $pid=>$arr) foreach($arr as $c) if(isset($c['ip']) && $c['ip']===$ip && ($c['time'] ?? 0) > $cut) $count++; return $count; }
  return 0;
}

// Mailer Funktionalitaet, nutzt PHPMailer wenn konfiguriert
function sendMailSimple($to, $subject, $body, $alt=''){
  if(defined('SMTP_USE_PHPMailer') && SMTP_USE_PHPMailer && class_exists('PHPMailer\PHPMailer\PHPMailer')){
    try{
      $mail = new PHPMailer\PHPMailer\PHPMailer(true);
      $mail->isSMTP(); $mail->Host = SMTP_HOST; $mail->SMTPAuth = true; $mail->Username = SMTP_USER; $mail->Password = SMTP_PASS;
      $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = SMTP_PORT;
      $mail->setFrom(SMTP_FROM, SMTP_FROM_NAME);
      $mail->addAddress($to);
      $mail->isHTML(true); $mail->Subject = $subject; $mail->Body = $body; if($alt) $mail->AltBody = $alt;
      $mail->send(); return true;
    }catch(Exception $e){
      error_log('Mailer error '.$e->getMessage());
      // fallback mail
    }
  }
  $headers = "From: ".SMTP_FROM_NAME." <".SMTP_FROM."> \r\n";
  $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
  return mail($to, $subject, $body, $headers);
}

// CSV helper
function generateCSVForPetition($id){
  $sigs = readJson(SIGS_FILE);
  if(!isset($sigs[$id])) return null;
  $fh = fopen('php://memory', 'r+');
  fputcsv($fh, ['name','email','comment','ip','time','verified']);
  foreach($sigs[$id] as $s) fputcsv($fh, [$s['name'] ?? '', $s['email'] ?? '', $s['comment'] ?? '', $s['ip'] ?? '', date('c',$s['time'] ?? 0), isset($s['verified'])?($s['verified']?1:0):0]);
  rewind($fh); $csv = stream_get_contents($fh); fclose($fh);
  return $csv;
}

// ===================== AJAX Endpoints =====================
if(isset($_GET['action'])){
  $action = $_GET['action'];

  // Admin login
  if($action === 'admin_login' && $_SERVER['REQUEST_METHOD']==='POST'){
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $pw = $data['pw'] ?? '';
    if($pw === ADMIN_PASSWORD){ $_SESSION['admin'] = true; logAdmin('admin login success'); echo json_encode(['ok'=>true]); exit; }
    echo json_encode(['ok'=>false]); exit;
  }

  // Bulk admin delete
  if($action==='admin_bulk_delete' && $_SERVER['REQUEST_METHOD']==='POST'){
    if(empty($_SESSION['admin'])) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'auth']); exit; }
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $petitions = readJson(PETITIONS_FILE);
    $sigs = readJson(SIGS_FILE); $comments = readJson(COMMENTS_FILE);
    $ids = $data['ids'] ?? [];
    foreach($ids as $id){ $petitions = array_filter($petitions, fn($p)=>$p['id']!==$id); unset($sigs[$id]); unset($comments[$id]); logAdmin("deleted petition $id"); }
    writeJson(PETITIONS_FILE, array_values($petitions)); writeJson(SIGS_FILE,$sigs); writeJson(COMMENTS_FILE,$comments);
    echo json_encode(['ok'=>true]); exit;
  }

  // Bulk mark spam signatures
  if($action==='admin_mark_spam' && $_SERVER['REQUEST_METHOD']==='POST'){
    if(empty($_SESSION['admin'])) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'auth']); exit; }
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $pid = $data['petition_id'] ?? ''; $indexes = $data['indexes'] ?? [];
    $sigs = readJson(SIGS_FILE);
    if(!isset($sigs[$pid])) { echo json_encode(['ok'=>false,'error'=>'notfound']); exit; }
    foreach($indexes as $i){ if(isset($sigs[$pid][$i])){ $sigs[$pid][$i]['spam']=true; logAdmin("marked spam signature $i on $pid"); } }
    writeJson(SIGS_FILE,$sigs);
    echo json_encode(['ok'=>true]); exit;
  }

  // Export signatures per mail
  if($action==='admin_send_csv_email' && $_SERVER['REQUEST_METHOD']==='POST'){
    if(empty($_SESSION['admin'])) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'auth']); exit; }
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $id = $data['id'] ?? ''; $to = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL) ?: '';
    if(!$id || !$to) { echo json_encode(['ok'=>false,'error'=>'missing']); exit; }
    $csv = generateCSVForPetition($id);
    if($csv === null){ echo json_encode(['ok'=>false,'error'=>'no signatures']); exit; }
    $subject = "Signaturen Export $id";
    $body = "<p>Im Anhang die CSV Export Datei für Petition $id</p>";
    // wenn PHPMailer dann attach memory file
    if(defined('SMTP_USE_PHPMailer') && SMTP_USE_PHPMailer && class_exists('PHPMailer\PHPMailer\PHPMailer')){
      try{
        $mail = new PHPMailer\PHPMailer\PHPMailer(true);
        $mail->isSMTP(); $mail->Host = SMTP_HOST; $mail->SMTPAuth = true; $mail->Username = SMTP_USER; $mail->Password = SMTP_PASS;
        $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = SMTP_PORT;
        $mail->setFrom(SMTP_FROM, SMTP_FROM_NAME); $mail->addAddress($to); $mail->Subject = $subject; $mail->isHTML(true); $mail->Body = $body;
        $mail->addStringAttachment($csv, "signatures_$id.csv", 'base64', 'text/csv');
        $mail->send(); logAdmin("sent csv for $id to $to");
        echo json_encode(['ok'=>true]); exit;
      }catch(Exception $e){ error_log("mail error ".$e->getMessage()); echo json_encode(['ok'=>false,'error'=>'mailer']); exit; }
    } else {
      // fallback write temp file and use mail with no attachment fallback link
      $tmp = sys_get_temp_dir()."/sign_$id_".time().".csv";
      file_put_contents($tmp, $csv);
      // try to send with multipart email simple, but many hosts block attachments via mail()
      $sent = sendMailSimple($to, $subject, $body.'<p>CSV local path not attached on this host</p>');
      if($sent){ logAdmin("sent csv notice for $id to $to"); echo json_encode(['ok'=>true]); exit; }
      echo json_encode(['ok'=>false,'error'=>'sendfail']); exit;
    }
  }

  // create petition simplified
  if($action === 'create_petition' && $_SERVER['REQUEST_METHOD']==='POST'){
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $title = sanitize($data['title'] ?? ''); $body = sanitize($data['body'] ?? ''); $target = sanitize($data['target'] ?? '');
    $goal = max(10, intval($data['goal'] ?? 100)); $tags = array_filter(array_map('sanitize', explode(',', $data['tags'] ?? '')));
    if(!$title || !$body) { echo json_encode(['ok'=>false,'error'=>'missing']); exit; }
    $petitions = readJson(PETITIONS_FILE);
    $id = 'p'.randomToken(6);
    $now = time(); $owner = $_SESSION['uid'] ?? 'anon';
    $petition = ['id'=>$id,'title'=>$title,'body'=>$body,'target'=>$target,'goal'=>$goal,'tags'=>$tags,'owner'=>$owner,'created'=>$now,'updated'=>$now,'views'=>0,'status'=>'open','featured'=>false];
    array_unshift($petitions, $petition);
    writeJson(PETITIONS_FILE,$petitions);
    echo json_encode(['ok'=>true,'petition'=>$petition]); exit;
  }

  // sign petition with captcha and email verification flow
  if($action === 'sign' && $_SERVER['REQUEST_METHOD']==='POST'){
    $data = json_decode(file_get_contents('php://input'), true) ?: [];
    $id = sanitize($data['id'] ?? ''); $name = sanitize($data['name'] ?? ($_SESSION['uname'] ?? 'Gast'));
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL) ? strtolower($data['email']) : '';
    $comment = sanitize($data['comment'] ?? ''); $captcha = $data['captcha'] ?? '';
    if(!$id || !$name) { echo json_encode(['ok'=>false,'error'=>'missing']); exit; }
    if(!verifyCaptcha($captcha)){ echo json_encode(['ok'=>false,'error'=>'captcha']); exit; }
    if(isSpam($name) || isSpam($comment)) { echo json_encode(['ok'=>false,'error'=>'spam']); exit; }
    $ip = ipAddr();
    if(countRecentByIp('sig',$ip,1) >= RATE_LIMIT_SIGNATURES_PER_IP_PER_HOUR){ echo json_encode(['ok'=>false,'error'=>'ratelimit']); exit; }
    $petitions = readJson(PETITIONS_FILE); $exists=false; foreach($petitions as $p) if($p['id']===$id) $exists=true;
    if(!$exists) { echo json_encode(['ok'=>false,'error'=>'notfound']); exit; }
    $sigs = readJson(SIGS_FILE); if(!isset($sigs[$id])) $sigs[$id]=[];
    if($email){
      $meta = readJson(META_FILE); if(!isset($meta['pending_signatures'])) $meta['pending_signatures']=[];
      $token = randomToken(32);
      $meta['pending_signatures'][$token] = ['petition'=>$id,'name'=>$name,'email'=>$email,'comment'=>$comment,'ip'=>$ip,'time'=>time()];
      writeJson(META_FILE,$meta);
      $link = ROOT_URL.'?action=verify_email&token='.$token;
      $subject = 'Bitte bestätige deine Unterschrift';
      $body = "<p>Hallo ".htmlspecialchars($name)."</p><p>Bitte bestätige deine Unterschrift mit diesem Link:</p><p><a href=\"".htmlspecialchars($link)."\">Unterschrift bestätigen</a></p>";
      sendMailSimple($email,$subject,$body);
      echo json_encode(['ok'=>true,'verify_email'=>true]); exit;
    } else {
      $entry = ['uid'=>$_SESSION['uid'] ?? null,'name'=>$name,'email'=>'','comment'=>$comment,'ip'=>$ip,'time'=>time(),'verified'=>false];
      $sigs[$id][] = $entry; writeJson(SIGS_FILE,$sigs); echo json_encode(['ok'=>true,'signature'=>$entry]); exit;
    }
  }

  // verify token
  if($action === 'verify_email'){
    $token = sanitize($_GET['token'] ?? '');
    if(!$token){ echo "Fehler token fehlt"; exit; }
    $meta = readJson(META_FILE);
    if(!isset($meta['pending_signatures'][$token])){ echo "Ungültiger oder abgelaufener token"; exit; }
    $entry = $meta['pending_signatures'][$token];
    $sigs = readJson(SIGS_FILE); if(!isset($sigs[$entry['petition']])) $sigs[$entry['petition']]=[];
    $sig = ['uid'=>null,'name'=>$entry['name'],'email'=>$entry['email'],'comment'=>$entry['comment'],'ip'=>$entry['ip'],'time'=>time(),'verified'=>true];
    $sigs[$entry['petition']][] = $sig; writeJson(SIGS_FILE,$sigs);
    // mark user verified if exists
    $users = readJson(USERS_FILE);
    foreach($users as &$u){ if(isset($u['email']) && $u['email']===$entry['email']){ $u['verified']=true; } }
    writeJson(USERS_FILE,$users);
    unset($meta['pending_signatures'][$token]); writeJson(META_FILE,$meta);
    echo "<html><body><h2>Danke, Unterschrift bestätigt</h2><p><a href=\"".ROOT_URL."\">Zurück</a></p></body></html>"; exit;
  }

  // embed, list, view, comment etc reusing earlier logic
  if($action==='list'){
    header('Content-Type: application/json; charset=utf-8');
    $q = sanitize($_GET['q'] ?? ''); $tag = sanitize($_GET['tag'] ?? ''); $sort = sanitize($_GET['sort'] ?? 'new');
    $page = max(1, intval($_GET['page'] ?? 1)); $per = min(50, max(6, intval($_GET['per'] ?? 12)));
    $petitions = readJson(PETITIONS_FILE);
    $filtered = array_filter($petitions, function($p) use($q,$tag){ if($q){ $hay = mb_strtolower($p['title'].' '.$p['body'].' '.implode(' ',$p['tags']??[])); if(mb_strpos($hay, mb_strtolower($q)) === false) return false; } if($tag){ if(!in_array($tag,$p['tags']??[])) return false; } return true; });
    $sigs = readJson(SIGS_FILE);
    $list = array_map(function($p) use($sigs){ $count = isset($sigs[$p['id']]) ? count($sigs[$p['id']]) : 0; $p['signatures']=$count; $p['progress']=round(($count/max(1,$p['goal']))*100); return $p; }, $filtered);
    usort($list, function($a,$b) use($sort){ if($sort==='popular') return ($b['signatures'] <=> $a['signatures']); if($sort==='goal') return ($b['progress'] <=> $a['progress']); if($sort==='trending') return (($b['views']+$b['signatures']*3) <=> ($a['views']+$a['signatures']*3)); return ($b['created'] <=> $a['created']); });
    $total = count($list); $start = ($page-1)*$per; $pageData = array_slice($list,$start,$per);
    echo json_encode(['ok'=>true,'total'=>$total,'page'=>$page,'per'=>$per,'data'=>array_values($pageData)]); exit;
  }

  if($action==='view'){
    header('Content-Type: application/json; charset=utf-8');
    $id = sanitize($_GET['id'] ?? ''); if(!$id){ echo json_encode(['ok'=>false]); exit; }
    $petitions = readJson(PETITIONS_FILE); $found=null; foreach($petitions as &$p){ if($p['id']===$id){ $found=$p; $p['views']=($p['views']??0)+1; $p['updated']=time(); break; } }
    if($found){ writeJson(PETITIONS_FILE,$petitions); $sigs = readJson(SIGS_FILE); $comments = readJson(COMMENTS_FILE); $found['signatures']=isset($sigs[$id])?$sigs[$id]:[]; $found['comments']=isset($comments[$id])?$comments[$id]:[]; echo json_encode(['ok'=>true,'petition'=>$found]); exit; } else { echo json_encode(['ok'=>false,'error'=>'notfound']); exit; }
  }

  if($action==='comment' && $_SERVER['REQUEST_METHOD']==='POST'){
    $data = json_decode(file_get_contents('php://input'), true) ?: []; $id=sanitize($data['id'] ?? ''); $name=sanitize($data['name'] ?? 'Gast'); $text=sanitize($data['text'] ?? '');
    if(!$id || !$text){ echo json_encode(['ok'=>false,'error'=>'missing']); exit; } if(isSpam($name) || isSpam($text)){ echo json_encode(['ok'=>false,'error'=>'spam']); exit; }
    $ip = ipAddr(); if(countRecentByIp('com',$ip,1) >= RATE_LIMIT_COMMENTS_PER_IP_PER_HOUR){ echo json_encode(['ok'=>false,'error'=>'ratelimit']); exit; }
    $comments = readJson(COMMENTS_FILE); if(!isset($comments[$id])) $comments[$id]=[]; $c = ['id'=>'c'.randomToken(4),'name'=>$name,'text'=>$text,'ip'=>$ip,'time'=>time()]; $comments[$id][]=$c; writeJson(COMMENTS_FILE,$comments); echo json_encode(['ok'=>true,'comment'=>$c]); exit;
  }

  // admin export csv direct download
  if($action==='export_csv'){
    if(empty($_SESSION['admin'])) { http_response_code(403); echo "auth required"; exit; }
    $id = sanitize($_GET['id'] ?? ''); if(!$id){ http_response_code(400); echo "missing id"; exit; }
    $csv = generateCSVForPetition($id);
    if($csv === null){ http_response_code(404); echo "not found"; exit; }
    header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="signatures_'.$id.'.csv"');
    echo $csv; exit;
  }

  echo json_encode(['ok'=>false,'error'=>'unknown']); exit;
}

// ===================== UI =====================
// Die UI ist erweitert, Admin Panel Bulk Aktionen, Captcha Hinweis, Moderation Tools
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Petitions Portal erweitert</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
  body{font-family:Inter,system-ui,Arial,sans-serif;background:linear-gradient(180deg,#071022 0%, #06101a 100%);color:#e6eef8}
  .glass{background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02)); border:1px solid rgba(255,255,255,0.04); backdrop-filter:blur(6px); border-radius:12px}
  .card{box-shadow:0 8px 30px rgba(2,6,23,0.7)}
  summary::-webkit-details-marker{display:none}
  .muted{color:#93c5fd}
  .small{font-size:13px;color:#9fb8e6}
  .tag{background:rgba(6,182,212,0.08);color:#9be9f5;padding:6px 8px;border-radius:999px;font-weight:600;font-size:12px}
</style>
</head>
<body class="antialiased min-h-screen">

<header class="py-6">
  <div class="max-w-7xl mx-auto px-4 flex items-center justify-between">
    <div>
      <h1 class="text-2xl font-extrabold">Petitions Portal</h1>
      <p class="small mt-1">PHPMailer SMTP, Captcha, Moderation, CSV per Mail</p>
    </div>
    <div class="flex items-center gap-3">
      <div id="userArea" class="text-right small"></div>
      <button id="oauthGoogle" class="px-3 py-2 rounded-lg glass">Mit Google einloggen</button>
      <button id="adminBtn" class="px-3 py-2 rounded-lg glass">Admin</button>
    </div>
  </div>
</header>

<main class="max-w-7xl mx-auto px-4 pb-20 grid grid-cols-1 lg:grid-cols-3 gap-6">
  <section class="lg:col-span-2 space-y-4">
    <div class="flex items-center justify-between gap-4">
      <div class="flex items-center gap-3">
        <input id="search" placeholder="Suchen Titel Stichworte" class="px-3 py-2 rounded-lg glass small w-80" />
        <select id="filterTag" class="px-3 py-2 rounded-lg glass small"><option value="">Alle Themen</option></select>
        <select id="sort" class="px-3 py-2 rounded-lg glass small"><option value="new">Neueste</option><option value="popular">Beliebt</option><option value="goal">Ziel Fortschritt</option><option value="trending">Trending</option></select>
      </div>
      <div class="flex items-center gap-2">
        <button id="embedBtn" class="small glass px-3 py-2 rounded-lg">Widget</button>
      </div>
    </div>

    <div id="feed" class="space-y-4"></div>
    <div id="pager" class="flex items-center gap-3 justify-center small muted"></div>
  </section>

  <aside class="space-y-4">
    <div id="createBox" class="glass p-4 card">
      <h3 class="font-bold">Petition erstellen</h3>
      <div class="mt-2 space-y-2 small">
        <input id="ctitle" placeholder="Titel" class="w-full px-3 py-2 rounded-lg glass" />
        <input id="ctarget" placeholder="Adressat" class="w-full px-3 py-2 rounded-lg glass" />
        <input id="ctags" placeholder="Tags Komma getrennt" class="w-full px-3 py-2 rounded-lg glass" />
        <textarea id="cbody" rows="6" placeholder="Kurztext" class="w-full px-3 py-2 rounded-lg glass"></textarea>
        <div class="flex gap-2">
          <input id="cgoal" type="number" min="10" value="500" class="px-3 py-2 rounded-lg glass w-28" />
          <input id="cimage" placeholder="Bild URL optional" class="px-3 py-2 rounded-lg glass flex-1" />
        </div>
        <div class="flex justify-end">
          <button id="createSubmit" class="px-4 py-2 rounded-lg bg-sky-500 text-white font-bold">Erstellen</button>
        </div>
      </div>
    </div>

    <div id="moderation" class="glass p-3 card small hidden">
      <h4 class="font-bold">Moderation</h4>
      <div class="mt-2 space-y-2">
        <div class="small">Bulk Auswahl, Markieren als Spam, CSV Mail Versand</div>
        <div class="mt-2">
          <button id="bulkDeleteBtn" class="w-full px-3 py-2 rounded-lg glass">Bulk Löschen</button>
          <button id="bulkSpamBtn" class="w-full px-3 py-2 rounded-lg glass mt-2">Markiere ausgewählte Signaturen als Spam</button>
        </div>
        <div class="mt-3">
          <input id="csvEmail" placeholder="Empfänger Email für CSV" class="w-full px-3 py-2 rounded-lg glass" />
          <button id="sendCsvBtn" class="w-full px-3 py-2 rounded-lg glass mt-2">CSV per Mail senden</button>
        </div>
      </div>
    </div>

    <div class="glass p-3 card small">
      <h4 class="font-bold">Export Tools</h4>
      <div class="mt-2 space-y-2">
        <button id="exportAll" class="w-full px-3 py-2 rounded-lg glass">Export JSON aller Petitionen</button>
        <button id="downloadCSV" class="w-full px-3 py-2 rounded-lg glass">CSV einer Petition</button>
      </div>
    </div>

  </aside>
</main>

<footer class="py-8 small text-center muted">
  © <?=date('Y')?> <a href="https://dreamcodes.net" target="_blank">Dreamcodes</a> — Nur mit Zustimmung teilen
</footer>

<!-- client scripts -->
<script>
const api = (path, opts={}) => fetch(path, opts).then(r=>r.json().catch(()=>r.text()));
let state = {page:1, per:12, tag:'', sort:'new', q:''};
let selectedForBulk = []; // petition ids for bulk actions

document.addEventListener('DOMContentLoaded', ()=>{ loadTags(); loadFeed(); setupHandlers(); updateUserArea(); });

async function loadTags(){
  const res = await api('?action=list&per=1000&page=1');
  if(!res.ok) return;
  const tags = new Set(); res.data.forEach(p=> (p.tags||[]).forEach(t=>tags.add(t)));
  const sel = document.getElementById('filterTag');
  tags.forEach(t=> { const o = document.createElement('option'); o.value=t; o.textContent=t; sel.appendChild(o); });
}

async function loadFeed(page=1){
  state.page = page;
  const feed = document.getElementById('feed'); feed.innerHTML = '<div class="small muted">Lade…</div>';
  const q = encodeURIComponent(state.q || '');
  const res = await api(`?action=list&page=${page}&per=${state.per}&q=${q}&tag=${encodeURIComponent(state.tag)}&sort=${state.sort}`);
  if(!res.ok){ feed.innerHTML = '<div class="small muted">Fehler beim Laden</div>'; return; }
  feed.innerHTML = '';
  res.data.forEach(p => feed.appendChild(renderPetitionCard(p)));
  setupPager(res.total, res.page, res.per);
}

function renderPetitionCard(p){
  const wrap = document.createElement('div'); wrap.className = 'glass p-4 card';
  const header = document.createElement('div'); header.className = 'flex items-start gap-4';
  const chk = document.createElement('input'); chk.type='checkbox'; chk.className='mr-2'; chk.onclick = ()=> toggleSelect(p.id, chk.checked);
  const imgwrap = document.createElement('div'); imgwrap.className='w-24 h-24 bg-slate-800 rounded-lg flex items-center justify-center';
  if(p.image){ const i=document.createElement('img'); i.src=p.image; i.className='w-full h-full object-cover rounded-lg'; imgwrap.innerHTML=''; imgwrap.appendChild(i); }
  const body = document.createElement('div'); body.className='flex-1';
  const title = document.createElement('h3'); title.className='font-bold text-lg'; title.textContent=p.title;
  const meta = document.createElement('div'); meta.className='small muted mt-1'; meta.innerHTML = `${p.signatures || 0} Unterschriften · Ziel ${p.goal} · ${p.progress || 0}%`;
  const tags = document.createElement('div'); tags.className='mt-2 flex gap-2';
  (p.tags||[]).slice(0,4).forEach(t => { const tg = document.createElement('div'); tg.className='tag'; tg.textContent=t; tags.appendChild(tg); });
  const btnRow = document.createElement('div'); btnRow.className='mt-3 flex gap-2';
  const open = document.createElement('button'); open.className='px-3 py-2 rounded-lg glass small'; open.textContent='Ansehen'; open.onclick = ()=> openPetition(p.id);
  const sign = document.createElement('button'); sign.className='px-3 py-2 rounded-lg bg-sky-500 text-white small'; sign.textContent='Unterschreiben'; sign.onclick = ()=> openPetition(p.id, true);
  btnRow.appendChild(open); btnRow.appendChild(sign);
  body.appendChild(title); body.appendChild(meta); body.appendChild(tags); body.appendChild(btnRow);
  header.appendChild(chk); header.appendChild(imgwrap); header.appendChild(body);
  wrap.appendChild(header);
  return wrap;
}

function toggleSelect(id, checked){
  if(checked){ if(!selectedForBulk.includes(id)) selectedForBulk.push(id); } else { selectedForBulk = selectedForBulk.filter(x=>x!==id); }
}

function setupPager(total,page,per){
  const pager = document.getElementById('pager'); pager.innerHTML=''; const pages = Math.ceil(total/per);
  for(let i=1;i<=pages;i++){ const b = document.createElement('button'); b.className='px-3 py-1 rounded-lg glass small'; b.textContent = i; if(i===page) { b.classList.add('bg-sky-500','text-white'); } b.onclick = ()=> loadFeed(i); pager.appendChild(b); }
}

function debounce(fn, ms=200){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), ms); }; }

function setupHandlers(){
  document.getElementById('search').addEventListener('input', debounce(e=>{ state.q=e.target.value; loadFeed(1); }, 400));
  document.getElementById('filterTag').addEventListener('change', e=>{ state.tag=e.target.value; loadFeed(1); });
  document.getElementById('sort').addEventListener('change', e=>{ state.sort=e.target.value; loadFeed(1); });
  document.getElementById('createSubmit').addEventListener('click', async ()=>{
    const title = document.getElementById('ctitle').value.trim(); const body = document.getElementById('cbody').value.trim();
    const target = document.getElementById('ctarget').value.trim(); const tags = document.getElementById('ctags').value.trim();
    const goal = parseInt(document.getElementById('cgoal').value) || 100; const image = document.getElementById('cimage').value.trim();
    if(!title || !body) return alert('Titel und Text erforderlich');
    const res = await fetch('?action=create_petition',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title,body,target,tags,goal,image})});
    const j = await res.json(); if(j.ok){ alert('Petition erstellt'); document.getElementById('ctitle').value=''; document.getElementById('cbody').value=''; loadFeed(); } else alert('Fehler');
  });

  document.getElementById('embedBtn').addEventListener('click', async ()=>{ const id = prompt('Petition ID'); if(!id) return; const res = await api('?action=embed&id='+encodeURIComponent(id)); if(res.ok) prompt('Widget Code', res.widget); else alert('Fehler'); });

  document.getElementById('exportAll').addEventListener('click', async ()=>{ const res = await api('?action=list&per=1000&page=1'); if(!res.ok) return alert('Fehler'); const blob = new Blob([JSON.stringify(res.data, null, 2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'petitions.json'; a.click(); });

  document.getElementById('downloadCSV').addEventListener('click', async ()=>{ const id = prompt('Petition ID'); if(!id) return; const pw = prompt('Admin Passwort'); if(!pw) return alert('Benötigt'); const login = await fetch('?action=admin_login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pw})}); const jl = await login.json(); if(!jl.ok) return alert('Auth fehlgeschlagen'); window.location.href='?action=export_csv&id='+encodeURIComponent(id); });

  const adminBtn = document.getElementById('adminBtn'); adminBtn.addEventListener('click', async ()=>{ const pw = prompt('Admin Passwort'); if(!pw) return; const res = await fetch('?action=admin_login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pw})}); const j = await res.json(); if(j.ok){ alert('Admin aktiviert'); document.getElementById('moderation').classList.remove('hidden'); } else alert('Falsch'); });

  document.getElementById('bulkDeleteBtn').addEventListener('click', async ()=>{ if(!selectedForBulk.length) return alert('Keine ausgewählt'); if(!confirm('Ausgewählte Petitionen wirklich löschen')) return; const res = await fetch('?action=admin_bulk_delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:selectedForBulk})}); const j = await res.json(); if(j.ok){ alert('Gelöscht'); selectedForBulk=[]; loadFeed(); } else alert('Fehler'); });

  document.getElementById('bulkSpamBtn').addEventListener('click', async ()=>{ const pid = prompt('Petition ID für Spam Markierung'); if(!pid) return; const idxs = prompt('Kommaseparierte Indexe der Signaturen z. B. 0,1,2'); if(!idxs) return; const arr = idxs.split(',').map(x=>parseInt(x.trim())).filter(x=>!isNaN(x)); const res = await fetch('?action=admin_mark_spam',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({petition_id:pid,indexes:arr})}); const j = await res.json(); if(j.ok) alert('Markiert'); else alert('Fehler'); });

  document.getElementById('sendCsvBtn').addEventListener('click', async ()=>{ const id = prompt('Petition ID für CSV'); if(!id) return; const email = document.getElementById('csvEmail').value.trim() || prompt('Empfänger Email'); if(!email) return alert('Email erforderlich'); const res = await fetch('?action=admin_send_csv_email',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id, email})}); const j = await res.json(); if(j.ok) alert('CSV Versand gestartet'); else alert('Fehler'); });
}

async function openPetition(id){
  const res = await fetch('?action=view&id='+encodeURIComponent(id)).then(r=>r.json());
  if(!res.ok) return alert('Fehler'); const p=res.petition;
  alert('Öffne Petition '+p.title);
}

function updateUserArea(){ document.getElementById('userArea').innerHTML = '<span class="muted">Gast</span>'; }
</script>
</body>
</html>

Installation

Rufen Sie die Script-Datei im Browser auf, z. B. https://www.ihredomain.de/namederdatei.php?install=1.

  • Das Script erstellt automatisch alle Tabellen und Grunddaten.
Vorheriges Tutorial
Nächstes Tutorial

Hier etwas für dich dabei?