643 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			643 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| declare(strict_types=1);
 | |
| 
 | |
| // ===== Session Setup (einheitlich in allen Entry Points) =====
 | |
| $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
 | |
| session_set_cookie_params([
 | |
|   'lifetime' => 0,
 | |
|   'path' => '/',
 | |
|   'secure' => $secure,
 | |
|   'httponly' => true,
 | |
|   'samesite' => 'Lax',
 | |
| ]);
 | |
| session_name('WLISTSESSID');
 | |
| session_start();
 | |
| 
 | |
| require_once __DIR__ . '/config/config.php';
 | |
| require_once __DIR__ . '/include/listgenerator.php';
 | |
| 
 | |
| // ===== Debug Toggle =====
 | |
| if (!empty($app_debug)) {
 | |
|   ini_set('display_errors', '1');
 | |
|   ini_set('display_startup_errors', '1');
 | |
|   ini_set('log_errors', '1');
 | |
|   if (!is_dir(__DIR__ . '/logs')) {
 | |
|     @mkdir(__DIR__ . '/logs', 0750, true);
 | |
|   }
 | |
|   ini_set('error_log', __DIR__ . '/logs/php-error.log');
 | |
|   error_reporting(E_ALL);
 | |
| } else {
 | |
|   ini_set('display_errors', '0');
 | |
|   ini_set('display_startup_errors', '0');
 | |
|   ini_set('log_errors', '1');
 | |
|   if (!is_dir(__DIR__ . '/logs')) {
 | |
|     @mkdir(__DIR__ . '/logs', 0750, true);
 | |
|   }
 | |
|   ini_set('error_log', __DIR__ . '/logs/php-error.log');
 | |
|   error_reporting(E_ALL);
 | |
| }
 | |
| 
 | |
| $message = null;
 | |
| 
 | |
| if (!empty($_SESSION['flash']) && is_array($_SESSION['flash'])) {
 | |
|   $message = $_SESSION['flash']; // ['msg'=>..., 'type'=> success|warning|danger]
 | |
|   unset($_SESSION['flash']);
 | |
| }
 | |
| 
 | |
| // ===== Helpers =====
 | |
| function e(string $s): string
 | |
| {
 | |
|   return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
 | |
| }
 | |
| function db(): mysqli
 | |
| {
 | |
|   global $servername, $username, $password, $db;
 | |
|   $m = new mysqli($servername, $username, $password, $db);
 | |
|   if ($m->connect_error) {
 | |
|     http_response_code(500);
 | |
|     exit('Interner Fehler (DB)');
 | |
|   }
 | |
|   $m->set_charset('utf8mb4');
 | |
|   return $m;
 | |
| }
 | |
| function ensure_csrf_token(): void
 | |
| {
 | |
|   if (empty($_SESSION['csrf'])) {
 | |
|     $_SESSION['csrf'] = bin2hex(random_bytes(32));
 | |
|   }
 | |
| }
 | |
| function require_csrf(): void
 | |
| {
 | |
|   if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 | |
|     $ok = isset($_POST['csrf']) && hash_equals($_SESSION['csrf'] ?? '', (string) $_POST['csrf']);
 | |
|     if (!$ok) {
 | |
|       http_response_code(403);
 | |
|       exit('Ungültiges CSRF-Token');
 | |
|     }
 | |
|   }
 | |
| }
 | |
| ensure_csrf_token();
 | |
| // ===== URL-Param: Liste per UUID (oder Alt-ID -> Redirect) =====
 | |
| $ListID = -1;
 | |
| $ListUUID = '';
 | |
| $showCreateEmptyState = false;
 | |
| 
 | |
