Compare commits

...

12 Commits

Author SHA1 Message Date
admin 1af18f1029 Merge pull request #17 from metacube2/claude/link-sharing-platform-01MxxjTZD3mi1H7nAVq16TYy
Build internal link sharing platform
2025-12-06 11:02:01 +01:00
admin 05295f4b2a Merge pull request #16 from metacube2/claude/bitcoin-trading-signals-01UzsK9jXmmNYJqv5mayUdNq
Bitcoin trading signals with market analysis
2025-12-06 10:21:28 +01:00
Claude bcc19577eb Add project backup ZIP archive
Created backup archive for download containing all project files
2025-12-06 09:18:27 +00:00
Claude ef7ea500a9 Add Bitcoin Trading Signal System with MACD and News Sentiment Analysis
- Implement comprehensive Bitcoin trading signal system
- Add MACD (Moving Average Convergence Divergence) indicator
- Integrate news sentiment analysis from CryptoCompare
- Combine technical analysis with market sentiment
- Generate trading recommendations (Strong Buy, Buy, Hold, Sell, Strong Sell)

Features:
- Real-time Bitcoin price data from Binance and CoinGecko APIs
- Historical data analysis with MACD indicator
- News sentiment analysis with keyword-based scoring
- Weighted signal combination (60% MACD, 40% Sentiment)
- Confidence scoring for each recommendation
- Detailed reasoning for trading signals
- CLI interface with verbose and quick modes

Components:
- data_fetcher.py: Bitcoin price and market data retrieval
- macd_indicator.py: MACD calculation and signal generation
- news_sentiment.py: News analysis and sentiment scoring
- signal_generator.py: Combined signal generation
- bitcoin_trader.py: Main CLI application

Usage:
  python bitcoin_trading/bitcoin_trader.py [--verbose] [--days N] [--quick]

Documentation in bitcoin_trading/README.md
2025-12-02 22:45:17 +00:00
admin d6694d97a3 Merge pull request #15 from metacube2/claude/add-band-email-feature-01F6etH4329FWCEMsSZpdvCj
Implement email functionality and improve booking system
2025-12-02 22:46:17 +01:00
admin 797afb4b17 Merge branch 'main' into claude/add-band-email-feature-01F6etH4329FWCEMsSZpdvCj 2025-12-02 22:46:11 +01:00
admin 29e836822f Merge pull request #14 from metacube2/claude/new-project-setup-018BzEVHSs5k9RShTHsPQsmE
Set up modern PHP MVC project structure for GetYourBand platform
2025-12-02 22:45:41 +01:00
admin 8f7be04301 Merge pull request #13 from metacube2/claude/organize-project-files-01C4TfggxYSQyiBmexbjTXun
Organize all project files into src subdirectory
2025-12-02 22:45:24 +01:00
admin e62c61e255 Merge pull request #12 from metacube2/claude/add-paypal-image-uploads-01XtWuh5yLHza7HPpMd7BKLq
Add PayPal integration and band image upload features
2025-12-02 22:45:06 +01:00
Claude fbbad82975 Organize all project files into src subdirectory
- Created new src/ directory
- Copied all project files and folders to src/
- Keeps project structure organized and consolidated
2025-12-02 21:13:51 +00:00
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
Claude acc50dbb5d Implement email functionality and improve booking system
Features:
- Add real email sending with PHP mail() function
- Create HTML email templates for bookings
- Send booking notifications to bands
- Send confirmation emails to customers
- Add email field to bands table and profile
- Enable guest bookings without login
- Improve form validation and UX
- Add migration script for database updates

