Compare commits

..

1 Commits

Author SHA1 Message Date
admin a3891a59f0 Resume audio context on pointer down 2025-11-10 20:57:39 +01:00
23 changed files with 325 additions and 6216 deletions
-2
View File
@@ -1,2 +0,0 @@
storage/*
!storage/.gitkeep
-5
View File
@@ -1,5 +0,0 @@
Options -Indexes
AddDefaultCharset UTF-8
<IfModule mod_rewrite.c>
RewriteEngine On
</IfModule>
-48
View File
@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
changeBandStatus((int) $_POST['band_id'], $_POST['status']);
}
$bands = moderationItems('bands');
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Bands moderieren <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Bandfreigaben</h1>
<table class="table">
<thead><tr><th>Name</th><th>Ort</th><th>Status</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach ($bands as $band): ?>
<tr>
<td><?= htmlspecialchars($band['name']) ?></td>
<td><?= htmlspecialchars($band['city']) ?></td>
<td><?= htmlspecialchars($band['status']) ?></td>
<td>
<form method="post" style="display:inline-flex; gap:4px;">
<input type="hidden" name="band_id" value="<?= $band['id'] ?>">
<select name="status" class="form-control" style="width:auto;">
<option value="aktiv">Freigeben</option>
<option value="archiv">Archivieren</option>
<option value="prüfung">Zur Prüfung</option>
</select>
<button class="btn-primary">Speichern</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$bands): ?><p>Keine Bands warten auf Moderation.</p><?php endif; ?>
</main>
</body>
</html>
-48
View File
@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
changeReviewStatus((int) $_POST['review_id'], $_POST['status']);
}
$reviews = moderationItems('reviews');
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Bewertungen prüfen <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Bewertungen moderieren</h1>
<table class="table">
<thead><tr><th>Band</th><th>Autor</th><th>Bewertung</th><th>Kommentar</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach ($reviews as $review): ?>
<tr>
<td><?= htmlspecialchars($review['band_name']) ?></td>
<td><?= htmlspecialchars($review['author']) ?></td>
<td><?= (int) $review['rating'] ?> ★</td>
<td><?= htmlspecialchars($review['comment']) ?></td>
<td>
<form method="post" style="display:inline-flex; gap:4px;">
<input type="hidden" name="review_id" value="<?= $review['id'] ?>">
<select name="status" class="form-control" style="width:auto;">
<option value="freigegeben">Freigeben</option>
<option value="abgelehnt">Ablehnen</option>
</select>
<button class="btn-primary">Speichern</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$reviews): ?><p>Keine Bewertungen in Moderation.</p><?php endif; ?>
</main>
</body>
</html>
-37
View File
@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
$pdo = db();
$stats = [
'bands' => (int) $pdo->query("SELECT COUNT(*) FROM bands")->fetchColumn(),
'requests' => (int) $pdo->query("SELECT COUNT(*) FROM requests")->fetchColumn(),
'reviews' => (int) $pdo->query("SELECT COUNT(*) FROM reviews")->fetchColumn(),
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Admin Dashboard <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header>
<div class="admin-nav">
<a href="dashboard.php">Dashboard</a>
<a href="bands.php">Bandfreigaben</a>
<a href="bewertungen.php">Bewertungen</a>
<a href="settings.php">Settings</a>
</div>
</header>
<main>
<section class="band-grid">
<article class="band-card"><h3>Bands</h3><p><?= $stats['bands'] ?></p></article>
<article class="band-card"><h3>Anfragen</h3><p><?= $stats['requests'] ?></p></article>
<article class="band-card"><h3>Bewertungen</h3><p><?= $stats['reviews'] ?></p></article>
</section>
</main>
</body>
</html>
-37
View File
@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
updateSetting('paypal_enabled', isset($_POST['paypal_enabled']) ? '1' : '0');
updateSetting('service_fee', (string) (int) $_POST['service_fee']);
$message = 'Einstellungen gespeichert.';
}
$config = settings();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Settings <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Vermittlungsgebühr & PayPal</h1>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<form method="post">
<label>
<input type="checkbox" name="paypal_enabled" <?= $config['paypal_enabled'] === '1' ? 'checked' : '' ?>> PayPal aktivieren
</label>
<label>Service Fee (%)
<input type="number" class="form-control" name="service_fee" value="<?= htmlspecialchars($config['service_fee']) ?>">
</label>
<button class="btn-primary">Speichern</button>
</form>
</main>
</body>
</html>
-77
View File
@@ -1,77 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
$bandId = isset($_GET['band_id']) ? (int) $_GET['band_id'] : 0;
$band = $bandId ? findBand($bandId) : null;
if (!$band) {
http_response_code(404);
echo 'Band nicht gefunden';
exit;
}
$user = currentUser();
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'band_id' => $bandId,
'user_id' => $user['id'] ?? null,
'event_date' => $_POST['event_date'] ?? '',
'location' => trim((string) $_POST['location'] ?? ''),
'budget' => (int) ($_POST['budget'] ?? 0),
'event_type' => trim((string) $_POST['event_type'] ?? ''),
'message' => trim((string) $_POST['message'] ?? ''),
];
if (!$data['event_date'] || !$data['location']) {
$error = 'Bitte Datum und Ort ausfüllen.';
} else {
createRequest($data);
$message = 'Anfrage gespeichert und an die Band gemeldet.';
sendEmail('info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch', 'Neue Anfrage', 'Neue Anfrage für ' . $band['name']);
}
}
$settings = settings();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anfrage <?= htmlspecialchars($band['name']) ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="band-detail.php?id=<?= $bandId ?>">← Zurück</a>
<h1>Anfrage an <?= htmlspecialchars($band['name']) ?></h1>
<p>PayPal Zahlungsabwicklung ist <?= $settings['paypal_enabled'] === '1' ? 'aktiviert' : 'optional' ?>, Service Fee: <?= htmlspecialchars($settings['service_fee']) ?>%.</p>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post">
<label>Event-Datum
<input type="date" class="form-control" name="event_date" required>
</label>
<label>Ort / Location
<input type="text" class="form-control" name="location" placeholder="Zürich, Kaufleuten" required>
</label>
<label>Event-Typ
<input type="text" class="form-control" name="event_type" placeholder="Hochzeit, Firmenfeier">
</label>
<label>Budget (CHF)
<input type="number" class="form-control" name="budget" placeholder="4500">
</label>
<label>Nachricht
<textarea class="form-control" name="message" rows="4"></textarea>
</label>
<button class="btn-primary">Anfrage senden</button>
</form>
</main>
</body>
</html>
-237
View File
@@ -1,237 +0,0 @@
:root {
--primary: #ffb703;
--secondary: #fb8500;
--dark: #0b0d17;
--darker: #090b13;
--light: #fefae0;
--gray: #8d99ae;
--gradient: linear-gradient(120deg, #ffb703, #fb5607, #ff006e);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Space Grotesk', 'Segoe UI', system-ui, sans-serif;
margin: 0;
background: radial-gradient(circle at 10% 20%, rgba(255, 183, 3, 0.25), rgba(9, 11, 19, 0.95)), var(--dark);
color: var(--light);
min-height: 100vh;
}
header {
padding: 40px 5vw 20px;
}
.hero {
background: var(--darker);
border-radius: 24px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
align-items: center;
}
.hero h1 {
font-size: clamp(2.2rem, 5vw, 3.6rem);
margin-bottom: 10px;
}
.hero p {
color: rgba(255, 255, 255, 0.8);
}
.badge-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.badge {
background: rgba(255, 255, 255, 0.08);
padding: 6px 18px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.9rem;
}
.search-panel {
margin-top: 30px;
background: rgba(0, 0, 0, 0.35);
border-radius: 18px;
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 15px;
}
.search-panel input,
.search-panel select {
width: 100%;
padding: 12px 16px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: var(--light);
}
.btn-primary {
padding: 14px 24px;
border-radius: 16px;
border: none;
font-weight: bold;
background: var(--gradient);
color: var(--dark);
cursor: pointer;
transition: transform 0.2s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
}
main {
padding: 30px 5vw 80px;
}
.band-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.band-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 20px;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, border-color 0.3s ease;
}
.band-card:hover {
transform: translateY(-4px);
border-color: var(--primary);
}
.band-card h3 {
margin-top: 0;
margin-bottom: 8px;
}
.price-tag {
font-size: 1.1rem;
color: var(--primary);
font-weight: bold;
}
.card-meta {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
}
footer {
background: rgba(0, 0, 0, 0.4);
padding: 30px 5vw;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 15px;
}
.cookie-banner {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--darker);
border-radius: 16px;
padding: 20px;
width: min(360px, calc(100% - 40px));
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.5);
display: none;
}
.cookie-banner.active {
display: block;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
text-align: left;
}
.form-control {
width: 100%;
padding: 12px 16px;
margin-bottom: 15px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: var(--light);
}
.alert {
padding: 15px 18px;
border-radius: 12px;
margin-bottom: 15px;
}
.alert-success {
background: rgba(56, 142, 60, 0.2);
border: 1px solid rgba(56, 142, 60, 0.4);
}
.alert-error {
background: rgba(213, 0, 0, 0.2);
border: 1px solid rgba(213, 0, 0, 0.4);
}
.band-detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
margin-top: 30px;
}
.gallery img {
width: 100%;
border-radius: 16px;
margin-bottom: 16px;
}
.badge-rating {
background: rgba(255, 183, 3, 0.2);
border-color: rgba(255, 183, 3, 0.5);
}
.admin-nav {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.admin-nav a {
color: var(--light);
text-decoration: none;
padding: 10px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.08);
}
@media (max-width: 768px) {
.hero,
footer {
padding: 24px;
}
}
-20
View File
@@ -1,20 +0,0 @@
const cookieBanner = document.querySelector('.cookie-banner');
const cookieAccept = document.querySelector('[data-cookie-accept]');
if (cookieBanner && cookieAccept) {
const consent = localStorage.getItem('gyb-cookie');
if (!consent) {
cookieBanner.classList.add('active');
}
cookieAccept.addEventListener('click', () => {
localStorage.setItem('gyb-cookie', 'accepted');
cookieBanner.classList.remove('active');
});
}
const filterForm = document.querySelector('[data-filter-form]');
if (filterForm) {
filterForm.addEventListener('input', () => {
filterForm.submit();
});
}
-143
View File
@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$bandId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$band = $bandId ? findBand($bandId) : null;
if (!$band) {
http_response_code(404);
echo 'Band nicht gefunden';
exit;
}
$media = bandMedia($bandId);
$availability = bandAvailability($bandId);
$reviews = bandReviews($bandId);
$user = currentUser();
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user) {
if (isset($_POST['review'])) {
if (!eligibleForReview($bandId, (int) $user['id'])) {
$error = 'Für Bewertungen ist eine bestätigte Buchung nötig.';
} else {
$comment = trim((string) ($_POST['comment'] ?? ''));
if (mb_strlen($comment) > 200) {
$error = 'Maximal 200 Zeichen erlaubt.';
} else {
storeReview([
'band_id' => $bandId,
'user_id' => (int) $user['id'],
'rating' => (int) $_POST['rating'],
'comment' => $comment,
]);
$message = 'Danke! Deine Bewertung wartet auf Freigabe.';
}
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($band['name']) ?> <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Zurück</a>
<div class="hero" style="margin-top: 20px;">
<div>
<p class="badge"><?= htmlspecialchars($band['genre']) ?></p>
<h1><?= htmlspecialchars($band['name']) ?></h1>
<p><?= nl2br(htmlspecialchars($band['description'])) ?></p>
<div class="badge-list">
<span class="badge">Ort: <?= htmlspecialchars($band['city'] ?? '') ?></span>
<span class="badge">ab <?= formatPrice((int) $band['price']) ?></span>
</div>
<div class="badge-list">
<?php foreach (array_filter(array_map('trim', explode(',', (string) $band['style_tags']))) as $tag): ?>
<span class="badge">#<?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<p>
<a class="btn-primary" href="anfrage.php?band_id=<?= $bandId ?>">Verfügbarkeit anfragen</a>
</p>
</div>
<div>
<?php if (!empty($band['video_url'])): ?>
<iframe width="100%" height="280" src="<?= htmlspecialchars($band['video_url']) ?>" title="Bandvideo" allowfullscreen></iframe>
<?php endif; ?>
</div>
</div>
</header>
<main>
<section class="band-detail-grid">
<div class="gallery">
<h3>Galerie</h3>
<?php foreach ($media as $item): ?>
<?php if ($item['type'] === 'image'): ?>
<img src="<?= htmlspecialchars($item['url']) ?>" alt="Bandbild">
<?php endif; ?>
<?php endforeach; ?>
</div>
<div>
<h3>Verfügbarkeit</h3>
<table class="table">
<thead>
<tr><th>Datum</th><th>Status</th></tr>
</thead>
<tbody>
<?php foreach ($availability as $slot): ?>
<tr>
<td><?= htmlspecialchars((new DateTimeImmutable($slot['event_date']))->format('d.m.Y')) ?></td>
<td><?= htmlspecialchars(ucfirst($slot['status'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<section style="margin-top: 40px;">
<h3>Bewertungen</h3>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php foreach ($reviews as $review): ?>
<article class="band-card">
<p><strong><?= htmlspecialchars($review['author']) ?></strong> <?= (int) $review['rating'] ?> ★</p>
<p><?= htmlspecialchars($review['comment']) ?></p>
<p class="card-meta"><?= (new DateTimeImmutable($review['created_at']))->format('d.m.Y') ?></p>
</article>
<?php endforeach; ?>
<?php if (!$reviews): ?>
<p>Noch keine freigegebenen Bewertungen.</p>
<?php endif; ?>
</section>
<?php if ($user && $user['role'] === 'kunde'): ?>
<section style="margin-top: 40px;">
<h3>Eigene Bewertung</h3>
<form method="post">
<input type="hidden" name="review" value="1">
<label>Sterne
<select class="form-control" name="rating">
<?php for ($i = 5; $i >= 1; $i--): ?>
<option value="<?= $i ?>"><?= $i ?></option>
<?php endfor; ?>
</select>
</label>
<label>Kommentar (max. 200 Zeichen)
<textarea class="form-control" name="comment" maxlength="200"></textarea>
</label>
<button class="btn-primary">Bewertung senden</button>
</form>
</section>
<?php endif; ?>
</main>
</body>
</html>
-76
View File
@@ -1,76 +0,0 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'kunde',
city TEXT,
verified INTEGER NOT NULL DEFAULT 0,
verification_token TEXT,
created_at TEXT
);
CREATE TABLE IF NOT EXISTS bands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
city TEXT,
genre TEXT,
price INTEGER DEFAULT 0,
description TEXT,
status TEXT NOT NULL DEFAULT 'prüfung',
style_tags TEXT,
video_url TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS band_media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
type TEXT NOT NULL,
url TEXT NOT NULL,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS band_availability (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
event_date TEXT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
user_id INTEGER,
event_date TEXT,
location TEXT,
budget INTEGER,
event_type TEXT,
message TEXT,
status TEXT NOT NULL DEFAULT 'neu',
created_at TEXT,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
rating INTEGER NOT NULL,
comment TEXT,
status TEXT NOT NULL DEFAULT 'wartend',
created_at TEXT,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-307
View File
@@ -1,307 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/config.php';
$options = array_slice($argv, 1);
if (in_array('--fresh', $options, true) && file_exists(DB_PATH)) {
unlink(DB_PATH);
echo "Bestehende Datenbank entfernt wird neu initialisiert.\n";
}
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
if (!isset($_SERVER['REQUEST_METHOD'])) {
$_SERVER['REQUEST_METHOD'] = 'GET';
}
final class FunctionalTestRunner
{
private PDO $pdo;
private array $results = [];
private int $passed = 0;
private int $failed = 0;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function run(string $title, callable $test, bool $transactional = false): void
{
try {
if ($transactional) {
$this->pdo->beginTransaction();
}
$details = $test($this->pdo);
if ($transactional && $this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
$this->record('PASS', $title, $details);
} catch (Throwable $e) {
if ($transactional && $this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
$this->record('FAIL', $title, $e->getMessage());
}
}
private function record(string $status, string $title, ?string $details): void
{
$status === 'PASS' ? $this->passed++ : $this->failed++;
$this->results[] = [
'status' => $status,
'title' => $title,
'details' => $details ?? '',
];
}
public function summary(): void
{
foreach ($this->results as $result) {
$symbol = $result['status'] === 'PASS' ? '\u{2705}' : '\u{274C}';
echo sprintf("%s %s\n", $symbol, $result['title']);
if ($result['details'] !== '') {
echo sprintf(" %s\n", $result['details']);
}
}
echo str_repeat('-', 50) . "\n";
echo sprintf("Ergebnis: %d bestanden, %d fehlgeschlagen\n", $this->passed, $this->failed);
exit($this->failed === 0 ? 0 : 1);
}
}
function renderPage(string $file, array $get = [], array $post = []): string
{
$previousGet = $_GET ?? [];
$previousPost = $_POST ?? [];
$previousMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$_GET = $get;
$_POST = $post;
$_SERVER['REQUEST_METHOD'] = empty($post) ? 'GET' : 'POST';
ob_start();
include __DIR__ . '/' . ltrim($file, '/');
$output = ob_get_clean();
$_GET = $previousGet;
$_POST = $previousPost;
$_SERVER['REQUEST_METHOD'] = $previousMethod;
return $output;
}
function restartSession(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
$pdo = db();
$runner = new FunctionalTestRunner($pdo);
$runner->run('Storage-Verzeichnis vorhanden', function () {
if (!is_dir(__DIR__ . '/storage')) {
throw new RuntimeException('Ordner storage fehlt.');
}
return 'Pfad: ' . realpath(__DIR__ . '/storage');
});
$runner->run('Datenbank initialisiert', function (PDO $pdo) {
if (!file_exists(DB_PATH)) {
throw new RuntimeException('database.sqlite wurde nicht erstellt.');
}
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")
->fetchAll(PDO::FETCH_COLUMN);
$required = ['users', 'bands', 'requests', 'reviews', 'settings'];
foreach ($required as $table) {
if (!in_array($table, $tables, true)) {
throw new RuntimeException('Tabelle ' . $table . ' fehlt.');
}
}
return 'Tabellen gefunden: ' . implode(', ', $required);
});
$runner->run('Seed-Daten verfügbar', function (PDO $pdo) {
$users = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
$bands = (int) $pdo->query('SELECT COUNT(*) FROM bands')->fetchColumn();
if ($users < 3 || $bands < 2) {
throw new RuntimeException('Seed-Daten unvollständig.');
}
return sprintf('Users: %d, Bands: %d', $users, $bands);
});
$runner->run('Login / Logout Workflow', function () {
if (!login('david@example.com', 'secret123')) {
throw new RuntimeException('Login schlug fehl.');
}
$user = currentUser();
if (!$user || $user['role'] !== 'kunde') {
throw new RuntimeException('Session liefert keinen Kunden.');
}
logout();
restartSession();
if (currentUser()) {
throw new RuntimeException('Logout hat Session nicht geleert.');
}
return 'Login erfolgreich für ' . $user['name'];
});
$runner->run('Band-Filter & Durchschnitt', function () {
$bands = allBands(['genre' => 'Funk']);
if (!$bands) {
throw new RuntimeException('Filter lieferte keine Band.');
}
$rating = averageRating((int) $bands[0]['id']);
if ($rating === null) {
throw new RuntimeException('Keine Bewertung vorhanden.');
}
return sprintf('%d Bands, Ø Bewertung %.1f★', count($bands), $rating);
});
$runner->run('Medien & Verfügbarkeiten geladen', function () {
$media = bandMedia(1);
$availability = bandAvailability(1);
$reviews = bandReviews(1);
if (!$media || !$availability || !$reviews) {
throw new RuntimeException('Band 1 hat unvollständige Daten.');
}
return sprintf('Medien: %d, Slots: %d, Reviews: %d', count($media), count($availability), count($reviews));
});
$runner->run('Anfrage speichern (Transaktion)', function (PDO $pdo) {
$before = (int) $pdo->query('SELECT COUNT(*) FROM requests')->fetchColumn();
createRequest([
'band_id' => 1,
'user_id' => 3,
'event_date' => (new DateTimeImmutable('+60 days'))->format('Y-m-d'),
'location' => 'Teststadt',
'budget' => 4500,
'event_type' => 'Testevent',
'message' => 'Funktionstest Anfrage',
]);
$after = (int) $pdo->query('SELECT COUNT(*) FROM requests')->fetchColumn();
if ($after !== $before + 1) {
throw new RuntimeException('Anfrage wurde nicht gespeichert.');
}
return 'Requests gesamt (temporär): ' . $after;
}, true);
$runner->run('Bewertungen speichern & Eligibility', function (PDO $pdo) {
if (!eligibleForReview(1, 3)) {
throw new RuntimeException('User 3 sollte berechtigt sein.');
}
$before = (int) $pdo->query('SELECT COUNT(*) FROM reviews')->fetchColumn();
storeReview([
'band_id' => 1,
'user_id' => 3,
'rating' => 4,
'comment' => 'Testkommentar',
]);
$after = (int) $pdo->query('SELECT COUNT(*) FROM reviews')->fetchColumn();
if ($after !== $before + 1) {
throw new RuntimeException('Review wurde nicht gespeichert.');
}
return 'Reviews gesamt (temporär): ' . $after;
}, true);
$runner->run('Einstellungen lesen & aktualisieren', function () {
$current = settings();
$originalFee = $current['service_fee'] ?? '0';
updateSetting('service_fee', '12');
$updated = settings();
if (($updated['service_fee'] ?? null) !== '12') {
throw new RuntimeException('Service Fee konnte nicht aktualisiert werden.');
}
updateSetting('service_fee', $originalFee);
return 'Service Fee temporär auf 12 gesetzt.';
}, true);
$runner->run('Moderations-Aktionen', function (PDO $pdo) {
changeBandStatus(1, 'prüfung');
$status = $pdo->query('SELECT status FROM bands WHERE id = 1')->fetchColumn();
if ($status !== 'prüfung') {
throw new RuntimeException('Bandstatus änderte sich nicht.');
}
changeReviewStatus(1, 'gesperrt');
$reviewStatus = $pdo->query('SELECT status FROM reviews WHERE id = 1')->fetchColumn();
if ($reviewStatus !== 'gesperrt') {
throw new RuntimeException('Reviewstatus änderte sich nicht.');
}
return 'Statusänderungen durchgeführt.';
}, true);
$runner->run('Registrierung legt Band an', function (PDO $pdo) {
$email = 'tester+' . uniqid('', true) . '@example.com';
$result = register([
'name' => 'Functional Tester',
'email' => $email,
'password' => 'secret123',
'role' => 'band',
'city' => 'Testingen',
'band_name' => 'QA Ensemble',
'genre' => 'QA Funk',
]);
if (empty($result['token']) || strlen($result['token']) < 20) {
throw new RuntimeException('Verifikationstoken fehlt.');
}
$user = $pdo->prepare('SELECT id, role FROM users WHERE email = :email');
$user->execute([':email' => $email]);
$userRow = $user->fetch(PDO::FETCH_ASSOC);
if (!$userRow || $userRow['role'] !== 'band') {
throw new RuntimeException('User wurde nicht gespeichert.');
}
$band = $pdo->prepare('SELECT status FROM bands WHERE user_id = :id');
$band->execute([':id' => $userRow['id']]);
$bandRow = $band->fetch(PDO::FETCH_ASSOC);
if (!$bandRow || $bandRow['status'] !== 'prüfung') {
throw new RuntimeException('Bandprofil wurde nicht angelegt.');
}
return 'Token erstellt und Bandstatus "prüfung" bestätigt.';
}, true);
$runner->run('Startseite rendert fehlerfrei', function () {
$html = renderPage('index.php');
if (strpos($html, 'Aktive Bands') === false) {
throw new RuntimeException('Indexseite liefert keinen Inhalt.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('Band-Detailseite rendert', function () {
$html = renderPage('band-detail.php', ['id' => 1]);
if (strpos($html, 'Verfügbarkeit') === false) {
throw new RuntimeException('Band-Detailseite unvollständig.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('Anfrageformular rendert', function () {
$html = renderPage('anfrage.php', ['band_id' => 1]);
if (strpos($html, 'Anfrage an') === false) {
throw new RuntimeException('Anfrageformular fehlgeschlagen.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('E-Mail Logging (kein Versand)', function () {
$logDir = __DIR__ . '/storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
$logFile = $logDir . '/mail.log';
$before = file_exists($logFile) ? filesize($logFile) : 0;
sendEmail('qa@example.com', 'Functional Test', 'Nur Logeintrag kein Versand.');
$after = filesize($logFile);
if ($after <= $before) {
throw new RuntimeException('Mail-Log wurde nicht aktualisiert.');
}
return 'Logeintrag ergänzt, Versand erfolgt nur als Datei.';
});
$runner->summary();
-93
View File
@@ -1,93 +0,0 @@
<?php
require_once __DIR__ . '/functions.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function currentUser(): ?array
{
if (empty($_SESSION['user_id'])) {
return null;
}
static $user;
if ($user) {
return $user;
}
$stmt = db()->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
return $user;
}
function login(string $email, string $password): bool
{
$stmt = db()->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password'])) {
return false;
}
if ((int) $user['verified'] !== 1) {
throw new RuntimeException('Bitte verifiziere zuerst deine E-Mail.');
}
$_SESSION['user_id'] = $user['id'];
return true;
}
function logout(): void
{
session_destroy();
}
function register(array $data): array
{
$token = bin2hex(random_bytes(16));
$stmt = db()->prepare('INSERT INTO users (name, email, password, role, city, verified, verification_token, created_at)
VALUES (:name, :email, :password, :role, :city, 0, :token, :created)');
$stmt->execute([
':name' => $data['name'],
':email' => $data['email'],
':password' => password_hash($data['password'], PASSWORD_DEFAULT),
':role' => $data['role'],
':city' => $data['city'] ?? null,
':token' => $token,
':created' => (new DateTimeImmutable())->format('c'),
]);
if ($data['role'] === 'band') {
$band = db()->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status)
VALUES (:user_id, :name, :city, :genre, :price, :description, :status)');
$band->execute([
':user_id' => (int) db()->lastInsertId(),
':name' => $data['band_name'] ?? 'Neue Band',
':city' => $data['city'] ?? '',
':genre' => $data['genre'] ?? '',
':price' => 0,
':description' => 'Bitte Profil ergänzen.',
':status' => 'prüfung',
]);
}
return ['token' => $token];
}
function requireLogin(): void
{
if (!currentUser()) {
header('Location: login.php');
exit;
}
}
function requireAdmin(): void
{
$user = currentUser();
if (!$user || $user['role'] !== 'admin') {
http_response_code(403);
echo 'Keine Berechtigung';
exit;
}
}
-10
View File
@@ -1,10 +0,0 @@
<?php
const SITE_NAME = 'GetYourBand';
const DB_PATH = __DIR__ . '/../storage/database.sqlite';
const SUPPORT_EMAIL = 'support@getyourband.ch';
const BASE_URL = '';
const COOKIE_NAME = 'gyb_consent';
if (!is_dir(__DIR__ . '/../storage')) {
mkdir(__DIR__ . '/../storage', 0775, true);
}
-144
View File
@@ -1,144 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$dsn = 'sqlite:' . DB_PATH;
$pdo = new PDO($dsn);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
initializeDatabase($pdo);
return $pdo;
}
function initializeDatabase(PDO $pdo): void
{
$schema = file_get_contents(__DIR__ . '/../database.sql');
$pdo->exec($schema);
seedData($pdo);
}
function seedData(PDO $pdo): void
{
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
if ($count > 0) {
return;
}
$now = (new DateTimeImmutable())->format('c');
$password = password_hash('secret123', PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (name, email, password, role, verified, verification_token, created_at)
VALUES (:name, :email, :password, :role, :verified, :token, :created)');
$stmt->execute([
':name' => 'Admin',
':email' => 'admin@getyourband.ch',
':password' => $password,
':role' => 'admin',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$stmt->execute([
':name' => 'Maya Keller',
':email' => 'maya@getyourband.ch',
':password' => $password,
':role' => 'band',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$stmt->execute([
':name' => 'David Graf',
':email' => 'david@example.com',
':password' => $password,
':role' => 'kunde',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$bands = [
[
'user_id' => 2,
'name' => 'Neon Groove Kollektiv',
'city' => 'Zürich',
'genre' => 'Funk / Soul',
'price' => 4200,
'description' => '7-köpfige Funk- und Soulband mit knalligem Brass-Sound und interaktiver Show.',
'status' => 'aktiv',
'style_tags' => 'Funk,Retro,Showband',
'video_url' => 'https://www.youtube.com/embed/dQw4w9WgXcQ',
],
[
'user_id' => null,
'name' => 'Sonnenblitz Orchester',
'city' => 'Bern',
'genre' => 'Pop / Party',
'price' => 3700,
'description' => 'Party-Coverband mit LED-Lichtshow und zweistimmigem Gesang.',
'status' => 'aktiv',
'style_tags' => 'Pop,Party,LED',
'video_url' => 'https://www.youtube.com/embed/5NV6Rdv1a3I',
],
];
$bandStmt = $pdo->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status, style_tags, video_url)
VALUES (:user_id, :name, :city, :genre, :price, :description, :status, :style_tags, :video_url)');
foreach ($bands as $band) {
$bandStmt->execute([
':user_id' => $band['user_id'],
':name' => $band['name'],
':city' => $band['city'],
':genre' => $band['genre'],
':price' => $band['price'],
':description' => $band['description'],
':status' => $band['status'],
':style_tags' => $band['style_tags'],
':video_url' => $band['video_url'],
]);
$bandId = (int) $pdo->lastInsertId();
$mediaStmt = $pdo->prepare('INSERT INTO band_media (band_id, type, url) VALUES (:band_id, :type, :url)');
$mediaStmt->execute([':band_id' => $bandId, ':type' => 'image', ':url' => 'https://images.unsplash.com/photo-1507878866276-a947ef722fee']);
$mediaStmt->execute([':band_id' => $bandId, ':type' => 'image', ':url' => 'https://images.unsplash.com/photo-1489515217757-5fd1be406fef']);
$availStmt = $pdo->prepare('INSERT INTO band_availability (band_id, event_date, status) VALUES (:band_id, :event_date, :status)');
for ($i = 0; $i < 4; $i++) {
$availStmt->execute([
':band_id' => $bandId,
':event_date' => (new DateTimeImmutable('+' . ($i + 1) * 7 . ' days'))->format('Y-m-d'),
':status' => $i % 2 === 0 ? 'frei' : 'option',
]);
}
}
$pdo->exec("INSERT INTO settings (key, value) VALUES ('paypal_enabled', '0'), ('service_fee', '8')");
$requestStmt = $pdo->prepare('INSERT INTO requests (band_id, user_id, event_date, location, budget, event_type, message, status, created_at)
VALUES (:band_id, :user_id, :event_date, :location, :budget, :event_type, :message, :status, :created)');
$requestStmt->execute([
':band_id' => 1,
':user_id' => 3,
':event_date' => (new DateTimeImmutable('+30 days'))->format('Y-m-d'),
':location' => 'Basel',
':budget' => 5000,
':event_type' => 'Firmenfeier',
':message' => 'Wir suchen einen funky Act für die Sommerparty.',
':status' => 'bestätigt',
':created' => $now,
]);
$pdo->exec("INSERT INTO reviews (band_id, user_id, rating, comment, status, created_at) VALUES (1, 3, 5, 'Mega Stimmung und super Show!', 'freigegeben', datetime('now'))");
}
-10
View File
@@ -1,10 +0,0 @@
<?php
function sendEmail(string $to, string $subject, string $message): void
{
$logDir = __DIR__ . '/../storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
$entry = sprintf("%s\nTo: %s\nSubject: %s\n%s\n---\n", date('c'), $to, $subject, $message);
file_put_contents($logDir . '/mail.log', $entry, FILE_APPEND);
}
-159
View File
@@ -1,159 +0,0 @@
<?php
require_once __DIR__ . '/database.php';
function allBands(array $filters = []): array
{
$pdo = db();
$where = ['status = :status'];
$params = [':status' => 'aktiv'];
if (!empty($filters['genre'])) {
$where[] = 'genre LIKE :genre';
$params[':genre'] = '%' . $filters['genre'] . '%';
}
if (!empty($filters['city'])) {
$where[] = 'city LIKE :city';
$params[':city'] = '%' . $filters['city'] . '%';
}
$sql = 'SELECT * FROM bands WHERE ' . implode(' AND ', $where) . ' ORDER BY name';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function findBand(int $id): ?array
{
$stmt = db()->prepare('SELECT * FROM bands WHERE id = :id');
$stmt->execute([':id' => $id]);
$band = $stmt->fetch(PDO::FETCH_ASSOC);
return $band ?: null;
}
function bandMedia(int $bandId): array
{
$stmt = db()->prepare('SELECT * FROM band_media WHERE band_id = :id');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function bandAvailability(int $bandId): array
{
$stmt = db()->prepare('SELECT * FROM band_availability WHERE band_id = :id ORDER BY event_date');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function bandReviews(int $bandId): array
{
$stmt = db()->prepare('SELECT r.*, u.name AS author
FROM reviews r
JOIN users u ON u.id = r.user_id
WHERE r.band_id = :id AND r.status = "freigegeben"
ORDER BY r.created_at DESC');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function averageRating(int $bandId): ?float
{
$stmt = db()->prepare('SELECT AVG(rating) FROM reviews WHERE band_id = :id AND status = "freigegeben"');
$stmt->execute([':id' => $bandId]);
$value = $stmt->fetchColumn();
return $value ? round((float) $value, 1) : null;
}
function formatPrice(int $amount): string
{
return number_format($amount, 0, ',', '.') . ' CHF';
}
function createRequest(array $data): void
{
$stmt = db()->prepare('INSERT INTO requests (band_id, user_id, event_date, location, budget, event_type, message, status, created_at)
VALUES (:band_id, :user_id, :event_date, :location, :budget, :event_type, :message, :status, :created_at)');
$stmt->execute([
':band_id' => $data['band_id'],
':user_id' => $data['user_id'],
':event_date' => $data['event_date'],
':location' => $data['location'],
':budget' => $data['budget'],
':event_type' => $data['event_type'],
':message' => $data['message'],
':status' => 'neu',
':created_at' => (new DateTimeImmutable())->format('c'),
]);
}
function userRequests(int $userId): array
{
$stmt = db()->prepare('SELECT * FROM requests WHERE user_id = :id ORDER BY created_at DESC');
$stmt->execute([':id' => $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function storeReview(array $data): void
{
$stmt = db()->prepare('INSERT INTO reviews (band_id, user_id, rating, comment, status, created_at)
VALUES (:band_id, :user_id, :rating, :comment, :status, :created_at)');
$stmt->execute([
':band_id' => $data['band_id'],
':user_id' => $data['user_id'],
':rating' => $data['rating'],
':comment' => $data['comment'],
':status' => 'wartend',
':created_at' => (new DateTimeImmutable())->format('c'),
]);
}
function eligibleForReview(int $bandId, int $userId): bool
{
$stmt = db()->prepare('SELECT COUNT(*) FROM requests WHERE band_id = :band AND user_id = :user AND status = "bestätigt"');
$stmt->execute([':band' => $bandId, ':user' => $userId]);
return (int) $stmt->fetchColumn() > 0;
}
function settings(): array
{
$stmt = db()->query('SELECT key, value FROM settings');
$data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
return $data ?: ['paypal_enabled' => '0', 'service_fee' => '0'];
}
function updateSetting(string $key, string $value): void
{
$stmt = db()->prepare('INSERT INTO settings (key, value) VALUES (:key, :value)
ON CONFLICT(key) DO UPDATE SET value = excluded.value');
$stmt->execute([':key' => $key, ':value' => $value]);
}
function moderationItems(string $type): array
{
$pdo = db();
if ($type === 'bands') {
return $pdo->query('SELECT * FROM bands WHERE status != "aktiv"')->fetchAll(PDO::FETCH_ASSOC);
}
if ($type === 'reviews') {
return $pdo->query('SELECT r.*, b.name AS band_name, u.name AS author
FROM reviews r
JOIN bands b ON b.id = r.band_id
JOIN users u ON u.id = r.user_id
WHERE r.status = "wartend"')->fetchAll(PDO::FETCH_ASSOC);
}
return [];
}
function changeBandStatus(int $bandId, string $status): void
{
$stmt = db()->prepare('UPDATE bands SET status = :status WHERE id = :id');
$stmt->execute([':status' => $status, ':id' => $bandId]);
}
function changeReviewStatus(int $reviewId, string $status): void
{
$stmt = db()->prepare('UPDATE reviews SET status = :status WHERE id = :id');
$stmt->execute([':status' => $status, ':id' => $reviewId]);
}
+325 -98
View File
@@ -1,111 +1,338 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$filters = [
'genre' => $_GET['genre'] ?? '',
'city' => $_GET['city'] ?? '',
];
$bands = allBands($filters);
$settings = settings();
$user = currentUser();
$title = "MausSynth Lab";
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= SITE_NAME ?> Bands buchen</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
<title><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
:root {
--bg: radial-gradient(circle at center, #1f014d 0%, #05010d 100%);
--accent: #ff2bd7;
--glow: rgba(255, 43, 215, 0.35);
--text: #f7f3ff;
}
* {
box-sizing: border-box;
cursor: none;
}
body {
margin: 0;
font-family: 'Orbitron', 'Segoe UI', sans-serif;
color: var(--text);
background: var(--bg);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 0.5rem;
text-shadow: 0 0 8px var(--accent);
}
p.description {
margin: 0;
opacity: 0.7;
letter-spacing: 0.08em;
text-align: center;
max-width: 420px;
}
.stage {
position: relative;
width: min(90vw, 900px);
height: min(70vh, 520px);
border-radius: 24px;
border: 2px solid rgba(255, 255, 255, 0.15);
background: rgba(10, 5, 25, 0.7);
backdrop-filter: blur(6px);
box-shadow: 0 0 30px rgba(5, 0, 20, 0.7);
overflow: hidden;
}
canvas#visualizer {
width: 100%;
height: 100%;
display: block;
}
.cursor {
position: absolute;
top: 0;
left: 0;
width: 80px;
height: 80px;
margin-top: -40px;
margin-left: -40px;
border-radius: 50%;
pointer-events: none;
border: 2px solid var(--accent);
box-shadow: 0 0 30px var(--accent), inset 0 0 20px var(--glow);
mix-blend-mode: screen;
transition: transform 0.1s ease-out;
}
.hud {
position: absolute;
right: 16px;
bottom: 16px;
padding: 12px 16px;
background: rgba(10, 5, 25, 0.8);
border-radius: 12px;
font-size: 0.85rem;
letter-spacing: 0.05em;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.hud span.label {
color: rgba(255,255,255,0.6);
margin-right: 8px;
}
.hint {
margin-top: 1rem;
font-size: 0.9rem;
letter-spacing: 0.05em;
opacity: 0.75;
}
@media (max-width: 600px) {
* {
cursor: default;
}
.cursor {
display: none;
}
}
</style>
</head>
<body>
<header>
<nav class="badge-list" style="justify-content: flex-end;">
<?php if ($user): ?>
<span class="badge">Hallo <?= htmlspecialchars($user['name']) ?></span>
<a class="badge" href="profil.php">Mein Profil</a>
<?php if ($user['role'] === 'admin'): ?>
<a class="badge" href="admin/dashboard.php">Admin</a>
<?php endif; ?>
<a class="badge" href="login.php?action=logout">Logout</a>
<?php else: ?>
<a class="badge" href="login.php">Login / Registrieren</a>
<?php endif; ?>
</nav>
<section class="hero">
<div>
<p class="badge">Schritt 3 · Frontend Release</p>
<h1>Finde deine <span style="color: var(--primary);">Funky Liveband</span></h1>
<p>GetYourBand bringt verifizierte Live-Acts mit Veranstalter:innen in der ganzen Schweiz zusammen. Mit Bewertungen,
moderner Suche und aktivierbarer Vermittlungsgebühr.</p>
<div class="badge-list">
<span class="badge">Bewertungen geprüft</span>
<span class="badge badge-rating">PayPal <?= $settings['paypal_enabled'] === '1' ? 'aktiv' : 'optional' ?></span>
<span class="badge">Service Fee <?= htmlspecialchars($settings['service_fee']) ?>%</span>
</div>
</div>
<form class="search-panel" method="get" data-filter-form>
<div>
<label for="genre">Stil / Genre</label>
<input type="text" id="genre" name="genre" value="<?= htmlspecialchars($filters['genre']) ?>" placeholder="Funk, Party, Jazz">
</div>
<div>
<label for="city">Ort / PLZ</label>
<input type="text" id="city" name="city" value="<?= htmlspecialchars($filters['city']) ?>" placeholder="Zürich, Basel">
</div>
<div>
<label>&nbsp;</label>
<button type="submit" class="btn-primary">Filtern</button>
</div>
</form>
</section>
</header>
<main>
<h2>Aktive Bands (<?= count($bands) ?>)</h2>
<section class="band-grid">
<?php foreach ($bands as $band): $rating = averageRating((int) $band['id']); ?>
<article class="band-card">
<p class="badge"><?= htmlspecialchars($band['genre']) ?></p>
<h3><?= htmlspecialchars($band['name']) ?></h3>
<p class="card-meta">Standort: <?= htmlspecialchars($band['city'] ?? '') ?></p>
<p><?= htmlspecialchars($band['description']) ?></p>
<p class="price-tag">ab <?= formatPrice((int) $band['price']) ?></p>
<?php if ($rating): ?>
<p class="card-meta">Bewertung: <?= $rating ?> ★</p>
<?php endif; ?>
<div class="badge-list">
<?php foreach (array_filter(array_map('trim', explode(',', (string) $band['style_tags']))) as $tag): ?>
<span class="badge">#<?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<p>
<a class="btn-primary" href="band-detail.php?id=<?= (int) $band['id'] ?>">Band ansehen</a>
</p>
</article>
<?php endforeach; ?>
<?php if (!$bands): ?>
<p>Keine Bands gefunden ändere deine Filter.</p>
<?php endif; ?>
</section>
</main>
<footer>
<div>
<strong>Legal</strong><br>
<a href="#">Datenschutz</a> · <a href="#">AGB</a>
</div>
<div>
<strong>Kontakt</strong><br>
support@getyourband.ch
</div>
</footer>
<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></h1>
<p class="description">Schiebe deine Maus über das Klangfeld, um Frequenz, Filter und verrückte Modulation zu steuern. Klicke, um Beats zu triggern.</p>
<div class="cookie-banner">
<p>Wir verwenden Cookies für Performance-Analysen. Mit Klick auf "Okay" akzeptierst du das.</p>
<button class="btn-primary" data-cookie-accept>Okay!</button>
<div class="stage" id="stage">
<canvas id="visualizer"></canvas>
<div class="cursor" id="cursor"></div>
<div class="hud" id="hud">
<div><span class="label">Freq</span><span id="freqReadout">-- Hz</span></div>
<div><span class="label">Reso</span><span id="resoReadout">--</span></div>
<div><span class="label">Noise</span><span id="noiseReadout">--</span></div>
</div>
</div>
<script src="assets/js/app.js" defer></script>
<p class="hint">Tipp: Halte die Maustaste gedrückt, bewege dich in Kreisen &ndash; und genieße den abgefahrenen Klangteppich!</p>
<script>
const stage = document.getElementById('stage');
const cursor = document.getElementById('cursor');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
const freqReadout = document.getElementById('freqReadout');
const resoReadout = document.getElementById('resoReadout');
const noiseReadout = document.getElementById('noiseReadout');
let audioCtx;
let masterGain;
let filter;
let lfo;
let lfoGain;
let noise;
let noiseGain;
let compressor;
let isPressed = false;
let analyser;
let bufferLength;
let dataArray;
function setupAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;
filter.Q.value = 12;
lfo = audioCtx.createOscillator();
lfo.type = 'sine';
lfo.frequency.value = 4;
lfoGain = audioCtx.createGain();
lfoGain.gain.value = 1200;
noise = createNoiseSource();
noiseGain = audioCtx.createGain();
noiseGain.gain.value = 0.05;
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.0;
compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.value = -24;
compressor.ratio.value = 12;
compressor.attack.value = 0.005;
compressor.release.value = 0.2;
analyser = audioCtx.createAnalyser();
analyser.fftSize = 1024;
bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
osc.connect(filter);
filter.connect(masterGain);
lfo.connect(lfoGain);
lfoGain.connect(filter.frequency);
noise.connect(noiseGain).connect(masterGain);
masterGain.connect(compressor).connect(audioCtx.destination);
masterGain.connect(analyser);
osc.start();
lfo.start();
noise.start(0);
}
function createNoiseSource() {
const rate = audioCtx.sampleRate;
const bufferSize = rate * 2;
const buffer = audioCtx.createBuffer(1, bufferSize, rate);
const output = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const noiseSource = audioCtx.createBufferSource();
noiseSource.buffer = buffer;
noiseSource.loop = true;
return noiseSource;
}
function resizeCanvas() {
canvas.width = stage.clientWidth;
canvas.height = stage.clientHeight;
}
function updateAudio(x, y) {
if (!audioCtx) return;
const rect = stage.getBoundingClientRect();
const normX = (x - rect.left) / rect.width;
const normY = (y - rect.top) / rect.height;
const minFreq = 80;
const maxFreq = 1200;
const freq = minFreq * Math.pow(maxFreq / minFreq, normX);
filter.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.02);
const q = 4 + normY * 20;
filter.Q.setTargetAtTime(q, audioCtx.currentTime, 0.02);
const noiseLevel = normY * 0.3;
noiseGain.gain.setTargetAtTime(noiseLevel, audioCtx.currentTime, 0.05);
const lfoSpeed = 0.5 + normX * 8;
lfo.frequency.setTargetAtTime(lfoSpeed, audioCtx.currentTime, 0.05);
freqReadout.textContent = `${freq.toFixed(1)} Hz`;
resoReadout.textContent = q.toFixed(2);
noiseReadout.textContent = noiseLevel.toFixed(2);
}
function animate() {
requestAnimationFrame(animate);
if (!analyser) return;
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 255 * canvas.height;
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsla(${hue}, 90%, 60%, 0.6)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
function pointerMove(event) {
const x = event.clientX;
const y = event.clientY;
cursor.style.transform = `translate(${x}px, ${y}px) scale(${isPressed ? 1.2 : 1})`;
updateAudio(x, y);
}
function pointerDown() {
isPressed = true;
if (!audioCtx) {
setupAudio();
}
if (!audioCtx) return;
const resumePromise = audioCtx.state === 'suspended'
? audioCtx.resume()
: Promise.resolve();
resumePromise
.then(() => {
masterGain.gain.cancelScheduledValues(audioCtx.currentTime);
masterGain.gain.setTargetAtTime(0.7, audioCtx.currentTime, 0.02);
lfoGain.gain.setTargetAtTime(900, audioCtx.currentTime, 0.08);
})
.catch((error) => {
console.error('Failed to resume AudioContext', error);
});
}
function pointerUp() {
isPressed = false;
if (!audioCtx) return;
masterGain.gain.setTargetAtTime(0.0, audioCtx.currentTime, 0.05);
lfoGain.gain.setTargetAtTime(200, audioCtx.currentTime, 0.08);
}
stage.addEventListener('pointermove', pointerMove);
stage.addEventListener('pointerdown', pointerDown);
window.addEventListener('pointerup', pointerUp);
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
animate();
document.addEventListener('keydown', (event) => {
if (!audioCtx) return;
if (event.code === 'Space') {
const now = audioCtx.currentTime;
const burst = audioCtx.createOscillator();
burst.type = 'square';
burst.frequency.value = filter.frequency.value * 0.5;
const burstGain = audioCtx.createGain();
burstGain.gain.value = 0;
burst.connect(burstGain).connect(masterGain);
burst.start(now);
burstGain.gain.setValueAtTime(0, now);
burstGain.gain.linearRampToValueAtTime(0.8, now + 0.05);
burstGain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
burst.stop(now + 0.6);
}
});
</script>
</body>
</html>
-115
View File
@@ -1,115 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
$action = $_GET['action'] ?? '';
if ($action === 'logout') {
logout();
header('Location: index.php');
exit;
}
$error = '';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['login'])) {
try {
if (!login($_POST['email'], $_POST['password'])) {
$error = 'Login fehlgeschlagen.';
} else {
header('Location: index.php');
exit;
}
} catch (RuntimeException $ex) {
$error = $ex->getMessage();
}
} elseif (isset($_POST['register'])) {
if ($_POST['password'] !== $_POST['password_confirm']) {
$error = 'Passwörter stimmen nicht überein.';
} else {
$result = register([
'name' => trim((string) $_POST['name']),
'email' => trim((string) $_POST['email']),
'password' => $_POST['password'],
'role' => $_POST['role'],
'city' => trim((string) $_POST['city']),
'band_name' => $_POST['band_name'] ?? null,
'genre' => $_POST['genre'] ?? null,
]);
$verificationLink = BASE_URL . '/verify-email.php?token=' . urlencode($result['token']);
sendEmail($_POST['email'], 'E-Mail bestätigen', 'Bitte bestätige dein Konto: ' . $verificationLink);
$message = 'Check deine Inbox wir haben dir den Verifizierungslink geschickt: ' . htmlspecialchars($verificationLink);
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login / Registrierung <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Zurück</a>
<h1>Login / Registrieren</h1>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= $message ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<section class="band-detail-grid">
<div>
<h2>Login</h2>
<form method="post">
<input type="hidden" name="login" value="1">
<label>E-Mail
<input class="form-control" type="email" name="email" required>
</label>
<label>Passwort
<input class="form-control" type="password" name="password" required>
</label>
<button class="btn-primary">Einloggen</button>
</form>
</div>
<div>
<h2>Registrierung</h2>
<form method="post">
<input type="hidden" name="register" value="1">
<label>Name
<input class="form-control" type="text" name="name" required>
</label>
<label>E-Mail
<input class="form-control" type="email" name="email" required>
</label>
<label>Ort
<input class="form-control" type="text" name="city">
</label>
<label>Rolle
<select class="form-control" name="role">
<option value="kunde">Veranstalter:in</option>
<option value="band">Band</option>
</select>
</label>
<label>Bandname (falls Band)
<input class="form-control" type="text" name="band_name">
</label>
<label>Genre
<input class="form-control" type="text" name="genre">
</label>
<label>Passwort
<input class="form-control" type="password" name="password" required>
</label>
<label>Passwort wiederholen
<input class="form-control" type="password" name="password_confirm" required>
</label>
<button class="btn-primary">Account anlegen</button>
</form>
</div>
</section>
</main>
</body>
</html>
-90
View File
@@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
requireLogin();
$user = currentUser();
$band = null;
$message = '';
if ($user['role'] === 'band') {
$stmt = db()->prepare('SELECT * FROM bands WHERE user_id = :id');
$stmt->execute([':id' => $user['id']]);
$band = $stmt->fetch(PDO::FETCH_ASSOC);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = db()->prepare('UPDATE bands SET name = :name, city = :city, genre = :genre, price = :price, description = :description, style_tags = :tags WHERE id = :id');
$stmt->execute([
':name' => $_POST['name'],
':city' => $_POST['city'],
':genre' => $_POST['genre'],
':price' => (int) $_POST['price'],
':description' => $_POST['description'],
':tags' => $_POST['style_tags'],
':id' => $band['id'],
]);
$message = 'Bandprofil aktualisiert (wartet ggf. auf Freigabe).';
$band = findBand((int) $band['id']);
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mein Bereich <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Startseite</a>
<h1>Hallo <?= htmlspecialchars($user['name']) ?></h1>
<p>Rolle: <?= htmlspecialchars($user['role']) ?></p>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($band): ?>
<h2>Bandprofil</h2>
<form method="post">
<label>Bandname
<input class="form-control" name="name" value="<?= htmlspecialchars($band['name']) ?>">
</label>
<label>Ort
<input class="form-control" name="city" value="<?= htmlspecialchars($band['city']) ?>">
</label>
<label>Genre
<input class="form-control" name="genre" value="<?= htmlspecialchars($band['genre']) ?>">
</label>
<label>Tags
<input class="form-control" name="style_tags" value="<?= htmlspecialchars($band['style_tags']) ?>">
</label>
<label>Preis (CHF)
<input class="form-control" type="number" name="price" value="<?= (int) $band['price'] ?>">
</label>
<label>Beschreibung
<textarea class="form-control" name="description" rows="4"><?= htmlspecialchars($band['description']) ?></textarea>
</label>
<button class="btn-primary">Speichern</button>
</form>
<?php else: ?>
<p>Du hast noch kein Bandprofil angelegt.</p>
<?php endif; ?>
<?php if ($user['role'] === 'kunde'): ?>
<h2>Meine Anfragen</h2>
<table class="table">
<thead><tr><th>Band</th><th>Datum</th><th>Status</th></tr></thead>
<tbody>
<?php foreach (userRequests((int) $user['id']) as $request): $bandName = findBand((int) $request['band_id']); ?>
<tr>
<td><?= htmlspecialchars($bandName['name'] ?? 'Band #' . $request['band_id']) ?></td>
<td><?= htmlspecialchars($request['event_date']) ?></td>
<td><?= htmlspecialchars($request['status']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main>
</body>
</html>
-4429
View File
File diff suppressed because it is too large Load Diff
View File
-31
View File
@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/functions.php';
$token = $_GET['token'] ?? '';
$message = 'Ungültiger Token.';
if ($token) {
$stmt = db()->prepare('UPDATE users SET verified = 1, verification_token = NULL WHERE verification_token = :token');
$stmt->execute([':token' => $token]);
if ($stmt->rowCount() > 0) {
$message = 'Perfekt! Dein Account ist nun verifiziert. Du kannst dich einloggen.';
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Verifizierung <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<main style="padding: 60px 5vw;">
<div class="hero">
<p><?= htmlspecialchars($message) ?></p>
<a class="btn-primary" href="login.php">Zum Login</a>
</div>
</main>
</body>
</html>