| if (!isset($_GET['list'])) {
 | |
|   // Kein Parameter => nur Empty-State ohne Message
 | |
|   $showCreateEmptyState = true;
 | |
| 
 | |
| } else {
 | |
|   $raw = trim((string) $_GET['list']);
 | |
| 
 | |
|   if ($raw === '') {
 | |
|     // Leerer Parameter => nur Empty-State ohne Message
 | |
|     $showCreateEmptyState = true;
 | |
| 
 | |
|   } elseif (preg_match('/^[0-9a-fA-F-]{32,36}$/', $raw)) {
 | |
|     // UUID übergeben
 | |
|     $c = db();
 | |
|     $s = $c->prepare('SELECT ID, uuid FROM lists WHERE uuid=?');
 | |
|     $s->bind_param('s', $raw);
 | |
|     $s->execute();
 | |
|     $r = $s->get_result();
 | |
|     if ($r && ($row = $r->fetch_assoc())) {
 | |
|       $ListID = (int) $row['ID'];
 | |
|       $ListUUID = (string) $row['uuid'];
 | |
|     } else {
 | |
|       // UUID-Format ok, aber nicht vorhanden => Message + Empty-State
 | |
|       $message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
 | |
|       $showCreateEmptyState = true;
 | |
|     }
 | |
|     $s->close();
 | |
|     $c->close();
 | |
| 
 | |
|   } elseif (preg_match('/^\d+$/', $raw)) {
 | |
|     // Numerische ID übergeben -> auf UUID umleiten, wenn vorhanden
 | |
|     $id = (int) $raw;
 | |
|     $c = db();
 | |
|     $s = $c->prepare('SELECT uuid FROM lists WHERE ID=?');
 | |
|     $s->bind_param('i', $id);
 | |
|     $s->execute();
 | |
|     $r = $s->get_result();
 | |
|     if ($r && ($row = $r->fetch_assoc())) {
 | |
|       $uuid = (string) $row['uuid'];
 | |
|       $scheme = $secure ? 'https' : 'http';
 | |
|       $host = $_SERVER['HTTP_HOST'] ?? '';
 | |
|       header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($uuid), true, 301);
 | |
|       exit;
 | |
|     } else {
 | |
|       // ID-Format ok, aber nicht vorhanden => Message + Empty-State
 | |
|       $message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
 | |
|       $showCreateEmptyState = true;
 | |
|     }
 | |
|     $s->close();
 | |
|     $c->close();
 | |
| 
 | |
|   } else {
 | |
|     // Weder gültige UUID noch Zahl => Message + Empty-State
 | |
|     $message = ['msg' => 'Diese Liste gibt es nicht. Lege eine neue an oder prüfe den Link.', 'type' => 'warning'];
 | |
|     $showCreateEmptyState = true;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // ===== Sortierung (Whitelist) =====
 | |
| $sortby = 'priority';
 | |
| if (isset($_POST['sortby']))
 | |
|   $sortby = (string) $_POST['sortby'];
 | |
| elseif (isset($_POST['sortby_transfer']))
 | |
|   $sortby = (string) $_POST['sortby_transfer'];
 | |
| $allowedOrder = ['priority' => 'priority DESC', 'price_asc' => 'price ASC', 'price_desc' => 'price DESC', 'date_desc' => 'date DESC', 'date_asc' => 'date ASC', 'random' => 'RAND()'];
 | |
| if (!array_key_exists($sortby, $allowedOrder))
 | |
|   $sortby = 'priority';
 | |
| 
 | |
| // ===== Login-Status =====
 | |
| $loggedin = (isset($_SESSION['listid']) && $ListID === (int) $_SESSION['listid']);
 | |
| $GLOBALS['loggedin'] = $loggedin; // für listgenerator.php
 | |
| 
 | |
| // ===== POST-Actions (mit CSRF) =====
 | |
| if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 | |
|   require_csrf();
 | |
| 
 | |
|   // LOGIN
 | |
|   if (isset($_POST['login'])) {
 | |
|     $ListPassword = (string) ($_POST['ListPassword'] ?? '');
 | |
|     $ListIdFromForm = (int) ($_POST['ListID'] ?? -1);
 | |
|     $c = db();
 | |
|     $s = $c->prepare('SELECT edit_pw, uuid FROM lists WHERE ID=?');
 | |
|     $s->bind_param('i', $ListIdFromForm);
 | |
|     $s->execute();
 | |
|     $r = $s->get_result();
 | |
|     if ($r && ($row = $r->fetch_assoc())) {
 | |
|       if (password_verify($ListPassword, (string) $row['edit_pw'])) {
 | |
|         $_SESSION['listid'] = $ListIdFromForm;
 | |
|         $loggedin = true;
 | |
|         $ListUUID = (string) $row['uuid'];
 | |
|         $message = ['msg' => 'Login erfolgreich', 'type' => 'success'];
 | |
|       } else
 | |
|         $message = ['msg' => 'Falsches Passwort', 'type' => 'warning'];
 | |
|     } else
 | |
|       $message = ['msg' => 'Liste nicht gefunden', 'type' => 'warning'];
 | |
|     $s->close();
 | |
|     $c->close();
 | |
|   }
 | |
| 
 | |
|   // LISTE ANLEGEN
 | |
|   if (isset($_POST['listadd'])) {
 | |
|     $listName = (string) ($_POST['listName'] ?? '');
 | |
|     $listPasswordRaw = (string) ($_POST['listPassword'] ?? '');
 | |
|     $listDescription = (string) ($_POST['listDescription'] ?? '');
 | |
|     $listPassword = password_hash($listPasswordRaw, PASSWORD_DEFAULT);
 | |
| 
 | |
|     $c = db();
 | |
|     $s = $c->prepare('INSERT INTO lists (uuid, title, description, edit_pw) VALUES (UUID(), ?, ?, ?)');
 | |
|     $s->bind_param('sss', $listName, $listDescription, $listPassword);
 | |
|     if ($s->execute()) {
 | |
|       $last_id = $c->insert_id;
 | |
|       $g = $c->prepare('SELECT uuid FROM lists WHERE ID=?');
 | |
|       $g->bind_param('i', $last_id);
 | |
|       $g->execute();
 | |
|       $gr = $g->get_result();
 | |
|       $uuid = ($gr && ($row = $gr->fetch_assoc())) ? (string) $row['uuid'] : '';
 | |
|       $g->close();
 | |
|       $_SESSION['listid'] = $last_id;
 | |
|       $loggedin = true;
 | |
|       $scheme = $secure ? 'https' : 'http';
 | |
|       $host = $_SERVER['HTTP_HOST'] ?? '';
 | |
|       header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($uuid));
 | |
|       exit;
 | |
|     } else {
 | |
|       $message = ['msg' => 'Unerwarteter Fehler beim Anlegen', 'type' => 'danger'];
 | |
|     }
 | |
|     $s->close();
 | |
|     $c->close();
 | |
|   }
 | |
