Compare commits
2 Commits
b9d360542d
...
16c4d58def
Author | SHA1 | Date | |
---|---|---|---|
16c4d58def | |||
fb182a1aec |
@@ -194,21 +194,32 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.btn-pill i { font-size: 1rem; line-height: 1; margin: 0 .5rem; }
|
||||
.btn-pill i {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
.btn-pill span {
|
||||
width: 0; opacity: 0; overflow: hidden; white-space: nowrap; margin-left: 0;
|
||||
transition: width .18s ease, opacity .18s ease, margin-left .18s ease;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-left: 0;
|
||||
transition: width 0.18s ease, opacity 0.18s ease, margin-left 0.18s ease;
|
||||
}
|
||||
|
||||
/* Hover: Button wächst, Text klappt auf */
|
||||
.btn-pill:hover {
|
||||
width: auto;
|
||||
flex: 0 1 16rem;
|
||||
padding: .5rem .8rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
/* Keine Ausrichtung hier setzen – die Utility-Klasse bleibt wirksam */
|
||||
}
|
||||
.btn-pill:hover span { width: auto; opacity: 1; margin-left: .35rem; }
|
||||
.btn-pill:hover span {
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
|
||||
/* Fokus sichtbar (zugänglich) */
|
||||
.btn-pill:focus-visible {
|
||||
@@ -216,6 +227,14 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ================= Footer ================== */
|
||||
|
||||
.icon-sm {
|
||||
width: 24px; /* oder 24px, wenn du es etwas größer magst */
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ============ Responsive Tweaks ============ */
|
||||
@media (max-width: 575.98px) {
|
||||
.card-body {
|
||||
|
BIN
img/heart-icon.png
Normal file
BIN
img/heart-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.5 MiB |
BIN
img/paw-icon.png
Normal file
BIN
img/paw-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
img/shakaru-icon.png
Normal file
BIN
img/shakaru-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@@ -270,12 +270,22 @@ function wishlistMainBuilder(int $ListID, string $sortby = 'priority'): void
|
||||
|
||||
// 3) Wünsche laden
|
||||
$sql = "
|
||||
SELECT w.ID, w.image, w.title, w.link, w.price, w.description,
|
||||
w.date, w.qty,
|
||||
(SELECT COUNT(*) FROM wishes_reservations r WHERE r.wish_id = w.ID) AS reserved_count
|
||||
SELECT
|
||||
w.ID,
|
||||
w.image,
|
||||
w.title,
|
||||
w.link,
|
||||
w.price,
|
||||
w.description,
|
||||
w.date,
|
||||
w.qty,
|
||||
COALESCE(rc.reserved_count, 0) AS reserved_count
|
||||
FROM wishes w
|
||||
LEFT JOIN v_wish_reserved_counts rc
|
||||
ON rc.wish_id = w.ID
|
||||
WHERE w.wishlist = ?
|
||||
ORDER BY {$orderSql}";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('i', $ListID);
|
||||
$stmt->execute();
|
||||
@@ -311,7 +321,7 @@ HTML;
|
||||
$row['link'] !== null ? (string) $row['link'] : null,
|
||||
(int) $row['price'],
|
||||
$row['description'] !== null ? (string) $row['description'] : null,
|
||||
(int) $row['reserved_count'], // echte Anzahl Reservierungen
|
||||
(int) $row['reserved_count'],
|
||||
$row['date'] !== null ? (string) $row['date'] : null,
|
||||
isset($row['qty']) ? (int) $row['qty'] : 1
|
||||
);
|
||||
|
123
index.php
123
index.php
@@ -77,13 +77,24 @@ function require_csrf(): void
|
||||
}
|
||||
}
|
||||
ensure_csrf_token();
|
||||
|
||||
// ===== URL-Param: Liste per UUID (oder Alt-ID -> Redirect) =====
|
||||
$ListID = -1;
|
||||
$ListUUID = '';
|
||||
if (isset($_GET['list'])) {
|
||||
$showCreateEmptyState = false;
|
||||
|
||||
if (!isset($_GET['list'])) {
|
||||
// Kein Parameter => nur Empty-State ohne Message
|
||||
$showCreateEmptyState = true;
|
||||
|
||||
} else {
|
||||
$raw = trim((string) $_GET['list']);
|
||||
if (preg_match('/^[0-9a-fA-F-]{32,36}$/', $raw)) {
|
||||
|
||||
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);
|
||||
@@ -92,27 +103,40 @@ if (isset($_GET['list'])) {
|
||||
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();
|
||||
} else {
|
||||
$ListID = (int) $raw;
|
||||
if ($ListID >= 0) {
|
||||
|
||||
} 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', $ListID);
|
||||
$s->bind_param('i', $id);
|
||||
$s->execute();
|
||||
$r = $s->get_result();
|
||||
if ($r && ($row = $r->fetch_assoc())) {
|
||||
$ListUUID = (string) $row['uuid'];
|
||||
$uuid = (string) $row['uuid'];
|
||||
$scheme = $secure ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
header('Location: ' . $scheme . '://' . $host . '/?list=' . urlencode($ListUUID), true, 301);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +301,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
</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) ?>"
|
||||
@@ -300,6 +327,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
<option <?= $sortby === 'random' ? 'selected' : '' ?> value="random">Zufall</option>
|
||||
</select>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,13 +342,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
</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="text-muted py-5">
|
||||
<footer class="mt-5 text-center text-muted small">
|
||||
<div class="container">
|
||||
<p class="float-end mb-1"><a href="#">Back to top</a></p>
|
||||
<p class="mb-1">Simple Wishlist © by Marcel Peterkau</p>
|
||||
<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>
|
||||
|
||||
@@ -478,6 +539,42 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
</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>
|
||||
|
165
wishlist.sql
165
wishlist.sql
@@ -1,91 +1,98 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.0
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: localhost
|
||||
-- Erstellungszeit: 04. Okt 2022 um 22:36
|
||||
-- Server-Version: 10.5.15-MariaDB-0+deb11u1
|
||||
-- PHP-Version: 8.0.22
|
||||
-- ============================================================
|
||||
-- Simple Wishlist – Minimal-Schema
|
||||
-- Tested on MariaDB 10.x / MySQL 8.x
|
||||
-- ============================================================
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
/* Optional: eigene DB anlegen/verwenden
|
||||
CREATE DATABASE IF NOT EXISTS `wishlist`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
USE `wishlist`;
|
||||
*/
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Tabellen löschen (idempotent für wiederholte Importe)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `wishes_reservations`;
|
||||
DROP TABLE IF EXISTS `wishes`;
|
||||
DROP TABLE IF EXISTS `lists`;
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Datenbank: `wishlist`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Tabellenstruktur für Tabelle `lists`
|
||||
--
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- LISTEN (Metadaten & Admin-Passwort)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `lists` (
|
||||
`ID` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`edit_pw` varchar(64) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Tabellenstruktur für Tabelle `wishes`
|
||||
--
|
||||
`ID` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`uuid` CHAR(36) NOT NULL, -- öffentliche, stabile ID (Links)
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`edit_pw` VARCHAR(255) NOT NULL, -- Passwort-Hash (password_hash)
|
||||
PRIMARY KEY (`ID`),
|
||||
UNIQUE KEY `uq_lists_uuid` (`uuid`)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- WÜNSCHE (einer Liste zugeordnet)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `wishes` (
|
||||
`ID` int(11) NOT NULL,
|
||||
`wishlist` int(11) NOT NULL DEFAULT 0,
|
||||
`title` varchar(128) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`link` text NOT NULL,
|
||||
`image` text NOT NULL,
|
||||
`price` int(11) NOT NULL,
|
||||
`reserved` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`pass_hash` varchar(64) NOT NULL DEFAULT '',
|
||||
`date` date NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`ID` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`wishlist` INT(11) NOT NULL, -- FK -> lists.ID
|
||||
`title` VARCHAR(128) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`link` TEXT NOT NULL, -- optionaler Anbieter-Link (leer erlaubt)
|
||||
`image` TEXT DEFAULT NULL, -- Dateiname (lokal) oder NULL
|
||||
`price` INT(11) NOT NULL DEFAULT 0, -- Preis in Cent
|
||||
`date` DATE NOT NULL DEFAULT (CURRENT_DATE),
|
||||
`priority` INT(11) NOT NULL DEFAULT 0, -- Sortierung (höher = weiter oben)
|
||||
`qty` INT(11) NOT NULL DEFAULT 1, -- Anzahl benötigter Exemplare
|
||||
|
||||
--
|
||||
-- Indizes der exportierten Tabellen
|
||||
--
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `idx_wishes_wishlist` (`wishlist`),
|
||||
CONSTRAINT `fk_wishes_list`
|
||||
FOREIGN KEY (`wishlist`) REFERENCES `lists` (`ID`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Indizes für die Tabelle `lists`
|
||||
--
|
||||
ALTER TABLE `lists`
|
||||
ADD PRIMARY KEY (`ID`);
|
||||
-- ------------------------------------------------------------
|
||||
-- RESERVIERUNGEN (pro Wunsch mehrere Einträge, je Reservierung einer)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE `wishes_reservations` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`wish_id` INT(11) NOT NULL, -- FK -> wishes.ID
|
||||
`pass_hash` VARCHAR(255) NOT NULL, -- Passwort-Hash (password_hash)
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_wr_wish` (`wish_id`),
|
||||
CONSTRAINT `fk_wr_wish`
|
||||
FOREIGN KEY (`wish_id`) REFERENCES `wishes` (`ID`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Indizes für die Tabelle `wishes`
|
||||
--
|
||||
ALTER TABLE `wishes`
|
||||
ADD PRIMARY KEY (`ID`);
|
||||
-- ------------------------------------------------------------
|
||||
-- OPTIONALE HILFS-VIEW: reserved_count je Wunsch
|
||||
-- Erleichtert das Rendern (Badge/Buttons) ohne Subquery im Code.
|
||||
-- Nutzung: SELECT w.*, rc.reserved_count FROM wishes w
|
||||
-- LEFT JOIN v_wish_reserved_counts rc ON rc.wish_id = w.ID
|
||||
-- ------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW `v_wish_reserved_counts` AS
|
||||
SELECT
|
||||
wr.wish_id,
|
||||
COUNT(*) AS reserved_count
|
||||
FROM wishes_reservations wr
|
||||
GROUP BY wr.wish_id;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT für exportierte Tabellen
|
||||
--
|
||||
-- ------------------------------------------------------------
|
||||
-- EMPFOHLENE INDIZES (optional – je nach Abfragen)
|
||||
-- ------------------------------------------------------------
|
||||
-- Wenn häufig nach (wishlist, priority DESC) sortiert wird:
|
||||
CREATE INDEX `idx_wishes_wishlist_priority` ON `wishes` (`wishlist`, `priority`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT für Tabelle `lists`
|
||||
--
|
||||
ALTER TABLE `lists`
|
||||
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
|
||||
-- Wenn oft nach Datum sortiert/gefiltert wird:
|
||||
CREATE INDEX `idx_wishes_wishlist_date` ON `wishes` (`wishlist`, `date`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT für Tabelle `wishes`
|
||||
--
|
||||
ALTER TABLE `wishes`
|
||||
MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
-- Fertig.
|
||||
|
Reference in New Issue
Block a user