From 143fe3d4882ea51e613861e306c8e3e62eb92404 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Dec 2025 21:31:08 +0000 Subject: [PATCH] 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 --- .env.example | 45 +++ .gitignore | 39 ++ .htaccess | 5 + README.md | 356 ++++++++++++++++++ app/Controllers/HomeController.php | 26 ++ app/Core/Controller.php | 99 +++++ app/Core/Model.php | 100 +++++ app/Core/Router.php | 118 ++++++ app/Middleware/AuthMiddleware.php | 14 + app/Middleware/RoleMiddleware.php | 19 + app/Models/Band.php | 96 +++++ app/Models/User.php | 49 +++ app/Views/home.php | 126 +++++++ app/Views/layouts/app.php | 104 +++++ app/helpers.php | 100 +++++ bootstrap.php | 50 +++ composer.json | 40 ++ config/app.php | 28 ++ config/database.php | 17 + database/Database.php | 63 ++++ .../migrations/001_create_users_table.sql | 21 ++ .../migrations/002_create_bands_table.sql | 38 ++ .../003_create_band_media_table.sql | 17 + .../migrations/004_create_bookings_table.sql | 26 ++ .../migrations/005_create_reviews_table.sql | 23 ++ .../006_create_availability_table.sql | 16 + migrate.php | 20 + package.json | 22 ++ postcss.config.js | 6 + public/.htaccess | 31 ++ public/index.php | 28 ++ public/uploads/.gitkeep | 0 resources/css/app.css | 49 +++ resources/js/app.js | 99 +++++ routes/web.php | 49 +++ tailwind.config.js | 45 +++ vite.config.js | 31 ++ 37 files changed, 2015 insertions(+) create mode 100644 .env.example create mode 100644 README.md create mode 100644 app/Controllers/HomeController.php create mode 100644 app/Core/Controller.php create mode 100644 app/Core/Model.php create mode 100644 app/Core/Router.php create mode 100644 app/Middleware/AuthMiddleware.php create mode 100644 app/Middleware/RoleMiddleware.php create mode 100644 app/Models/Band.php create mode 100644 app/Models/User.php create mode 100644 app/Views/home.php create mode 100644 app/Views/layouts/app.php create mode 100644 app/helpers.php create mode 100644 bootstrap.php create mode 100644 composer.json create mode 100644 config/app.php create mode 100644 config/database.php create mode 100644 database/Database.php create mode 100644 database/migrations/001_create_users_table.sql create mode 100644 database/migrations/002_create_bands_table.sql create mode 100644 database/migrations/003_create_band_media_table.sql create mode 100644 database/migrations/004_create_bookings_table.sql create mode 100644 database/migrations/005_create_reviews_table.sql create mode 100644 database/migrations/006_create_availability_table.sql create mode 100644 migrate.php create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/.htaccess create mode 100644 public/index.php create mode 100644 public/uploads/.gitkeep create mode 100644 resources/css/app.css create mode 100644 resources/js/app.js create mode 100644 routes/web.php create mode 100644 tailwind.config.js create mode 100644 vite.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..99c1b37 --- /dev/null +++ b/.env.example @@ -0,0 +1,45 @@ +# Application +APP_NAME="GetYourBand" +APP_ENV=local +APP_DEBUG=true +APP_URL=http://localhost + +# Database +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=getyourband +DB_USERNAME=root +DB_PASSWORD= + +# Mail (SMTP) +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS=noreply@getyourband.ch +MAIL_FROM_NAME="${APP_NAME}" + +# Payment +PAYPAL_MODE=sandbox +PAYPAL_CLIENT_ID= +PAYPAL_SECRET= +PAYMENT_ENABLED=false +COMMISSION_RATE=0.10 + +# Upload Settings +MAX_UPLOAD_SIZE=5242880 +ALLOWED_IMAGE_TYPES=jpg,jpeg,png,webp +ALLOWED_VIDEO_TYPES=mp4,webm + +# Security +SESSION_LIFETIME=120 +SESSION_DRIVER=file +HASH_ALGO=bcrypt + +# Features +REQUIRE_EMAIL_VERIFICATION=true +REQUIRE_BAND_APPROVAL=true +ENABLE_REVIEWS=true diff --git a/.gitignore b/.gitignore index cf382d3..b0f8e19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,41 @@ +# Environment +.env +.env.local + +# Dependencies +/vendor/ +/node_modules/ + +# Build assets +/public/dist/ +/public/hot + +# Storage storage/* !storage/.gitkeep +storage/cache/* +storage/logs/* +storage/sessions/* +storage/uploads/* + +# IDE +.vscode/ +.idea/ +*.sublime-* +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Composer +composer.lock + +# NPM +package-lock.json +yarn.lock + +# Testing +.phpunit.result.cache diff --git a/.htaccess b/.htaccess index afb951a..785201c 100644 --- a/.htaccess +++ b/.htaccess @@ -1,5 +1,10 @@ Options -Indexes AddDefaultCharset UTF-8 + RewriteEngine On + +# Redirect to public directory +RewriteCond %{REQUEST_URI} !^/public/ +RewriteRule ^(.*)$ /public/$1 [L,QSA] diff --git a/README.md b/README.md new file mode 100644 index 0000000..31fe966 --- /dev/null +++ b/README.md @@ -0,0 +1,356 @@ +# 🎸 GetYourBand - Bandvermittlungsplattform + +Eine moderne, professionelle Plattform für die Vermittlung von Live-Bands in der Schweiz. + +## 🚀 Features + +- ✨ **Moderne MVC-Architektur** - Saubere Trennung von Logik, Daten und Präsentation +- 🎨 **Tailwind CSS** - Modernes, responsives Design mit gelben Farbtönen +- ⚡ **Alpine.js** - Leichtgewichtige JavaScript-Interaktivität +- 🔐 **Authentifizierung** - Login, Registrierung, E-Mail-Verifizierung +- 👥 **Mehrere Rollen** - Admin, Band, Kunde +- 🔍 **Erweiterte Suche** - Nach Genre, Ort, Preis filtern +- ⭐ **Bewertungssystem** - Nur nach Buchung möglich +- 📅 **Verfügbarkeitskalender** - Bands können Verfügbarkeit verwalten +- 💳 **PayPal-Integration** - Optional aktivierbare Zahlungen +- 📧 **E-Mail-Benachrichtigungen** - Automatische Updates +- 🛡️ **DSGVO-konform** - Cookie-Banner, Datenschutz +- 📱 **Mobile-First** - Optimiert für alle Geräte + +## 📋 Voraussetzungen + +- PHP 8.3 oder höher +- MySQL 5.7+ oder MariaDB 10.3+ +- Apache mit mod_rewrite +- Composer +- Node.js & npm (für Frontend-Build) + +## 🔧 Installation + +### 1. Repository klonen + +```bash +git clone +cd ai_playgroud +``` + +### 2. PHP-Abhängigkeiten installieren + +```bash +composer install +``` + +### 3. Frontend-Abhängigkeiten installieren + +```bash +npm install +``` + +### 4. Umgebungskonfiguration + +```bash +cp .env.example .env +``` + +Passe die `.env`-Datei an: + +```env +# Datenbank +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=getyourband +DB_USERNAME=root +DB_PASSWORD=dein_passwort + +# Mail (SMTP) +MAIL_HOST=smtp.gmail.com +MAIL_PORT=587 +MAIL_USERNAME=deine@email.ch +MAIL_PASSWORD=dein_passwort + +# Optional: PayPal +PAYPAL_CLIENT_ID=deine_client_id +PAYPAL_SECRET=dein_secret +PAYMENT_ENABLED=true +``` + +### 5. Datenbank erstellen + +```bash +mysql -u root -p -e "CREATE DATABASE getyourband CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +``` + +### 6. Migrationen ausführen + +```bash +php migrate.php +``` + +### 7. Frontend-Assets kompilieren + +**Entwicklung:** +```bash +npm run dev +``` + +**Produktion:** +```bash +npm run build +``` + +### 8. Berechtigungen setzen + +```bash +chmod -R 755 storage +chmod -R 755 public/uploads +``` + +## 🌐 Entwicklungsserver + +### Option 1: PHP Built-in Server + +```bash +cd public +php -S localhost:8000 +``` + +Öffne: http://localhost:8000 + +### Option 2: Apache/XAMPP + +1. Erstelle einen Virtual Host oder nutze htdocs +2. Stelle sicher, dass `mod_rewrite` aktiviert ist +3. DocumentRoot sollte auf das Hauptverzeichnis zeigen (nicht /public!) + +## 📁 Projektstruktur + +``` +. +├── app/ +│ ├── Controllers/ # Controller-Klassen +│ ├── Models/ # Datenmodelle +│ ├── Views/ # View-Templates +│ ├── Middleware/ # Middleware (Auth, etc.) +│ ├── Core/ # Kern-Framework (Router, Controller, Model) +│ └── helpers.php # Helper-Funktionen +├── config/ # Konfigurationsdateien +├── database/ +│ ├── migrations/ # SQL-Migrationen +│ └── Database.php # Datenbankverbindung +├── public/ # Öffentliches Verzeichnis (DocumentRoot) +│ ├── index.php # Entry Point +│ ├── .htaccess # Apache-Konfiguration +│ ├── css/ # Kompilierte CSS +│ ├── js/ # Kompilierte JS +│ └── uploads/ # User-Uploads +├── resources/ +│ ├── css/ # Quell-CSS (Tailwind) +│ └── js/ # Quell-JavaScript +├── routes/ +│ └── web.php # Route-Definitionen +├── storage/ # Temporäre Dateien, Logs, Cache +├── .env # Umgebungsvariablen (nicht committen!) +├── composer.json # PHP-Abhängigkeiten +├── package.json # Frontend-Abhängigkeiten +├── tailwind.config.js # Tailwind-Konfiguration +└── vite.config.js # Vite-Build-Konfiguration +``` + +## 🎨 Design & Farben + +Das Projekt nutzt ein modernes gelbes Farbschema: + +- **Primary**: Gelb-Orange-Töne (#fbbf24 - #f59e0b) +- **Accent**: Helles Gelb (#eab308 - #facc15) +- **Schrift**: Inter (Body), Poppins (Headlines) + +## 🔐 Standard-Admin erstellen + +Nach der Migration kannst du einen Admin-Account manuell in der Datenbank erstellen: + +```sql +INSERT INTO users (email, password, name, role, email_verified_at, is_active) +VALUES ( + 'admin@getyourband.ch', + '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- "password" + 'Admin', + 'admin', + NOW(), + 1 +); +``` + +**Login:** admin@getyourband.ch +**Passwort:** password + +⚠️ **Wichtig:** Ändere das Passwort nach dem ersten Login! + +## 📝 Routen-Übersicht + +### Öffentlich +- `GET /` - Homepage +- `GET /bands` - Band-Liste +- `GET /bands/{slug}` - Band-Detail +- `GET /login` - Login-Formular +- `POST /login` - Login-Verarbeitung +- `GET /register` - Registrierungs-Formular +- `POST /register` - Registrierung + +### Geschützt (Authentifiziert) +- `GET /profile` - User-Profil +- `POST /profile/update` - Profil aktualisieren +- `POST /bookings/create` - Buchung erstellen +- `GET /my-bookings` - Meine Buchungen + +### Band-Bereich +- `GET /band/manage` - Band-Verwaltung +- `POST /band/update` - Band aktualisieren +- `GET /band/bookings` - Eingehende Buchungsanfragen + +### Admin-Bereich +- `GET /admin` - Admin-Dashboard +- `GET /admin/bands` - Band-Verwaltung +- `POST /admin/bands/{id}/approve` - Band freischalten +- `GET /admin/reviews` - Bewertungen moderieren + +## 🧪 Entwicklung + +### Tailwind-Klassen neu kompilieren + +```bash +npm run watch +``` + +Dies startet einen Watch-Modus, der bei Änderungen automatisch neu kompiliert. + +### Neue Migration erstellen + +Erstelle eine neue SQL-Datei in `database/migrations/`: + +```bash +touch database/migrations/007_create_new_table.sql +``` + +Führe sie aus: + +```bash +php migrate.php +``` + +### Neuen Controller erstellen + +```php +view('my-view', [ + 'data' => 'value' + ]); + } +} +``` + +### Neues Model erstellen + +```php +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, + ]); + } +} diff --git a/app/Core/Controller.php b/app/Core/Controller.php new file mode 100644 index 0000000..fb3cdda --- /dev/null +++ b/app/Core/Controller.php @@ -0,0 +1,99 @@ +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']); + } +} diff --git a/app/Core/Model.php b/app/Core/Model.php new file mode 100644 index 0000000..84d270b --- /dev/null +++ b/app/Core/Model.php @@ -0,0 +1,100 @@ +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)); + } +} diff --git a/app/Core/Router.php b/app/Core/Router.php new file mode 100644 index 0000000..b118a6f --- /dev/null +++ b/app/Core/Router.php @@ -0,0 +1,118 @@ +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); + } + } +} diff --git a/app/Middleware/AuthMiddleware.php b/app/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..13221b6 --- /dev/null +++ b/app/Middleware/AuthMiddleware.php @@ -0,0 +1,14 @@ +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]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..8180ef1 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,49 @@ +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); + } +} diff --git a/app/Views/home.php b/app/Views/home.php new file mode 100644 index 0000000..b5fb374 --- /dev/null +++ b/app/Views/home.php @@ -0,0 +1,126 @@ + + + +
+
+