| 
 | |
|   // LOGOUT
 | |
|   if (isset($_POST['logout'])) {
 | |
|     session_destroy();
 | |
|     $loggedin = false;
 | |
|     $message = ['msg' => 'Logout erfolgreich', 'type' => 'success'];
 | |
|   }
 | |
| 
 | |
|   // PRIORITÄT PUSHEN
 | |
|   if (isset($_POST['pushprio'])) {
 | |
|     $wishId = (int) ($_POST['WhishID'] ?? -1);
 | |
|     $c = db();
 | |
|     $s = $c->prepare('SELECT COALESCE(MAX(priority),0) AS maxprio FROM wishes WHERE wishlist=?');
 | |
|     $s->bind_param('i', $ListID);
 | |
|     $s->execute();
 | |
|     $r = $s->get_result();
 | |
|     $next = 1;
 | |
|     if ($r && ($row = $r->fetch_assoc()))
 | |
|       $next = ((int) $row['maxprio']) + 1;
 | |
|     $s->close();
 | |
|     $u = $c->prepare('UPDATE wishes SET priority=? WHERE ID=?');
 | |
|     $u->bind_param('ii', $next, $wishId);
 | |
|     $message = $u->execute() ? ['msg' => 'Wunschpriorität aktualisiert', 'type' => 'success'] : ['msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger'];
 | |
|     $u->close();
 | |
|     $c->close();
 | |
|   }
 | |
| 
 | |
|   // LÖSCHEN (nur eingeloggt)
 | |
