Compare commits

...

17 Commits

Author SHA1 Message Date
admin f02f9e81a3 Merge branch 'main' into codex/projekt-fur-bandreservierung-erstellen-70yja1 2025-11-17 22:23:15 +01:00
admin 2c1cb35025 Add contact email support and band contact form 2025-11-17 22:17:59 +01:00
admin a3936c4c6f Merge pull request #8 from metacube2/codex/projekt-fur-bandreservierung-erstellen-tgjoyd
Integrate auroraalt mail transport
2025-11-17 21:50:16 +01:00
admin e04055103c Merge branch 'main' into codex/projekt-fur-bandreservierung-erstellen-tgjoyd 2025-11-17 21:50:05 +01:00
admin 619a1bf663 Wire up auroraalt mailer 2025-11-17 21:47:15 +01:00
admin a92ffc0ab9 Create auroraalt.php 2025-11-17 21:39:28 +01:00
admin 4e5a73a643 Create aurora.php 2025-11-17 21:29:08 +01:00
admin 1659940271 Merge pull request #7 from metacube2/codex/create-advanced-mouse-synthesizer-in-synth-folder
Add Bitcoin-reactive mouse synth playground
2025-11-17 16:49:40 +01:00
admin aa8ea8c6f2 Add Bitcoin-reactive mouse synth playground 2025-11-17 16:49:21 +01:00
admin 206125854a Merge pull request #6 from metacube2/codex/erstelle-seite-fur-top-10-altkos
Add MA200 signal overview for top altcoins
2025-11-17 16:17:14 +01:00
admin 98be743f35 Add MA200 signal overview for top altcoins 2025-11-17 16:16:50 +01:00
admin 5da0df88e0 Merge pull request #5 from metacube2/codex/projekt-fur-bandreservierung-erstellen-rx0gem
Add CLI functional test harness
2025-11-17 15:36:57 +01:00
admin 52c796d2db Add CLI functional test harness 2025-11-17 15:36:28 +01:00
admin bc9e3367b7 Merge pull request #4 from metacube2/codex/projekt-fur-bandreservierung-erstellen
Implement Bandreservierung Plattform
2025-11-17 15:05:10 +01:00
admin ab3a6e3711 Implement Bandreservierung Plattform 2025-11-17 15:02:58 +01:00
admin a7a619079a Create requirement.md 2025-11-17 14:53:12 +01:00
admin 14cd4c210f Merge pull request #3 from metacube2/codex/create-php-page-for-modern-date-app
Add hypermodern date dashboard
2025-11-17 14:51:09 +01:00
28 changed files with 12756 additions and 303 deletions
+2
View File
@@ -0,0 +1,2 @@
storage/*
!storage/.gitkeep
+5
View File
@@ -0,0 +1,5 @@
Options -Indexes
AddDefaultCharset UTF-8
<IfModule mod_rewrite.c>
RewriteEngine On
</IfModule>
+48
View File
@@ -0,0 +1,48 @@
<?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
@@ -0,0 +1,48 @@
<?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
@@ -0,0 +1,37 @@
<?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
@@ -0,0 +1,37 @@
<?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>
+342
View File
@@ -0,0 +1,342 @@
<?php
$altcoins = [
[
'name' => 'Ethereum (ETH)',
'symbol' => 'ETH',
'price' => 3725.42,
'ma200' => 3450.15,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'BNB',
'symbol' => 'BNB',
'price' => 598.12,
'ma200' => 612.77,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Solana (SOL)',
'symbol' => 'SOL',
'price' => 158.34,
'ma200' => 143.05,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'XRP',
'symbol' => 'XRP',
'price' => 0.57,
'ma200' => 0.63,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Dogecoin (DOGE)',
'symbol' => 'DOGE',
'price' => 0.19,
'ma200' => 0.15,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Cardano (ADA)',
'symbol' => 'ADA',
'price' => 0.48,
'ma200' => 0.62,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Avalanche (AVAX)',
'symbol' => 'AVAX',
'price' => 47.22,
'ma200' => 44.61,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Polkadot (DOT)',
'symbol' => 'DOT',
'price' => 8.81,
'ma200' => 7.29,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Chainlink (LINK)',
'symbol' => 'LINK',
'price' => 17.02,
'ma200' => 18.40,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Polygon (MATIC)',
'symbol' => 'MATIC',
'price' => 0.92,
'ma200' => 0.98,
'last_update' => '2024-04-21 14:00 UTC',
],
];
function determineSignal(float $price, float $ma200): array
{
if ($price >= $ma200) {
return ['LONG', 'Preis notiert über dem 200-Tage-Durchschnitt.'];
}
return ['SHORT', 'Preis notiert unter dem 200-Tage-Durchschnitt.'];
}
function formatNumber(float $value, int $decimals = 2): string
{
return number_format($value, $decimals, ',', '.');
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Top 10 Altcoins MA200 Signale</title>
<style>
:root {
color-scheme: light dark;
--bg: #0f172a;
--fg: #f8fafc;
--card: #1e293b;
--accent: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--muted: #94a3b8;
}
* {
box-sizing: border-box;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, rgba(16,185,129,0.25), transparent 55%),
var(--bg);
color: var(--fg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
main {
width: min(1100px, 100%);
background: rgba(15,23,42,0.85);
border: 1px solid rgba(148,163,184,0.2);
border-radius: 24px;
box-shadow: 0 15px 60px rgba(0,0,0,0.45);
padding: 2rem 2.5rem 2.5rem;
}
header {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 2rem;
}
header h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.2;
}
header p {
margin: 0;
color: var(--muted);
}
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
thead th {
text-align: left;
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid rgba(148,163,184,0.2);
padding-bottom: 0.75rem;
}
tbody td {
padding: 1rem 0;
border-bottom: 1px solid rgba(148,163,184,0.1);
}
tbody tr:last-child td {
border-bottom: none;
}
.symbol {
color: var(--muted);
font-size: 0.9rem;
}
.price {
font-variant-numeric: tabular-nums;
}
.signal {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.05em;
}
.signal-long {
background: rgba(16,185,129,0.15);
border: 1px solid rgba(16,185,129,0.5);
color: var(--accent);
}
.signal-short {
background: rgba(239,68,68,0.15);
border: 1px solid rgba(239,68,68,0.5);
color: var(--danger);
}
.telegram-card {
margin-top: 2rem;
background: rgba(30,41,59,0.9);
border: 1px solid rgba(59,130,246,0.4);
border-radius: 18px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.telegram-card h2 {
margin: 0;
font-size: 1.2rem;
}
.telegram-card p {
margin: 0;
color: var(--muted);
line-height: 1.5;
}
.telegram-card pre {
margin: 0;
padding: 1rem;
background: rgba(15,23,42,0.8);
border-radius: 12px;
border: 1px solid rgba(148,163,184,0.15);
font-size: 0.9rem;
white-space: pre-wrap;
}
@media (max-width: 720px) {
main {
padding: 1.5rem;
}
table, thead, tbody, tr, td, th {
display: block;
}
thead {
display: none;
}
tbody tr {
border: 1px solid rgba(148,163,184,0.2);
border-radius: 18px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
tbody td {
border: none;
padding: 0.35rem 0;
}
tbody td::before {
content: attr(data-label);
display: block;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.2rem;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>Top 10 Altcoins &amp; MA200 Signale</h1>
<p>Überblick über mögliche Einstiegszonen basierend auf dem 200-Tage-Durchschnitt. Die Signale (Long oder Short)
können direkt an den Telegram-Channel weitergeleitet werden.</p>
</header>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Preis (USD)</th>
<th>MA200 (USD)</th>
<th>Abweichung</th>
<th>Signal</th>
<th>Zuletzt aktualisiert</th>
</tr>
</thead>
<tbody>
<?php foreach ($altcoins as $coin):
[$signal, $reason] = determineSignal($coin['price'], $coin['ma200']);
$diff = $coin['price'] - $coin['ma200'];
$diffPercent = ($coin['ma200'] > 0)
? ($diff / $coin['ma200']) * 100
: 0;
?>
<tr>
<td data-label="Asset">
<strong><?php echo htmlspecialchars($coin['name']); ?></strong>
<div class="symbol"><?php echo htmlspecialchars($coin['symbol']); ?></div>
</td>
<td data-label="Preis" class="price">$ <?php echo formatNumber($coin['price']); ?></td>
<td data-label="MA200" class="price">$ <?php echo formatNumber($coin['ma200']); ?></td>
<td data-label="Abweichung">
<?php echo ($diff >= 0 ? '+' : '-') . formatNumber(abs($diff)); ?>
<span class="symbol">(<?php echo ($diffPercent >= 0 ? '+' : '-') . formatNumber(abs($diffPercent)); ?> %)</span>
</td>
<td data-label="Signal">
<span class="signal signal-<?php echo strtolower($signal); ?>">
<?php echo $signal; ?>
</span>
<div class="symbol"><?php echo $reason; ?></div>
</td>
<td data-label="Update" class="symbol"><?php echo htmlspecialchars($coin['last_update']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
$signals = array_map(function ($coin) {
[$signal, $reason] = determineSignal($coin['price'], $coin['ma200']);
return sprintf('%s (%s): %s %s', $coin['name'], $coin['symbol'], $signal, $reason);
}, $altcoins);
?>
<section class="telegram-card">
<h2>Telegram Broadcast</h2>
<p>Kopiere die Zusammenfassung, um sie in deinem Signal-Channel zu posten, sobald eine MA200-Überschreitung
stattfindet.</p>
<pre><?php echo htmlspecialchars(implode("\n", $signals)); ?></pre>
</section>
</main>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<?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
@@ -0,0 +1,237 @@
: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
@@ -0,0 +1,20 @@
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();
});
}
+1720
View File
File diff suppressed because it is too large Load Diff
+4045
View File
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
<?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
@@ -0,0 +1,76 @@
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
@@ -0,0 +1,307 @@
#!/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
@@ -0,0 +1,93 @@
<?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
@@ -0,0 +1,10 @@
<?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
@@ -0,0 +1,144 @@
<?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
@@ -0,0 +1,10 @@
<?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
@@ -0,0 +1,159 @@
<?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]);
}
+91 -303
View File
@@ -1,323 +1,111 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
date_default_timezone_set('Europe/Berlin');
$now = new DateTimeImmutable('now');
$dayOfYear = (int) $now->format('z') + 1;
$weekOfYear = (int) $now->format('W');
$swatchBeats = (int) floor((($now->getTimestamp() % 86400) / 86.4));
$moonPhase = (int) floor((($now->getTimestamp() / 2551443) - floor($now->getTimestamp() / 2551443)) * 100);
$worldCities = [
['label' => 'Berlin', 'zone' => 'Europe/Berlin'],
['label' => 'Tokyo', 'zone' => 'Asia/Tokyo'],
['label' => 'San Francisco', 'zone' => 'America/Los_Angeles'],
['label' => 'São Paulo', 'zone' => 'America/Sao_Paulo'],
['label' => 'Kapstadt', 'zone' => 'Africa/Johannesburg'],
$filters = [
'genre' => $_GET['genre'] ?? '',
'city' => $_GET['city'] ?? '',
];
$worldTimes = array_map(
static function (array $city): array {
$dt = new DateTimeImmutable('now', new DateTimeZone($city['zone']));
return [
'label' => $city['label'],
'time' => $dt->format('H:i'),
'date' => $dt->format('d.m.Y'),
'weekday' => $dt->format('l'),
'offset' => $dt->format('P'),
];
},
$worldCities
);
$timeline = [];
for ($i = 1; $i <= 5; $i++) {
$future = $now->modify('+' . $i * 37 . ' minutes');
$timeline[] = [
'label' => "+" . $i * 37 . " min",
'time' => $future->format('H:i'),
'micro' => $future->format('s.u'),
'iso' => $future->format(DateTimeInterface::ATOM),
];
}
$startMillis = (int) round(((float) $now->format('U.u')) * 1000);
$bands = allBands($filters);
$settings = settings();
$user = currentUser();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hypermodern Temporal Hub</title>
<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;500;600&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
color-scheme: dark;
--bg: radial-gradient(circle at 20% 20%, #1d1646 0%, #05000f 60%, #020203 100%);
--card: rgba(13, 6, 30, 0.8);
--stroke: rgba(255, 255, 255, 0.12);
--glow: #74f9ff;
--accent: #ff43c1;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Space Grotesk', 'Segoe UI', sans-serif;
min-height: 100vh;
background: var(--bg);
color: #f5f5ff;
padding: clamp(1rem, 3vw, 4rem);
display: flex;
flex-direction: column;
gap: 2rem;
}
header h1 {
font-size: clamp(2.2rem, 4vw, 3.8rem);
margin: 0 0 0.3rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
header p {
margin: 0;
max-width: 520px;
color: rgba(255, 255, 255, 0.75);
font-size: 1rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.2rem;
}
.card {
background: var(--card);
border: 1px solid var(--stroke);
border-radius: 20px;
padding: 1.5rem;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgba(255,67,193,0.35), transparent 45%);
opacity: 0.5;
pointer-events: none;
}
.card h2 {
margin: 0 0 0.8rem;
font-size: 1rem;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--glow);
}
.primary-time {
font-size: clamp(3rem, 8vw, 4.5rem);
font-weight: 600;
letter-spacing: 0.08em;
}
.primary-date {
font-family: 'IBM Plex Mono', monospace;
color: rgba(255,255,255,0.8);
font-size: 1rem;
}
.metrics {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1rem;
}
.metric {
flex: 1;
min-width: 120px;
}
.metric span {
display: block;
font-family: 'IBM Plex Mono', monospace;
color: rgba(255,255,255,0.6);
font-size: 0.8rem;
}
.metric strong {
font-size: 1.4rem;
}
ul.timeline {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
}
ul.timeline li {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 0.9rem;
font-family: 'IBM Plex Mono', monospace;
}
ul.timeline li span {
display: block;
color: rgba(255,255,255,0.6);
font-size: 0.75rem;
}
.worldtime {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.city {
display: flex;
justify-content: space-between;
font-family: 'IBM Plex Mono', monospace;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 0.5rem;
}
.city:last-child {
border-bottom: none;
padding-bottom: 0;
}
.city strong {
font-size: 1rem;
}
.city span {
color: rgba(255,255,255,0.65);
}
.nano-clock {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
margin-top: 0.8rem;
color: var(--accent);
}
footer {
font-size: 0.8rem;
color: rgba(255,255,255,0.55);
text-align: center;
}
@media (max-width: 600px) {
body {
padding: 1.2rem;
}
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
<header>
<h1>Hypermodern Temporal Hub</h1>
<p>Ein futuristisches Datums-Dashboard, das terrestrische, kosmische und spekulative Zeitsysteme in einem einzigen, vibrierenden Interface verschmilzt.</p>
</header>
<section class="grid">
<article class="card">
<h2>JETZT</h2>
<div class="primary-time" id="clock" aria-live="polite"><?= htmlspecialchars($now->format('H:i:s'), ENT_QUOTES, 'UTF-8'); ?></div>
<div class="primary-date">Tag <?= $dayOfYear; ?> · Woche <?= $weekOfYear; ?> · ISO <?= htmlspecialchars($now->format(DateTimeInterface::ATOM), ENT_QUOTES, 'UTF-8'); ?></div>
<div class="nano-clock" id="nano">µ<?= $now->format('u'); ?></div>
<div class="metrics">
<div class="metric">
<span>Swatch Beats</span>
<strong>@<?= str_pad((string) $swatchBeats, 3, '0', STR_PAD_LEFT); ?></strong>
</div>
<div class="metric">
<span>Gravitationsphase</span>
<strong><?= $moonPhase; ?>%</strong>
</div>
<div class="metric">
<span>Unix-Epoche</span>
<strong><?= number_format($now->getTimestamp(), 0, ',', '.'); ?></strong>
</div>
</div>
</article>
<article class="card">
<h2>WELTWELLEN</h2>
<div class="worldtime">
<?php foreach ($worldTimes as $city): ?>
<div class="city">
<div>
<strong><?= htmlspecialchars($city['label'], ENT_QUOTES, 'UTF-8'); ?></strong>
<span><?= htmlspecialchars($city['weekday'], ENT_QUOTES, 'UTF-8'); ?> · <?= htmlspecialchars($city['date'], ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div>
<strong><?= htmlspecialchars($city['time'], ENT_QUOTES, 'UTF-8'); ?></strong>
<span><?= htmlspecialchars($city['offset'], ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<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>
</article>
<div>
<strong>Kontakt</strong><br>
support@getyourband.ch
</div>
</footer>
<article class="card">
<h2>TEMPORAL-LINSE</h2>
<ul class="timeline">
<?php foreach ($timeline as $moment): ?>
<li>
<span><?= htmlspecialchars($moment['label'], ENT_QUOTES, 'UTF-8'); ?></span>
<strong><?= htmlspecialchars($moment['time'], ENT_QUOTES, 'UTF-8'); ?></strong>
<span><?= htmlspecialchars($moment['micro'], ENT_QUOTES, 'UTF-8'); ?></span>
<span><?= htmlspecialchars($moment['iso'], ENT_QUOTES, 'UTF-8'); ?></span>
</li>
<?php endforeach; ?>
</ul>
</article>
</section>
<footer>
Synchronisiert mit Serverzeit · Hypermodernität trifft Präzision auf <?= htmlspecialchars($now->format('d.m.Y'), ENT_QUOTES, 'UTF-8'); ?>.
</footer>
<script>
const serverMillis = <?= json_encode($startMillis, JSON_THROW_ON_ERROR); ?>;
const clientBoot = Date.now();
const drift = serverMillis - clientBoot;
const clock = document.getElementById('clock');
const nano = document.getElementById('nano');
function pad(value, length = 2) {
return String(value).padStart(length, '0');
}
function tick() {
const precise = new Date(Date.now() + drift);
const hours = pad(precise.getHours());
const minutes = pad(precise.getMinutes());
const seconds = pad(precise.getSeconds());
const millis = precise.getMilliseconds();
clock.textContent = `${hours}:${minutes}:${seconds}`;
nano.textContent = `µ${pad(millis, 3)}${String(Math.floor((millis / 1000) * 1000)).padStart(3, '0')}`;
requestAnimationFrame(tick);
}
tick();
</script>
<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>
<script src="assets/js/app.js" defer></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<?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
@@ -0,0 +1,90 @@
<?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
+178
View File
@@ -0,0 +1,178 @@
<?php
$btcPrice = null;
$btcChange = null;
$btcSource = 'https://api.coindesk.com/v1/bpi/currentprice/USD.json';
try {
$response = @file_get_contents($btcSource);
if ($response !== false) {
$payload = json_decode($response, true);
if (isset($payload['bpi']['USD']['rate_float'])) {
$btcPrice = (float) $payload['bpi']['USD']['rate_float'];
}
if (isset($payload['chartName'])) {
$btcChange = $payload['chartName'];
}
}
} catch (Throwable $e) {
// Silently ignore network failures, we degrade gracefully in the UI.
}
$btcLabel = $btcPrice ? number_format($btcPrice, 2) . ' $' : 'unbekannt';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse Synth Lab</title>
<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">
<style>
:root {
color-scheme: dark;
font-family: 'Space Grotesk', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: radial-gradient(circle at top, #10152b, #050608 60%);
color: #e4f6ff;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 2rem;
}
.panel {
background: rgba(15, 18, 40, 0.85);
border: 1px solid rgba(102, 204, 255, 0.25);
border-radius: 20px;
padding: 2rem;
width: min(960px, 100%);
box-shadow: 0 30px 60px rgba(5, 10, 50, 0.55);
backdrop-filter: blur(10px);
}
h1 {
margin: 0 0 1rem 0;
font-size: clamp(2rem, 4vw, 3rem);
letter-spacing: 0.04em;
text-transform: uppercase;
}
p {
margin: 0 0 1rem 0;
line-height: 1.6;
}
.synth-pad {
border-radius: 24px;
background: linear-gradient(135deg, rgba(35, 58, 122, 0.9), rgba(161, 92, 255, 0.75));
border: 1px solid rgba(255,255,255,0.2);
height: 320px;
position: relative;
overflow: hidden;
cursor: crosshair;
}
.synth-pad::after {
content: "";
position: absolute;
inset: 1rem;
border: 1px dashed rgba(255,255,255,0.2);
border-radius: 18px;
}
.pad-indicator {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #ffffff;
pointer-events: none;
transform: translate(-50%, -50%);
transition: transform 0.1s ease-out;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
label {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255,255,255,0.65);
}
input[type="range"] {
width: 100%;
}
button {
border: none;
border-radius: 999px;
padding: 0.85rem 1.8rem;
font-size: 1rem;
cursor: pointer;
background: linear-gradient(135deg, #6a5af9, #32d9ff);
color: #050608;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 20px 30px rgba(50, 217, 255, 0.3);
}
.status {
font-size: 0.9rem;
color: rgba(255,255,255,0.7);
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.status span {
font-weight: 600;
color: #7fffd4;
}
</style>
</head>
<body data-btc-price="<?= htmlspecialchars((string) ($btcPrice ?? '')) ?>">
<div class="panel">
<h1>Mouse Synth Lab</h1>
<p>
Zieh deine Maus durch den Pad und verwandle Bewegungen in Klang. Drei LFOs,
FM-Experimente und ein Delay/Distortion-Hybrid werden live verschaltet.
Der aktuelle Bitcoin-Kurs (<strong><?= htmlspecialchars($btcLabel) ?></strong>)
steuert, wie aggressiv der Mix moduliert und rückgekoppelt wird.
</p>
<div class="status" id="btc-status">
<div>Bitcoin Quelle: <span><?= htmlspecialchars($btcSource) ?></span></div>
<div>Letzter Wert: <span><?= htmlspecialchars($btcLabel) ?></span></div>
</div>
<div class="controls">
<div>
<label for="fm-depth">FM-Intensität</label>
<input id="fm-depth" type="range" min="10" max="800" value="320">
</div>
<div>
<label for="lfo-speed">LFO-Speed</label>
<input id="lfo-speed" type="range" min="0.05" max="18" value="6" step="0.05">
</div>
<div>
<label for="texture">Texture Morph</label>
<input id="texture" type="range" min="0" max="1" step="0.01" value="0.4">
</div>
<div style="display:flex;align-items:end;gap:0.75rem;">
<button id="start-btn">Synth starten</button>
<button id="randomize-btn" type="button">Chaos Patch</button>
</div>
</div>
<div class="synth-pad" id="synth-pad">
<div class="pad-indicator" id="pad-indicator" style="left:50%;top:50%;"></div>
</div>
<p style="font-size:0.9rem;color:rgba(255,255,255,0.65);margin-top:1.5rem;">
Tipp: Halte die Maus gedrückt, damit der AudioContext aktiv bleibt, und lass den Cursor
Kreise fahren. Je nach Bitcoin-Laune schalten sich neue Rückkopplungen zu.
</p>
</div>
<script src="synth.js" type="module"></script>
</body>
</html>
+262
View File
@@ -0,0 +1,262 @@
const btcPrice = parseFloat(document.body.dataset.btcPrice || 'NaN');
const normalizedCoin = Number.isFinite(btcPrice)
? Math.min(Math.max((btcPrice - 15000) / 25000, 0), 1)
: 0.5;
const pad = document.getElementById('synth-pad');
const indicator = document.getElementById('pad-indicator');
const fmDepthInput = document.getElementById('fm-depth');
const lfoSpeedInput = document.getElementById('lfo-speed');
const textureInput = document.getElementById('texture');
const startBtn = document.getElementById('start-btn');
const randomizeBtn = document.getElementById('randomize-btn');
class MouseSynth {
constructor(options = {}) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
this.started = false;
this.coinBlend = options.coinBlend ?? 0.5;
this.setupNodes();
this.#bindEvents();
}
setupNodes() {
const ctx = this.ctx;
this.masterGain = ctx.createGain();
this.masterGain.gain.value = 0.0;
this.carrier = ctx.createOscillator();
this.carrier.type = 'sawtooth';
this.harmonic = ctx.createOscillator();
this.harmonic.type = 'triangle';
this.harmonic.detune.value = 702; // perfect fifth
this.fmOsc = ctx.createOscillator();
this.fmGain = ctx.createGain();
this.fmGain.gain.value = 320;
this.ampLfo = ctx.createOscillator();
this.ampLfo.type = 'sine';
this.ampLfo.frequency.value = 6;
this.ampLfoGain = ctx.createGain();
this.ampLfoGain.gain.value = 0.5;
this.filterLfo = ctx.createOscillator();
this.filterLfo.type = 'triangle';
this.filterLfo.frequency.value = 0.5;
this.filterLfoGain = ctx.createGain();
this.filterLfoGain.gain.value = 800;
this.sampleHold = ctx.createOscillator();
this.sampleHold.type = 'square';
this.sampleHold.frequency.value = 8;
this.sampleHoldGain = ctx.createGain();
this.sampleHoldGain.gain.value = 0.0025;
this.filter = ctx.createBiquadFilter();
this.filter.type = 'bandpass';
this.filter.frequency.value = 600;
this.filter.Q.value = 8;
this.delay = ctx.createDelay(1.2);
this.delay.delayTime.value = 0.45;
this.feedback = ctx.createGain();
this.feedback.gain.value = 0.32;
this.noise = this.#createNoise();
this.noiseGain = ctx.createGain();
this.noiseGain.gain.value = 0.0;
this.distortion = ctx.createWaveShaper();
this.#setDrive(400);
this.coinMorph = ctx.createGain();
this.coinMorph.gain.value = this.coinBlend;
this.reverb = ctx.createConvolver();
this.reverb.buffer = this.#makeImpulse(2.5);
this.reverbGain = ctx.createGain();
this.reverbGain.gain.value = 0.25;
// Connections
this.fmOsc.connect(this.fmGain).connect(this.carrier.frequency);
this.harmonic.connect(this.filter);
this.carrier.connect(this.filter);
this.ampLfo.connect(this.ampLfoGain).connect(this.masterGain.gain);
this.filterLfo.connect(this.filterLfoGain).connect(this.filter.frequency);
this.sampleHold.connect(this.sampleHoldGain).connect(this.filter.detune);
this.filter.connect(this.distortion);
this.noise.connect(this.noiseGain).connect(this.filter);
const wet = ctx.createGain();
const dry = ctx.createGain();
this.distortion.connect(dry).connect(this.masterGain);
this.distortion.connect(this.delay);
this.delay.connect(this.feedback).connect(this.delay);
this.delay.connect(this.coinMorph);
this.coinMorph.connect(wet);
wet.connect(this.reverb);
this.reverb.connect(this.reverbGain).connect(this.masterGain);
this.masterGain.connect(ctx.destination);
this.carrier.start();
this.harmonic.start();
this.fmOsc.start();
this.ampLfo.start();
this.filterLfo.start();
this.sampleHold.start();
this.noise.start();
}
async start() {
if (this.started) return;
await this.ctx.resume();
this.masterGain.gain.linearRampToValueAtTime(0.8, this.ctx.currentTime + 0.5);
this.started = true;
}
#bindEvents() {
fmDepthInput.addEventListener('input', () => {
this.fmGain.gain.setTargetAtTime(parseFloat(fmDepthInput.value), this.ctx.currentTime, 0.05);
});
lfoSpeedInput.addEventListener('input', () => {
const rate = parseFloat(lfoSpeedInput.value);
this.ampLfo.frequency.setTargetAtTime(rate, this.ctx.currentTime, 0.1);
this.filterLfo.frequency.setTargetAtTime(rate * 0.25, this.ctx.currentTime, 0.1);
});
textureInput.addEventListener('input', () => {
this.#updateTexture(parseFloat(textureInput.value));
});
randomizeBtn.addEventListener('click', () => this.randomize());
}
handlePointer(event) {
if (!this.started) return;
const rect = pad.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = (event.clientY - rect.top) / rect.height;
const freq = 120 + (1 - y) * 1080;
this.carrier.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.05);
this.harmonic.frequency.setTargetAtTime(freq * 1.5, this.ctx.currentTime, 0.05);
this.filter.frequency.setTargetAtTime(200 + x * 5200, this.ctx.currentTime, 0.08);
this.filter.Q.setTargetAtTime(4 + y * 18, this.ctx.currentTime, 0.1);
this.noiseGain.gain.setTargetAtTime(x * 0.3, this.ctx.currentTime, 0.2);
this.sampleHold.frequency.setTargetAtTime(4 + x * 20, this.ctx.currentTime, 0.1);
this.delay.delayTime.setTargetAtTime(0.15 + y * 0.6, this.ctx.currentTime, 0.2);
this.feedback.gain.setTargetAtTime(0.2 + x * 0.7 * this.coinBlend, this.ctx.currentTime, 0.2);
this.coinMorph.gain.setTargetAtTime(this.coinBlend * (0.4 + y * 0.6), this.ctx.currentTime, 0.3);
this.fmGain.gain.setTargetAtTime(parseFloat(fmDepthInput.value) + x * 200, this.ctx.currentTime, 0.05);
this.#updateTexture(textureInput.value, x, y);
}
handlePointerLeave() {
if (!this.started) return;
this.masterGain.gain.cancelScheduledValues(this.ctx.currentTime);
this.masterGain.gain.setTargetAtTime(0.15, this.ctx.currentTime, 0.5);
}
randomize() {
const fm = 80 + Math.random() * 720;
fmDepthInput.value = fm.toFixed(0);
this.fmGain.gain.setTargetAtTime(fm, this.ctx.currentTime, 0.1);
const lfo = 0.1 + Math.random() * 16;
lfoSpeedInput.value = lfo.toFixed(2);
this.ampLfo.frequency.setTargetAtTime(lfo, this.ctx.currentTime, 0.2);
this.filterLfo.frequency.setTargetAtTime(lfo * 0.3, this.ctx.currentTime, 0.2);
const texture = Math.random();
textureInput.value = texture.toFixed(2);
this.#updateTexture(texture);
}
#updateTexture(value, x = 0.5, y = 0.5) {
const amount = parseFloat(value);
const drive = 150 + amount * 850 + this.coinBlend * 400;
this.#setDrive(drive);
const morph = amount * (0.6 + this.coinBlend * 0.8);
this.distortion.oversample = morph > 0.5 ? '4x' : '2x';
this.reverbGain.gain.setTargetAtTime(0.15 + morph * 0.6, this.ctx.currentTime, 0.3);
const filterType = morph > 0.7 ? 'notch' : morph > 0.35 ? 'bandpass' : 'lowpass';
this.filter.type = filterType;
this.coinMorph.gain.setTargetAtTime(this.coinBlend * (0.4 + morph), this.ctx.currentTime, 0.3);
this.delay.delayTime.setTargetAtTime(0.2 + morph * 0.4 + (x * y) * 0.2, this.ctx.currentTime, 0.2);
}
#setDrive(amount) {
const curve = new Float32Array(1024);
for (let i = 0; i < curve.length; i++) {
const x = (i / curve.length) * 2 - 1;
curve[i] = Math.tanh(x * amount * 0.01);
}
this.distortion.curve = curve;
}
#createNoise() {
const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * 4, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
noise.loop = true;
return noise;
}
#makeImpulse(seconds) {
const rate = this.ctx.sampleRate;
const length = rate * seconds;
const impulse = this.ctx.createBuffer(2, length, rate);
for (let ch = 0; ch < 2; ch++) {
const data = impulse.getChannelData(ch);
for (let i = 0; i < length; i++) {
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2);
}
}
return impulse;
}
}
const synth = new MouseSynth({ coinBlend: normalizedCoin });
startBtn.addEventListener('click', () => synth.start());
pad.addEventListener('pointerdown', async (event) => {
await synth.start();
pad.setPointerCapture(event.pointerId);
indicator.style.opacity = '1';
});
pad.addEventListener('pointermove', (event) => {
indicator.style.left = `${event.offsetX}px`;
indicator.style.top = `${event.offsetY}px`;
synth.handlePointer(event);
});
pad.addEventListener('pointerup', (event) => {
pad.releasePointerCapture(event.pointerId);
synth.handlePointerLeave();
});
pad.addEventListener('pointerleave', () => synth.handlePointerLeave());
if (!Number.isFinite(btcPrice)) {
const status = document.getElementById('btc-status');
if (status) {
status.insertAdjacentHTML('beforeend', '<div style="color:#ffa3a3;">BTC Feed nicht erreichbar Synth läuft im Fantasy-Modus.</div>');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?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>