Set up modern PHP MVC project structure for GetYourBand platform
- Implemented clean MVC architecture with Router, Controller, and Model base classes - Created database migrations for users, bands, bookings, reviews, and availability - Set up Tailwind CSS with yellow color scheme and modern design - Added Alpine.js for reactive JavaScript components - Configured Vite for asset building and hot module replacement - Created authentication and role-based middleware - Implemented helper functions and configuration system - Added comprehensive README with setup instructions - Configured Apache with proper rewrite rules and security headers - Set up Composer and npm package management with modern dependencies
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Band;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
$bandModel = new Band();
|
||||
|
||||
// Get top-rated bands
|
||||
$featuredBands = $bandModel->query(
|
||||
"SELECT * FROM bands
|
||||
WHERE is_approved = 1 AND is_active = 1
|
||||
ORDER BY average_rating DESC, total_reviews DESC
|
||||
LIMIT 6"
|
||||
);
|
||||
|
||||
$this->view('home', [
|
||||
'featuredBands' => $featuredBands,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Controller
|
||||
{
|
||||
protected function view(string $view, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
|
||||
$viewPath = __DIR__ . '/../Views/' . str_replace('.', '/', $view) . '.php';
|
||||
|
||||
if (!file_exists($viewPath)) {
|
||||
throw new \RuntimeException("View not found: {$view}");
|
||||
}
|
||||
|
||||
require_once $viewPath;
|
||||
}
|
||||
|
||||
protected function json($data, int $status = 200): void
|
||||
{
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function redirect(string $path): void
|
||||
{
|
||||
header("Location: {$path}");
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function back(): void
|
||||
{
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '/';
|
||||
$this->redirect($referer);
|
||||
}
|
||||
|
||||
protected function input(string $key, $default = null)
|
||||
{
|
||||
return $_POST[$key] ?? $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
protected function validate(array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($rules as $field => $fieldRules) {
|
||||
$value = $this->input($field);
|
||||
$fieldRules = explode('|', $fieldRules);
|
||||
|
||||
foreach ($fieldRules as $rule) {
|
||||
if ($rule === 'required' && empty($value)) {
|
||||
$errors[$field][] = ucfirst($field) . ' is required';
|
||||
}
|
||||
|
||||
if (str_starts_with($rule, 'min:')) {
|
||||
$min = (int) substr($rule, 4);
|
||||
if (strlen($value) < $min) {
|
||||
$errors[$field][] = ucfirst($field) . " must be at least {$min} characters";
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($rule, 'max:')) {
|
||||
$max = (int) substr($rule, 4);
|
||||
if (strlen($value) > $max) {
|
||||
$errors[$field][] = ucfirst($field) . " must not exceed {$max} characters";
|
||||
}
|
||||
}
|
||||
|
||||
if ($rule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field][] = ucfirst($field) . ' must be a valid email';
|
||||
}
|
||||
}
|
||||
|
||||
$data[$field] = $value;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$_SESSION['errors'] = $errors;
|
||||
$_SESSION['old'] = $data;
|
||||
$this->back();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function auth()
|
||||
{
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
protected function isAuthenticated(): bool
|
||||
{
|
||||
return isset($_SESSION['user']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Database\Database;
|
||||
use PDO;
|
||||
|
||||
abstract class Model
|
||||
{
|
||||
protected PDO $db;
|
||||
protected string $table;
|
||||
protected string $primaryKey = 'id';
|
||||
protected array $fillable = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connect();
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
$stmt = $this->db->query("SELECT * FROM {$this->table}");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function find(int $id): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1");
|
||||
$stmt->execute([$id]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public function where(string $column, $value): array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = ?");
|
||||
$stmt->execute([$value]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function first(string $column, $value): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = ? LIMIT 1");
|
||||
$stmt->execute([$value]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
$data = $this->filterFillable($data);
|
||||
$columns = implode(', ', array_keys($data));
|
||||
$placeholders = implode(', ', array_fill(0, count($data), '?'));
|
||||
|
||||
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute(array_values($data));
|
||||
|
||||
return (int) $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$data = $this->filterFillable($data);
|
||||
$set = implode(' = ?, ', array_keys($data)) . ' = ?';
|
||||
|
||||
$sql = "UPDATE {$this->table} SET {$set} WHERE {$this->primaryKey} = ?";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
return $stmt->execute([...array_values($data), $id]);
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
public function query(string $sql, array $params = []): array
|
||||
{
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function execute(string $sql, array $params = []): bool
|
||||
{
|
||||
$stmt = $this->db->prepare($sql);
|
||||
return $stmt->execute($params);
|
||||
}
|
||||
|
||||
protected function filterFillable(array $data): array
|
||||
{
|
||||
if (empty($this->fillable)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return array_intersect_key($data, array_flip($this->fillable));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $middlewareStack = [];
|
||||
|
||||
public function get(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
public function group(array $attributes, callable $callback): void
|
||||
{
|
||||
$previousMiddleware = $this->middlewareStack;
|
||||
|
||||
if (isset($attributes['middleware'])) {
|
||||
$this->middlewareStack = array_merge(
|
||||
$this->middlewareStack,
|
||||
(array) $attributes['middleware']
|
||||
);
|
||||
}
|
||||
|
||||
$callback($this);
|
||||
|
||||
$this->middlewareStack = $previousMiddleware;
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, $handler): void
|
||||
{
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'handler' => $handler,
|
||||
'middleware' => $this->middlewareStack,
|
||||
];
|
||||
}
|
||||
|
||||
public function dispatch(string $requestMethod, string $requestUri): void
|
||||
{
|
||||
$requestUri = parse_url($requestUri, PHP_URL_PATH);
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $requestMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->convertToPattern($route['path']);
|
||||
|
||||
if (preg_match($pattern, $requestUri, $matches)) {
|
||||
array_shift($matches); // Remove full match
|
||||
|
||||
// Execute middleware
|
||||
foreach ($route['middleware'] as $middleware) {
|
||||
$this->executeMiddleware($middleware);
|
||||
}
|
||||
|
||||
// Execute handler
|
||||
$this->executeHandler($route['handler'], $matches);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
http_response_code(404);
|
||||
echo "404 - Page Not Found";
|
||||
}
|
||||
|
||||
private function convertToPattern(string $path): string
|
||||
{
|
||||
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([^/]+)', $path);
|
||||
return '#^' . $pattern . '$#';
|
||||
}
|
||||
|
||||
private function executeMiddleware(string $middleware): void
|
||||
{
|
||||
$parts = explode(':', $middleware);
|
||||
$name = $parts[0];
|
||||
$params = $parts[1] ?? null;
|
||||
|
||||
$middlewareClass = "App\\Middleware\\" . ucfirst($name) . "Middleware";
|
||||
|
||||
if (!class_exists($middlewareClass)) {
|
||||
throw new \RuntimeException("Middleware not found: {$middlewareClass}");
|
||||
}
|
||||
|
||||
$instance = new $middlewareClass();
|
||||
$instance->handle($params);
|
||||
}
|
||||
|
||||
private function executeHandler($handler, array $params): void
|
||||
{
|
||||
if (is_array($handler)) {
|
||||
[$class, $method] = $handler;
|
||||
$controller = new $class();
|
||||
call_user_func_array([$controller, $method], $params);
|
||||
} elseif (is_callable($handler)) {
|
||||
call_user_func_array($handler, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
public function handle($params = null): void
|
||||
{
|
||||
if (!isset($_SESSION['user'])) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
public function handle($role = null): void
|
||||
{
|
||||
if (!isset($_SESSION['user'])) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($role && $_SESSION['user']['role'] !== $role) {
|
||||
http_response_code(403);
|
||||
die('403 - Forbidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class Band extends Model
|
||||
{
|
||||
protected string $table = 'bands';
|
||||
|
||||
protected array $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'genre',
|
||||
'location',
|
||||
'postal_code',
|
||||
'price_min',
|
||||
'price_max',
|
||||
'member_count',
|
||||
'phone',
|
||||
'website',
|
||||
'facebook',
|
||||
'instagram',
|
||||
'youtube',
|
||||
'profile_image',
|
||||
'cover_image',
|
||||
'is_approved',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function findBySlug(string $slug): ?array
|
||||
{
|
||||
return $this->first('slug', $slug);
|
||||
}
|
||||
|
||||
public function search(array $filters): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table} WHERE is_approved = 1 AND is_active = 1";
|
||||
$params = [];
|
||||
|
||||
if (!empty($filters['genre'])) {
|
||||
$sql .= " AND genre = ?";
|
||||
$params[] = $filters['genre'];
|
||||
}
|
||||
|
||||
if (!empty($filters['location'])) {
|
||||
$sql .= " AND (location LIKE ? OR postal_code LIKE ?)";
|
||||
$params[] = "%{$filters['location']}%";
|
||||
$params[] = "%{$filters['location']}%";
|
||||
}
|
||||
|
||||
if (!empty($filters['price_max'])) {
|
||||
$sql .= " AND price_min <= ?";
|
||||
$params[] = $filters['price_max'];
|
||||
}
|
||||
|
||||
if (!empty($filters['q'])) {
|
||||
$sql .= " AND MATCH(name, description, genre) AGAINST (? IN NATURAL LANGUAGE MODE)";
|
||||
$params[] = $filters['q'];
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY average_rating DESC, total_reviews DESC";
|
||||
|
||||
return $this->query($sql, $params);
|
||||
}
|
||||
|
||||
public function incrementViews(int $id): bool
|
||||
{
|
||||
return $this->execute(
|
||||
"UPDATE {$this->table} SET view_count = view_count + 1 WHERE id = ?",
|
||||
[$id]
|
||||
);
|
||||
}
|
||||
|
||||
public function updateRating(int $bandId): void
|
||||
{
|
||||
$sql = "
|
||||
UPDATE bands
|
||||
SET average_rating = (
|
||||
SELECT AVG(rating)
|
||||
FROM reviews
|
||||
WHERE band_id = ? AND is_approved = 1
|
||||
),
|
||||
total_reviews = (
|
||||
SELECT COUNT(*)
|
||||
FROM reviews
|
||||
WHERE band_id = ? AND is_approved = 1
|
||||
)
|
||||
WHERE id = ?
|
||||
";
|
||||
|
||||
$this->execute($sql, [$bandId, $bandId, $bandId]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected string $table = 'users';
|
||||
|
||||
protected array $fillable = [
|
||||
'email',
|
||||
'password',
|
||||
'name',
|
||||
'role',
|
||||
'verification_token',
|
||||
'email_verified_at',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function findByEmail(string $email): ?array
|
||||
{
|
||||
return $this->first('email', $email);
|
||||
}
|
||||
|
||||
public function verifyEmail(string $token): bool
|
||||
{
|
||||
$user = $this->first('verification_token', $token);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->update($user['id'], [
|
||||
'email_verified_at' => date('Y-m-d H:i:s'),
|
||||
'verification_token' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function hashPassword(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
public static function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="bg-gradient-to-br from-primary-500 via-accent-500 to-primary-600 text-white py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-display font-bold mb-6 text-balance">
|
||||
Finde die perfekte Band für dein Event
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-8 text-primary-50 max-w-3xl mx-auto text-balance">
|
||||
Professionelle Live-Bands in der ganzen Schweiz. Einfach buchen, perfekt performen.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/bands" class="btn bg-white text-primary-600 hover:bg-gray-100 text-lg px-8 py-3">
|
||||
Bands entdecken
|
||||
</a>
|
||||
<a href="/register" class="btn bg-primary-700 text-white hover:bg-primary-800 text-lg px-8 py-3">
|
||||
Als Band registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Search Section -->
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-gray-50 rounded-2xl shadow-lg p-8" x-data="searchBands">
|
||||
<h2 class="text-3xl font-display font-bold text-center mb-8">Suche deine Band</h2>
|
||||
|
||||
<form @submit.prevent="search" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
x-model="query"
|
||||
placeholder="Band, Genre, Stil..."
|
||||
class="input-field"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
x-model="filters.location"
|
||||
placeholder="Ort oder PLZ"
|
||||
class="input-field"
|
||||
>
|
||||
<select x-model="filters.genre" class="input-field">
|
||||
<option value="">Alle Genres</option>
|
||||
<option value="Rock">Rock</option>
|
||||
<option value="Pop">Pop</option>
|
||||
<option value="Jazz">Jazz</option>
|
||||
<option value="Blues">Blues</option>
|
||||
<option value="Funk">Funk</option>
|
||||
<option value="Cover">Cover</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Suchen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Bands -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-4xl font-display font-bold text-center mb-12">Top bewertete Bands</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<?php foreach ($featuredBands ?? [] as $band): ?>
|
||||
<div class="card group hover:scale-105 transition-transform">
|
||||
<div class="aspect-video bg-gray-200 rounded-lg mb-4 overflow-hidden">
|
||||
<?php if ($band['cover_image']): ?>
|
||||
<img src="<?= $band['cover_image'] ?>" alt="<?= $band['name'] ?>" class="w-full h-full object-cover">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="text-xl font-bold text-gray-900"><?= htmlspecialchars($band['name']) ?></h3>
|
||||
<span class="badge badge-yellow"><?= htmlspecialchars($band['genre']) ?></span>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4 line-clamp-2"><?= htmlspecialchars($band['description']) ?></p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="text-yellow-500 mr-1">⭐</span>
|
||||
<span class="font-semibold"><?= number_format($band['average_rating'], 1) ?></span>
|
||||
<span class="text-gray-500 text-sm ml-1">(<?= $band['total_reviews'] ?>)</span>
|
||||
</div>
|
||||
<a href="/bands/<?= $band['slug'] ?>" class="text-primary-600 hover:text-primary-700 font-medium">
|
||||
Details →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How it Works -->
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-4xl font-display font-bold text-center mb-12">So funktioniert's</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span class="text-3xl">🔍</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2">1. Suchen</h3>
|
||||
<p class="text-gray-600">Finde die perfekte Band für dein Event mit unseren Suchfiltern.</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span class="text-3xl">📧</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2">2. Anfragen</h3>
|
||||
<p class="text-gray-600">Sende eine unverbindliche Anfrage mit deinen Event-Details.</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span class="text-3xl">🎉</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2">3. Buchen</h3>
|
||||
<p class="text-gray-600">Bestätige die Buchung und freue dich auf ein unvergessliches Event!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php $content = ob_get_clean(); ?>
|
||||
<?php $title = 'Home'; ?>
|
||||
<?php include __DIR__ . '/layouts/app.php'; ?>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title><?= $title ?? 'GetYourBand' ?> - Bandvermittlung Schweiz</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<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=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/dist/css/app.css">
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="/dist/js/app.js"></script>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white shadow-sm sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-2xl font-display font-bold text-primary-600">
|
||||
🎸 GetYourBand
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="/" class="text-gray-700 hover:text-primary-600 transition">Home</a>
|
||||
<a href="/bands" class="text-gray-700 hover:text-primary-600 transition">Bands</a>
|
||||
|
||||
<?php if (isset($_SESSION['user'])): ?>
|
||||
<a href="/profile" class="text-gray-700 hover:text-primary-600 transition">Profil</a>
|
||||
<form action="/logout" method="POST" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="btn btn-secondary">Logout</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="/login" class="text-gray-700 hover:text-primary-600 transition">Login</a>
|
||||
<a href="/register" class="btn btn-primary">Registrieren</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
||||
<?= $_SESSION['success'] ?>
|
||||
<?php unset($_SESSION['success']); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
<?= $_SESSION['error'] ?>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $content ?? '' ?>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-white mt-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-primary-400 mb-4">GetYourBand</h3>
|
||||
<p class="text-gray-400">Die Plattform für professionelle Bandvermittlung in der Schweiz.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Links</h4>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/" class="text-gray-400 hover:text-white transition">Home</a></li>
|
||||
<li><a href="/bands" class="text-gray-400 hover:text-white transition">Bands</a></li>
|
||||
<li><a href="/register" class="text-gray-400 hover:text-white transition">Als Band registrieren</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Rechtliches</h4>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/impressum" class="text-gray-400 hover:text-white transition">Impressum</a></li>
|
||||
<li><a href="/datenschutz" class="text-gray-400 hover:text-white transition">Datenschutz</a></li>
|
||||
<li><a href="/agb" class="text-gray-400 hover:text-white transition">AGB</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© <?= date('Y') ?> GetYourBand. Alle Rechte vorbehalten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Helper functions available globally
|
||||
*/
|
||||
|
||||
if (!function_exists('env')) {
|
||||
function env(string $key, $default = null)
|
||||
{
|
||||
return $_ENV[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('asset')) {
|
||||
function asset(string $path): string
|
||||
{
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('url')) {
|
||||
function url(string $path = ''): string
|
||||
{
|
||||
$baseUrl = env('APP_URL', 'http://localhost');
|
||||
return rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('redirect')) {
|
||||
function redirect(string $path): void
|
||||
{
|
||||
header("Location: {$path}");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('old')) {
|
||||
function old(string $key, $default = '')
|
||||
{
|
||||
return $_SESSION['old'][$key] ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('error')) {
|
||||
function error(string $key): ?string
|
||||
{
|
||||
return $_SESSION['errors'][$key][0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('csrf_token')) {
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('csrf_field')) {
|
||||
function csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('dd')) {
|
||||
function dd(...$vars): void
|
||||
{
|
||||
foreach ($vars as $var) {
|
||||
var_dump($var);
|
||||
}
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('formatPrice')) {
|
||||
function formatPrice($price): string
|
||||
{
|
||||
return 'CHF ' . number_format($price, 2, '.', '\'');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('formatDate')) {
|
||||
function formatDate($date): string
|
||||
{
|
||||
return date('d.m.Y', strtotime($date));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('generateSlug')) {
|
||||
function generateSlug(string $text): string
|
||||
{
|
||||
$text = mb_strtolower($text, 'UTF-8');
|
||||
$text = preg_replace('/[^a-z0-9\s-]/', '', $text);
|
||||
$text = preg_replace('/[\s-]+/', '-', $text);
|
||||
return trim($text, '-');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user