|   if (isset($_POST['delete']) && $loggedin === true) {
 | |
|     $WhishID = (int) ($_POST['WhishID'] ?? -1);
 | |
|     $WhishTitle = '';
 | |
|     $c = db();
 | |
|     $s = $c->prepare('SELECT image, title FROM wishes WHERE ID=?');
 | |
|     $s->bind_param('i', $WhishID);
 | |
|     $s->execute();
 | |
|     $r = $s->get_result();
 | |
|     if ($r && ($row = $r->fetch_assoc())) {
 | |
|       $WhishTitle = (string) $row['title'];
 | |
|       $imageFile = (string) $row['image'];
 | |
|       if (!empty($imageFile)) {
 | |
|         global $imagedir;
 | |
|         $full = rtrim($imagedir, '/') . '/' . $imageFile;
 | |
|         if (is_file($full))
 | |
|           @unlink($full);
 | |
|       }
 | |
|     }
 | |
|     $s->close();
 | |
|     $d = $c->prepare('DELETE FROM wishes WHERE ID=?');
 | |
|     $d->bind_param('i', $WhishID);
 | |
|     $message = $d->execute() ? ['msg' => 'Wunsch <b>"' . e($WhishTitle) . '"</b> gelöscht', 'type' => 'success'] : ['msg' => 'Uups, irgendwas ist schief gegangen!', 'type' => 'danger'];
 | |
|     $d->close();
 | |
|     $c->close();
 | |
|   }
 | |
| }
 | |
| 
 | |
| ?>
 | |
| <!DOCTYPE html>
 | |
| <html lang="de">
 | |
| 
 | |
| <head>
 | |
|   <meta charset="utf-8" />
 | |
|   <title>Simple Wishlist</title>
 | |
|   <meta name="viewport" content="width=device-width, initial-scale=1">
 | |
|   <link rel="stylesheet" href="css/bootstrap.min.css">
 | |
|   <link rel="stylesheet" href="css/tweaks.css">
 | |
|   <link rel="stylesheet" href="css/fontawesome.min.css" />
 | |
| 
 | |
|   <link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
 | |
|   <link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
 | |
|   <link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
 | |
|   <link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#5bbad5">
 | |
|   <link rel="shortcut icon" href="img/favicon.ico">
 | |
|   <meta name="theme-color" content="#ffffff">
 | |
| </head>
 | |
| 
 | |
| <body>
 | |
| 
 | |
|   <header>
 | |
|     <div class="navbar navbar-dark bg-dark shadow-sm">
 | |
|       <div class="container">
 | |
|         <a href="#" class="navbar-brand d-flex align-items-center">
 | |
|           <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2 bi bi-gift"
 | |
|             viewBox="0 0 16 16">
 | |
|             <path
 | |
|               d="M3 2.5a2.5 2.5 0 0 1 5 0 2.5 2.5 0 0 1 5 0v.006c0 .07 0 .27-.038.494H15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 14.5V7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.038A2.968 2.968 0 0 1 3 2.506V2.5zm1.068.5H7v-.5a1.5 1.5 0 1 0-3 0c0 .085.002.274.045.43a.522.522 0 0 0 .023.07zM9 3h2.932a.56.56 0 0 0 .023-.07c.043-.156.045-.345.045-.43a1.5 1.5 0 0 0-3 0V3zM1 4v2h6V4H1zm8 0v2h6V4H9zm5 3H9v8h4.5a.5.5 0 0 0 .5-.5V7zm-7 8V7H2v7.5a.5.5 0 0 0 .5.5H7z" />
 | |
|           </svg>
 | |
|           <strong>Simple Wishlist</strong>
 | |
|         </a>
 | |
|         <div class="nav navbar-nav navbar-right">
 | |
|           <div class="d-grid gap-2 d-flex">
 | |
|             <button type="button" class="btn btn-outline-success my-2 my-sm-0" data-bs-toggle="modal"
 | |
|               data-bs-target="#createListModal">Neue Liste</button>
 | |
|             <?php if ($showCreateEmptyState == false): ?>
 | |
|               <?php if ($loggedin): ?>
 | |
|                 <button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
 | |
|                   data-bs-target="#itemModal" data-mode="add" data-listuuid="<?= e($ListUUID) ?>"
 | |
|                   data-sort="<?= e($sortby) ?>">Add Item</button>
 | |
|                 <form class="form-inline" action="" method="POST">
 | |
|                   <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|                   <button type="submit" class="btn btn-outline-secondary my-2 my-sm-0" name="logout">Logout</button>
 | |
|                 </form>
 | |
|               <?php else: ?>
 | |
|                 <button type="button" class="btn btn-outline-secondary my-2 my-sm-0" data-bs-toggle="modal"
 | |
|                   data-bs-target="#loginModal">Login</button>
 | |
|               <?php endif; ?>
 | |
