Compare commits

..

31 Commits

Author SHA1 Message Date
Claude 615866d215 Add PayPal integration and band image upload features
- Implemented complete PayPal payment integration with checkout flow
- Added payments table to database for transaction tracking
- Created image upload functionality for band profiles
- Added gallery management in band profiles
- Updated booking flow to support PayPal payments
- Added payment status display in user profiles
- Included comprehensive documentation for new features

New files:
- upload-handler.php: REST API for image uploads
- paypal-checkout.php: PayPal checkout page
- paypal-process.php: Payment processing backend
- PAYPAL_UPLOAD_FEATURES.md: Complete documentation
- storage/uploads/bands/: Upload directory

Modified files:
- database.sql: Added payments table
- profil.php: Added gallery and payment tracking
- anfrage.php: Integrated PayPal payment option
2025-12-02 21:11:04 +00:00
admin 798a2785e7 Merge pull request #11 from metacube2/codex/create-advanced-mouse-synthesizer-in-synth-folder-235xid
Add extra modulation effects to mouse synth
2025-11-18 11:15:37 +01:00
admin 84c58e965c Merge branch 'main' into codex/create-advanced-mouse-synthesizer-in-synth-folder-235xid 2025-11-18 11:15:26 +01:00
admin c352851adb Add extra modulation effects to mouse synth 2025-11-18 11:13:33 +01:00
admin 2f4362e856 test 2025-11-18 10:41:04 +01:00
admin a0bca62a67 asd 2025-11-18 10:40:41 +01:00
admin e3c8c9717e sadf2 2025-11-18 10:27:54 +01:00
admin c1ed600d4e we 2025-11-18 10:23:49 +01:00
admin 4291ef6a05 Merge pull request #10 from metacube2/codex/create-advanced-mouse-synthesizer-in-synth-folder-grrihv
Expand synth with multi-source BTC modulation
2025-11-18 07:35:28 +01:00
admin 088ba9e8b1 Merge branch 'main' into codex/create-advanced-mouse-synthesizer-in-synth-folder-grrihv 2025-11-18 07:35:19 +01:00
admin bfb351d33e Expand synth with multi-source BTC modulation 2025-11-18 07:32:24 +01:00
admin 31fa44f6a7 Merge pull request #9 from metacube2/codex/projekt-fur-bandreservierung-erstellen-70yja1
Add band contact emails and contact form
2025-11-17 22:23:34 +01:00
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
admin 1864a6a812 Add hypermodern date dashboard 2025-11-17 14:50:48 +01:00
admin f8eb38e3bf Merge pull request #1 from metacube2/codex/create-php-website-with-mouse-synthesizer
Add MausSynth Web Audio playground
2025-11-10 20:27:32 +01:00
33 changed files with 13492 additions and 325 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>
+12
View File
@@ -0,0 +1,12 @@
{
"name": "Mein Webserver",
"host": "192.168.178.88",
"protocol": "sftp",
"port": 22,
"username": "root",
"password": "934290",
"remotePath": "/var/www/html/",
"uploadOnSave": true,
"useTempFile": false,
"openSsh": false
}
+180
View File
@@ -0,0 +1,180 @@
# Neue Features: PayPal-Integration & Bild-Upload
Dieses Dokument beschreibt die neu hinzugefügten Features für die GetYourBand-Plattform.
## 🖼️ Bild-Upload für Bands
### Features
- **Upload-Funktionalität**: Bands können eigene Bilder hochladen
- **Galerie-Verwaltung**: Anzeige und Verwaltung aller hochgeladenen Bilder
- **Löschen**: Bilder können jederzeit gelöscht werden
- **Validierung**:
- Erlaubte Formate: JPG, PNG, GIF, WEBP
- Maximale Dateigröße: 5MB
- Automatische Dateinamens-Generierung
### Technische Details
- **Upload-Verzeichnis**: `/storage/uploads/bands/`
- **Handler**: `upload-handler.php`
- **Frontend**: AJAX-basierter Upload mit Fetch API
- **Dateinamensschema**: `band_{band_id}_{unique_id}.{extension}`
### Verwendung
1. Als Band-User einloggen
2. Zum Profil navigieren (`profil.php`)
3. Sektion "Band-Galerie" finden
4. Auf "+ Bild hochladen" klicken
5. Bild auswählen (wird automatisch hochgeladen)
### Sicherheit
- Nur authentifizierte Band-User können uploaden
- Strenge Dateitypprüfung (MIME-Type + Extension)
- Größenlimit verhindert DoS
- Sichere Dateinamen ohne User-Input
---
## 💳 PayPal-Integration
### Features
- **Zahlungsabwicklung**: Kunden können Buchungen direkt mit PayPal bezahlen
- **Service Fee**: Konfigurierbare Servicegebühr (in Admin-Settings)
- **Zahlungs-Tracking**: Alle Zahlungen werden in der Datenbank gespeichert
- **Status-Updates**: Anfragen werden automatisch auf "bestätigt" gesetzt
- **Email-Benachrichtigungen**: Kunde und Band erhalten Bestätigungen
### Komponenten
#### 1. Datenbank
Neue Tabelle `payments`:
```sql
CREATE TABLE payments (
id INTEGER PRIMARY KEY,
request_id INTEGER NOT NULL,
amount REAL NOT NULL,
service_fee REAL NOT NULL,
total_amount REAL NOT NULL,
paypal_order_id TEXT,
paypal_payer_id TEXT,
status TEXT DEFAULT 'pending',
created_at TEXT,
completed_at TEXT
);
```
#### 2. Checkout-Seite
**Datei**: `paypal-checkout.php`
- Zeigt Buchungsdetails und Zahlungsübersicht
- Integriert PayPal JavaScript SDK
- Berechnet Gesamtbetrag (Band-Gage + Service Fee)
#### 3. Payment Processing
**Datei**: `paypal-process.php`
- Speichert erfolgreiche Zahlungen
- Aktualisiert Request-Status
- Sendet Bestätigungs-Emails
#### 4. Integration in Buchungsflow
**Änderungen in `anfrage.php`**:
- Nach erfolgreicher Anfrage wird PayPal-Button angezeigt (wenn aktiviert)
- Direkter Link zum Checkout
**Änderungen in `profil.php`**:
- Zahlungsstatus für jede Anfrage angezeigt
- "Jetzt bezahlen"-Button für ausstehende Zahlungen
### PayPal-Konfiguration
#### Admin-Einstellungen
Im Admin-Panel (`admin/settings.php`):
- `paypal_enabled`: 0/1 (aktiviert/deaktiviert)
- `service_fee`: Prozentsatz (z.B. 8 für 8%)
#### PayPal API Credentials
In `paypal-checkout.php` Zeile 80:
```javascript
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_PAYPAL_CLIENT_ID&currency=CHF"></script>
```
**Wichtig**: `YOUR_PAYPAL_CLIENT_ID` durch echte Client-ID ersetzen!
#### PayPal Developer Setup
1. Gehen Sie zu https://developer.paypal.com
2. Erstellen Sie eine App in "My Apps & Credentials"
3. Kopieren Sie die Client-ID
4. Für Produktion: Aktivieren Sie Live-Modus und verwenden Sie Live-Credentials
### Zahlungsablauf
1. **Kunde erstellt Anfrage** → Request wird in DB gespeichert
2. **PayPal-Link erscheint** → Kunde klickt auf "Mit PayPal bezahlen"
3. **Checkout-Seite** → Übersicht und PayPal-Button
4. **PayPal-Zahlung** → Kunde loggt sich in PayPal ein und zahlt
5. **Payment Processing** → Zahlung wird in DB gespeichert
6. **Status-Update** → Request → "bestätigt", Emails versandt
7. **Rückkehr zum Profil** → Erfolgsmeldung
### Testmodus
Die aktuelle Implementation läuft im **Sandbox-Modus**:
- Verwenden Sie PayPal Sandbox-Accounts zum Testen
- Keine echten Transaktionen werden durchgeführt
- Für Produktion: Client-ID auf Live-Credentials umstellen
### Sicherheit
- Zahlung nur für eigene Requests möglich
- Doppelzahlungen werden verhindert
- Transaktions-IDs werden gespeichert
- Server-seitige Validierung aller Zahlungsdaten
---
## 📂 Neue Dateien
| Datei | Beschreibung |
|-------|--------------|
| `upload-handler.php` | REST-API für Bild-Uploads (POST/DELETE) |
| `paypal-checkout.php` | PayPal Checkout-Seite |
| `paypal-process.php` | PayPal Payment Processing Backend |
| `storage/uploads/bands/` | Upload-Verzeichnis für Band-Bilder |
| `PAYPAL_UPLOAD_FEATURES.md` | Diese Dokumentation |
## 🔄 Geänderte Dateien
| Datei | Änderungen |
|-------|------------|
| `database.sql` | + `payments` Tabelle |
| `profil.php` | + Galerie-Sektion, + Zahlungsstatus in Anfragen |
| `anfrage.php` | + PayPal-Button nach erfolgreicher Anfrage |
## 🚀 Deployment-Checklist
- [ ] `storage/uploads/` Verzeichnis erstellen mit Schreibrechten
- [ ] PayPal Developer Account erstellen
- [ ] Client-ID in `paypal-checkout.php` eintragen
- [ ] Admin-Panel: PayPal aktivieren und Service Fee setzen
- [ ] Für Produktion: Auf Live-Credentials umstellen
- [ ] SSL-Zertifikat für HTTPS (PayPal requirement)
## 🐛 Bekannte Einschränkungen
1. **PayPal Client-ID**: Muss manuell konfiguriert werden
2. **Keine Rückerstattungen**: Keine Admin-UI für Refunds
3. **Email-System**: Aktuell nur Logging, kein echtes SMTP
4. **Sandbox-Modus**: Standardmäßig aktiviert
## 📝 Nächste Schritte (Optional)
- Webhook-Integration für PayPal IPN (Instant Payment Notification)
- Admin-Dashboard für Zahlungsübersicht
- Automatische Rechnungserstellung (PDF)
- Stripe als alternative Zahlungsmethode
- Bulk-Upload für mehrere Bilder
- Bildkompression/Optimierung
- Thumbnail-Generierung
---
**Entwickelt für**: GetYourBand Platform
**Datum**: 2025-12-02
**Version**: 1.0
+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>
+94
View File
@@ -0,0 +1,94 @@
<?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 = '';
$requestId = null;
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);
$requestId = (int) db()->lastInsertId();
$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) ?>
<?php if ($requestId && $settings['paypal_enabled'] === '1'): ?>
<div style="margin-top: 1rem;">
<a href="paypal-checkout.php?request_id=<?= $requestId ?>" class="btn-primary" style="display: inline-block; padding: 0.75rem 1.5rem; text-decoration: none;">
Jetzt mit PayPal bezahlen
</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if (!$message): ?>
<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>
<?php endif; ?>
</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>
+90
View File
@@ -0,0 +1,90 @@
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
);
CREATE TABLE IF NOT EXISTS payments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
amount REAL NOT NULL,
service_fee REAL NOT NULL,
total_amount REAL NOT NULL,
paypal_order_id TEXT,
paypal_payer_id TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
completed_at TEXT,
FOREIGN KEY(request_id) REFERENCES requests(id) ON DELETE CASCADE
);
+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]);
}
+97 -324
View File
@@ -1,338 +1,111 @@
<?php <?php
$title = "MausSynth Lab"; declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$filters = [
'genre' => $_GET['genre'] ?? '',
'city' => $_GET['city'] ?? '',
];
$bands = allBands($filters);
$settings = settings();
$user = currentUser();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></title> <title><?= SITE_NAME ?> Bands buchen</title>
<style> <link rel="stylesheet" href="assets/css/style.css">
:root { <link rel="preconnect" href="https://fonts.googleapis.com">
--bg: radial-gradient(circle at center, #1f014d 0%, #05010d 100%); <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
--accent: #ff2bd7; <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
--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> </head>
<body> <body>
<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></h1> <header>
<p class="description">Schiebe deine Maus über das Klangfeld, um Frequenz, Filter und verrückte Modulation zu steuern. Klicke, um Beats zu triggern.</p> <nav class="badge-list" style="justify-content: flex-end;">
<?php if ($user): ?>
<div class="stage" id="stage"> <span class="badge">Hallo <?= htmlspecialchars($user['name']) ?></span>
<canvas id="visualizer"></canvas> <a class="badge" href="profil.php">Mein Profil</a>
<div class="cursor" id="cursor"></div> <?php if ($user['role'] === 'admin'): ?>
<div class="hud" id="hud"> <a class="badge" href="admin/dashboard.php">Admin</a>
<div><span class="label">Freq</span><span id="freqReadout">-- Hz</span></div> <?php endif; ?>
<div><span class="label">Reso</span><span id="resoReadout">--</span></div> <a class="badge" href="login.php?action=logout">Logout</a>
<div><span class="label">Noise</span><span id="noiseReadout">--</span></div> <?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>
</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>
<p class="hint">Tipp: Halte die Maustaste gedrückt, bewege dich in Kreisen &ndash; und genieße den abgefahrenen Klangteppich!</p> <div class="cookie-banner">
<p>Wir verwenden Cookies für Performance-Analysen. Mit Klick auf "Okay" akzeptierst du das.</p>
<script> <button class="btn-primary" data-cookie-accept>Okay!</button>
const stage = document.getElementById('stage'); </div>
const cursor = document.getElementById('cursor'); <script src="assets/js/app.js" defer></script>
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> </body>
</html> </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>
+167
View File
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$requestId = isset($_GET['request_id']) ? (int) $_GET['request_id'] : 0;
if (!$requestId) {
http_response_code(400);
echo 'Keine Anfrage-ID angegeben';
exit;
}
$user = currentUser();
// Get request details
$stmt = db()->prepare('SELECT r.*, b.name as band_name, b.price as band_price
FROM requests r
JOIN bands b ON b.id = r.band_id
WHERE r.id = :id AND r.user_id = :user_id');
$stmt->execute([':id' => $requestId, ':user_id' => $user['id']]);
$request = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$request) {
http_response_code(404);
echo 'Anfrage nicht gefunden';
exit;
}
$settings = settings();
if ($settings['paypal_enabled'] !== '1') {
http_response_code(403);
echo 'PayPal-Zahlungen sind derzeit nicht aktiviert';
exit;
}
// Calculate amounts
$bandPrice = (int) $request['band_price'];
$serviceFeePercent = (float) $settings['service_fee'];
$serviceFee = $bandPrice * ($serviceFeePercent / 100);
$totalAmount = $bandPrice + $serviceFee;
// Check if already paid
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
$stmt->execute([':id' => $requestId]);
$existingPayment = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingPayment) {
$message = 'Diese Buchung wurde bereits bezahlt.';
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PayPal Zahlung <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="profil.php">← Zurück zum Profil</a>
<h1>Zahlung für Buchung</h1>
</header>
<main style="max-width: 600px; margin: 0 auto;">
<?php if (isset($message)): ?>
<div class="alert alert-success"><?= htmlspecialchars($message) ?></div>
<?php else: ?>
<h2>Buchungsdetails</h2>
<table class="table" style="margin-bottom: 2rem;">
<tr><td><strong>Band:</strong></td><td><?= htmlspecialchars($request['band_name']) ?></td></tr>
<tr><td><strong>Event-Datum:</strong></td><td><?= htmlspecialchars($request['event_date']) ?></td></tr>
<tr><td><strong>Location:</strong></td><td><?= htmlspecialchars($request['location']) ?></td></tr>
<tr><td><strong>Event-Typ:</strong></td><td><?= htmlspecialchars($request['event_type']) ?></td></tr>
</table>
<h2>Zahlungsübersicht</h2>
<table class="table" style="margin-bottom: 2rem;">
<tr><td><strong>Band-Gage:</strong></td><td><?= formatPrice($bandPrice) ?></td></tr>
<tr><td><strong>Service Fee (<?= htmlspecialchars($serviceFeePercent) ?>%):</strong></td><td><?= formatPrice((int) $serviceFee) ?></td></tr>
<tr style="border-top: 2px solid #ffb703;"><td><strong>Gesamtbetrag:</strong></td><td><strong><?= formatPrice((int) $totalAmount) ?></strong></td></tr>
</table>
<div id="payment-status" style="display:none; padding: 1rem; margin-bottom: 1rem; border-radius: 4px;"></div>
<!-- PayPal Button Container -->
<div id="paypal-button-container" style="margin: 2rem 0;"></div>
<p style="color: #666; font-size: 0.875rem; margin-top: 2rem;">
<strong>Hinweis:</strong> Dies ist eine Demo-Integration. Für die Produktivumgebung benötigen Sie echte PayPal API-Credentials.
Aktuell wird im Sandbox-Modus gearbeitet.
</p>
<?php endif; ?>
</main>
<?php if (!isset($message)): ?>
<!-- PayPal SDK -->
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_PAYPAL_CLIENT_ID&currency=CHF"></script>
<script>
paypal.Buttons({
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: {
value: '<?= number_format($totalAmount, 2, '.', '') ?>',
currency_code: 'CHF',
breakdown: {
item_total: {
value: '<?= number_format($bandPrice, 2, '.', '') ?>',
currency_code: 'CHF'
},
tax_total: {
value: '<?= number_format($serviceFee, 2, '.', '') ?>',
currency_code: 'CHF'
}
}
},
description: 'Buchung: <?= htmlspecialchars($request['band_name']) ?> - <?= htmlspecialchars($request['event_date']) ?>'
}]
});
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Save payment to database
const statusDiv = document.getElementById('payment-status');
statusDiv.style.display = 'block';
statusDiv.style.background = '#28a745';
statusDiv.style.color = 'white';
statusDiv.textContent = 'Zahlung erfolgreich! Verarbeite Transaktion...';
fetch('paypal-process.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: <?= $requestId ?>,
amount: <?= $bandPrice ?>,
service_fee: <?= number_format($serviceFee, 2, '.', '') ?>,
total_amount: <?= number_format($totalAmount, 2, '.', '') ?>,
paypal_order_id: data.orderID,
paypal_payer_id: details.payer.payer_id
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
statusDiv.textContent = 'Zahlung erfolgreich abgeschlossen! Sie werden weitergeleitet...';
setTimeout(() => {
window.location.href = 'profil.php?payment_success=1';
}, 2000);
} else {
statusDiv.style.background = '#dc3545';
statusDiv.textContent = 'Fehler beim Speichern der Zahlung: ' + result.error;
}
});
});
},
onError: function(err) {
const statusDiv = document.getElementById('payment-status');
statusDiv.style.display = 'block';
statusDiv.style.background = '#dc3545';
statusDiv.style.color = 'white';
statusDiv.textContent = 'Fehler bei der Zahlung: ' + err;
}
}).render('#paypal-button-container');
</script>
<?php endif; ?>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
requireLogin();
header('Content-Type: application/json');
$user = currentUser();
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['request_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Ungültige Anfrage']);
exit;
}
$requestId = (int) $input['request_id'];
$amount = (float) $input['amount'];
$serviceFee = (float) $input['service_fee'];
$totalAmount = (float) $input['total_amount'];
$paypalOrderId = $input['paypal_order_id'] ?? '';
$paypalPayerId = $input['paypal_payer_id'] ?? '';
// Verify request belongs to user
$stmt = db()->prepare('SELECT r.*, b.name as band_name, b.user_id as band_user_id
FROM requests r
JOIN bands b ON b.id = r.band_id
WHERE r.id = :id AND r.user_id = :user_id');
$stmt->execute([':id' => $requestId, ':user_id' => $user['id']]);
$request = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$request) {
http_response_code(404);
echo json_encode(['error' => 'Anfrage nicht gefunden']);
exit;
}
// Check if already paid
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
$stmt->execute([':id' => $requestId]);
if ($stmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'Diese Buchung wurde bereits bezahlt']);
exit;
}
try {
// Save payment
$stmt = db()->prepare('INSERT INTO payments (request_id, amount, service_fee, total_amount, paypal_order_id, paypal_payer_id, status, completed_at)
VALUES (:request_id, :amount, :service_fee, :total_amount, :paypal_order_id, :paypal_payer_id, :status, :completed_at)');
$stmt->execute([
':request_id' => $requestId,
':amount' => $amount,
':service_fee' => $serviceFee,
':total_amount' => $totalAmount,
':paypal_order_id' => $paypalOrderId,
':paypal_payer_id' => $paypalPayerId,
':status' => 'completed',
':completed_at' => (new DateTimeImmutable())->format('c')
]);
// Update request status to confirmed
$stmt = db()->prepare('UPDATE requests SET status = :status WHERE id = :id');
$stmt->execute([':status' => 'bestätigt', ':id' => $requestId]);
// Send confirmation emails
sendEmail($user['email'], 'Zahlungsbestätigung',
'Ihre Zahlung für die Buchung von ' . $request['band_name'] . ' wurde erfolgreich verarbeitet.');
// Notify band
if ($request['band_user_id']) {
$bandUserStmt = db()->prepare('SELECT email FROM users WHERE id = :id');
$bandUserStmt->execute([':id' => $request['band_user_id']]);
$bandUser = $bandUserStmt->fetch(PDO::FETCH_ASSOC);
if ($bandUser) {
sendEmail($bandUser['email'], 'Neue bezahlte Buchung',
'Sie haben eine neue bezahlte Buchung für ' . $request['event_date'] . ' erhalten.');
}
}
echo json_encode([
'success' => true,
'message' => 'Zahlung erfolgreich verarbeitet',
'payment_id' => (int) db()->lastInsertId()
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Fehler beim Speichern der Zahlung: ' . $e->getMessage()]);
}
+208
View File
@@ -0,0 +1,208 @@
<?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>
<h2 style="margin-top: 2rem;">Band-Galerie</h2>
<div id="upload-status" style="display:none; padding: 1rem; margin-bottom: 1rem; background: #28a745; color: white; border-radius: 4px;"></div>
<div style="margin-bottom: 1rem;">
<label class="btn-primary" style="display: inline-block; cursor: pointer;">
<input type="file" id="image-upload" accept="image/*" style="display: none;">
+ Bild hochladen
</label>
<small style="display: block; margin-top: 0.5rem; color: #666;">Max 5MB (JPG, PNG, GIF, WEBP)</small>
</div>
<div id="gallery" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;">
<?php foreach (bandMedia((int) $band['id']) as $media): ?>
<div class="gallery-item" data-media-id="<?= $media['id'] ?>">
<img src="<?= htmlspecialchars($media['url']) ?>" alt="Band Foto" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
<button class="delete-image" data-id="<?= $media['id'] ?>" style="margin-top: 0.5rem; background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; width: 100%;">Löschen</button>
</div>
<?php endforeach; ?>
</div>
<script>
document.getElementById('image-upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
const statusDiv = document.getElementById('upload-status');
statusDiv.style.display = 'block';
statusDiv.style.background = '#ffc107';
statusDiv.textContent = 'Uploading...';
fetch('upload-handler.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.style.background = '#28a745';
statusDiv.textContent = data.message;
// Add to gallery
const gallery = document.getElementById('gallery');
const div = document.createElement('div');
div.className = 'gallery-item';
div.setAttribute('data-media-id', data.id);
div.innerHTML = `
<img src="${data.url}" alt="Band Foto" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
<button class="delete-image" data-id="${data.id}" style="margin-top: 0.5rem; background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; width: 100%;">Löschen</button>
`;
gallery.appendChild(div);
setTimeout(() => { statusDiv.style.display = 'none'; }, 3000);
} else {
statusDiv.style.background = '#dc3545';
statusDiv.textContent = data.error;
}
})
.catch(error => {
statusDiv.style.background = '#dc3545';
statusDiv.textContent = 'Upload fehlgeschlagen: ' + error.message;
});
e.target.value = '';
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-image')) {
if (!confirm('Bild wirklich löschen?')) return;
const mediaId = e.target.getAttribute('data-id');
const galleryItem = e.target.closest('.gallery-item');
fetch('upload-handler.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'media_id=' + mediaId
})
.then(response => response.json())
.then(data => {
if (data.success) {
galleryItem.remove();
} else {
alert(data.error);
}
});
}
});
</script>
<?php else: ?>
<p>Du hast noch kein Bandprofil angelegt.</p>
<?php endif; ?>
<?php if ($user['role'] === 'kunde'): ?>
<?php if (isset($_GET['payment_success'])): ?>
<div class="alert alert-success">Zahlung erfolgreich abgeschlossen! Vielen Dank für Ihre Buchung.</div>
<?php endif; ?>
<h2>Meine Anfragen</h2>
<table class="table">
<thead><tr><th>Band</th><th>Datum</th><th>Status</th><th>Zahlung</th><th>Aktion</th></tr></thead>
<tbody>
<?php
$settings = settings();
foreach (userRequests((int) $user['id']) as $request):
$bandName = findBand((int) $request['band_id']);
// Check payment status
$stmt = db()->prepare('SELECT * FROM payments WHERE request_id = :id AND status = "completed"');
$stmt->execute([':id' => $request['id']]);
$payment = $stmt->fetch(PDO::FETCH_ASSOC);
?>
<tr>
<td><?= htmlspecialchars($bandName['name'] ?? 'Band #' . $request['band_id']) ?></td>
<td><?= htmlspecialchars($request['event_date']) ?></td>
<td><?= htmlspecialchars($request['status']) ?></td>
<td>
<?php if ($payment): ?>
<span style="color: #28a745;">✓ Bezahlt</span><br>
<small style="color: #666;"><?= formatPrice((int) $payment['total_amount']) ?></small>
<?php else: ?>
<span style="color: #dc3545;">Ausstehend</span>
<?php endif; ?>
</td>
<td>
<?php if (!$payment && $settings['paypal_enabled'] === '1'): ?>
<a href="paypal-checkout.php?request_id=<?= $request['id'] ?>" class="badge" style="background: #0070ba; color: white; text-decoration: none;">
PayPal bezahlen
</a>
<?php endif; ?>
</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
+184
View File
@@ -0,0 +1,184 @@
<?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>');
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
requireLogin();
header('Content-Type: application/json');
$user = currentUser();
if ($user['role'] !== 'band') {
http_response_code(403);
echo json_encode(['error' => 'Nur Bands können Bilder hochladen']);
exit;
}
// Get band
$stmt = db()->prepare('SELECT * FROM bands WHERE user_id = :id');
$stmt->execute([':id' => $user['id']]);
$band = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$band) {
http_response_code(404);
echo json_encode(['error' => 'Kein Bandprofil gefunden']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
$file = $_FILES['image'];
// Validate file
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
if (!in_array($file['type'], $allowedTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiger Dateityp. Erlaubt sind: JPG, PNG, GIF, WEBP']);
exit;
}
if ($file['size'] > $maxSize) {
http_response_code(400);
echo json_encode(['error' => 'Datei zu groß (max 5MB)']);
exit;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(500);
echo json_encode(['error' => 'Upload-Fehler']);
exit;
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'band_' . $band['id'] . '_' . uniqid() . '.' . $extension;
$uploadPath = __DIR__ . '/storage/uploads/bands/' . $filename;
// Move file
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
http_response_code(500);
echo json_encode(['error' => 'Datei konnte nicht gespeichert werden']);
exit;
}
// Save to database
$url = 'storage/uploads/bands/' . $filename;
$stmt = db()->prepare('INSERT INTO band_media (band_id, type, url) VALUES (:band_id, :type, :url)');
$stmt->execute([
':band_id' => $band['id'],
':type' => 'image',
':url' => $url
]);
$mediaId = (int) db()->lastInsertId();
echo json_encode([
'success' => true,
'id' => $mediaId,
'url' => $url,
'message' => 'Bild erfolgreich hochgeladen'
]);
exit;
}
// Delete image
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
parse_str(file_get_contents('php://input'), $deleteData);
$mediaId = (int) ($deleteData['media_id'] ?? 0);
if (!$mediaId) {
http_response_code(400);
echo json_encode(['error' => 'Keine Media-ID angegeben']);
exit;
}
// Check ownership
$stmt = db()->prepare('SELECT * FROM band_media WHERE id = :id AND band_id = :band_id');
$stmt->execute([':id' => $mediaId, ':band_id' => $band['id']]);
$media = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$media) {
http_response_code(404);
echo json_encode(['error' => 'Bild nicht gefunden']);
exit;
}
// Delete file
$filePath = __DIR__ . '/' . $media['url'];
if (file_exists($filePath) && strpos($media['url'], 'storage/uploads/') === 0) {
unlink($filePath);
}
// Delete from database
$stmt = db()->prepare('DELETE FROM band_media WHERE id = :id');
$stmt->execute([':id' => $mediaId]);
echo json_encode(['success' => true, 'message' => 'Bild gelöscht']);
exit;
}
http_response_code(400);
echo json_encode(['error' => 'Ungültige Anfrage']);
+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>