+ Finde die perfekte Band für dein Event +

+

+ Professionelle Live-Bands in der ganzen Schweiz. Einfach buchen, perfekt performen. +

+ +
+
+ + +
+
+
+

Suche deine Band

+ +
+ + + + +
+
+
+
+ + +
+
+

Top bewertete Bands

+ +
+ +
+
+ + <?= $band['name'] ?> + +
+
+

+ +
+

+
+
+ + + () +
+ + Details → + +
+
+ +
+
+
+ + +
+
+

So funktioniert's

+ +
+
+
+ 🔍 +
+

1. Suchen

+

Finde die perfekte Band für dein Event mit unseren Suchfiltern.

+
+
+
+ 📧 +
+

2. Anfragen

+

Sende eine unverbindliche Anfrage mit deinen Event-Details.

+
+
+
+ 🎉 +
+

3. Buchen

+

Bestätige die Buchung und freue dich auf ein unvergessliches Event!

+
+
+
+
+ + + + diff --git a/app/Views/layouts/app.php b/app/Views/layouts/app.php new file mode 100644 index 0000000..ef1780e --- /dev/null +++ b/app/Views/layouts/app.php @@ -0,0 +1,104 @@ + + + + + + + <?= $title ?? 'GetYourBand' ?> - Bandvermittlung Schweiz + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+ + + + + diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..83e2b3c --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,100 @@ +'; + } +} + +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, '-'); + } +} diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..e59de3f --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,50 @@ +load(); + +// Set timezone +date_default_timezone_set('Europe/Zurich'); + +// Error reporting based on environment +if (env('APP_DEBUG', false)) { + error_reporting(E_ALL); + ini_set('display_errors', 1); +} else { + error_reporting(0); + ini_set('display_errors', 0); +} + +// Load configuration +$config = []; +$configFiles = glob(__DIR__ . '/config/*.php'); +foreach ($configFiles as $file) { + $key = basename($file, '.php'); + $config[$key] = require $file; +} + +// Make config globally accessible +define('CONFIG', $config); + +// Helper function to access config +function config(string $key, $default = null) +{ + $keys = explode('.', $key); + $value = CONFIG; + + foreach ($keys as $k) { + if (!isset($value[$k])) { + return $default; + } + $value = $value[$k]; + } + + return $value; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..45247e7 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "getyourband/platform", + "description": "Modern band booking platform", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.3", + "ext-pdo": "*", + "ext-mbstring": "*", + "ext-json": "*", + "vlucas/phpdotenv": "^5.6", + "twig/twig": "^3.8", + "phpmailer/phpmailer": "^6.9", + "respect/validation": "^2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\": "database/" + }, + "files": [ + "app/helpers.php" + ] + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "minimum-stability": "stable", + "prefer-stable": true, + "scripts": { + "post-autoload-dump": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ] + } +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..fd8c9a4 --- /dev/null +++ b/config/app.php @@ -0,0 +1,28 @@ + env('APP_NAME', 'GetYourBand'), + 'env' => env('APP_ENV', 'production'), + 'debug' => env('APP_DEBUG', false), + 'url' => env('APP_URL', 'http://localhost'), + + 'timezone' => 'Europe/Zurich', + 'locale' => 'de_CH', + + 'features' => [ + 'email_verification' => env('REQUIRE_EMAIL_VERIFICATION', true), + 'band_approval' => env('REQUIRE_BAND_APPROVAL', true), + 'reviews' => env('ENABLE_REVIEWS', true), + 'payment' => env('PAYMENT_ENABLED', false), + ], + + 'upload' => [ + 'max_size' => env('MAX_UPLOAD_SIZE', 5242880), // 5MB + 'allowed_images' => explode(',', env('ALLOWED_IMAGE_TYPES', 'jpg,jpeg,png,webp')), + 'allowed_videos' => explode(',', env('ALLOWED_VIDEO_TYPES', 'mp4,webm')), + ], + + 'pagination' => [ + 'per_page' => 12, + ], +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..a31f76f --- /dev/null +++ b/config/database.php @@ -0,0 +1,17 @@ + env('DB_CONNECTION', 'mysql'), + + 'connections' => [ + 'mysql' => [ + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'getyourband'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ], + ], +]; diff --git a/database/Database.php b/database/Database.php new file mode 100644 index 0000000..0dd30d7 --- /dev/null +++ b/database/Database.php @@ -0,0 +1,63 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } catch (PDOException $e) { + throw new \RuntimeException("Database connection failed: " . $e->getMessage()); + } + } + + return self::$instance; + } + + public static function disconnect(): void + { + self::$instance = null; + } + + public static function runMigrations(string $migrationsPath): void + { + $db = self::connect(); + $files = glob($migrationsPath . '/*.sql'); + sort($files); + + foreach ($files as $file) { + echo "Running migration: " . basename($file) . "\n"; + $sql = file_get_contents($file); + + try { + $db->exec($sql); + echo "✓ Migration completed successfully\n"; + } catch (PDOException $e) { + echo "✗ Migration failed: " . $e->getMessage() . "\n"; + throw $e; + } + } + + echo "\nAll migrations completed!\n"; + } +} diff --git a/database/migrations/001_create_users_table.sql b/database/migrations/001_create_users_table.sql new file mode 100644 index 0000000..536c11f --- /dev/null +++ b/database/migrations/001_create_users_table.sql @@ -0,0 +1,21 @@ +-- Migration: Create users table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + role ENUM('admin', 'band', 'customer') NOT NULL DEFAULT 'customer', + email_verified_at TIMESTAMP NULL, + verification_token VARCHAR(64) NULL, + reset_token VARCHAR(64) NULL, + reset_token_expires TIMESTAMP NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_email (email), + INDEX idx_role (role), + INDEX idx_verification_token (verification_token), + INDEX idx_reset_token (reset_token) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/002_create_bands_table.sql b/database/migrations/002_create_bands_table.sql new file mode 100644 index 0000000..53991d5 --- /dev/null +++ b/database/migrations/002_create_bands_table.sql @@ -0,0 +1,38 @@ +-- Migration: Create bands table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS bands ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + genre VARCHAR(100), + location VARCHAR(255), + postal_code VARCHAR(10), + price_min DECIMAL(10, 2), + price_max DECIMAL(10, 2), + member_count INT, + phone VARCHAR(50), + website VARCHAR(255), + facebook VARCHAR(255), + instagram VARCHAR(255), + youtube VARCHAR(255), + profile_image VARCHAR(255), + cover_image VARCHAR(255), + is_approved BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + view_count INT DEFAULT 0, + average_rating DECIMAL(3, 2) DEFAULT 0.00, + total_reviews INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_slug (slug), + INDEX idx_genre (genre), + INDEX idx_location (location), + INDEX idx_postal_code (postal_code), + INDEX idx_is_approved (is_approved), + INDEX idx_average_rating (average_rating), + FULLTEXT idx_search (name, description, genre) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/003_create_band_media_table.sql b/database/migrations/003_create_band_media_table.sql new file mode 100644 index 0000000..26eceea --- /dev/null +++ b/database/migrations/003_create_band_media_table.sql @@ -0,0 +1,17 @@ +-- Migration: Create band_media table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS band_media ( + id INT AUTO_INCREMENT PRIMARY KEY, + band_id INT NOT NULL, + type ENUM('image', 'video') NOT NULL, + url VARCHAR(500) NOT NULL, + title VARCHAR(255), + is_featured BOOLEAN DEFAULT FALSE, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE, + INDEX idx_band_id (band_id), + INDEX idx_type (type), + INDEX idx_sort_order (sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/004_create_bookings_table.sql b/database/migrations/004_create_bookings_table.sql new file mode 100644 index 0000000..3934b61 --- /dev/null +++ b/database/migrations/004_create_bookings_table.sql @@ -0,0 +1,26 @@ +-- Migration: Create bookings table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS bookings ( + id INT AUTO_INCREMENT PRIMARY KEY, + band_id INT NOT NULL, + customer_id INT NOT NULL, + event_date DATE NOT NULL, + event_time TIME, + event_location VARCHAR(255) NOT NULL, + event_type VARCHAR(100), + budget DECIMAL(10, 2), + guest_count INT, + message TEXT, + status ENUM('pending', 'accepted', 'rejected', 'completed', 'cancelled') DEFAULT 'pending', + band_response TEXT, + responded_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE, + FOREIGN KEY (customer_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_band_id (band_id), + INDEX idx_customer_id (customer_id), + INDEX idx_status (status), + INDEX idx_event_date (event_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/005_create_reviews_table.sql b/database/migrations/005_create_reviews_table.sql new file mode 100644 index 0000000..f25929c --- /dev/null +++ b/database/migrations/005_create_reviews_table.sql @@ -0,0 +1,23 @@ +-- Migration: Create reviews table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS reviews ( + id INT AUTO_INCREMENT PRIMARY KEY, + band_id INT NOT NULL, + booking_id INT NOT NULL, + customer_id INT NOT NULL, + rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5), + comment TEXT, + is_approved BOOLEAN DEFAULT FALSE, + is_visible BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE, + FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE CASCADE, + FOREIGN KEY (customer_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY unique_booking_review (booking_id), + INDEX idx_band_id (band_id), + INDEX idx_customer_id (customer_id), + INDEX idx_rating (rating), + INDEX idx_is_approved (is_approved) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/006_create_availability_table.sql b/database/migrations/006_create_availability_table.sql new file mode 100644 index 0000000..d0be3d9 --- /dev/null +++ b/database/migrations/006_create_availability_table.sql @@ -0,0 +1,16 @@ +-- Migration: Create band_availability table +-- Created: 2025-12-02 + +CREATE TABLE IF NOT EXISTS band_availability ( + id INT AUTO_INCREMENT PRIMARY KEY, + band_id INT NOT NULL, + date DATE NOT NULL, + is_available BOOLEAN DEFAULT TRUE, + notes VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE, + UNIQUE KEY unique_band_date (band_id, date), + INDEX idx_band_id (band_id), + INDEX idx_date (date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..40451a1 --- /dev/null +++ b/migrate.php @@ -0,0 +1,20 @@ +#!/usr/bin/env php +load(); + +try { + echo "Starting database migrations...\n\n"; + Database::runMigrations(__DIR__ . '/database/migrations'); + echo "\n✓ All migrations completed successfully!\n"; +} catch (Exception $e) { + echo "\n✗ Migration failed: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..efebd6c --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "getyourband-platform", + "version": "1.0.0", + "description": "Modern band booking platform", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "watch": "vite build --watch" + }, + "devDependencies": { + "vite": "^5.0.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.32", + "autoprefixer": "^10.4.16", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10" + }, + "dependencies": { + "alpinejs": "^3.13.3" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..5bfb25a --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,31 @@ +RewriteEngine On + +# Redirect all requests to index.php +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [L,QSA] + +# Security headers + + Header set X-Content-Type-Options "nosniff" + Header set X-Frame-Options "SAMEORIGIN" + Header set X-XSS-Protection "1; mode=block" + + +# Disable directory browsing +Options -Indexes + +# Compress assets + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json + + +# Browser caching + + ExpiresActive On + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/webp "access plus 1 year" + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..f239601 --- /dev/null +++ b/public/index.php @@ -0,0 +1,28 @@ +dispatch($requestMethod, $requestUri); +} catch (Exception $e) { + if (config('app.debug')) { + echo "

Error

"; + echo "

{$e->getMessage()}

"; + echo "
{$e->getTraceAsString()}
"; + } else { + http_response_code(500); + echo "500 - Internal Server Error"; + } +} diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..a5ac92a --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + @apply scroll-smooth; + } + + body { + @apply bg-gray-50 text-gray-900 antialiased; + } +} + +@layer components { + .btn { + @apply px-4 py-2 rounded-lg font-medium transition-all duration-200 inline-flex items-center justify-center; + } + + .btn-primary { + @apply bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700; + } + + .btn-secondary { + @apply bg-gray-200 text-gray-800 hover:bg-gray-300 active:bg-gray-400; + } + + .card { + @apply bg-white rounded-xl shadow-md p-6 transition-shadow hover:shadow-lg; + } + + .input-field { + @apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent; + } + + .badge { + @apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium; + } + + .badge-yellow { + @apply bg-accent-100 text-accent-800; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..567a522 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,99 @@ +import Alpine from 'alpinejs'; + +// Make Alpine available globally +window.Alpine = Alpine; + +// Alpine Components +Alpine.data('searchBands', () => ({ + query: '', + filters: { + genre: '', + location: '', + priceMin: '', + priceMax: '', + }, + results: [], + loading: false, + + init() { + console.log('Search component initialized'); + }, + + async search() { + this.loading = true; + try { + const params = new URLSearchParams({ + q: this.query, + ...this.filters + }); + const response = await fetch(`/api/bands/search?${params}`); + this.results = await response.json(); + } catch (error) { + console.error('Search error:', error); + } finally { + this.loading = false; + } + } +})); + +Alpine.data('bookingForm', () => ({ + formData: { + bandId: '', + eventDate: '', + location: '', + budget: '', + eventType: '', + message: '' + }, + submitting: false, + + async submit() { + this.submitting = true; + try { + const response = await fetch('/api/bookings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(this.formData) + }); + + if (response.ok) { + alert('Buchungsanfrage erfolgreich gesendet!'); + this.reset(); + } + } catch (error) { + console.error('Booking error:', error); + alert('Es gab einen Fehler. Bitte versuchen Sie es erneut.'); + } finally { + this.submitting = false; + } + }, + + reset() { + this.formData = { + bandId: '', + eventDate: '', + location: '', + budget: '', + eventType: '', + message: '' + }; + } +})); + +// Initialize Alpine +Alpine.start(); + +// Smooth scroll for anchor links +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ behavior: 'smooth' }); + } + }); + }); +}); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..9363c08 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,49 @@ +get('/', [HomeController::class, 'index']); +$router->get('/bands', [BandController::class, 'index']); +$router->get('/bands/{slug}', [BandController::class, 'show']); + +// Authentication routes +$router->get('/login', [AuthController::class, 'showLogin']); +$router->post('/login', [AuthController::class, 'login']); +$router->get('/register', [AuthController::class, 'showRegister']); +$router->post('/register', [AuthController::class, 'register']); +$router->post('/logout', [AuthController::class, 'logout']); +$router->get('/verify-email/{token}', [AuthController::class, 'verifyEmail']); + +// Protected routes (require authentication) +$router->group(['middleware' => 'auth'], function($router) { + // Profile + $router->get('/profile', [ProfileController::class, 'show']); + $router->post('/profile/update', [ProfileController::class, 'update']); + + // Booking routes + $router->post('/bookings/create', [BookingController::class, 'create']); + $router->get('/my-bookings', [BookingController::class, 'myBookings']); + + // Band management (for band users) + $router->group(['middleware' => 'role:band'], function($router) { + $router->get('/band/manage', [BandController::class, 'manage']); + $router->post('/band/update', [BandController::class, 'update']); + $router->get('/band/bookings', [BookingController::class, 'bandBookings']); + $router->post('/band/bookings/{id}/respond', [BookingController::class, 'respond']); + }); + + // Admin routes + $router->group(['middleware' => 'role:admin'], function($router) { + $router->get('/admin', [AdminController::class, 'dashboard']); + $router->get('/admin/bands', [AdminController::class, 'bands']); + $router->post('/admin/bands/{id}/approve', [AdminController::class, 'approveBand']); + $router->get('/admin/reviews', [AdminController::class, 'reviews']); + $router->post('/admin/reviews/{id}/moderate', [AdminController::class, 'moderateReview']); + }); +}); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..bd6b5a3 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,45 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./app/Views/**/*.php", + "./public/**/*.js", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + }, + accent: { + 50: '#fefce8', + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + } + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + display: ['Poppins', 'system-ui', 'sans-serif'], + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + ], +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c28e2f4 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + root: '.', + build: { + outDir: 'public/dist', + emptyOutDir: true, + manifest: true, + rollupOptions: { + input: { + main: path.resolve(__dirname, 'resources/js/app.js'), + css: path.resolve(__dirname, 'resources/css/app.css'), + }, + output: { + entryFileNames: 'js/[name].[hash].js', + chunkFileNames: 'js/[name].[hash].js', + assetFileNames: (assetInfo) => { + if (assetInfo.name.endsWith('.css')) { + return 'css/[name].[hash][extname]'; + } + return 'assets/[name].[hash][extname]'; + }, + }, + }, + }, + server: { + port: 3000, + strictPort: false, + }, +});