|               <form class="form-inline" action="" method="POST">
 | |
|                 <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|                 <select class="form-control" name="sortby" id="sortby">
 | |
|                   <option <?= $sortby === 'priority' ? 'selected' : '' ?> value="priority">Priorität</option>
 | |
|                   <option <?= $sortby === 'price_asc' ? 'selected' : '' ?> value="price_asc">Preis aufsteigend</option>
 | |
|                   <option <?= $sortby === 'price_desc' ? 'selected' : '' ?> value="price_desc">Preis absteigend</option>
 | |
|                   <option <?= $sortby === 'date_desc' ? 'selected' : '' ?> value="date_desc">Datum, neu → alt</option>
 | |
|                   <option <?= $sortby === 'date_asc' ? 'selected' : '' ?> value="date_asc">Datum, alt → neu</option>
 | |
|                   <option <?= $sortby === 'random' ? 'selected' : '' ?> value="random">Zufall</option>
 | |
|                 </select>
 | |
|               </form>
 | |
|             <?php endif; ?>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   </header>
 | |
| 
 | |
|   <main class="pb-4">
 | |
|     <?php if (isset($message)): ?>
 | |
|       <div class="alert alert-<?= e($message['type']) ?> alert-dismissible fade show m-3" role="alert">
 | |
|         <?= $message['msg'] ?>
 | |
|         <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | |
|       </div>
 | |
|     <?php endif; ?>
 | |
| 
 | |
|     <?php if (!empty($showCreateEmptyState)): ?>
 | |
|       <!-- Empty State -->
 | |
|       <section class="py-5 text-center container">
 | |
|         <div class="row py-lg-4">
 | |
|           <div class="col-lg-8 col-md-10 mx-auto">
 | |
|             <h1 class="fw-light">Willkommen bei Simple Wishlist</h1>
 | |
|             <p class="lead text-muted">
 | |
|               Es wurde keine gültige Liste ausgewählt. Du kannst jetzt eine neue Liste anlegen.
 | |
|             </p>
 | |
|             <p>
 | |
|               <button class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#createListModal">
 | |
|                 Neue Liste anlegen
 | |
|               </button>
 | |
|             </p>
 | |
|           </div>
 | |
|         </div>
 | |
|       </section>
 | |
|     <?php else: ?>
 | |
|       <?php wishlistMainBuilder($ListID, $sortby); ?>
 | |
|     <?php endif; ?>
 | |
|   </main>
 | |
| 
 | |
|   <footer class="mt-5 text-center text-muted small">
 | |
|     <div class="container">
 | |
|       <hr class="mb-3 opacity-25">
 | |
|       <p class="mb-1">
 | |
|         Made with
 | |
|         <img src="img/heart-icon.png" alt="Shakaru" class="icon-sm mx-1">
 | |
|         and
 | |
|         <img src="img/paw-icon.png" alt="Paw" class="icon-sm mx-1">
 | |
|         by
 | |
|         <strong>
 | |
|           <img src="img/shakaru-icon.png" alt="Shakaru" class="icon-sm mx-1">
 | |
|           Shakaru
 | |
|         </strong>
 | |
|       </p>
 | |
|       <p class="mb-0">
 | |
|         © <?= date('Y') ?> Simple Wishlist · Marcel Peterkau
 | |
|         · <a href="#" class="text-decoration-none">Back to top ↑</a>
 | |
|       </p>
 | |
|     </div>
 | |
|   </footer>
 | |
| 
 | |
|   <?php if (!$loggedin): ?>
 | |
|     <!-- Login Modal -->
 | |
|     <div class="modal fade" id="loginModal" tabindex="-1" aria-hidden="true">
 | |
|       <div class="modal-dialog">
 | |
|         <div class="modal-content">
 | |
|           <div class="modal-header">
 | |
|             <h5 class="modal-title">Login</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button>
 | |
|           </div>
 | |
|           <form action="" method="POST">
 | |
|             <div class="modal-body">
 | |
|               <label for="ListPassword" class="form-label">Passwort</label>
 | |
|               <input type="password" class="form-control" id="ListPassword" name="ListPassword" required>
 | |
|               <input type="hidden" id="ListID" name="ListID" value="<?= (int) $ListID ?>">
 | |
|             </div>
 | |