This fixes the non-working email system and improves the reservation/booking process significantly.
2025-12-02 21:01:02 +00:00
49 changed files with 15508 additions and 13 deletions
+163
View File
@@ -0,0 +1,163 @@
# Email & Buchungssystem Updates
## Änderungen vom 2. Dezember 2025
### ✅ Implementierte Features:
#### 1. **Echte Email-Funktionalität** (`includes/email.php`)
- ✨ PHP `mail()` Funktion implementiert statt nur Logging
- ✅ HTML-Email-Support mit professionellen Templates
- 📧 Automatische Headers (From, Reply-To, Content-Type)
- 📝 Logging bleibt erhalten für Debugging
#### 2. **Email-Template-System**
- 🎨 Professionelle HTML-Email-Templates mit Styling
- 🎸 "booking_request" - Email an die Band
- ✅ "booking_confirmation" - Bestätigung an den Kunden
- 🎨 Gelbes Branding (#f4b807) passend zur Plattform
#### 3. **Verbesserte Buchungsanfragen** (`anfrage.php`)
- 📧 Automatische Email an Band bei neuer Anfrage
- ✅ Bestätigungs-Email an Kunden
- 👥 **Gäste-Buchungen** ohne Login möglich
- ✔️ Bessere Formular-Validierung
- 📅 Datum-Mindestauswahl (nur zukünftige Daten)
#### 4. **Band-Email-Verwaltung** (`profil.php`)
- 📧 Bands können eigene Email-Adresse hinterlegen
- 📝 Klare Beschriftung: "Email für Buchungsanfragen"
- 💾 Email wird in der Datenbank gespeichert
#### 5. **Datenbank-Updates** (`database.sql`)
- 🗄️ Neue Spalte `email` in `bands` Tabelle
- 📜 Migration-Script: `migrate_add_band_email.php`
---
## 📋 Installations-Anleitung
### Schritt 1: Migration ausführen
```bash
php migrate_add_band_email.php
```
### Schritt 2: Mail-Server konfigurieren
Stelle sicher, dass PHP's `mail()` Funktion auf dem Server konfiguriert ist:
- Ubuntu/Debian: `sudo apt-get install sendmail`
- Oder verwende einen SMTP-Relay wie Postfix
### Schritt 3: Testen
1. Als Band einloggen
2. Profil bearbeiten und Email-Adresse hinzufügen
3. Als Gast oder Kunde eine Buchungsanfrage senden
4. Prüfe die Emails (und `storage/logs/mail.log`)
---
## 🎯 Neue Funktionen im Detail
### Email an Band (booking_request)
```
Enthält:
- Event-Datum, Ort, Typ
- Budget
- Nachricht des Kunden
- Kontaktdaten (Name, Email)
- Professionelles Layout
```
### Email an Kunde (booking_confirmation)
```
Enthält:
- Bestätigung der Anfrage
- Event-Details
- Hinweis auf Rückmeldung der Band
- Support-Kontakt
```
### Gäste-Buchungen
```
- Keine Registrierung nötig
- Name + Email Pflichtfelder
- Email-Validierung
- Gleiche Funktionalität wie eingeloggte User
```
---
## 🔧 Konfiguration
### Email-Absender
In `includes/config.php`:
```php
const SITE_NAME = 'GetYourBand';
const SUPPORT_EMAIL = 'support@getyourband.ch';
```
### Band-Email Fallback
Falls Band keine Email hinterlegt hat:
```php
info@[bandname].ch
```
(Leerzeichen werden entfernt, lowercase)
---
## 📝 Nächste Schritte (Optional)
### Empfohlene Erweiterungen:
1. **PHPMailer Integration** für SMTP-Support
2. **Email-Queue** für große Mengen
3. **Email-Templates per Datenbank** konfigurierbar
4. **Email-Benachrichtigungen** für:
- Status-Änderungen von Anfragen
- Neue Bewertungen
- Profil-Freigaben
### SMTP mit PHPMailer (Beispiel):
```bash
composer require phpmailer/phpmailer
```
Dann in `includes/email.php` ersetzen:
```php
use PHPMailer\PHPMailer\PHPMailer;
// ... SMTP Konfiguration
```
---
## 🐛 Debugging
### Email kommt nicht an?
1. Prüfe `storage/logs/mail.log` - werden Emails geloggt?
2. Prüfe Server Mail-Logs: `tail -f /var/log/mail.log`
3. Teste PHP mail(): `php -r "mail('test@example.com', 'Test', 'Test');"`
4. Prüfe Spam-Ordner
### Häufige Probleme:
- **sendmail nicht installiert**: `sudo apt-get install sendmail`
- **Port 25 blockiert**: Verwende SMTP-Relay
- **SPF/DKIM fehlt**: Emails landen im Spam
---
## ✨ Zusammenfassung
**Vorher:**
- ❌ Emails wurden nur geloggt
- ❌ Keine echten Email-Benachrichtigungen
- ❌ Gäste konnten nicht buchen
- ❌ Bands hatten keine Email-Verwaltung
**Nachher:**
- ✅ Echte Email-Versand mit HTML-Templates
- ✅ Automatische Benachrichtigungen an Band & Kunde
- ✅ Gäste-Buchungen möglich
- ✅ Bands verwalten ihre Email-Adresse
- ✅ Professionelles Design
- ✅ Bessere Validierung
---
**Viel Erfolg! 🎸🎵**
+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
Binary file not shown.
+46 -8
View File
@@ -15,6 +15,8 @@ $user = currentUser();
$message = '';
$error = '';
$requestId = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'band_id' => $bandId,
@@ -26,10 +28,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'message' => trim((string) $_POST['message'] ?? ''),
];
$guestName = trim($_POST['guest_name'] ?? '');
$guestEmail = trim($_POST['guest_email'] ?? '');
if (!$data['event_date'] || !$data['location']) {
$error = 'Bitte Datum und Ort ausfüllen.';
} elseif (!$user && (!$guestName || !$guestEmail)) {
$error = 'Bitte geben Sie Ihren Namen und Email-Adresse an.';
} elseif (!$user && !filter_var($guestEmail, FILTER_VALIDATE_EMAIL)) {
$error = 'Bitte geben Sie eine gültige Email-Adresse an.';
} 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']);
}
@@ -52,26 +62,54 @@ $settings = settings();
<p>PayPal Zahlungsabwicklung ist <?= $settings['paypal_enabled'] === '1' ? 'aktiviert' : 'optional' ?>, Service Fee: <?= htmlspecialchars($settings['service_fee']) ?>%.</p>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($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>
<?php if (!$user): ?>
<div style="background: #fff3cd; padding: 15px; margin-bottom: 20px; border-radius: 4px;">
<strong>Gast-Buchung</strong>
<p style="margin: 5px 0 0 0; font-size: 14px;">Sie sind nicht eingeloggt. Bitte geben Sie Ihre Kontaktdaten an.</p>
</div>
<label>Ihr Name *
<input type="text" class="form-control" name="guest_name" required>
</label>
<label>Ihre Email *
<input type="email" class="form-control" name="guest_email" required>
</label>
<hr style="margin: 20px 0;">
<?php endif; ?>
<label>Event-Datum *
<input type="date" class="form-control" name="event_date" min="<?= date('Y-m-d') ?>" required>
</label>
<label>Ort / Location
<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">
<input type="text" class="form-control" name="event_type" placeholder="Hochzeit, Firmenfeier, Geburtstag">
</label>
<label>Budget (CHF)
<input type="number" class="form-control" name="budget" placeholder="4500">
<input type="number" class="form-control" name="budget" placeholder="4500" min="0">
</label>
<label>Nachricht
<textarea class="form-control" name="message" rows="4"></textarea>
<label>Nachricht / Besondere Wünsche
<textarea class="form-control" name="message" rows="4" placeholder="Erzählen Sie uns mehr über Ihr Event..."></textarea>
</label>
<button class="btn-primary">Anfrage senden</button>
</form>
<?php endif; ?>
</main>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Environment
.env
.env.local
# OS
.DS_Store
Thumbs.db
+274
View File
@@ -0,0 +1,274 @@
# 🪙 Bitcoin Trading Signal System
Ein intelligentes Trading-Signal-System für Bitcoin, das **technische Analyse (MACD)** mit **News-Sentiment-Analyse** kombiniert, um fundierte Kauf- und Verkaufsempfehlungen zu generieren.
## 📋 Features
### 🔍 Technische Analyse
- **MACD-Indikator** (Moving Average Convergence Divergence)
- Erkennung von Bullish/Bearish Crossovers
- Histogramm-Analyse für Momentum-Erkennung
- Trend-Analyse über multiple Zeitperioden
### 📰 Sentiment-Analyse
- Echtzeit-Analyse von Bitcoin-News
- Keyword-basierte Sentiment-Bewertung
- Multiple News-Quellen (CryptoCompare, optional NewsAPI)
- Aggregierung von positiven/negativen Marktsignalen
### 🎯 Kombinierte Signale
- Gewichtete Kombination aus MACD + Sentiment
- 5 Signal-Stufen: Starker Kauf, Kauf, Halten, Verkauf, Starker Verkauf
- Konfidenz-Bewertung für jedes Signal
- Detaillierte Begründungen für Empfehlungen
## 🚀 Installation
### Voraussetzungen
- Python 3.8 oder höher
- pip (Python Package Manager)
### 1. Dependencies installieren
```bash
cd bitcoin_trading
pip install -r requirements.txt
```
### 2. Optional: NewsAPI-Schlüssel
Für erweiterte News-Analyse kannst du einen kostenlosen NewsAPI-Schlüssel erhalten:
1. Registriere dich auf [NewsAPI.org](https://newsapi.org)
2. Hole dir deinen API-Schlüssel
3. Verwende ihn mit `--newsapi-key` Parameter
## 💻 Verwendung
### Basis-Analyse (empfohlen)
```bash
python bitcoin_trader.py
```
Dies führt eine vollständige Analyse durch mit:
- Aktuellen Bitcoin-Preisdaten
- 30 Tage historische MACD-Daten
- Aktuelle News-Sentiment-Analyse
- Kombinierter Trading-Empfehlung
### Erweiterte Optionen
**Ausführliche Ausgabe mit Ladestatus:**
```bash
python bitcoin_trader.py --verbose
```
**Mehr historische Daten (z.B. 60 Tage):**
```bash
python bitcoin_trader.py --days 60
```
**Schnelles Signal (nur Empfehlung):**
```bash
python bitcoin_trader.py --quick
```
**Mit NewsAPI-Schlüssel:**
```bash
python bitcoin_trader.py --newsapi-key YOUR_API_KEY
```
**Alle Optionen kombiniert:**
```bash
python bitcoin_trader.py --verbose --days 90 --newsapi-key YOUR_KEY
```
### Als Python-Modul verwenden
```python
from bitcoin_trading import BitcoinTrader
# Initialisiere Trader
trader = BitcoinTrader(verbose=True)
# Führe Analyse durch
trader.run_analysis(days=30)
# Oder hole schnelles Signal
signal = trader.get_quick_signal()
print(signal)
```
## 📊 Output-Beispiel
```
╔══════════════════════════════════════════════════════════════════╗
║ BITCOIN TRADING SIGNAL - 2024-12-02 15:30 ║
╚══════════════════════════════════════════════════════════════════╝
📊 EMPFEHLUNG: 🟢 KAUF
💯 KONFIDENZ: 72%
💰 AKTUELLER PREIS: $42,583.50
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📈 TECHNISCHE ANALYSE (MACD)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Signal: KAUF
MACD: 125.34
Signal Line: 98.21
Histogram: 27.13
Preis-Änderung (10 Tage): +5.67%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📰 SENTIMENT-ANALYSE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Markt-Sentiment: POSITIV
Sentiment-Score: 0.425
Analysierte Artikel: 28
├─ Positiv: 16
├─ Neutral: 8
└─ Negativ: 4
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 BEGRÜNDUNG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. MACD-Signal: KAUF (Konfidenz: 70%)
2. Bullish Crossover: MACD kreuzt Signal-Linie von unten
3. Positives Momentum: Histogramm steigt
4. Markt-Sentiment: POSITIV (Konfidenz: 75%, Score: 0.425)
5. ✅ MACD und Sentiment stimmen überein → Starkes Signal
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 HANDLUNGSEMPFEHLUNG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 KAUFGELEGENHEIT
→ Erwäge einen Einstieg mit kleiner Position
→ Warte ggf. auf Bestätigung durch weitere Signale
⚠️ Risiko-Hinweis: Diese Analyse hat eine Konfidenz von 72%
⚠️ Keine Anlageberatung - Trading auf eigenes Risiko!
╚══════════════════════════════════════════════════════════════════╝
```
## 🔧 Technische Details
### MACD-Parameter
- **Fast EMA**: 12 Perioden
- **Slow EMA**: 26 Perioden
- **Signal Line**: 9 Perioden
### Signal-Logik
- **Starker Kauf**: Bullish Crossover im negativen Bereich
- **Kauf**: Bullish Crossover oder positives Momentum
- **Halten**: Keine klare Richtung
- **Verkauf**: Bearish Crossover oder negatives Momentum
- **Starker Verkauf**: Bearish Crossover im positiven Bereich
### Gewichtung
- MACD (Technische Analyse): **60%**
- News-Sentiment: **40%**
- Bonus bei übereinstimmenden Signalen: **+10%**
## 📁 Projektstruktur
```
bitcoin_trading/
├── __init__.py # Package Initialisierung
├── bitcoin_trader.py # Hauptprogramm (CLI)
├── data_fetcher.py # Bitcoin-Preisdaten-Abruf
├── macd_indicator.py # MACD-Indikator-Berechnung
├── news_sentiment.py # News-Sentiment-Analyse
├── signal_generator.py # Signal-Kombination & Empfehlung
├── requirements.txt # Python-Dependencies
└── README.md # Diese Datei
```
## 🔌 API-Quellen
### Preisdaten
- **Binance API** (primär) - Schnelle, zuverlässige Preisdaten
- **CoinGecko API** (fallback) - Backup-Datenquelle
### News
- **CryptoCompare News API** (kostenlos) - Crypto-spezifische News
- **NewsAPI** (optional) - Erweiterte News-Abdeckung
## ⚠️ Wichtige Hinweise
### Disclaimer
- **Dies ist KEINE Anlageberatung**
- Trading mit Kryptowährungen ist hochriskant
- Vergangene Performance garantiert keine zukünftigen Ergebnisse
- Investiere nur Geld, das du dir leisten kannst zu verlieren
- Führe deine eigene Due Diligence durch
### Risiken
- Marktvolatilität kann Signale schnell ungültig machen
- Technische Indikatoren sind nicht 100% zuverlässig
- News-Sentiment kann manipuliert sein
- API-Ausfälle können Daten beeinträchtigen
### Best Practices
- Verwende Signale als einen von mehreren Faktoren
- Setze immer Stop-Loss-Orders
- Diversifiziere dein Portfolio
- Handel nur mit klarem Kopf
- Dokumentiere deine Trades
## 🐛 Troubleshooting
### Fehler: "Konnte Bitcoin-Preis nicht abrufen"
- Überprüfe Internetverbindung
- APIs könnten temporär down sein
- Warte kurz und versuche es erneut
### Fehler: "Nicht genug Daten für MACD"
- Erhöhe `--days` Parameter (mindestens 30 Tage empfohlen)
- Stelle sicher, dass historische Daten geladen werden
### Sentiment zeigt immer "NEUTRAL"
- Möglicherweise keine aktuellen News verfügbar
- Verwende `--newsapi-key` für mehr News-Quellen
- News-APIs könnten Rate-Limits haben
## 🔄 Updates & Erweiterungen
### Geplante Features
- [ ] RSI (Relative Strength Index) Integration
- [ ] Bollinger Bands Analyse
- [ ] Machine Learning Modelle
- [ ] Email/Telegram Benachrichtigungen
- [ ] Backtesting-Funktionalität
- [ ] WebSocket Real-time Updates
- [ ] Multi-Coin Support (ETH, etc.)
### Erweiterungsmöglichkeiten
- Integration weiterer technischer Indikatoren
- Social Media Sentiment (Twitter/Reddit)
- On-Chain-Metriken (Wallet-Bewegungen)
- Advanced ML/AI Modelle
- Portfolio-Management-Features
## 📝 Lizenz
Dieses Projekt ist Teil des GetYourBand-Projekts.
## 🤝 Beitragen
Contributions sind willkommen! Bitte öffne ein Issue oder Pull Request.
## 📧 Support
Bei Fragen oder Problemen erstelle ein Issue im Repository.
---
**Made with 📊 and ₿ for informed trading decisions**
⚠️ **Remember: Don't invest more than you can afford to lose!**
+26
View File
@@ -0,0 +1,26 @@
"""
Bitcoin Trading Signal System
Ein System zur Generierung von Kauf-/Verkaufsempfehlungen für Bitcoin
basierend auf MACD-Indikatoren und News-Sentiment-Analyse
"""
__version__ = "1.0.0"
__author__ = "Bitcoin Trading Signal System"
from .data_fetcher import BitcoinDataFetcher
from .macd_indicator import MACDIndicator, MACDSignal
from .news_sentiment import NewsSentimentAnalyzer, SentimentScore
from .signal_generator import SignalGenerator, TradingAction, TradingSignal
from .bitcoin_trader import BitcoinTrader
__all__ = [
'BitcoinDataFetcher',
'MACDIndicator',
'MACDSignal',
'NewsSentimentAnalyzer',
'SentimentScore',
'SignalGenerator',
'TradingAction',
'TradingSignal',
'BitcoinTrader',
]
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Bitcoin Trading Signal System
Hauptprogramm für Bitcoin Trading-Empfehlungen basierend auf MACD und News-Sentiment
"""
import sys
import argparse
from datetime import datetime
from typing import Optional
from data_fetcher import BitcoinDataFetcher
from signal_generator import SignalGenerator
class BitcoinTrader:
"""
Haupt-Klasse für das Bitcoin Trading Signal System
"""
def __init__(self, newsapi_key: Optional[str] = None, verbose: bool = False):
"""
Args:
newsapi_key: Optional NewsAPI-Schlüssel
verbose: Ausführliche Ausgabe
"""
self.data_fetcher = BitcoinDataFetcher()
self.signal_generator = SignalGenerator(newsapi_key=newsapi_key)
self.verbose = verbose
def run_analysis(self, days: int = 30) -> None:
"""
Führt komplette Trading-Analyse durch
Args:
days: Anzahl Tage für historische Daten
"""
print("╔══════════════════════════════════════════════════════════════════╗")
print("║ BITCOIN TRADING SIGNAL SYSTEM v1.0 ║")
print("╚══════════════════════════════════════════════════════════════════╝")
print()
# 1. Lade aktuelle Preisdaten
if self.verbose:
print("📊 Lade aktuelle Bitcoin-Preisdaten...")
current_price = self.data_fetcher.get_current_price()
if not current_price:
print("❌ Fehler: Konnte aktuellen Bitcoin-Preis nicht abrufen!")
sys.exit(1)
if self.verbose:
print(f"✓ Aktueller BTC-Preis: ${current_price:,.2f}")
# 2. Lade historische Daten
if self.verbose:
print(f"📈 Lade historische Daten ({days} Tage)...")
price_df = self.data_fetcher.get_historical_data(days=days)
if price_df.empty:
print("❌ Fehler: Konnte historische Daten nicht abrufen!")
sys.exit(1)
if self.verbose:
print(f"{len(price_df)} Datenpunkte geladen")
# 3. Lade Marktdaten (optional)
if self.verbose:
print("💹 Lade erweiterte Marktdaten...")
market_data = self.data_fetcher.get_market_data()
if market_data and self.verbose:
print(f"✓ Marktdaten geladen")
if market_data.get('price_change_24h'):
change_24h = market_data['price_change_24h']
emoji = "📈" if change_24h > 0 else "📉"
print(f" {emoji} 24h Veränderung: {change_24h:+.2f}%")
# 4. Generiere Trading-Signal
if self.verbose:
print("\n🔍 Analysiere MACD-Indikatoren...")
print("📰 Analysiere News-Sentiment...")
print("🎯 Generiere Trading-Signal...\n")
signal = self.signal_generator.generate_signal(price_df, current_price)
# 5. Zeige Empfehlung
recommendation = self.signal_generator.get_recommendation_text(signal)
print(recommendation)
# 6. Zusätzliche Marktinformationen
if market_data and self.verbose:
print("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
print("📊 ZUSÄTZLICHE MARKTINFORMATIONEN")
print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
if market_data.get('market_cap'):
print(f" Marktkapitalisierung: ${market_data['market_cap']:,.0f}")
if market_data.get('total_volume'):
print(f" 24h Handelsvolumen: ${market_data['total_volume']:,.0f}")
if market_data.get('high_24h') and market_data.get('low_24h'):
print(f" 24h Hoch: ${market_data['high_24h']:,.2f}")
print(f" 24h Tief: ${market_data['low_24h']:,.2f}")
if market_data.get('price_change_7d'):
print(f" 7-Tage Veränderung: {market_data['price_change_7d']:+.2f}%")
if market_data.get('price_change_30d'):
print(f" 30-Tage Veränderung: {market_data['price_change_30d']:+.2f}%")
print()
def get_quick_signal(self) -> str:
"""
Gibt schnelles Trading-Signal zurück (nur Empfehlung)
Returns:
Signal-String
"""
current_price = self.data_fetcher.get_current_price()
if not current_price:
return "❌ Fehler beim Abrufen der Daten"
price_df = self.data_fetcher.get_historical_data(days=30)
if price_df.empty:
return "❌ Fehler beim Abrufen der Daten"
signal = self.signal_generator.generate_signal(price_df, current_price)
return f"{signal.action.value} (Konfidenz: {signal.confidence}%) @ ${signal.price:,.2f}"
def main():
"""Hauptfunktion"""
parser = argparse.ArgumentParser(
description='Bitcoin Trading Signal System - MACD + News Sentiment Analyse',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Beispiele:
%(prog)s # Standard-Analyse
%(prog)s --verbose # Ausführliche Ausgabe
%(prog)s --days 60 # 60 Tage historische Daten
%(prog)s --quick # Schnelles Signal
%(prog)s --newsapi-key YOUR_KEY # Mit NewsAPI-Schlüssel
Hinweis:
- NewsAPI-Schlüssel optional (erhöht News-Quellen)
- Kostenlos bei https://newsapi.org
"""
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Ausführliche Ausgabe mit Ladestatus'
)
parser.add_argument(
'-d', '--days',
type=int,
default=30,
help='Anzahl Tage für historische Daten (Standard: 30)'
)
parser.add_argument(
'-q', '--quick',
action='store_true',
help='Schnelles Signal ohne Details'
)
parser.add_argument(
'--newsapi-key',
type=str,
default=None,
help='NewsAPI-Schlüssel für erweiterte News-Analyse'
)
args = parser.parse_args()
# Initialisiere Trader
trader = BitcoinTrader(
newsapi_key=args.newsapi_key,
verbose=args.verbose
)
try:
if args.quick:
# Schnelles Signal
signal = trader.get_quick_signal()
print(signal)
else:
# Vollständige Analyse
trader.run_analysis(days=args.days)
except KeyboardInterrupt:
print("\n\n⚠️ Analyse abgebrochen durch Benutzer")
sys.exit(0)
except Exception as e:
print(f"\n❌ Fehler: {e}")
if args.verbose:
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+175
View File
@@ -0,0 +1,175 @@
"""
Bitcoin Price Data Fetcher
Ruft aktuelle und historische Bitcoin-Preisdaten von verschiedenen APIs ab
"""
import requests
from datetime import datetime, timedelta
import pandas as pd
from typing import Dict, List, Optional
import time
class BitcoinDataFetcher:
"""Fetches Bitcoin price data from various sources"""
def __init__(self):
self.base_url_coingecko = "https://api.coingecko.com/api/v3"
self.base_url_binance = "https://api.binance.com/api/v3"
def get_current_price(self) -> Optional[float]:
"""
Holt den aktuellen Bitcoin-Preis in USD
Returns:
float: Aktueller BTC/USD Preis oder None bei Fehler
"""
try:
# Versuche zuerst Binance (schneller und zuverlässiger)
url = f"{self.base_url_binance}/ticker/price"
params = {"symbol": "BTCUSDT"}
response = requests.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
return float(data['price'])
except Exception as e:
print(f"Fehler beim Abrufen von Binance: {e}")
try:
# Fallback zu CoinGecko
url = f"{self.base_url_coingecko}/simple/price"
params = {
"ids": "bitcoin",
"vs_currencies": "usd"
}
response = requests.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
return float(data['bitcoin']['usd'])
except Exception as e:
print(f"Fehler beim Abrufen von CoinGecko: {e}")
return None
def get_historical_data(self, days: int = 30) -> pd.DataFrame:
"""
Holt historische Bitcoin-Preisdaten
Args:
days: Anzahl der Tage zurück
Returns:
DataFrame mit Spalten: timestamp, price, volume
"""
try:
# Binance Klines (Candlestick-Daten)
url = f"{self.base_url_binance}/klines"
# Berechne Zeitstempel
end_time = int(datetime.now().timestamp() * 1000)
start_time = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
params = {
"symbol": "BTCUSDT",
"interval": "1h", # Stündliche Daten
"startTime": start_time,
"endTime": end_time,
"limit": 1000
}
response = requests.get(url, params=params, timeout=30)
if response.status_code == 200:
data = response.json()
# Konvertiere zu DataFrame
df = pd.DataFrame(data, columns=[
'timestamp', 'open', 'high', 'low', 'close',
'volume', 'close_time', 'quote_volume', 'trades',
'taker_buy_base', 'taker_buy_quote', 'ignore'
])
# Behalte nur relevante Spalten
df = df[['timestamp', 'close', 'volume']]
df.columns = ['timestamp', 'price', 'volume']
# Konvertiere Datentypen
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df['price'] = df['price'].astype(float)
df['volume'] = df['volume'].astype(float)
return df
except Exception as e:
print(f"Fehler beim Abrufen historischer Daten: {e}")
# Fallback: Leeres DataFrame
return pd.DataFrame(columns=['timestamp', 'price', 'volume'])
def get_market_data(self) -> Dict:
"""
Holt erweiterte Marktdaten für Bitcoin
Returns:
Dict mit Marktdaten (Volumen, Marktkapitalisierung, etc.)
"""
try:
url = f"{self.base_url_coingecko}/coins/bitcoin"
params = {
"localization": "false",
"tickers": "false",
"market_data": "true",
"community_data": "false",
"developer_data": "false"
}
response = requests.get(url, params=params, timeout=15)
if response.status_code == 200:
data = response.json()
market_data = data.get('market_data', {})
return {
'current_price': market_data.get('current_price', {}).get('usd'),
'market_cap': market_data.get('market_cap', {}).get('usd'),
'total_volume': market_data.get('total_volume', {}).get('usd'),
'price_change_24h': market_data.get('price_change_percentage_24h'),
'price_change_7d': market_data.get('price_change_percentage_7d'),
'price_change_30d': market_data.get('price_change_percentage_30d'),
'high_24h': market_data.get('high_24h', {}).get('usd'),
'low_24h': market_data.get('low_24h', {}).get('usd'),
}
except Exception as e:
print(f"Fehler beim Abrufen von Marktdaten: {e}")
return {}
if __name__ == "__main__":
# Test
fetcher = BitcoinDataFetcher()
print("=== Bitcoin Preis ===")
price = fetcher.get_current_price()
if price:
print(f"Aktueller BTC/USD Preis: ${price:,.2f}")
print("\n=== Marktdaten ===")
market = fetcher.get_market_data()
for key, value in market.items():
if value is not None:
if 'price' in key or 'cap' in key or 'volume' in key or 'high' in key or 'low' in key:
print(f"{key}: ${value:,.2f}")
else:
print(f"{key}: {value:.2f}%")
print("\n=== Historische Daten (letzte 7 Tage) ===")
df = fetcher.get_historical_data(days=7)
if not df.empty:
print(f"Anzahl Datenpunkte: {len(df)}")
print(df.tail())
+254
View File
@@ -0,0 +1,254 @@
"""
MACD (Moving Average Convergence Divergence) Indicator
Berechnet MACD-Signale für Bitcoin Trading
"""
import pandas as pd
import numpy as np
from typing import Dict, Tuple, Optional
from enum import Enum
class MACDSignal(Enum):
"""MACD Signal-Typen"""
STRONG_BUY = "STARKER KAUF"
BUY = "KAUF"
NEUTRAL = "NEUTRAL"
SELL = "VERKAUF"
STRONG_SELL = "STARKER VERKAUF"
class MACDIndicator:
"""
MACD-Indikator für technische Analyse
Standard-Parameter:
- Fast EMA: 12 Perioden
- Slow EMA: 26 Perioden
- Signal: 9 Perioden
"""
def __init__(self, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9):
"""
Args:
fast_period: Schnelle EMA-Periode (Standard: 12)
slow_period: Langsame EMA-Periode (Standard: 26)
signal_period: Signal-Linie-Periode (Standard: 9)
"""
self.fast_period = fast_period
self.slow_period = slow_period
self.signal_period = signal_period
def calculate_ema(self, data: pd.Series, period: int) -> pd.Series:
"""
Berechnet Exponential Moving Average (EMA)
Args:
data: Preisdaten
period: EMA-Periode
Returns:
EMA-Serie
"""
return data.ewm(span=period, adjust=False).mean()
def calculate_macd(self, prices: pd.Series) -> pd.DataFrame:
"""
Berechnet MACD-Indikator
Args:
prices: Preis-Serie (normalerweise Close-Preise)
Returns:
DataFrame mit MACD, Signal und Histogram
"""
# Berechne EMAs
ema_fast = self.calculate_ema(prices, self.fast_period)
ema_slow = self.calculate_ema(prices, self.slow_period)
# MACD-Linie
macd_line = ema_fast - ema_slow
# Signal-Linie (9-Tage EMA der MACD-Linie)
signal_line = self.calculate_ema(macd_line, self.signal_period)
# MACD-Histogramm (Differenz zwischen MACD und Signal)
histogram = macd_line - signal_line
# Erstelle DataFrame
df = pd.DataFrame({
'macd': macd_line,
'signal': signal_line,
'histogram': histogram
})
return df
def get_signal(self, df: pd.DataFrame) -> Tuple[MACDSignal, Dict]:
"""
Generiert Trading-Signal basierend auf MACD
Args:
df: DataFrame mit price-Spalte
Returns:
Tuple aus (Signal, Details-Dict)
"""
if df.empty or len(df) < self.slow_period + self.signal_period:
return MACDSignal.NEUTRAL, {
'reason': 'Nicht genug Daten für MACD-Berechnung',
'confidence': 0
}
# Berechne MACD
macd_df = self.calculate_macd(df['price'])
# Hole aktuelle und vorherige Werte
current_macd = macd_df['macd'].iloc[-1]
current_signal = macd_df['signal'].iloc[-1]
current_histogram = macd_df['histogram'].iloc[-1]
prev_macd = macd_df['macd'].iloc[-2]
prev_signal = macd_df['signal'].iloc[-2]
prev_histogram = macd_df['histogram'].iloc[-2]
# Bestimme Signal-Typ
signal = MACDSignal.NEUTRAL
confidence = 0
reasons = []
# 1. Bullish Crossover (MACD kreuzt Signal von unten)
if prev_macd <= prev_signal and current_macd > current_signal:
signal = MACDSignal.BUY
confidence = 70
reasons.append("Bullish Crossover: MACD kreuzt Signal-Linie von unten")
# Starkes Kaufsignal, wenn zusätzlich im negativen Bereich
if current_macd < 0:
signal = MACDSignal.STRONG_BUY
confidence = 85
reasons.append("MACD im negativen Bereich → Überkauft")
# 2. Bearish Crossover (MACD kreuzt Signal von oben)
elif prev_macd >= prev_signal and current_macd < current_signal:
signal = MACDSignal.SELL
confidence = 70
reasons.append("Bearish Crossover: MACD kreuzt Signal-Linie von oben")
# Starkes Verkaufssignal, wenn zusätzlich im positiven Bereich
if current_macd > 0:
signal = MACDSignal.STRONG_SELL
confidence = 85
reasons.append("MACD im positiven Bereich → Überverkauft")
# 3. Divergenz-Analyse (Histogramm)
else:
histogram_trend = current_histogram - prev_histogram
if histogram_trend > 0 and current_histogram > 0:
signal = MACDSignal.BUY
confidence = 55
reasons.append("Positives Momentum: Histogramm steigt")
elif histogram_trend < 0 and current_histogram < 0:
signal = MACDSignal.SELL
confidence = 55
reasons.append("Negatives Momentum: Histogramm fällt")
else:
signal = MACDSignal.NEUTRAL
confidence = 30
reasons.append("Kein klarer Trend erkennbar")
# Zusätzliche Analyse: Preis-Trend
recent_prices = df['price'].tail(10)
price_change = ((recent_prices.iloc[-1] - recent_prices.iloc[0]) / recent_prices.iloc[0]) * 100
if abs(price_change) > 5:
if price_change > 0:
reasons.append(f"Preis-Trend: +{price_change:.2f}% (aufwärts)")
else:
reasons.append(f"Preis-Trend: {price_change:.2f}% (abwärts)")
details = {
'macd': float(current_macd),
'signal': float(current_signal),
'histogram': float(current_histogram),
'prev_histogram': float(prev_histogram),
'crossover': current_macd > current_signal,
'confidence': confidence,
'reasons': reasons,
'price_change_10d': float(price_change)
}
return signal, details
def analyze_trend(self, df: pd.DataFrame, periods: int = 20) -> str:
"""
Analysiert den Trend der letzten Perioden
Args:
df: DataFrame mit MACD-Daten
periods: Anzahl Perioden zur Analyse
Returns:
Trend-Beschreibung
"""
macd_df = self.calculate_macd(df['price'])
if len(macd_df) < periods:
return "Nicht genug Daten"
recent_histogram = macd_df['histogram'].tail(periods)
# Zähle positive/negative Balken
positive_bars = (recent_histogram > 0).sum()
negative_bars = (recent_histogram < 0).sum()
if positive_bars > negative_bars * 1.5:
return "Starker Aufwärtstrend"
elif positive_bars > negative_bars:
return "Leichter Aufwärtstrend"
elif negative_bars > positive_bars * 1.5:
return "Starker Abwärtstrend"
elif negative_bars > positive_bars:
return "Leichter Abwärtstrend"
else:
return "Seitwärtstrend"
if __name__ == "__main__":
# Test mit simulierten Daten
print("=== MACD Indicator Test ===\n")
# Erstelle Test-Daten
dates = pd.date_range(start='2024-01-01', periods=100, freq='D')
prices = 40000 + np.cumsum(np.random.randn(100) * 500) # Random Walk
df = pd.DataFrame({
'timestamp': dates,
'price': prices
})
# Initialisiere MACD
macd = MACDIndicator()
# Berechne MACD
macd_df = macd.calculate_macd(df['price'])
print("Letzte 5 MACD-Werte:")
print(macd_df.tail())
print("\n=== Trading Signal ===")
signal, details = macd.get_signal(df)
print(f"Signal: {signal.value}")
print(f"Konfidenz: {details['confidence']}%")
print(f"\nMACD: {details['macd']:.2f}")
print(f"Signal: {details['signal']:.2f}")
print(f"Histogram: {details['histogram']:.2f}")
print(f"\nGründe:")
for reason in details['reasons']:
print(f" - {reason}")
print(f"\nTrend-Analyse: {macd.analyze_trend(df)}")
+300
View File
@@ -0,0 +1,300 @@
"""
News Sentiment Analyzer für Bitcoin
Analysiert Nachrichten und bestimmt das Markt-Sentiment
"""
import requests
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
import re
class SentimentScore(Enum):
"""Sentiment-Score-Typen"""
VERY_POSITIVE = "SEHR POSITIV"
POSITIVE = "POSITIV"
NEUTRAL = "NEUTRAL"
NEGATIVE = "NEGATIV"
VERY_NEGATIVE = "SEHR NEGATIV"
class NewsSentimentAnalyzer:
"""
Analysiert Bitcoin-bezogene Nachrichten und bestimmt das Sentiment
"""
def __init__(self, api_key: Optional[str] = None):
"""
Args:
api_key: NewsAPI-Schlüssel (optional, verwendet Free Tier wenn None)
"""
self.api_key = api_key
self.newsapi_url = "https://newsapi.org/v2/everything"
# Sentiment-Keyword-Listen
self.positive_keywords = [
'bullish', 'surge', 'rally', 'gain', 'rise', 'soar', 'jump',
'breakthrough', 'adoption', 'institutional', 'buy', 'investment',
'growth', 'profit', 'support', 'upgrade', 'positive', 'optimistic',
'breakout', 'moon', 'accumulation', 'bull run', 'all-time high',
'ath', 'recovery', 'uptrend', 'momentum', 'strong'
]
self.negative_keywords = [
'bearish', 'crash', 'plunge', 'fall', 'drop', 'decline', 'sell',
'regulation', 'ban', 'warning', 'risk', 'fear', 'panic', 'loss',
'hack', 'fraud', 'scam', 'bubble', 'concern', 'volatile',
'downtrend', 'resistance', 'bear market', 'dump', 'correction',
'selling pressure', 'oversold', 'weak', 'uncertainty'
]
# Crypto News Sources (kostenlos zugänglich)
self.crypto_news_sources = [
'https://cryptopanic.com/api/v1/posts/',
'https://min-api.cryptocompare.com/data/v2/news/',
]
def fetch_news_newsapi(self, days: int = 1) -> List[Dict]:
"""
Holt Bitcoin-News von NewsAPI
Args:
days: Anzahl Tage zurück
Returns:
Liste von News-Artikeln
"""
if not self.api_key:
return []
try:
from_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
params = {
'q': 'bitcoin OR BTC OR cryptocurrency',
'from': from_date,
'sortBy': 'publishedAt',
'language': 'en',
'apiKey': self.api_key
}
response = requests.get(self.newsapi_url, params=params, timeout=15)
if response.status_code == 200:
data = response.json()
return data.get('articles', [])
except Exception as e:
print(f"Fehler beim Abrufen von NewsAPI: {e}")
return []
def fetch_news_cryptocompare(self, limit: int = 50) -> List[Dict]:
"""
Holt Bitcoin-News von CryptoCompare (kostenlos)
Args:
limit: Anzahl News-Artikel
Returns:
Liste von News-Artikeln
"""
try:
url = "https://min-api.cryptocompare.com/data/v2/news/"
params = {
'categories': 'BTC',
'lang': 'EN'
}
response = requests.get(url, params=params, timeout=15)
if response.status_code == 200:
data = response.json()
news_list = data.get('Data', [])
# Formatiere zu einheitlichem Format
formatted_news = []
for item in news_list[:limit]:
formatted_news.append({
'title': item.get('title', ''),
'description': item.get('body', ''),
'publishedAt': datetime.fromtimestamp(item.get('published_on', 0)),
'source': item.get('source', ''),
'url': item.get('url', '')
})
return formatted_news
except Exception as e:
print(f"Fehler beim Abrufen von CryptoCompare: {e}")
return []
def analyze_text_sentiment(self, text: str) -> Tuple[float, Dict]:
"""
Analysiert Sentiment eines Textes
Args:
text: Zu analysierender Text
Returns:
Tuple aus (Score -1 bis +1, Details)
"""
if not text:
return 0.0, {'positive': 0, 'negative': 0}
text_lower = text.lower()
# Zähle positive und negative Keywords
positive_count = sum(1 for keyword in self.positive_keywords if keyword in text_lower)
negative_count = sum(1 for keyword in self.negative_keywords if keyword in text_lower)
# Berechne Score (-1 bis +1)
total_keywords = positive_count + negative_count
if total_keywords == 0:
score = 0.0
else:
score = (positive_count - negative_count) / total_keywords
details = {
'positive': positive_count,
'negative': negative_count,
'total': total_keywords
}
return score, details
def analyze_news_sentiment(self, days: int = 1, limit: int = 50) -> Tuple[SentimentScore, Dict]:
"""
Analysiert Gesamt-Sentiment der aktuellen Bitcoin-Nachrichten
Args:
days: Anzahl Tage zurück
limit: Max. Anzahl Artikel
Returns:
Tuple aus (Sentiment, Details)
"""
# Hole News von verschiedenen Quellen
news_articles = []
# Versuche NewsAPI (falls API-Key vorhanden)
if self.api_key:
news_articles.extend(self.fetch_news_newsapi(days))
# Hole von CryptoCompare (kostenlos)
crypto_news = self.fetch_news_cryptocompare(limit)
news_articles.extend(crypto_news)
if not news_articles:
return SentimentScore.NEUTRAL, {
'reason': 'Keine News gefunden',
'confidence': 0,
'articles_analyzed': 0
}
# Analysiere jeden Artikel
sentiment_scores = []
positive_articles = 0
negative_articles = 0
neutral_articles = 0
for article in news_articles[:limit]:
title = article.get('title', '')
description = article.get('description', '')
combined_text = f"{title} {description}"
score, details = self.analyze_text_sentiment(combined_text)
sentiment_scores.append(score)
if score > 0.2:
positive_articles += 1
elif score < -0.2:
negative_articles += 1
else:
neutral_articles += 1
# Berechne Durchschnitts-Sentiment
avg_sentiment = sum(sentiment_scores) / len(sentiment_scores) if sentiment_scores else 0
# Bestimme Gesamt-Sentiment
if avg_sentiment > 0.4:
sentiment = SentimentScore.VERY_POSITIVE
confidence = min(95, int(70 + abs(avg_sentiment) * 50))
elif avg_sentiment > 0.15:
sentiment = SentimentScore.POSITIVE
confidence = min(85, int(60 + abs(avg_sentiment) * 50))
elif avg_sentiment < -0.4:
sentiment = SentimentScore.VERY_NEGATIVE
confidence = min(95, int(70 + abs(avg_sentiment) * 50))
elif avg_sentiment < -0.15:
sentiment = SentimentScore.NEGATIVE
confidence = min(85, int(60 + abs(avg_sentiment) * 50))
else:
sentiment = SentimentScore.NEUTRAL
confidence = 50
details = {
'average_sentiment': float(avg_sentiment),
'confidence': confidence,
'articles_analyzed': len(news_articles),
'positive_articles': positive_articles,
'negative_articles': negative_articles,
'neutral_articles': neutral_articles,
'top_articles': news_articles[:5] # Top 5 für Details
}
return sentiment, details
def get_trending_topics(self, news_articles: List[Dict]) -> List[str]:
"""
Extrahiert Trend-Topics aus News
Args:
news_articles: Liste von News-Artikeln
Returns:
Liste der häufigsten Topics
"""
topics = []
keywords = [
'regulation', 'etf', 'institutional', 'mining',
'halving', 'adoption', 'sec', 'fed', 'inflation',
'blockchain', 'defi', 'nft', 'altcoin'
]
for article in news_articles:
text = f"{article.get('title', '')} {article.get('description', '')}".lower()
for keyword in keywords:
if keyword in text:
topics.append(keyword)
# Zähle Häufigkeit
from collections import Counter
topic_counts = Counter(topics)
return [topic for topic, count in topic_counts.most_common(5)]
if __name__ == "__main__":
# Test
print("=== Bitcoin News Sentiment Analyzer ===\n")
analyzer = NewsSentimentAnalyzer()
print("Lade Bitcoin-Nachrichten...")
sentiment, details = analyzer.analyze_news_sentiment(days=1, limit=30)
print(f"\n=== Sentiment-Analyse ===")
print(f"Gesamt-Sentiment: {sentiment.value}")
print(f"Konfidenz: {details['confidence']}%")
print(f"Durchschnitts-Score: {details['average_sentiment']:.3f}")
print(f"\nAnalysierte Artikel: {details['articles_analyzed']}")
print(f" Positiv: {details['positive_articles']}")
print(f" Neutral: {details['neutral_articles']}")
print(f" Negativ: {details['negative_articles']}")
if details.get('top_articles'):
print(f"\n=== Top 3 Schlagzeilen ===")
for i, article in enumerate(details['top_articles'][:3], 1):
print(f"{i}. {article.get('title', 'N/A')}")
+20
View File
@@ -0,0 +1,20 @@
# Bitcoin Trading Signal System - Python Dependencies
# Data Processing
pandas>=2.0.0
numpy>=1.24.0
# HTTP Requests
requests>=2.31.0
# Optional: Enhanced Sentiment Analysis
# textblob>=0.17.0
# vaderSentiment>=3.3.2
# Optional: Advanced Technical Analysis
# ta>=0.11.0
# pandas-ta>=0.3.14b0
# Development/Testing (optional)
# pytest>=7.4.0
# pytest-cov>=4.1.0
+295
View File
@@ -0,0 +1,295 @@
"""
Trading Signal Generator
Kombiniert MACD-Indikatoren und News-Sentiment für Trading-Empfehlungen
"""
from typing import Dict, Tuple
from enum import Enum
from dataclasses import dataclass
from datetime import datetime
from macd_indicator import MACDIndicator, MACDSignal
from news_sentiment import NewsSentimentAnalyzer, SentimentScore
class TradingAction(Enum):
"""Trading-Empfehlungs-Typen"""
STRONG_BUY = "🟢 STARKER KAUF"
BUY = "🟢 KAUF"
HOLD = "🟡 HALTEN"
SELL = "🔴 VERKAUF"
STRONG_SELL = "🔴 STARKER VERKAUF"
@dataclass
class TradingSignal:
"""Datenklasse für Trading-Signale"""
action: TradingAction
confidence: int # 0-100
price: float
timestamp: datetime
macd_signal: MACDSignal
sentiment: SentimentScore
reasons: list
technical_details: dict
sentiment_details: dict
def __str__(self):
return f"""
╔══════════════════════════════════════════════════════════════════╗
║ BITCOIN TRADING SIGNAL - {self.timestamp.strftime('%Y-%m-%d %H:%M')}
╚══════════════════════════════════════════════════════════════════╝
📊 EMPFEHLUNG: {self.action.value}
💯 KONFIDENZ: {self.confidence}%
💰 AKTUELLER PREIS: ${self.price:,.2f}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📈 TECHNISCHE ANALYSE (MACD)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Signal: {self.macd_signal.value}
MACD: {self.technical_details.get('macd', 0):.2f}
Signal Line: {self.technical_details.get('signal', 0):.2f}
Histogram: {self.technical_details.get('histogram', 0):.2f}
Preis-Änderung (10 Tage): {self.technical_details.get('price_change_10d', 0):.2f}%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📰 SENTIMENT-ANALYSE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Markt-Sentiment: {self.sentiment.value}
Sentiment-Score: {self.sentiment_details.get('average_sentiment', 0):.3f}
Analysierte Artikel: {self.sentiment_details.get('articles_analyzed', 0)}
├─ Positiv: {self.sentiment_details.get('positive_articles', 0)}
├─ Neutral: {self.sentiment_details.get('neutral_articles', 0)}
└─ Negativ: {self.sentiment_details.get('negative_articles', 0)}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 BEGRÜNDUNG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
def format_reasons(self):
"""Formatiert die Gründe"""
output = ""
for i, reason in enumerate(self.reasons, 1):
output += f" {i}. {reason}\n"
return output
class SignalGenerator:
"""
Generiert Trading-Signale durch Kombination von MACD und Sentiment
"""
def __init__(self, newsapi_key: str = None):
"""
Args:
newsapi_key: Optional NewsAPI-Schlüssel für erweiterte News
"""
self.macd_indicator = MACDIndicator()
self.sentiment_analyzer = NewsSentimentAnalyzer(api_key=newsapi_key)
# Gewichtungen für Signal-Berechnung
self.macd_weight = 0.6 # 60% Gewichtung für technische Analyse
self.sentiment_weight = 0.4 # 40% Gewichtung für Sentiment
def calculate_combined_score(self, macd_signal: MACDSignal,
sentiment: SentimentScore,
macd_confidence: int,
sentiment_confidence: int) -> Tuple[float, int]:
"""
Berechnet kombinierten Score aus MACD und Sentiment
Returns:
Tuple aus (Score -1 bis +1, Gesamt-Konfidenz)
"""
# Konvertiere Signale zu Scores (-1 bis +1)
macd_score_map = {
MACDSignal.STRONG_BUY: 1.0,
MACDSignal.BUY: 0.5,
MACDSignal.NEUTRAL: 0.0,
MACDSignal.SELL: -0.5,
MACDSignal.STRONG_SELL: -1.0
}
sentiment_score_map = {
SentimentScore.VERY_POSITIVE: 1.0,
SentimentScore.POSITIVE: 0.5,
SentimentScore.NEUTRAL: 0.0,
SentimentScore.NEGATIVE: -0.5,
SentimentScore.VERY_NEGATIVE: -1.0
}
macd_score = macd_score_map.get(macd_signal, 0)
sentiment_score = sentiment_score_map.get(sentiment, 0)
# Gewichteter kombinierter Score
combined_score = (macd_score * self.macd_weight +
sentiment_score * self.sentiment_weight)
# Gewichtete kombinierte Konfidenz
combined_confidence = int(
macd_confidence * self.macd_weight +
sentiment_confidence * self.sentiment_weight
)
# Bonus für übereinstimmende Signale
if (macd_score > 0 and sentiment_score > 0) or \
(macd_score < 0 and sentiment_score < 0):
combined_confidence = min(100, combined_confidence + 10)
return combined_score, combined_confidence
def generate_signal(self, price_df, current_price: float) -> TradingSignal:
"""
Generiert Trading-Signal
Args:
price_df: DataFrame mit historischen Preisen
current_price: Aktueller Bitcoin-Preis
Returns:
TradingSignal mit Empfehlung
"""
# Hole MACD-Signal
macd_signal, macd_details = self.macd_indicator.get_signal(price_df)
# Hole Sentiment
sentiment, sentiment_details = self.sentiment_analyzer.analyze_news_sentiment(
days=1, limit=30
)
# Berechne kombinierten Score
combined_score, confidence = self.calculate_combined_score(
macd_signal,
sentiment,
macd_details['confidence'],
sentiment_details['confidence']
)
# Bestimme Trading-Action
if combined_score >= 0.6:
action = TradingAction.STRONG_BUY
elif combined_score >= 0.2:
action = TradingAction.BUY
elif combined_score <= -0.6:
action = TradingAction.STRONG_SELL
elif combined_score <= -0.2:
action = TradingAction.SELL
else:
action = TradingAction.HOLD
# Sammle Gründe
reasons = []
# MACD-Gründe
reasons.append(f"MACD-Signal: {macd_signal.value} (Konfidenz: {macd_details['confidence']}%)")
reasons.extend(macd_details.get('reasons', []))
# Sentiment-Gründe
reasons.append(
f"Markt-Sentiment: {sentiment.value} "
f"(Konfidenz: {sentiment_details['confidence']}%, "
f"Score: {sentiment_details['average_sentiment']:.3f})"
)
# Zusätzliche Hinweise
if macd_signal in [MACDSignal.STRONG_BUY, MACDSignal.BUY] and \
sentiment in [SentimentScore.VERY_POSITIVE, SentimentScore.POSITIVE]:
reasons.append("✅ MACD und Sentiment stimmen überein → Starkes Signal")
elif macd_signal in [MACDSignal.STRONG_SELL, MACDSignal.SELL] and \
sentiment in [SentimentScore.VERY_NEGATIVE, SentimentScore.NEGATIVE]:
reasons.append("✅ MACD und Sentiment stimmen überein → Starkes Signal")
elif (macd_signal in [MACDSignal.STRONG_BUY, MACDSignal.BUY] and
sentiment in [SentimentScore.NEGATIVE, SentimentScore.VERY_NEGATIVE]) or \
(macd_signal in [MACDSignal.STRONG_SELL, MACDSignal.SELL] and
sentiment in [SentimentScore.POSITIVE, SentimentScore.VERY_POSITIVE]):
reasons.append("⚠️ MACD und Sentiment widersprechen sich → Vorsicht geboten")
confidence = max(30, confidence - 20) # Reduziere Konfidenz
# Erstelle Trading-Signal
signal = TradingSignal(
action=action,
confidence=confidence,
price=current_price,
timestamp=datetime.now(),
macd_signal=macd_signal,
sentiment=sentiment,
reasons=reasons,
technical_details=macd_details,
sentiment_details=sentiment_details
)
return signal
def get_recommendation_text(self, signal: TradingSignal) -> str:
"""
Generiert Empfehlungstext
Args:
signal: Trading-Signal
Returns:
Formatierter Empfehlungstext
"""
recommendation = str(signal)
recommendation += signal.format_reasons()
# Füge Handlungsempfehlung hinzu
recommendation += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
recommendation += "💡 HANDLUNGSEMPFEHLUNG\n"
recommendation += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
if signal.action == TradingAction.STRONG_BUY:
recommendation += " 🟢 STARKE KAUFGELEGENHEIT\n"
recommendation += " → Erwäge einen Einstieg oder Aufstockung der Position\n"
recommendation += " → Setze Stop-Loss ca. 5-7% unter Einstiegspreis\n"
elif signal.action == TradingAction.BUY:
recommendation += " 🟢 KAUFGELEGENHEIT\n"
recommendation += " → Erwäge einen Einstieg mit kleiner Position\n"
recommendation += " → Warte ggf. auf Bestätigung durch weitere Signale\n"
elif signal.action == TradingAction.HOLD:
recommendation += " 🟡 ABWARTEN\n"
recommendation += " → Keine klare Richtung erkennbar\n"
recommendation += " → Behalte den Markt im Auge für klarere Signale\n"
elif signal.action == TradingAction.SELL:
recommendation += " 🔴 VERKAUFSSIGNAL\n"
recommendation += " → Erwäge Teilverkauf oder Gewinnmitnahme\n"
recommendation += " → Ziehe Stop-Loss nach, um Gewinne zu sichern\n"
elif signal.action == TradingAction.STRONG_SELL:
recommendation += " 🔴 STARKES VERKAUFSSIGNAL\n"
recommendation += " → Erwäge Ausstieg aus Position\n"
recommendation += " → Sichere Gewinne oder begrenze Verluste\n"
recommendation += f"\n ⚠️ Risiko-Hinweis: Diese Analyse hat eine Konfidenz von {signal.confidence}%\n"
recommendation += " ⚠️ Keine Anlageberatung - Trading auf eigenes Risiko!\n"
recommendation += "\n╚══════════════════════════════════════════════════════════════════╝\n"
return recommendation
if __name__ == "__main__":
# Test
from data_fetcher import BitcoinDataFetcher
print("=== Bitcoin Trading Signal Generator ===\n")
print("Lade Daten...\n")
# Hole Preisdaten
fetcher = BitcoinDataFetcher()
price_df = fetcher.get_historical_data(days=30)
current_price = fetcher.get_current_price()
if price_df.empty or not current_price:
print("Fehler beim Laden der Daten!")
else:
# Generiere Signal
generator = SignalGenerator()
signal = generator.generate_signal(price_df, current_price)
# Zeige Empfehlung
recommendation = generator.get_recommendation_text(signal)
print(recommendation)
+15
View File
@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS bands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
email TEXT,
city TEXT,
genre TEXT,
price INTEGER DEFAULT 0,
@@ -74,3 +75,17 @@ 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
);
+172 -1
View File
@@ -1,10 +1,181 @@
<?php
function sendEmail(string $to, string $subject, string $message): void
require_once __DIR__ . '/config.php';
function sendEmail(string $to, string $subject, string $message, bool $isHtml = true): bool
{
$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);
$headers = [
'From: ' . SITE_NAME . ' <' . SUPPORT_EMAIL . '>',
'Reply-To: ' . SUPPORT_EMAIL,
'X-Mailer: PHP/' . phpversion(),
'MIME-Version: 1.0'
];
if ($isHtml) {
$headers[] = 'Content-Type: text/html; charset=UTF-8';
} else {
$headers[] = 'Content-Type: text/plain; charset=UTF-8';
}
return mail($to, $subject, $message, implode("\r\n", $headers));
}
function sendBookingRequestEmail(array $band, array $requestData, ?array $customer = null): bool
{
$bandEmail = $band['email'] ?? 'info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch';
$subject = 'Neue Buchungsanfrage für ' . $band['name'];
$message = emailTemplate('booking_request', [
'band_name' => $band['name'],
'event_date' => date('d.m.Y', strtotime($requestData['event_date'])),
'location' => $requestData['location'],
'event_type' => $requestData['event_type'] ?: 'Nicht angegeben',
'budget' => $requestData['budget'] ? formatPrice($requestData['budget']) : 'Nicht angegeben',
'message' => $requestData['message'] ?: 'Keine Nachricht',
'customer_name' => $customer['name'] ?? 'Gast',
'customer_email' => $customer['email'] ?? 'Keine Email angegeben',
]);
return sendEmail($bandEmail, $subject, $message);
}
function sendBookingConfirmationEmail(string $customerEmail, array $band, array $requestData): bool
{
$subject = 'Ihre Anfrage an ' . $band['name'] . ' wurde gesendet';
$message = emailTemplate('booking_confirmation', [
'band_name' => $band['name'],
'event_date' => date('d.m.Y', strtotime($requestData['event_date'])),
'location' => $requestData['location'],
'site_name' => SITE_NAME,
]);
return sendEmail($customerEmail, $subject, $message);
}
function emailTemplate(string $templateName, array $data): string
{
$templates = [
'booking_request' => '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f4b807; padding: 20px; text-align: center; }
.content { background: #fff; padding: 20px; border: 1px solid #ddd; }
.info-row { margin: 10px 0; padding: 10px; background: #f9f9f9; }
.label { font-weight: bold; color: #666; }
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 style="margin:0; color: #fff;">🎸 Neue Buchungsanfrage</h1>
</div>
<div class="content">
<p>Hallo <strong>' . htmlspecialchars($data['band_name']) . '</strong>,</p>
<p>Sie haben eine neue Buchungsanfrage erhalten:</p>
<div class="info-row">
<span class="label">Event-Datum:</span> ' . htmlspecialchars($data['event_date']) . '
</div>
<div class="info-row">
<span class="label">Ort:</span> ' . htmlspecialchars($data['location']) . '
</div>
<div class="info-row">
<span class="label">Event-Typ:</span> ' . htmlspecialchars($data['event_type']) . '
</div>
<div class="info-row">
<span class="label">Budget:</span> ' . htmlspecialchars($data['budget']) . '
</div>
<div class="info-row">
<span class="label">Nachricht:</span><br>' . nl2br(htmlspecialchars($data['message'])) . '
</div>
<h3>Kontaktdaten:</h3>
<div class="info-row">
<span class="label">Name:</span> ' . htmlspecialchars($data['customer_name']) . '
</div>
<div class="info-row">
<span class="label">Email:</span> <a href="mailto:' . htmlspecialchars($data['customer_email']) . '">' . htmlspecialchars($data['customer_email']) . '</a>
</div>
<p style="margin-top: 20px;">
Bitte kontaktieren Sie den Kunden direkt, um die Details zu besprechen.
</p>
</div>
<div class="footer">
Gesendet von ' . SITE_NAME . ' - Ihre Band-Vermittlungsplattform
</div>
</div>
</body>
</html>
',
'booking_confirmation' => '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f4b807; padding: 20px; text-align: center; }
.content { background: #fff; padding: 20px; border: 1px solid #ddd; }
.info-row { margin: 10px 0; padding: 10px; background: #f9f9f9; }
.label { font-weight: bold; color: #666; }
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
.success { background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; margin: 15px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 style="margin:0; color: #fff;">✅ Anfrage gesendet</h1>
</div>
<div class="content">
<div class="success">
<strong>Ihre Anfrage wurde erfolgreich gesendet!</strong>
</div>
<p>Vielen Dank für Ihre Anfrage an <strong>' . htmlspecialchars($data['band_name']) . '</strong>.</p>
<h3>Details Ihrer Anfrage:</h3>
<div class="info-row">
<span class="label">Event-Datum:</span> ' . htmlspecialchars($data['event_date']) . '
</div>
<div class="info-row">
<span class="label">Ort:</span> ' . htmlspecialchars($data['location']) . '
</div>
<p style="margin-top: 20px;">
Die Band wird sich in Kürze bei Ihnen melden. Bitte überprüfen Sie auch Ihren Spam-Ordner.
</p>
<p>
Bei Fragen können Sie uns jederzeit unter <a href="mailto:' . SUPPORT_EMAIL . '">' . SUPPORT_EMAIL . '</a> erreichen.
</p>
</div>
<div class="footer">
Vielen Dank, dass Sie ' . htmlspecialchars($data['site_name']) . ' nutzen!
</div>
</div>
</body>
</html>
',
];
return $templates[$templateName] ?? '';
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Migration: Add email column to bands table
* Run this once to update existing databases
*/
require_once __DIR__ . '/includes/database.php';
try {
$pdo = db();
$columns = $pdo->query("PRAGMA table_info(bands)")->fetchAll(PDO::FETCH_ASSOC);
$hasEmail = false;
foreach ($columns as $column) {
if ($column['name'] === 'email') {
$hasEmail = true;
break;
}
}
if (!$hasEmail) {
echo "Adding email column to bands table...\n";
$pdo->exec("ALTER TABLE bands ADD COLUMN email TEXT");
echo "✓ Email column added successfully!\n";
} else {
echo "✓ Email column already exists.\n";
}
echo "\nMigration completed successfully!\n";
} catch (PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
+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()]);
}
+127 -4
View File
@@ -13,9 +13,10 @@ if ($user['role'] === 'band') {
$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 = db()->prepare('UPDATE bands SET name = :name, email = :email, city = :city, genre = :genre, price = :price, description = :description, style_tags = :tags WHERE id = :id');
$stmt->execute([
':name' => $_POST['name'],
':email' => $_POST['email'] ?? null,
':city' => $_POST['city'],
':genre' => $_POST['genre'],
':price' => (int) $_POST['price'],
@@ -47,7 +48,11 @@ if ($user['role'] === 'band') {
<h2>Bandprofil</h2>
<form method="post">
<label>Bandname
<input class="form-control" name="name" value="<?= htmlspecialchars($band['name']) ?>">
<input class="form-control" name="name" value="<?= htmlspecialchars($band['name']) ?>" required>
</label>
<label>Email für Buchungsanfragen
<input class="form-control" type="email" name="email" value="<?= htmlspecialchars($band['email'] ?? '') ?>" placeholder="band@example.ch">
<small>An diese Adresse werden Buchungsanfragen gesendet</small>
</label>
<label>Ort
<input class="form-control" name="city" value="<?= htmlspecialchars($band['city']) ?>">
@@ -66,20 +71,138 @@ if ($user['role'] === 'band') {
</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></tr></thead>
<thead><tr><th>Band</th><th>Datum</th><th>Status</th><th>Zahlung</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach (userRequests((int) $user['id']) as $request): $bandName = findBand((int) $request['band_id']); ?>
<?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>
+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
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
changeBandStatus((int) $_POST['band_id'], $_POST['status']);
}
$bands = moderationItems('bands');
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Bands moderieren <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Bandfreigaben</h1>
<table class="table">
<thead><tr><th>Name</th><th>Ort</th><th>Status</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach ($bands as $band): ?>
<tr>
<td><?= htmlspecialchars($band['name']) ?></td>
<td><?= htmlspecialchars($band['city']) ?></td>
<td><?= htmlspecialchars($band['status']) ?></td>
<td>
<form method="post" style="display:inline-flex; gap:4px;">
<input type="hidden" name="band_id" value="<?= $band['id'] ?>">
<select name="status" class="form-control" style="width:auto;">
<option value="aktiv">Freigeben</option>
<option value="archiv">Archivieren</option>
<option value="prüfung">Zur Prüfung</option>
</select>
<button class="btn-primary">Speichern</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$bands): ?><p>Keine Bands warten auf Moderation.</p><?php endif; ?>
</main>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
changeReviewStatus((int) $_POST['review_id'], $_POST['status']);
}
$reviews = moderationItems('reviews');
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Bewertungen prüfen <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Bewertungen moderieren</h1>
<table class="table">
<thead><tr><th>Band</th><th>Autor</th><th>Bewertung</th><th>Kommentar</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach ($reviews as $review): ?>
<tr>
<td><?= htmlspecialchars($review['band_name']) ?></td>
<td><?= htmlspecialchars($review['author']) ?></td>
<td><?= (int) $review['rating'] ?> ★</td>
<td><?= htmlspecialchars($review['comment']) ?></td>
<td>
<form method="post" style="display:inline-flex; gap:4px;">
<input type="hidden" name="review_id" value="<?= $review['id'] ?>">
<select name="status" class="form-control" style="width:auto;">
<option value="freigegeben">Freigeben</option>
<option value="abgelehnt">Ablehnen</option>
</select>
<button class="btn-primary">Speichern</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$reviews): ?><p>Keine Bewertungen in Moderation.</p><?php endif; ?>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
$pdo = db();
$stats = [
'bands' => (int) $pdo->query("SELECT COUNT(*) FROM bands")->fetchColumn(),
'requests' => (int) $pdo->query("SELECT COUNT(*) FROM requests")->fetchColumn(),
'reviews' => (int) $pdo->query("SELECT COUNT(*) FROM reviews")->fetchColumn(),
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Admin Dashboard <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header>
<div class="admin-nav">
<a href="dashboard.php">Dashboard</a>
<a href="bands.php">Bandfreigaben</a>
<a href="bewertungen.php">Bewertungen</a>
<a href="settings.php">Settings</a>
</div>
</header>
<main>
<section class="band-grid">
<article class="band-card"><h3>Bands</h3><p><?= $stats['bands'] ?></p></article>
<article class="band-card"><h3>Anfragen</h3><p><?= $stats['requests'] ?></p></article>
<article class="band-card"><h3>Bewertungen</h3><p><?= $stats['reviews'] ?></p></article>
</section>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/auth.php';
requireAdmin();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
updateSetting('paypal_enabled', isset($_POST['paypal_enabled']) ? '1' : '0');
updateSetting('service_fee', (string) (int) $_POST['service_fee']);
$message = 'Einstellungen gespeichert.';
}
$config = settings();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Settings <?= SITE_NAME ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<header><div class="admin-nav"><a href="dashboard.php">Dashboard</a><a href="bands.php">Bands</a><a href="bewertungen.php">Bewertungen</a><a href="settings.php">Settings</a></div></header>
<main>
<h1>Vermittlungsgebühr & PayPal</h1>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<form method="post">
<label>
<input type="checkbox" name="paypal_enabled" <?= $config['paypal_enabled'] === '1' ? 'checked' : '' ?>> PayPal aktivieren
</label>
<label>Service Fee (%)
<input type="number" class="form-control" name="service_fee" value="<?= htmlspecialchars($config['service_fee']) ?>">
</label>
<button class="btn-primary">Speichern</button>
</form>
</main>
</body>
</html>
+342
View File
@@ -0,0 +1,342 @@
<?php
$altcoins = [
[
'name' => 'Ethereum (ETH)',
'symbol' => 'ETH',
'price' => 3725.42,
'ma200' => 3450.15,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'BNB',
'symbol' => 'BNB',
'price' => 598.12,
'ma200' => 612.77,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Solana (SOL)',
'symbol' => 'SOL',
'price' => 158.34,
'ma200' => 143.05,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'XRP',
'symbol' => 'XRP',
'price' => 0.57,
'ma200' => 0.63,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Dogecoin (DOGE)',
'symbol' => 'DOGE',
'price' => 0.19,
'ma200' => 0.15,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Cardano (ADA)',
'symbol' => 'ADA',
'price' => 0.48,
'ma200' => 0.62,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Avalanche (AVAX)',
'symbol' => 'AVAX',
'price' => 47.22,
'ma200' => 44.61,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Polkadot (DOT)',
'symbol' => 'DOT',
'price' => 8.81,
'ma200' => 7.29,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Chainlink (LINK)',
'symbol' => 'LINK',
'price' => 17.02,
'ma200' => 18.40,
'last_update' => '2024-04-21 14:00 UTC',
],
[
'name' => 'Polygon (MATIC)',
'symbol' => 'MATIC',
'price' => 0.92,
'ma200' => 0.98,
'last_update' => '2024-04-21 14:00 UTC',
],
];
function determineSignal(float $price, float $ma200): array
{
if ($price >= $ma200) {
return ['LONG', 'Preis notiert über dem 200-Tage-Durchschnitt.'];
}
return ['SHORT', 'Preis notiert unter dem 200-Tage-Durchschnitt.'];
}
function formatNumber(float $value, int $decimals = 2): string
{
return number_format($value, $decimals, ',', '.');
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Top 10 Altcoins MA200 Signale</title>
<style>
:root {
color-scheme: light dark;
--bg: #0f172a;
--fg: #f8fafc;
--card: #1e293b;
--accent: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--muted: #94a3b8;
}
* {
box-sizing: border-box;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, rgba(16,185,129,0.25), transparent 55%),
var(--bg);
color: var(--fg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
main {
width: min(1100px, 100%);
background: rgba(15,23,42,0.85);
border: 1px solid rgba(148,163,184,0.2);
border-radius: 24px;
box-shadow: 0 15px 60px rgba(0,0,0,0.45);
padding: 2rem 2.5rem 2.5rem;
}
header {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 2rem;
}
header h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.2;
}
header p {
margin: 0;
color: var(--muted);
}
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
thead th {
text-align: left;
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid rgba(148,163,184,0.2);
padding-bottom: 0.75rem;
}
tbody td {
padding: 1rem 0;
border-bottom: 1px solid rgba(148,163,184,0.1);
}
tbody tr:last-child td {
border-bottom: none;
}
.symbol {
color: var(--muted);
font-size: 0.9rem;
}
.price {
font-variant-numeric: tabular-nums;
}
.signal {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.05em;
}
.signal-long {
background: rgba(16,185,129,0.15);
border: 1px solid rgba(16,185,129,0.5);
color: var(--accent);
}
.signal-short {
background: rgba(239,68,68,0.15);
border: 1px solid rgba(239,68,68,0.5);
color: var(--danger);
}
.telegram-card {
margin-top: 2rem;
background: rgba(30,41,59,0.9);
border: 1px solid rgba(59,130,246,0.4);
border-radius: 18px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.telegram-card h2 {
margin: 0;
font-size: 1.2rem;
}
.telegram-card p {
margin: 0;
color: var(--muted);
line-height: 1.5;
}
.telegram-card pre {
margin: 0;
padding: 1rem;
background: rgba(15,23,42,0.8);
border-radius: 12px;
border: 1px solid rgba(148,163,184,0.15);
font-size: 0.9rem;
white-space: pre-wrap;
}
@media (max-width: 720px) {
main {
padding: 1.5rem;
}
table, thead, tbody, tr, td, th {
display: block;
}
thead {
display: none;
}
tbody tr {
border: 1px solid rgba(148,163,184,0.2);
border-radius: 18px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
tbody td {
border: none;
padding: 0.35rem 0;
}
tbody td::before {
content: attr(data-label);
display: block;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.2rem;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>Top 10 Altcoins &amp; MA200 Signale</h1>
<p>Überblick über mögliche Einstiegszonen basierend auf dem 200-Tage-Durchschnitt. Die Signale (Long oder Short)
können direkt an den Telegram-Channel weitergeleitet werden.</p>
</header>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Preis (USD)</th>
<th>MA200 (USD)</th>
<th>Abweichung</th>
<th>Signal</th>
<th>Zuletzt aktualisiert</th>
</tr>
</thead>
<tbody>
<?php foreach ($altcoins as $coin):
[$signal, $reason] = determineSignal($coin['price'], $coin['ma200']);
$diff = $coin['price'] - $coin['ma200'];
$diffPercent = ($coin['ma200'] > 0)
? ($diff / $coin['ma200']) * 100
: 0;
?>
<tr>
<td data-label="Asset">
<strong><?php echo htmlspecialchars($coin['name']); ?></strong>
<div class="symbol"><?php echo htmlspecialchars($coin['symbol']); ?></div>
</td>
<td data-label="Preis" class="price">$ <?php echo formatNumber($coin['price']); ?></td>
<td data-label="MA200" class="price">$ <?php echo formatNumber($coin['ma200']); ?></td>
<td data-label="Abweichung">
<?php echo ($diff >= 0 ? '+' : '-') . formatNumber(abs($diff)); ?>
<span class="symbol">(<?php echo ($diffPercent >= 0 ? '+' : '-') . formatNumber(abs($diffPercent)); ?> %)</span>
</td>
<td data-label="Signal">
<span class="signal signal-<?php echo strtolower($signal); ?>">
<?php echo $signal; ?>
</span>
<div class="symbol"><?php echo $reason; ?></div>
</td>
<td data-label="Update" class="symbol"><?php echo htmlspecialchars($coin['last_update']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
$signals = array_map(function ($coin) {
[$signal, $reason] = determineSignal($coin['price'], $coin['ma200']);
return sprintf('%s (%s): %s %s', $coin['name'], $coin['symbol'], $signal, $reason);
}, $altcoins);
?>
<section class="telegram-card">
<h2>Telegram Broadcast</h2>
<p>Kopiere die Zusammenfassung, um sie in deinem Signal-Channel zu posten, sobald eine MA200-Überschreitung
stattfindet.</p>
<pre><?php echo htmlspecialchars(implode("\n", $signals)); ?></pre>
</section>
</main>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
$bandId = isset($_GET['band_id']) ? (int) $_GET['band_id'] : 0;
$band = $bandId ? findBand($bandId) : null;
if (!$band) {
http_response_code(404);
echo 'Band nicht gefunden';
exit;
}
$user = currentUser();
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'band_id' => $bandId,
'user_id' => $user['id'] ?? null,
'event_date' => $_POST['event_date'] ?? '',
'location' => trim((string) $_POST['location'] ?? ''),
'budget' => (int) ($_POST['budget'] ?? 0),
'event_type' => trim((string) $_POST['event_type'] ?? ''),
'message' => trim((string) $_POST['message'] ?? ''),
];
if (!$data['event_date'] || !$data['location']) {
$error = 'Bitte Datum und Ort ausfüllen.';
} else {
createRequest($data);
$message = 'Anfrage gespeichert und an die Band gemeldet.';
sendEmail('info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch', 'Neue Anfrage', 'Neue Anfrage für ' . $band['name']);
}
}
$settings = settings();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anfrage <?= htmlspecialchars($band['name']) ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="band-detail.php?id=<?= $bandId ?>">← Zurück</a>
<h1>Anfrage an <?= htmlspecialchars($band['name']) ?></h1>
<p>PayPal Zahlungsabwicklung ist <?= $settings['paypal_enabled'] === '1' ? 'aktiviert' : 'optional' ?>, Service Fee: <?= htmlspecialchars($settings['service_fee']) ?>%.</p>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post">
<label>Event-Datum
<input type="date" class="form-control" name="event_date" required>
</label>
<label>Ort / Location
<input type="text" class="form-control" name="location" placeholder="Zürich, Kaufleuten" required>
</label>
<label>Event-Typ
<input type="text" class="form-control" name="event_type" placeholder="Hochzeit, Firmenfeier">
</label>
<label>Budget (CHF)
<input type="number" class="form-control" name="budget" placeholder="4500">
</label>
<label>Nachricht
<textarea class="form-control" name="message" rows="4"></textarea>
</label>
<button class="btn-primary">Anfrage senden</button>
</form>
</main>
</body>
</html>
+237
View File
@@ -0,0 +1,237 @@
:root {
--primary: #ffb703;
--secondary: #fb8500;
--dark: #0b0d17;
--darker: #090b13;
--light: #fefae0;
--gray: #8d99ae;
--gradient: linear-gradient(120deg, #ffb703, #fb5607, #ff006e);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Space Grotesk', 'Segoe UI', system-ui, sans-serif;
margin: 0;
background: radial-gradient(circle at 10% 20%, rgba(255, 183, 3, 0.25), rgba(9, 11, 19, 0.95)), var(--dark);
color: var(--light);
min-height: 100vh;
}
header {
padding: 40px 5vw 20px;
}
.hero {
background: var(--darker);
border-radius: 24px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
align-items: center;
}
.hero h1 {
font-size: clamp(2.2rem, 5vw, 3.6rem);
margin-bottom: 10px;
}
.hero p {
color: rgba(255, 255, 255, 0.8);
}
.badge-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.badge {
background: rgba(255, 255, 255, 0.08);
padding: 6px 18px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.9rem;
}
.search-panel {
margin-top: 30px;
background: rgba(0, 0, 0, 0.35);
border-radius: 18px;
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 15px;
}
.search-panel input,
.search-panel select {
width: 100%;
padding: 12px 16px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: var(--light);
}
.btn-primary {
padding: 14px 24px;
border-radius: 16px;
border: none;
font-weight: bold;
background: var(--gradient);
color: var(--dark);
cursor: pointer;
transition: transform 0.2s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
}
main {
padding: 30px 5vw 80px;
}
.band-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.band-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 20px;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, border-color 0.3s ease;
}
.band-card:hover {
transform: translateY(-4px);
border-color: var(--primary);
}
.band-card h3 {
margin-top: 0;
margin-bottom: 8px;
}
.price-tag {
font-size: 1.1rem;
color: var(--primary);
font-weight: bold;
}
.card-meta {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
}
footer {
background: rgba(0, 0, 0, 0.4);
padding: 30px 5vw;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 15px;
}
.cookie-banner {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--darker);
border-radius: 16px;
padding: 20px;
width: min(360px, calc(100% - 40px));
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.5);
display: none;
}
.cookie-banner.active {
display: block;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
text-align: left;
}
.form-control {
width: 100%;
padding: 12px 16px;
margin-bottom: 15px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: var(--light);
}
.alert {
padding: 15px 18px;
border-radius: 12px;
margin-bottom: 15px;
}
.alert-success {
background: rgba(56, 142, 60, 0.2);
border: 1px solid rgba(56, 142, 60, 0.4);
}
.alert-error {
background: rgba(213, 0, 0, 0.2);
border: 1px solid rgba(213, 0, 0, 0.4);
}
.band-detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
margin-top: 30px;
}
.gallery img {
width: 100%;
border-radius: 16px;
margin-bottom: 16px;
}
.badge-rating {
background: rgba(255, 183, 3, 0.2);
border-color: rgba(255, 183, 3, 0.5);
}
.admin-nav {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.admin-nav a {
color: var(--light);
text-decoration: none;
padding: 10px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.08);
}
@media (max-width: 768px) {
.hero,
footer {
padding: 24px;
}
}
+20
View File
@@ -0,0 +1,20 @@
const cookieBanner = document.querySelector('.cookie-banner');
const cookieAccept = document.querySelector('[data-cookie-accept]');
if (cookieBanner && cookieAccept) {
const consent = localStorage.getItem('gyb-cookie');
if (!consent) {
cookieBanner.classList.add('active');
}
cookieAccept.addEventListener('click', () => {
localStorage.setItem('gyb-cookie', 'accepted');
cookieBanner.classList.remove('active');
});
}
const filterForm = document.querySelector('[data-filter-form]');
if (filterForm) {
filterForm.addEventListener('input', () => {
filterForm.submit();
});
}
+1720
View File
File diff suppressed because it is too large Load Diff
+4045
View File
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$bandId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$band = $bandId ? findBand($bandId) : null;
if (!$band) {
http_response_code(404);
echo 'Band nicht gefunden';
exit;
}
$media = bandMedia($bandId);
$availability = bandAvailability($bandId);
$reviews = bandReviews($bandId);
$user = currentUser();
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user) {
if (isset($_POST['review'])) {
if (!eligibleForReview($bandId, (int) $user['id'])) {
$error = 'Für Bewertungen ist eine bestätigte Buchung nötig.';
} else {
$comment = trim((string) ($_POST['comment'] ?? ''));
if (mb_strlen($comment) > 200) {
$error = 'Maximal 200 Zeichen erlaubt.';
} else {
storeReview([
'band_id' => $bandId,
'user_id' => (int) $user['id'],
'rating' => (int) $_POST['rating'],
'comment' => $comment,
]);
$message = 'Danke! Deine Bewertung wartet auf Freigabe.';
}
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($band['name']) ?> <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Zurück</a>
<div class="hero" style="margin-top: 20px;">
<div>
<p class="badge"><?= htmlspecialchars($band['genre']) ?></p>
<h1><?= htmlspecialchars($band['name']) ?></h1>
<p><?= nl2br(htmlspecialchars($band['description'])) ?></p>
<div class="badge-list">
<span class="badge">Ort: <?= htmlspecialchars($band['city'] ?? '') ?></span>
<span class="badge">ab <?= formatPrice((int) $band['price']) ?></span>
</div>
<div class="badge-list">
<?php foreach (array_filter(array_map('trim', explode(',', (string) $band['style_tags']))) as $tag): ?>
<span class="badge">#<?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<p>
<a class="btn-primary" href="anfrage.php?band_id=<?= $bandId ?>">Verfügbarkeit anfragen</a>
</p>
</div>
<div>
<?php if (!empty($band['video_url'])): ?>
<iframe width="100%" height="280" src="<?= htmlspecialchars($band['video_url']) ?>" title="Bandvideo" allowfullscreen></iframe>
<?php endif; ?>
</div>
</div>
</header>
<main>
<section class="band-detail-grid">
<div class="gallery">
<h3>Galerie</h3>
<?php foreach ($media as $item): ?>
<?php if ($item['type'] === 'image'): ?>
<img src="<?= htmlspecialchars($item['url']) ?>" alt="Bandbild">
<?php endif; ?>
<?php endforeach; ?>
</div>
<div>
<h3>Verfügbarkeit</h3>
<table class="table">
<thead>
<tr><th>Datum</th><th>Status</th></tr>
</thead>
<tbody>
<?php foreach ($availability as $slot): ?>
<tr>
<td><?= htmlspecialchars((new DateTimeImmutable($slot['event_date']))->format('d.m.Y')) ?></td>
<td><?= htmlspecialchars(ucfirst($slot['status'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<section style="margin-top: 40px;">
<h3>Bewertungen</h3>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php foreach ($reviews as $review): ?>
<article class="band-card">
<p><strong><?= htmlspecialchars($review['author']) ?></strong> <?= (int) $review['rating'] ?> ★</p>
<p><?= htmlspecialchars($review['comment']) ?></p>
<p class="card-meta"><?= (new DateTimeImmutable($review['created_at']))->format('d.m.Y') ?></p>
</article>
<?php endforeach; ?>
<?php if (!$reviews): ?>
<p>Noch keine freigegebenen Bewertungen.</p>
<?php endif; ?>
</section>
<?php if ($user && $user['role'] === 'kunde'): ?>
<section style="margin-top: 40px;">
<h3>Eigene Bewertung</h3>
<form method="post">
<input type="hidden" name="review" value="1">
<label>Sterne
<select class="form-control" name="rating">
<?php for ($i = 5; $i >= 1; $i--): ?>
<option value="<?= $i ?>"><?= $i ?></option>
<?php endfor; ?>
</select>
</label>
<label>Kommentar (max. 200 Zeichen)
<textarea class="form-control" name="comment" maxlength="200"></textarea>
</label>
<button class="btn-primary">Bewertung senden</button>
</form>
</section>
<?php endif; ?>
</main>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'kunde',
city TEXT,
verified INTEGER NOT NULL DEFAULT 0,
verification_token TEXT,
created_at TEXT
);
CREATE TABLE IF NOT EXISTS bands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
city TEXT,
genre TEXT,
price INTEGER DEFAULT 0,
description TEXT,
status TEXT NOT NULL DEFAULT 'prüfung',
style_tags TEXT,
video_url TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS band_media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
type TEXT NOT NULL,
url TEXT NOT NULL,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS band_availability (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
event_date TEXT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
user_id INTEGER,
event_date TEXT,
location TEXT,
budget INTEGER,
event_type TEXT,
message TEXT,
status TEXT NOT NULL DEFAULT 'neu',
created_at TEXT,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
band_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
rating INTEGER NOT NULL,
comment TEXT,
status TEXT NOT NULL DEFAULT 'wartend',
created_at TEXT,
FOREIGN KEY(band_id) REFERENCES bands(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
+307
View File
@@ -0,0 +1,307 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/config.php';
$options = array_slice($argv, 1);
if (in_array('--fresh', $options, true) && file_exists(DB_PATH)) {
unlink(DB_PATH);
echo "Bestehende Datenbank entfernt wird neu initialisiert.\n";
}
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
if (!isset($_SERVER['REQUEST_METHOD'])) {
$_SERVER['REQUEST_METHOD'] = 'GET';
}
final class FunctionalTestRunner
{
private PDO $pdo;
private array $results = [];
private int $passed = 0;
private int $failed = 0;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function run(string $title, callable $test, bool $transactional = false): void
{
try {
if ($transactional) {
$this->pdo->beginTransaction();
}
$details = $test($this->pdo);
if ($transactional && $this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
$this->record('PASS', $title, $details);
} catch (Throwable $e) {
if ($transactional && $this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
$this->record('FAIL', $title, $e->getMessage());
}
}
private function record(string $status, string $title, ?string $details): void
{
$status === 'PASS' ? $this->passed++ : $this->failed++;
$this->results[] = [
'status' => $status,
'title' => $title,
'details' => $details ?? '',
];
}
public function summary(): void
{
foreach ($this->results as $result) {
$symbol = $result['status'] === 'PASS' ? '\u{2705}' : '\u{274C}';
echo sprintf("%s %s\n", $symbol, $result['title']);
if ($result['details'] !== '') {
echo sprintf(" %s\n", $result['details']);
}
}
echo str_repeat('-', 50) . "\n";
echo sprintf("Ergebnis: %d bestanden, %d fehlgeschlagen\n", $this->passed, $this->failed);
exit($this->failed === 0 ? 0 : 1);
}
}
function renderPage(string $file, array $get = [], array $post = []): string
{
$previousGet = $_GET ?? [];
$previousPost = $_POST ?? [];
$previousMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$_GET = $get;
$_POST = $post;
$_SERVER['REQUEST_METHOD'] = empty($post) ? 'GET' : 'POST';
ob_start();
include __DIR__ . '/' . ltrim($file, '/');
$output = ob_get_clean();
$_GET = $previousGet;
$_POST = $previousPost;
$_SERVER['REQUEST_METHOD'] = $previousMethod;
return $output;
}
function restartSession(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
$pdo = db();
$runner = new FunctionalTestRunner($pdo);
$runner->run('Storage-Verzeichnis vorhanden', function () {
if (!is_dir(__DIR__ . '/storage')) {
throw new RuntimeException('Ordner storage fehlt.');
}
return 'Pfad: ' . realpath(__DIR__ . '/storage');
});
$runner->run('Datenbank initialisiert', function (PDO $pdo) {
if (!file_exists(DB_PATH)) {
throw new RuntimeException('database.sqlite wurde nicht erstellt.');
}
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")
->fetchAll(PDO::FETCH_COLUMN);
$required = ['users', 'bands', 'requests', 'reviews', 'settings'];
foreach ($required as $table) {
if (!in_array($table, $tables, true)) {
throw new RuntimeException('Tabelle ' . $table . ' fehlt.');
}
}
return 'Tabellen gefunden: ' . implode(', ', $required);
});
$runner->run('Seed-Daten verfügbar', function (PDO $pdo) {
$users = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
$bands = (int) $pdo->query('SELECT COUNT(*) FROM bands')->fetchColumn();
if ($users < 3 || $bands < 2) {
throw new RuntimeException('Seed-Daten unvollständig.');
}
return sprintf('Users: %d, Bands: %d', $users, $bands);
});
$runner->run('Login / Logout Workflow', function () {
if (!login('david@example.com', 'secret123')) {
throw new RuntimeException('Login schlug fehl.');
}
$user = currentUser();
if (!$user || $user['role'] !== 'kunde') {
throw new RuntimeException('Session liefert keinen Kunden.');
}
logout();
restartSession();
if (currentUser()) {
throw new RuntimeException('Logout hat Session nicht geleert.');
}
return 'Login erfolgreich für ' . $user['name'];
});
$runner->run('Band-Filter & Durchschnitt', function () {
$bands = allBands(['genre' => 'Funk']);
if (!$bands) {
throw new RuntimeException('Filter lieferte keine Band.');
}
$rating = averageRating((int) $bands[0]['id']);
if ($rating === null) {
throw new RuntimeException('Keine Bewertung vorhanden.');
}
return sprintf('%d Bands, Ø Bewertung %.1f★', count($bands), $rating);
});
$runner->run('Medien & Verfügbarkeiten geladen', function () {
$media = bandMedia(1);
$availability = bandAvailability(1);
$reviews = bandReviews(1);
if (!$media || !$availability || !$reviews) {
throw new RuntimeException('Band 1 hat unvollständige Daten.');
}
return sprintf('Medien: %d, Slots: %d, Reviews: %d', count($media), count($availability), count($reviews));
});
$runner->run('Anfrage speichern (Transaktion)', function (PDO $pdo) {
$before = (int) $pdo->query('SELECT COUNT(*) FROM requests')->fetchColumn();
createRequest([
'band_id' => 1,
'user_id' => 3,
'event_date' => (new DateTimeImmutable('+60 days'))->format('Y-m-d'),
'location' => 'Teststadt',
'budget' => 4500,
'event_type' => 'Testevent',
'message' => 'Funktionstest Anfrage',
]);
$after = (int) $pdo->query('SELECT COUNT(*) FROM requests')->fetchColumn();
if ($after !== $before + 1) {
throw new RuntimeException('Anfrage wurde nicht gespeichert.');
}
return 'Requests gesamt (temporär): ' . $after;
}, true);
$runner->run('Bewertungen speichern & Eligibility', function (PDO $pdo) {
if (!eligibleForReview(1, 3)) {
throw new RuntimeException('User 3 sollte berechtigt sein.');
}
$before = (int) $pdo->query('SELECT COUNT(*) FROM reviews')->fetchColumn();
storeReview([
'band_id' => 1,
'user_id' => 3,
'rating' => 4,
'comment' => 'Testkommentar',
]);
$after = (int) $pdo->query('SELECT COUNT(*) FROM reviews')->fetchColumn();
if ($after !== $before + 1) {
throw new RuntimeException('Review wurde nicht gespeichert.');
}
return 'Reviews gesamt (temporär): ' . $after;
}, true);
$runner->run('Einstellungen lesen & aktualisieren', function () {
$current = settings();
$originalFee = $current['service_fee'] ?? '0';
updateSetting('service_fee', '12');
$updated = settings();
if (($updated['service_fee'] ?? null) !== '12') {
throw new RuntimeException('Service Fee konnte nicht aktualisiert werden.');
}
updateSetting('service_fee', $originalFee);
return 'Service Fee temporär auf 12 gesetzt.';
}, true);
$runner->run('Moderations-Aktionen', function (PDO $pdo) {
changeBandStatus(1, 'prüfung');
$status = $pdo->query('SELECT status FROM bands WHERE id = 1')->fetchColumn();
if ($status !== 'prüfung') {
throw new RuntimeException('Bandstatus änderte sich nicht.');
}
changeReviewStatus(1, 'gesperrt');
$reviewStatus = $pdo->query('SELECT status FROM reviews WHERE id = 1')->fetchColumn();
if ($reviewStatus !== 'gesperrt') {
throw new RuntimeException('Reviewstatus änderte sich nicht.');
}
return 'Statusänderungen durchgeführt.';
}, true);
$runner->run('Registrierung legt Band an', function (PDO $pdo) {
$email = 'tester+' . uniqid('', true) . '@example.com';
$result = register([
'name' => 'Functional Tester',
'email' => $email,
'password' => 'secret123',
'role' => 'band',
'city' => 'Testingen',
'band_name' => 'QA Ensemble',
'genre' => 'QA Funk',
]);
if (empty($result['token']) || strlen($result['token']) < 20) {
throw new RuntimeException('Verifikationstoken fehlt.');
}
$user = $pdo->prepare('SELECT id, role FROM users WHERE email = :email');
$user->execute([':email' => $email]);
$userRow = $user->fetch(PDO::FETCH_ASSOC);
if (!$userRow || $userRow['role'] !== 'band') {
throw new RuntimeException('User wurde nicht gespeichert.');
}
$band = $pdo->prepare('SELECT status FROM bands WHERE user_id = :id');
$band->execute([':id' => $userRow['id']]);
$bandRow = $band->fetch(PDO::FETCH_ASSOC);
if (!$bandRow || $bandRow['status'] !== 'prüfung') {
throw new RuntimeException('Bandprofil wurde nicht angelegt.');
}
return 'Token erstellt und Bandstatus "prüfung" bestätigt.';
}, true);
$runner->run('Startseite rendert fehlerfrei', function () {
$html = renderPage('index.php');
if (strpos($html, 'Aktive Bands') === false) {
throw new RuntimeException('Indexseite liefert keinen Inhalt.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('Band-Detailseite rendert', function () {
$html = renderPage('band-detail.php', ['id' => 1]);
if (strpos($html, 'Verfügbarkeit') === false) {
throw new RuntimeException('Band-Detailseite unvollständig.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('Anfrageformular rendert', function () {
$html = renderPage('anfrage.php', ['band_id' => 1]);
if (strpos($html, 'Anfrage an') === false) {
throw new RuntimeException('Anfrageformular fehlgeschlagen.');
}
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
$runner->run('E-Mail Logging (kein Versand)', function () {
$logDir = __DIR__ . '/storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
$logFile = $logDir . '/mail.log';
$before = file_exists($logFile) ? filesize($logFile) : 0;
sendEmail('qa@example.com', 'Functional Test', 'Nur Logeintrag kein Versand.');
$after = filesize($logFile);
if ($after <= $before) {
throw new RuntimeException('Mail-Log wurde nicht aktualisiert.');
}
return 'Logeintrag ergänzt, Versand erfolgt nur als Datei.';
});
$runner->summary();
+93
View File
@@ -0,0 +1,93 @@
<?php
require_once __DIR__ . '/functions.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function currentUser(): ?array
{
if (empty($_SESSION['user_id'])) {
return null;
}
static $user;
if ($user) {
return $user;
}
$stmt = db()->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
return $user;
}
function login(string $email, string $password): bool
{
$stmt = db()->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password'])) {
return false;
}
if ((int) $user['verified'] !== 1) {
throw new RuntimeException('Bitte verifiziere zuerst deine E-Mail.');
}
$_SESSION['user_id'] = $user['id'];
return true;
}
function logout(): void
{
session_destroy();
}
function register(array $data): array
{
$token = bin2hex(random_bytes(16));
$stmt = db()->prepare('INSERT INTO users (name, email, password, role, city, verified, verification_token, created_at)
VALUES (:name, :email, :password, :role, :city, 0, :token, :created)');
$stmt->execute([
':name' => $data['name'],
':email' => $data['email'],
':password' => password_hash($data['password'], PASSWORD_DEFAULT),
':role' => $data['role'],
':city' => $data['city'] ?? null,
':token' => $token,
':created' => (new DateTimeImmutable())->format('c'),
]);
if ($data['role'] === 'band') {
$band = db()->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status)
VALUES (:user_id, :name, :city, :genre, :price, :description, :status)');
$band->execute([
':user_id' => (int) db()->lastInsertId(),
':name' => $data['band_name'] ?? 'Neue Band',
':city' => $data['city'] ?? '',
':genre' => $data['genre'] ?? '',
':price' => 0,
':description' => 'Bitte Profil ergänzen.',
':status' => 'prüfung',
]);
}
return ['token' => $token];
}
function requireLogin(): void
{
if (!currentUser()) {
header('Location: login.php');
exit;
}
}
function requireAdmin(): void
{
$user = currentUser();
if (!$user || $user['role'] !== 'admin') {
http_response_code(403);
echo 'Keine Berechtigung';
exit;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
const SITE_NAME = 'GetYourBand';
const DB_PATH = __DIR__ . '/../storage/database.sqlite';
const SUPPORT_EMAIL = 'support@getyourband.ch';
const BASE_URL = '';
const COOKIE_NAME = 'gyb_consent';
if (!is_dir(__DIR__ . '/../storage')) {
mkdir(__DIR__ . '/../storage', 0775, true);
}
+144
View File
@@ -0,0 +1,144 @@
<?php
require_once __DIR__ . '/config.php';
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$dsn = 'sqlite:' . DB_PATH;
$pdo = new PDO($dsn);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
initializeDatabase($pdo);
return $pdo;
}
function initializeDatabase(PDO $pdo): void
{
$schema = file_get_contents(__DIR__ . '/../database.sql');
$pdo->exec($schema);
seedData($pdo);
}
function seedData(PDO $pdo): void
{
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
if ($count > 0) {
return;
}
$now = (new DateTimeImmutable())->format('c');
$password = password_hash('secret123', PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (name, email, password, role, verified, verification_token, created_at)
VALUES (:name, :email, :password, :role, :verified, :token, :created)');
$stmt->execute([
':name' => 'Admin',
':email' => 'admin@getyourband.ch',
':password' => $password,
':role' => 'admin',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$stmt->execute([
':name' => 'Maya Keller',
':email' => 'maya@getyourband.ch',
':password' => $password,
':role' => 'band',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$stmt->execute([
':name' => 'David Graf',
':email' => 'david@example.com',
':password' => $password,
':role' => 'kunde',
':verified' => 1,
':token' => null,
':created' => $now,
]);
$bands = [
[
'user_id' => 2,
'name' => 'Neon Groove Kollektiv',
'city' => 'Zürich',
'genre' => 'Funk / Soul',
'price' => 4200,
'description' => '7-köpfige Funk- und Soulband mit knalligem Brass-Sound und interaktiver Show.',
'status' => 'aktiv',
'style_tags' => 'Funk,Retro,Showband',
'video_url' => 'https://www.youtube.com/embed/dQw4w9WgXcQ',
],
[
'user_id' => null,
'name' => 'Sonnenblitz Orchester',
'city' => 'Bern',
'genre' => 'Pop / Party',
'price' => 3700,
'description' => 'Party-Coverband mit LED-Lichtshow und zweistimmigem Gesang.',
'status' => 'aktiv',
'style_tags' => 'Pop,Party,LED',
'video_url' => 'https://www.youtube.com/embed/5NV6Rdv1a3I',
],
];
$bandStmt = $pdo->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status, style_tags, video_url)
VALUES (:user_id, :name, :city, :genre, :price, :description, :status, :style_tags, :video_url)');
foreach ($bands as $band) {
$bandStmt->execute([
':user_id' => $band['user_id'],
':name' => $band['name'],
':city' => $band['city'],
':genre' => $band['genre'],
':price' => $band['price'],
':description' => $band['description'],
':status' => $band['status'],
':style_tags' => $band['style_tags'],
':video_url' => $band['video_url'],
]);
$bandId = (int) $pdo->lastInsertId();
$mediaStmt = $pdo->prepare('INSERT INTO band_media (band_id, type, url) VALUES (:band_id, :type, :url)');
$mediaStmt->execute([':band_id' => $bandId, ':type' => 'image', ':url' => 'https://images.unsplash.com/photo-1507878866276-a947ef722fee']);
$mediaStmt->execute([':band_id' => $bandId, ':type' => 'image', ':url' => 'https://images.unsplash.com/photo-1489515217757-5fd1be406fef']);
$availStmt = $pdo->prepare('INSERT INTO band_availability (band_id, event_date, status) VALUES (:band_id, :event_date, :status)');
for ($i = 0; $i < 4; $i++) {
$availStmt->execute([
':band_id' => $bandId,
':event_date' => (new DateTimeImmutable('+' . ($i + 1) * 7 . ' days'))->format('Y-m-d'),
':status' => $i % 2 === 0 ? 'frei' : 'option',
]);
}
}
$pdo->exec("INSERT INTO settings (key, value) VALUES ('paypal_enabled', '0'), ('service_fee', '8')");
$requestStmt = $pdo->prepare('INSERT INTO requests (band_id, user_id, event_date, location, budget, event_type, message, status, created_at)
VALUES (:band_id, :user_id, :event_date, :location, :budget, :event_type, :message, :status, :created)');
$requestStmt->execute([
':band_id' => 1,
':user_id' => 3,
':event_date' => (new DateTimeImmutable('+30 days'))->format('Y-m-d'),
':location' => 'Basel',
':budget' => 5000,
':event_type' => 'Firmenfeier',
':message' => 'Wir suchen einen funky Act für die Sommerparty.',
':status' => 'bestätigt',
':created' => $now,
]);
$pdo->exec("INSERT INTO reviews (band_id, user_id, rating, comment, status, created_at) VALUES (1, 3, 5, 'Mega Stimmung und super Show!', 'freigegeben', datetime('now'))");
}
+10
View File
@@ -0,0 +1,10 @@
<?php
function sendEmail(string $to, string $subject, string $message): void
{
$logDir = __DIR__ . '/../storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
$entry = sprintf("%s\nTo: %s\nSubject: %s\n%s\n---\n", date('c'), $to, $subject, $message);
file_put_contents($logDir . '/mail.log', $entry, FILE_APPEND);
}
+159
View File
@@ -0,0 +1,159 @@
<?php
require_once __DIR__ . '/database.php';
function allBands(array $filters = []): array
{
$pdo = db();
$where = ['status = :status'];
$params = [':status' => 'aktiv'];
if (!empty($filters['genre'])) {
$where[] = 'genre LIKE :genre';
$params[':genre'] = '%' . $filters['genre'] . '%';
}
if (!empty($filters['city'])) {
$where[] = 'city LIKE :city';
$params[':city'] = '%' . $filters['city'] . '%';
}
$sql = 'SELECT * FROM bands WHERE ' . implode(' AND ', $where) . ' ORDER BY name';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function findBand(int $id): ?array
{
$stmt = db()->prepare('SELECT * FROM bands WHERE id = :id');
$stmt->execute([':id' => $id]);
$band = $stmt->fetch(PDO::FETCH_ASSOC);
return $band ?: null;
}
function bandMedia(int $bandId): array
{
$stmt = db()->prepare('SELECT * FROM band_media WHERE band_id = :id');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function bandAvailability(int $bandId): array
{
$stmt = db()->prepare('SELECT * FROM band_availability WHERE band_id = :id ORDER BY event_date');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function bandReviews(int $bandId): array
{
$stmt = db()->prepare('SELECT r.*, u.name AS author
FROM reviews r
JOIN users u ON u.id = r.user_id
WHERE r.band_id = :id AND r.status = "freigegeben"
ORDER BY r.created_at DESC');
$stmt->execute([':id' => $bandId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function averageRating(int $bandId): ?float
{
$stmt = db()->prepare('SELECT AVG(rating) FROM reviews WHERE band_id = :id AND status = "freigegeben"');
$stmt->execute([':id' => $bandId]);
$value = $stmt->fetchColumn();
return $value ? round((float) $value, 1) : null;
}
function formatPrice(int $amount): string
{
return number_format($amount, 0, ',', '.') . ' CHF';
}
function createRequest(array $data): void
{
$stmt = db()->prepare('INSERT INTO requests (band_id, user_id, event_date, location, budget, event_type, message, status, created_at)
VALUES (:band_id, :user_id, :event_date, :location, :budget, :event_type, :message, :status, :created_at)');
$stmt->execute([
':band_id' => $data['band_id'],
':user_id' => $data['user_id'],
':event_date' => $data['event_date'],
':location' => $data['location'],
':budget' => $data['budget'],
':event_type' => $data['event_type'],
':message' => $data['message'],
':status' => 'neu',
':created_at' => (new DateTimeImmutable())->format('c'),
]);
}
function userRequests(int $userId): array
{
$stmt = db()->prepare('SELECT * FROM requests WHERE user_id = :id ORDER BY created_at DESC');
$stmt->execute([':id' => $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function storeReview(array $data): void
{
$stmt = db()->prepare('INSERT INTO reviews (band_id, user_id, rating, comment, status, created_at)
VALUES (:band_id, :user_id, :rating, :comment, :status, :created_at)');
$stmt->execute([
':band_id' => $data['band_id'],
':user_id' => $data['user_id'],
':rating' => $data['rating'],
':comment' => $data['comment'],
':status' => 'wartend',
':created_at' => (new DateTimeImmutable())->format('c'),
]);
}
function eligibleForReview(int $bandId, int $userId): bool
{
$stmt = db()->prepare('SELECT COUNT(*) FROM requests WHERE band_id = :band AND user_id = :user AND status = "bestätigt"');
$stmt->execute([':band' => $bandId, ':user' => $userId]);
return (int) $stmt->fetchColumn() > 0;
}
function settings(): array
{
$stmt = db()->query('SELECT key, value FROM settings');
$data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
return $data ?: ['paypal_enabled' => '0', 'service_fee' => '0'];
}
function updateSetting(string $key, string $value): void
{
$stmt = db()->prepare('INSERT INTO settings (key, value) VALUES (:key, :value)
ON CONFLICT(key) DO UPDATE SET value = excluded.value');
$stmt->execute([':key' => $key, ':value' => $value]);
}
function moderationItems(string $type): array
{
$pdo = db();
if ($type === 'bands') {
return $pdo->query('SELECT * FROM bands WHERE status != "aktiv"')->fetchAll(PDO::FETCH_ASSOC);
}
if ($type === 'reviews') {
return $pdo->query('SELECT r.*, b.name AS band_name, u.name AS author
FROM reviews r
JOIN bands b ON b.id = r.band_id
JOIN users u ON u.id = r.user_id
WHERE r.status = "wartend"')->fetchAll(PDO::FETCH_ASSOC);
}
return [];
}
function changeBandStatus(int $bandId, string $status): void
{
$stmt = db()->prepare('UPDATE bands SET status = :status WHERE id = :id');
$stmt->execute([':status' => $status, ':id' => $bandId]);
}
function changeReviewStatus(int $reviewId, string $status): void
{
$stmt = db()->prepare('UPDATE reviews SET status = :status WHERE id = :id');
$stmt->execute([':status' => $status, ':id' => $reviewId]);
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
$filters = [
'genre' => $_GET['genre'] ?? '',
'city' => $_GET['city'] ?? '',
];
$bands = allBands($filters);
$settings = settings();
$user = currentUser();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= SITE_NAME ?> Bands buchen</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
<header>
<nav class="badge-list" style="justify-content: flex-end;">
<?php if ($user): ?>
<span class="badge">Hallo <?= htmlspecialchars($user['name']) ?></span>
<a class="badge" href="profil.php">Mein Profil</a>
<?php if ($user['role'] === 'admin'): ?>
<a class="badge" href="admin/dashboard.php">Admin</a>
<?php endif; ?>
<a class="badge" href="login.php?action=logout">Logout</a>
<?php else: ?>
<a class="badge" href="login.php">Login / Registrieren</a>
<?php endif; ?>
</nav>
<section class="hero">
<div>
<p class="badge">Schritt 3 · Frontend Release</p>
<h1>Finde deine <span style="color: var(--primary);">Funky Liveband</span></h1>
<p>GetYourBand bringt verifizierte Live-Acts mit Veranstalter:innen in der ganzen Schweiz zusammen. Mit Bewertungen,
moderner Suche und aktivierbarer Vermittlungsgebühr.</p>
<div class="badge-list">
<span class="badge">Bewertungen geprüft</span>
<span class="badge badge-rating">PayPal <?= $settings['paypal_enabled'] === '1' ? 'aktiv' : 'optional' ?></span>
<span class="badge">Service Fee <?= htmlspecialchars($settings['service_fee']) ?>%</span>
</div>
</div>
<form class="search-panel" method="get" data-filter-form>
<div>
<label for="genre">Stil / Genre</label>
<input type="text" id="genre" name="genre" value="<?= htmlspecialchars($filters['genre']) ?>" placeholder="Funk, Party, Jazz">
</div>
<div>
<label for="city">Ort / PLZ</label>
<input type="text" id="city" name="city" value="<?= htmlspecialchars($filters['city']) ?>" placeholder="Zürich, Basel">
</div>
<div>
<label>&nbsp;</label>
<button type="submit" class="btn-primary">Filtern</button>
</div>
</form>
</section>
</header>
<main>
<h2>Aktive Bands (<?= count($bands) ?>)</h2>
<section class="band-grid">
<?php foreach ($bands as $band): $rating = averageRating((int) $band['id']); ?>
<article class="band-card">
<p class="badge"><?= htmlspecialchars($band['genre']) ?></p>
<h3><?= htmlspecialchars($band['name']) ?></h3>
<p class="card-meta">Standort: <?= htmlspecialchars($band['city'] ?? '') ?></p>
<p><?= htmlspecialchars($band['description']) ?></p>
<p class="price-tag">ab <?= formatPrice((int) $band['price']) ?></p>
<?php if ($rating): ?>
<p class="card-meta">Bewertung: <?= $rating ?> ★</p>
<?php endif; ?>
<div class="badge-list">
<?php foreach (array_filter(array_map('trim', explode(',', (string) $band['style_tags']))) as $tag): ?>
<span class="badge">#<?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<p>
<a class="btn-primary" href="band-detail.php?id=<?= (int) $band['id'] ?>">Band ansehen</a>
</p>
</article>
<?php endforeach; ?>
<?php if (!$bands): ?>
<p>Keine Bands gefunden ändere deine Filter.</p>
<?php endif; ?>
</section>
</main>
<footer>
<div>
<strong>Legal</strong><br>
<a href="#">Datenschutz</a> · <a href="#">AGB</a>
</div>
<div>
<strong>Kontakt</strong><br>
support@getyourband.ch
</div>
</footer>
<div class="cookie-banner">
<p>Wir verwenden Cookies für Performance-Analysen. Mit Klick auf "Okay" akzeptierst du das.</p>
<button class="btn-primary" data-cookie-accept>Okay!</button>
</div>
<script src="assets/js/app.js" defer></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
initial
+115
View File
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/email.php';
$action = $_GET['action'] ?? '';
if ($action === 'logout') {
logout();
header('Location: index.php');
exit;
}
$error = '';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['login'])) {
try {
if (!login($_POST['email'], $_POST['password'])) {
$error = 'Login fehlgeschlagen.';
} else {
header('Location: index.php');
exit;
}
} catch (RuntimeException $ex) {
$error = $ex->getMessage();
}
} elseif (isset($_POST['register'])) {
if ($_POST['password'] !== $_POST['password_confirm']) {
$error = 'Passwörter stimmen nicht überein.';
} else {
$result = register([
'name' => trim((string) $_POST['name']),
'email' => trim((string) $_POST['email']),
'password' => $_POST['password'],
'role' => $_POST['role'],
'city' => trim((string) $_POST['city']),
'band_name' => $_POST['band_name'] ?? null,
'genre' => $_POST['genre'] ?? null,
]);
$verificationLink = BASE_URL . '/verify-email.php?token=' . urlencode($result['token']);
sendEmail($_POST['email'], 'E-Mail bestätigen', 'Bitte bestätige dein Konto: ' . $verificationLink);
$message = 'Check deine Inbox wir haben dir den Verifizierungslink geschickt: ' . htmlspecialchars($verificationLink);
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login / Registrierung <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Zurück</a>
<h1>Login / Registrieren</h1>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= $message ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<section class="band-detail-grid">
<div>
<h2>Login</h2>
<form method="post">
<input type="hidden" name="login" value="1">
<label>E-Mail
<input class="form-control" type="email" name="email" required>
</label>
<label>Passwort
<input class="form-control" type="password" name="password" required>
</label>
<button class="btn-primary">Einloggen</button>
</form>
</div>
<div>
<h2>Registrierung</h2>
<form method="post">
<input type="hidden" name="register" value="1">
<label>Name
<input class="form-control" type="text" name="name" required>
</label>
<label>E-Mail
<input class="form-control" type="email" name="email" required>
</label>
<label>Ort
<input class="form-control" type="text" name="city">
</label>
<label>Rolle
<select class="form-control" name="role">
<option value="kunde">Veranstalter:in</option>
<option value="band">Band</option>
</select>
</label>
<label>Bandname (falls Band)
<input class="form-control" type="text" name="band_name">
</label>
<label>Genre
<input class="form-control" type="text" name="genre">
</label>
<label>Passwort
<input class="form-control" type="password" name="password" required>
</label>
<label>Passwort wiederholen
<input class="form-control" type="password" name="password_confirm" required>
</label>
<button class="btn-primary">Account anlegen</button>
</form>
</div>
</section>
</main>
</body>
</html>
+90
View File
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth.php';
requireLogin();
$user = currentUser();
$band = null;
$message = '';
if ($user['role'] === 'band') {
$stmt = db()->prepare('SELECT * FROM bands WHERE user_id = :id');
$stmt->execute([':id' => $user['id']]);
$band = $stmt->fetch(PDO::FETCH_ASSOC);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = db()->prepare('UPDATE bands SET name = :name, city = :city, genre = :genre, price = :price, description = :description, style_tags = :tags WHERE id = :id');
$stmt->execute([
':name' => $_POST['name'],
':city' => $_POST['city'],
':genre' => $_POST['genre'],
':price' => (int) $_POST['price'],
':description' => $_POST['description'],
':tags' => $_POST['style_tags'],
':id' => $band['id'],
]);
$message = 'Bandprofil aktualisiert (wartet ggf. auf Freigabe).';
$band = findBand((int) $band['id']);
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mein Bereich <?= SITE_NAME ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<a class="badge" href="index.php">← Startseite</a>
<h1>Hallo <?= htmlspecialchars($user['name']) ?></h1>
<p>Rolle: <?= htmlspecialchars($user['role']) ?></p>
</header>
<main>
<?php if ($message): ?><div class="alert alert-success"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if ($band): ?>
<h2>Bandprofil</h2>
<form method="post">
<label>Bandname
<input class="form-control" name="name" value="<?= htmlspecialchars($band['name']) ?>">
</label>
<label>Ort
<input class="form-control" name="city" value="<?= htmlspecialchars($band['city']) ?>">
</label>
<label>Genre
<input class="form-control" name="genre" value="<?= htmlspecialchars($band['genre']) ?>">
</label>
<label>Tags
<input class="form-control" name="style_tags" value="<?= htmlspecialchars($band['style_tags']) ?>">
</label>
<label>Preis (CHF)
<input class="form-control" type="number" name="price" value="<?= (int) $band['price'] ?>">
</label>
<label>Beschreibung
<textarea class="form-control" name="description" rows="4"><?= htmlspecialchars($band['description']) ?></textarea>
</label>
<button class="btn-primary">Speichern</button>
</form>
<?php else: ?>
<p>Du hast noch kein Bandprofil angelegt.</p>
<?php endif; ?>
<?php if ($user['role'] === 'kunde'): ?>
<h2>Meine Anfragen</h2>
<table class="table">
<thead><tr><th>Band</th><th>Datum</th><th>Status</th></tr></thead>
<tbody>
<?php foreach (userRequests((int) $user['id']) as $request): $bandName = findBand((int) $request['band_id']); ?>
<tr>
<td><?= htmlspecialchars($bandName['name'] ?? 'Band #' . $request['band_id']) ?></td>
<td><?= htmlspecialchars($request['event_date']) ?></td>
<td><?= htmlspecialchars($request['status']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main>
</body>
</html>
+4429
View File
File diff suppressed because it is too large Load Diff
View File
+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>');
}
}
+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>
+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']);