|             <div class="modal-footer">
 | |
|               <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|               <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
 | |
|               <button type="submit" name="login" class="btn btn-primary">Login</button>
 | |
|             </div>
 | |
|           </form>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   <?php endif; ?>
 | |
| 
 | |
|   <!-- Add/Edit Modal (ein Modal für beides) -->
 | |
|   <div class="modal fade" id="itemModal" tabindex="-1" aria-hidden="true">
 | |
|     <div class="modal-dialog">
 | |
|       <div class="modal-content">
 | |
|         <div class="modal-header">
 | |
|           <h5 class="modal-title" id="itemModalTitle">Wunsch hinzufügen</h5><button type="button" class="btn-close"
 | |
|             data-bs-dismiss="modal"></button>
 | |
|         </div>
 | |
|         <form action="item.php" method="POST" id="itemForm" enctype="multipart/form-data">
 | |
|           <div class="modal-body">
 | |
|             <label class="form-label">Titel</label>
 | |
|             <input class="form-control" name="ItemTitle" id="ItemTitle" required>
 | |
| 
 | |
|             <label class="form-label mt-2">Beschreibung</label>
 | |
|             <textarea class="form-control" name="ItemDescription" id="ItemDescription" rows="3"></textarea>
 | |
| 
 | |
|             <label class="form-label mt-2">Preis</label>
 | |
|             <div class="input-group">
 | |
|               <input class="form-control" name="ItemPrice" id="ItemPrice" placeholder="0,00">
 | |
|               <span class="input-group-text">€</span>
 | |
|             </div>
 | |
| 
 | |
|             <label class="form-label mt-2">Anzahl</label>
 | |
|             <input class="form-control" name="ItemQty" id="ItemQty" type="number" min="1" step="1" value="1" required>
 | |
| 
 | |
|             <label class="form-label mt-2">Link zum Angebot</label>
 | |
|             <input class="form-control" name="ItemLink" id="ItemLink" type="url" pattern="https?://.+">
 | |
| 
 | |
|             <hr class="my-3">
 | |
| 
 | |
|             <label class="form-label">Bild hinzufügen</label>
 | |
|             <ul class="nav nav-tabs" id="imgTab" role="tablist">
 | |
|               <li class="nav-item" role="presentation">
 | |
|                 <button class="nav-link active" id="tab-url" data-bs-toggle="tab" data-bs-target="#pane-url"
 | |
|                   type="button" role="tab">Bild-URL</button>
 | |
|               </li>
 | |
|               <li class="nav-item" role="presentation">
 | |
|                 <button class="nav-link" id="tab-file" data-bs-toggle="tab" data-bs-target="#pane-file" type="button"
 | |
|                   role="tab">Datei-Upload</button>
 | |
|               </li>
 | |
|               <li class="nav-item" role="presentation">
 | |
|                 <button class="nav-link" id="tab-paste" data-bs-toggle="tab" data-bs-target="#pane-paste" type="button"
 | |
|                   role="tab">Aus Zwischenablage</button>
 | |
|               </li>
 | |
|             </ul>
 | |
| 
 | |
|             <div class="tab-content border border-top-0 rounded-bottom p-3" id="imgTabContent">
 | |
|               <!-- URL -->
 | |
|               <div class="tab-pane fade show active" id="pane-url" role="tabpanel" aria-labelledby="tab-url">
 | |
|                 <div class="mb-2">
 | |
|                   <input class="form-control" name="ItemImageUrl" id="ItemImageUrl" type="url"
 | |
|                     placeholder="https://…/bild.jpg" pattern="https?://.+">
 | |
|                   <div class="form-text">Falls der Download scheitert, bitte Datei hochladen oder per Zwischenablage
 | |
|                     einfügen.</div>
 | |
|                 </div>
 | |
|               </div>
 | |
| 
 | |
|               <!-- FILE -->
 | |
|               <div class="tab-pane fade" id="pane-file" role="tabpanel" aria-labelledby="tab-file">
 | |
|                 <div class="mb-2">
 | |
|                   <input class="form-control" name="ItemImageFile" id="ItemImageFile" type="file" accept="image/*">
 | |
|                   <div class="form-text">Max. 8 MB. Unterstützt gängige Formate (jpg, png, webp, avif…).</div>
 | |
|                 </div>
 | |
|               </div>
 | |
| 
 | |
|               <!-- PASTE -->
 | |
|               <div class="tab-pane fade" id="pane-paste" role="tabpanel" aria-labelledby="tab-paste">
 | |
|                 <div class="mb-2 d-flex align-items-center gap-2">
 | |
|                   <button type="button" class="btn btn-outline-secondary btn-sm" id="PasteActivator">
 | |
|                     Aus Zwischenablage einfügen (Strg/Cmd+V)
 | |
|                   </button>
 | |
|                   <small class="text-muted">Tipp: Rechtsklick → „Bild kopieren“, dann hier klicken und einfügen.</small>
 | |
|                 </div>
 | |
| 
 | |
|                 <!-- Unsichtbarer Paste-Catcher -->
 | |
|                 <div id="PasteZone" class="visually-hidden" contenteditable="true"></div>
 | |
| 
 | |
|                 <!-- Hidden: Base64 + Name -->
 | |
|                 <input type="hidden" name="ItemImagePaste" id="ItemImagePaste">
 | |
|                 <input type="hidden" name="ItemImagePasteName" id="ItemImagePasteName" value="clipboard.png">
 | |
|               </div>
 | |
|             </div>
 | |
| 
 | |
|             <!-- Live Preview (für File/Paste/URL wenn möglich) -->
 | |
|             <div class="mt-3" id="ImgPreviewWrap" style="display:none;">
 | |
|               <label class="form-label">Vorschau</label>
 | |
|               <img id="ImgPreview" src="" alt="Bildvorschau" class="img-fluid border rounded w-100"
 | |
|                 style="max-height:220px; object-fit:contain;">
 | |
|             </div>
 | |
| 
 | |
|             <div class="form-check mt-3" id="RemoveImageWrap" style="display:none;">
 | |
|               <input class="form-check-input" type="checkbox" value="1" id="RemoveImage" name="RemoveImage">
 | |
|               <label class="form-check-label" for="RemoveImage">Aktuelles Bild entfernen</label>
 | |
|             </div>
 | |
| 
 | |
|           </div>
 | |
|           <div class="modal-footer">
 | |
|             <input type="hidden" name="action" id="ItemAction" value="add">
 | |
|             <input type="hidden" name="WhishID" id="WhishID" value="-1">
 | |
|             <input type="hidden" name="ItemListUUID" value="<?= e($ListUUID) ?>">
 | |
|             <input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
 | |
|             <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|             <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
 | |
|             <button type="submit" class="btn btn-primary" id="ItemSubmitBtn">Speichern</button>
 | |
|           </div>
 | |
|         </form>
 | |
|       </div>
 | |
|     </div>
 | |
|   </div>
 | |
| 
 | |
|   <!-- Delete Modal -->
 | |
|   <div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
 | |
|     <div class="modal-dialog">
 | |
|       <div class="modal-content">
 | |
|         <div class="modal-header">
 | |
|           <h5 class="modal-title">Wunsch löschen</h5><button type="button" class="btn-close"
 | |
|             data-bs-dismiss="modal"></button>
 | |
|         </div>
 | |
|         <div class="modal-body">
 | |
|           <h5 id="del-whish-title">WunschTitel</h5>
 | |
|           <p>Soll dieser Wunsch wirklich gelöscht werden?</p>
 | |
|         </div>
 | |
|         <div class="modal-footer">
 | |
|           <form action="" method="POST">
 | |
|             <input type="hidden" name="WhishID" id="DelWhishID" value="-1">
 | |
|             <input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
 | |
|             <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|             <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Nein</button>
 | |
|             <button type="submit" name="delete" class="btn btn-danger">Löschen</button>
 | |
|           </form>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   </div>
 | |
| 
 | |
|   <!-- Push Priority Modal -->
 | |
|   <div class="modal fade" id="pushprioModal" tabindex="-1" aria-hidden="true">
 | |
|     <div class="modal-dialog">
 | |
|       <div class="modal-content">
 | |
|         <div class="modal-header">
 | |
|           <h5 class="modal-title">Wunschpriorität</h5><button type="button" class="btn-close"
 | |
|             data-bs-dismiss="modal"></button>
 | |
|         </div>
 | |
|         <div class="modal-body">
 | |
|           <h5 id="prio-whish-title">WunschTitel</h5>
 | |
|           <p>Priorität ganz nach oben setzen?</p>
 | |
|         </div>
 | |
|         <div class="modal-footer">
 | |
|           <form action="" method="POST">
 | |
|             <input type="hidden" name="WhishID" id="PrioWhishID" value="-1">
 | |
|             <input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
 | |
|             <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|             <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Nein</button>
 | |
|             <button type="submit" name="pushprio" class="btn btn-primary">Ja</button>
 | |
|           </form>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   </div>
 | |
| 
 | |
|   <!-- Reservation Modal (öffentlich) -->
 | |
|   <div class="modal fade" id="reservationModal" tabindex="-1" aria-hidden="true">
 | |
|     <div class="modal-dialog">
 | |
|       <div class="modal-content">
 | |
|         <div class="modal-header">
 | |
|           <h5 class="modal-title" id="reservationModalLabel">Wunsch reservieren</h5><button type="button"
 | |
|             class="btn-close" data-bs-dismiss="modal"></button>
 | |
|         </div>
 | |
|         <div class="modal-body">
 | |
|           <p id="ReservationInfoText">Bitte vergeben Sie ein Passwort, um diesen Wunsch zu reservieren. Nur mit diesem
 | |
|             Passwort (oder durch den Listeneigentümer) kann die Reservierung wieder aufgehoben werden.</p>
 | |
|           <form action="reservations.php" method="POST" id="reservationForm">
 | |
|             <label for="WishPassword" class="form-label">Passwort</label>
 | |
|             <input type="password" class="form-control" id="WishPassword" name="WishPassword" required>
 | |
|             <input type="hidden" name="wishid" id="modal-wishid" value="">
 | |
|             <input type="hidden" name="reservedstat" id="modal-reservedstat" value="">
 | |
|             <input type="hidden" name="sortby_transfer" value="<?= e($sortby) ?>">
 | |
|             <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|           </form>
 | |
|         </div>
 | |
|         <div class="modal-footer">
 | |
|           <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
 | |
|           <button type="submit" class="btn btn-primary" form="reservationForm"
 | |
|             id="reservation-submit">Reservieren</button>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   </div>
 | |
| 
 | |
|   <!-- Create List Modal -->
 | |
|   <div class="modal fade" id="createListModal" tabindex="-1" aria-hidden="true">
 | |
|     <div class="modal-dialog">
 | |
|       <div class="modal-content">
 | |
|         <div class="modal-header">
 | |
|           <h5 class="modal-title">Neue Liste anlegen</h5>
 | |
|           <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
 | |
|         </div>
 | |
|         <form action="" method="POST">
 | |
|           <div class="modal-body">
 | |
|             <div class="mb-3">
 | |
|               <label for="listName" class="form-label">Titel der Liste</label>
 | |
|               <input type="text" class="form-control" id="listName" name="listName" required maxlength="255">
 | |
|             </div>
 | |
|             <div class="mb-3">
 | |
|               <label for="listDescription" class="form-label">Beschreibung (optional)</label>
 | |
|               <textarea class="form-control" id="listDescription" name="listDescription" rows="3"></textarea>
 | |
|             </div>
 | |
|             <div class="mb-3">
 | |
|               <label for="listPassword" class="form-label">Admin-Passwort (zum Bearbeiten)</label>
 | |
|               <input type="password" class="form-control" id="listPassword" name="listPassword" required>
 | |
|               <div class="form-text">
 | |
|                 Dieses Passwort brauchst du später für Login/Bearbeitung der Liste.
 | |
|               </div>
 | |
|             </div>
 | |
|           </div>
 | |
|           <div class="modal-footer">
 | |
|             <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
 | |
|             <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
 | |
|             <button type="submit" class="btn btn-primary" name="listadd">Anlegen</button>
 | |
|           </div>
 | |
|         </form>
 | |
|       </div>
 | |
|     </div>
 | |
|   </div>
 | |
| 
 | |
|   <script src="js/bootstrap.bundle.min.js"></script>
 | |
|   <script src="js/jquery.min.js"></script>
 | |
|   <script src="js/wishlist.js"></script>
 | |
| </body>
 | |
| 
 | |
| </html>
 |