Initial Commit
This commit is contained in:
Executable
+88
@@ -0,0 +1,88 @@
|
||||
/node_modules
|
||||
/public/storage
|
||||
/vendor/*
|
||||
.idea
|
||||
.env
|
||||
/storage/debugbar
|
||||
/.scannerwork/
|
||||
/public/uploads/
|
||||
/docker/backup/
|
||||
/docker/config/
|
||||
/docker/data/
|
||||
/Local/
|
||||
/nginx
|
||||
/reports/
|
||||
/public/Scras_uploads/
|
||||
/database/sql/
|
||||
/docker/db2/
|
||||
/docker/mysql/data/
|
||||
/provision
|
||||
composer.lock
|
||||
.phpunit.result.cache
|
||||
docker-compose.override.yml
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/storage/*.key
|
||||
/public/hot
|
||||
*.phpbkp
|
||||
*.php-tmp
|
||||
|
||||
# ***********************************
|
||||
# Application specific excludes.
|
||||
# ***********************************
|
||||
|
||||
#/app/Http/Controllers/Admin/UtilitiesController.php
|
||||
#/app/Http/Controllers/Admin/OracleController.php
|
||||
#/resources/views/admin/oracle/index.blade.php
|
||||
|
||||
# ******************************************************************
|
||||
# Exclude specific config files for various hosts
|
||||
# do not exclude the default laravel ones as this will cause issues
|
||||
# ******************************************************************
|
||||
|
||||
#/config/exports.php
|
||||
#/config/AdcConf.php
|
||||
#/config/eproconf.php
|
||||
#/config/ethanol.php
|
||||
#/config/hmrcconf.php
|
||||
#/config/rolebased.php
|
||||
|
||||
# ***********************************
|
||||
# Composer Specific Excludes lock
|
||||
# file is ignored adn it should load
|
||||
# the php ver from the environment.
|
||||
# ***********************************
|
||||
|
||||
/local
|
||||
|
||||
# Docker Specific Excludes
|
||||
|
||||
**/.DS_Store
|
||||
/modules_statuses.json
|
||||
|
||||
# Environment Files
|
||||
.env.local
|
||||
.env.mgdev
|
||||
.env.todev
|
||||
bootstrap/.env
|
||||
config/.env
|
||||
/Modules/Core/Database/Seeders/data/
|
||||
/provision/db/
|
||||
/provision/sparos/
|
||||
|
||||
/Modules/Maximo/Database/data/
|
||||
Modules/SIDS/node_modules/
|
||||
/.env.prod_emu
|
||||
/modules/SIDS/Database/backups/
|
||||
/docker/opt/
|
||||
/docker/nginx/logs/
|
||||
/storage/backup/
|
||||
/Modules/Budgets/Provision/src/
|
||||
/Modules/MaterialsPlanning/Provision/
|
||||
/Modules/Budgets/Provision/MBR Files/
|
||||
/Modules/Budgets/Provision/blank.blade.php
|
||||
.php-cs-fixer.dist.php
|
||||
phpstan.neon
|
||||
pint.json
|
||||
@@ -0,0 +1,16 @@
|
||||
# Production implementation map
|
||||
|
||||
## Included foundation
|
||||
Public marketplace UI, responsive DJ search/profile, enquiry flow, role-ready user/business models, bookings/reviews schema, admin dashboard shell, API route, consent UI, legal placeholders, Stripe/Cashier dependencies, Redis-ready environment, generated design reference.
|
||||
|
||||
## Next production modules
|
||||
1. Install Laravel 13 framework/vendor files with Composer.
|
||||
2. Add Laravel starter auth (Fortify/Livewire) and verified email + 2FA.
|
||||
3. Publish/configure Spatie Permission and Media Library migrations.
|
||||
4. Add Business/DJ CRUD with policies, moderation and private verification documents.
|
||||
5. Add quotes, quote items, availability, booking state machine, conversations/messages.
|
||||
6. Add Cashier + Stripe Connect onboarding, deposits, refunds, payouts and webhook idempotency.
|
||||
7. Add database/email/push notification preferences and queued notifications.
|
||||
8. Add support ticketing, reports, audit viewer, privacy export/delete workflow.
|
||||
9. Add full feature/policy/payment tests and browser accessibility tests.
|
||||
10. Replace demo remote imagery with generated/owned image set and wire image derivatives/CDN.
|
||||
@@ -0,0 +1,15 @@
|
||||
# BeatBook — Laravel 13 DJ Marketplace
|
||||
|
||||
Professional DJ advertising, enquiry, booking, review and administration platform.
|
||||
|
||||
## Stack
|
||||
Laravel 13, PHP 8.3+, MySQL, Redis, Livewire/Flux-ready, Tailwind/Vite, Spatie Permission, Spatie Media Library, Stripe/Cashier.
|
||||
|
||||
## Quick start
|
||||
```bash
|
||||
composer install && cp .env.example .env && php artisan key:generate && php artisan migrate --seed && npm install && npm run build && php artisan serve
|
||||
```
|
||||
|
||||
Demo seed accounts: `admin@beatbook.test`, `dj@beatbook.test`, `client@beatbook.test` — password `password` (change immediately outside local development).
|
||||
|
||||
The generated visual direction is in `public/images/design-reference.png`. Replace the demo remote Unsplash backgrounds with owned/generated production imagery before launch.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Security baseline
|
||||
- Enforce HTTPS/HSTS in production.
|
||||
- Keep private/verification uploads outside public storage; authorize every download.
|
||||
- Validate MIME and image decoding; strip metadata where appropriate.
|
||||
- Use policies for every tenant-owned resource.
|
||||
- Enable 2FA for admin/DJ accounts.
|
||||
- Verify Stripe webhook signatures and make handlers idempotent.
|
||||
- Rate-limit auth, enquiry, review and messaging endpoints.
|
||||
- Audit privileged actions; hash IP addresses if retained.
|
||||
- Never store card details.
|
||||
- Run queues under Supervisor/systemd and keep dependencies patched.
|
||||
- Replace placeholder legal policies before launch.
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Booking;
|
||||
use App\Models\DjProfile;
|
||||
use App\Models\Review;
|
||||
use App\Models\User;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return view('admin.dashboard', ['stats' => ['users' => User::count(), 'djs' => DjProfile::count(), 'bookings' => Booking::count(), 'reviews' => Review::where('status', 'pending')->count()]]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller {}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DjProfile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DjController extends Controller
|
||||
{
|
||||
public function index(Request $r)
|
||||
{
|
||||
$q = DjProfile::query()->where('is_published', true)->with('genres');
|
||||
if ($r->filled('location')) {
|
||||
$q->where('city', 'like', '%'.$r->string('location').'%');
|
||||
} if ($r->filled('max_price')) {
|
||||
$q->where('price_from', '<=', $r->integer('max_price'));
|
||||
}
|
||||
|
||||
return view('djs.index', ['djs' => $q->orderByDesc('is_featured')->orderByDesc('average_rating')->paginate(12)->withQueryString()]);
|
||||
}
|
||||
|
||||
public function show(DjProfile $djProfile)
|
||||
{
|
||||
abort_unless($djProfile->is_published, 404);
|
||||
|
||||
return view('djs.show', ['dj' => $djProfile->load(['genres', 'reviews' => fn ($q) => $q->where('status', 'published')->latest()])]);
|
||||
}
|
||||
|
||||
public function apiIndex()
|
||||
{
|
||||
return DjProfile::where('is_published', true)->with('genres')->paginate(20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DjProfile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EnquiryController extends Controller
|
||||
{
|
||||
public function store(Request $r, DjProfile $djProfile)
|
||||
{
|
||||
$data = $r->validate(['event_type' => 'required|string|max:80', 'event_date' => 'required|date|after_or_equal:today', 'venue' => 'required|string|max:160', 'postcode' => 'nullable|string|max:12', 'guest_count' => 'nullable|integer|min:1|max:100000', 'budget' => 'nullable|numeric|min:0', 'message' => 'required|string|max:5000']);
|
||||
$djProfile->enquiries()->create($data + ['user_id' => $r->user()->id, 'status' => 'new']);
|
||||
|
||||
return back()->with('success', 'Your enquiry has been sent.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DjProfile;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('home.index', ['featured' => DjProfile::where('is_published', true)->where('is_featured', true)->orderByDesc('average_rating')->limit(5)->get()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReviewController extends Controller
|
||||
{
|
||||
public function store(Request $r, Booking $booking)
|
||||
{
|
||||
abort_unless($booking->client_id === $r->user()->id && $booking->status === 'completed', 403);
|
||||
$d = $r->validate(['rating' => 'required|integer|min:1|max:5', 'title' => 'nullable|string|max:100', 'body' => 'required|string|max:3000']);
|
||||
$booking->djProfile?->reviews()->create($d + ['booking_id' => $booking->id, 'user_id' => $r->user()->id, 'status' => 'pending']);
|
||||
|
||||
return back()->with('success', 'Review submitted for moderation.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Booking extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['enquiry_id', 'client_id', 'dj_profile_id', 'event_date', 'total', 'deposit', 'status', 'paid_at'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['event_date' => 'date', 'total' => 'decimal:2', 'deposit' => 'decimal:2', 'paid_at' => 'datetime'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Business extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['name', 'slug', 'description', 'email', 'phone', 'website', 'status', 'verified_at'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['verified_at' => 'datetime'];
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class)->withPivot(['role'])->withTimestamps();
|
||||
}
|
||||
|
||||
public function profiles()
|
||||
{
|
||||
return $this->hasMany(DjProfile::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DjProfile extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['user_id', 'business_id', 'stage_name', 'slug', 'headline', 'bio', 'city', 'service_radius_miles', 'price_from', 'profile_photo', 'cover_photo', 'verified_at', 'is_featured', 'is_published', 'average_rating', 'review_count'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['verified_at' => 'datetime', 'is_featured' => 'boolean', 'is_published' => 'boolean', 'price_from' => 'decimal:2', 'average_rating' => 'decimal:2'];
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function business()
|
||||
{
|
||||
return $this->belongsTo(Business::class);
|
||||
}
|
||||
|
||||
public function genres()
|
||||
{
|
||||
return $this->belongsToMany(Genre::class);
|
||||
}
|
||||
|
||||
public function reviews()
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
}
|
||||
|
||||
public function enquiries()
|
||||
{
|
||||
return $this->hasMany(Enquiry::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Enquiry extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['user_id', 'dj_profile_id', 'event_type', 'event_date', 'start_time', 'end_time', 'venue', 'postcode', 'guest_count', 'budget', 'message', 'status'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['event_date' => 'date', 'budget' => 'decimal:2'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Genre extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'slug'];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Review extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['booking_id', 'user_id', 'dj_profile_id', 'rating', 'title', 'body', 'status', 'dj_reply'];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens,HasRoles,Notifiable;
|
||||
|
||||
protected $fillable = ['name', 'email', 'password', 'phone', 'avatar_path', 'marketing_opt_in'];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['email_verified_at' => 'datetime', 'password' => 'hashed', 'marketing_opt_in' => 'boolean'];
|
||||
}
|
||||
|
||||
public function businesses()
|
||||
{
|
||||
return $this->belongsToMany(Business::class)->withPivot(['role'])->withTimestamps();
|
||||
}
|
||||
|
||||
public function djProfile()
|
||||
{
|
||||
return $this->hasOne(DjProfile::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
|
||||
);
|
||||
})->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\RsyncStudioLicensingServiceProvider;
|
||||
|
||||
return [
|
||||
RsyncStudioLicensingServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "absdev/beatbook",
|
||||
"type": "project",
|
||||
"description": "Professional DJ marketplace on Laravel 13",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/cashier": "^16.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"spatie/laravel-permission": "^7.0",
|
||||
"spatie/laravel-medialibrary": "^11.23.7",
|
||||
"predis/predis": "^3.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedSmallInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('connection');
|
||||
$table->string('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
|
||||
$table->index(['connection', 'queue', 'failed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('businesses', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->string('name');
|
||||
$t->string('slug')->unique();
|
||||
$t->text('description')->nullable();
|
||||
$t->string('email')->nullable();
|
||||
$t->string('phone')->nullable();
|
||||
$t->string('website')->nullable();
|
||||
$t->string('status')->default('active');
|
||||
$t->timestamp('verified_at')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('business_user', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->ulid('business_id');
|
||||
$t->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$t->string('role')->default('member');
|
||||
$t->timestamps();
|
||||
$t->unique(['business_id', 'user_id']);
|
||||
});
|
||||
Schema::create('dj_profiles', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$t->ulid('business_id')->nullable();
|
||||
$t->string('stage_name');
|
||||
$t->string('slug')->unique();
|
||||
$t->string('headline')->nullable();
|
||||
$t->text('bio')->nullable();
|
||||
$t->string('city')->index();
|
||||
$t->unsignedSmallInteger('service_radius_miles')->default(30);
|
||||
$t->decimal('price_from', 10, 2)->nullable();
|
||||
$t->string('profile_photo')->nullable();
|
||||
$t->string('cover_photo')->nullable();
|
||||
$t->timestamp('verified_at')->nullable();
|
||||
$t->boolean('is_featured')->default(false);
|
||||
$t->boolean('is_published')->default(false);
|
||||
$t->decimal('average_rating', 3, 2)->default(0);
|
||||
$t->unsignedInteger('review_count')->default(0);
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('genres', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->string('name');
|
||||
$t->string('slug')->unique();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('dj_profile_genre', function (Blueprint $t) {
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->foreignId('genre_id')->constrained()->cascadeOnDelete();
|
||||
$t->primary(['dj_profile_id', 'genre_id']);
|
||||
});
|
||||
Schema::create('enquiries', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained();
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->string('event_type');
|
||||
$t->date('event_date');
|
||||
$t->time('start_time')->nullable();
|
||||
$t->time('end_time')->nullable();
|
||||
$t->string('venue');
|
||||
$t->string('postcode')->nullable();
|
||||
$t->unsignedInteger('guest_count')->nullable();
|
||||
$t->decimal('budget', 10, 2)->nullable();
|
||||
$t->text('message');
|
||||
$t->string('status')->default('new')->index();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('bookings', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->ulid('enquiry_id')->nullable();
|
||||
$t->foreignId('client_id')->constrained('users');
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->date('event_date');
|
||||
$t->decimal('total', 10, 2);
|
||||
$t->decimal('deposit', 10, 2)->default(0);
|
||||
$t->string('status')->default('pending')->index();
|
||||
$t->timestamp('paid_at')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('reviews', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->ulid('booking_id');
|
||||
$t->foreignId('user_id')->constrained();
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->unsignedTinyInteger('rating');
|
||||
$t->string('title')->nullable();
|
||||
$t->text('body');
|
||||
$t->string('status')->default('pending')->index();
|
||||
$t->text('dj_reply')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('cookie_consents', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->string('anonymous_id')->nullable()->index();
|
||||
$t->boolean('analytics')->default(false);
|
||||
$t->boolean('marketing')->default(false);
|
||||
$t->string('policy_version');
|
||||
$t->timestamp('consented_at');
|
||||
$t->timestamps();
|
||||
});
|
||||
Schema::create('audit_logs', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->string('action')->index();
|
||||
$t->nullableMorphs('subject');
|
||||
$t->json('meta')->nullable();
|
||||
$t->string('ip_hash', 64)->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (['audit_logs', 'cookie_consents', 'reviews', 'bookings', 'enquiries', 'dj_profile_genre', 'genres', 'dj_profiles', 'business_user', 'businesses'] as $x) {
|
||||
Schema::dropIfExists($x);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\DjProfile;
|
||||
use App\Models\Genre;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
foreach (['super-admin', 'admin', 'dj', 'client', 'moderator', 'support'] as $r) {
|
||||
Role::firstOrCreate(['name' => $r]);
|
||||
} $admin = User::firstOrCreate(['email' => 'admin@beatbook.test'], ['name' => 'BeatBook Admin', 'password' => 'password']);
|
||||
$admin->assignRole('admin');
|
||||
$djUser = User::firstOrCreate(['email' => 'dj@beatbook.test'], ['name' => 'Luna Hart', 'password' => 'password']);
|
||||
$djUser->assignRole('dj');
|
||||
User::firstOrCreate(['email' => 'client@beatbook.test'], ['name' => 'Demo Client', 'password' => 'password'])->assignRole('client');
|
||||
$dj = DjProfile::firstOrCreate(['slug' => 'dj-luna'], ['user_id' => $djUser->id, 'stage_name' => 'DJ Luna', 'headline' => 'Weddings · House · Chart · Private events', 'bio' => 'Award-winning mobile DJ bringing polished production, packed dance floors and a personal soundtrack to weddings and events.', 'city' => 'London', 'price_from' => 350, 'profile_photo' => 'https://images.unsplash.com/photo-1516280440614-37939bbacd81?auto=format&fit=crop&w=900&q=85', 'cover_photo' => 'https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=1800&q=85', 'verified_at' => now(), 'is_featured' => true, 'is_published' => true, 'average_rating' => 4.9, 'review_count' => 128]);
|
||||
foreach (['House', 'Chart', 'Wedding', 'R&B'] as $g) {
|
||||
$genre = Genre::firstOrCreate(['slug' => strtolower(str_replace('&', 'and', $g))], ['name' => $g]);
|
||||
$dj->genres()->syncWithoutDetaching($genre);
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Deployer;
|
||||
|
||||
require 'recipe/laravel.php';
|
||||
|
||||
// System Config
|
||||
set('writable_use_sudo', true);
|
||||
set('clear_use_sudo', true);
|
||||
set('cleanup_use_sudo', true);
|
||||
set('keep_releases', 1);
|
||||
set('repository', 'git@git.absdev.net:mag/rsync_licensing_server.git');
|
||||
set('node_version', '22.12.0');
|
||||
set('allow_anonymous_stats', false);
|
||||
|
||||
add('shared_files', ['.env']);
|
||||
add('shared_dirs', ['storage']);
|
||||
add('writable_dirs', ['storage', 'bootstrap/cache']);
|
||||
|
||||
// Host Definitions
|
||||
host('main')
|
||||
->set('hostname', '217.154.47.197')
|
||||
->set('remote_user', 'mag')
|
||||
->set('branch', 'main')
|
||||
->set('deploy_path', '/var/www/rsyncstudio');
|
||||
|
||||
host('stage')
|
||||
->set('hostname', 'pegasus.absdev.net')
|
||||
->set('remote_user', 'mag')
|
||||
->set('branch', 'stage')
|
||||
->set('deploy_path', '/var/www/rsyncstudio');
|
||||
|
||||
task('build', function (): void {
|
||||
run('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use {{node_version}} && cd {{release_path}} && npm ci --no-audit --no-fund');
|
||||
run('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use {{node_version}} && cd {{release_path}} && npm run build');
|
||||
});
|
||||
task('queue:restart', function (): void {
|
||||
run('cd {{release_path}} && php artisan queue:restart');
|
||||
});
|
||||
task('deploy:write_revision_file', function (): void {
|
||||
run('cd {{release_path}} && echo "{{branch}}" > BRANCH');
|
||||
run('cd {{release_path}} && date -u +"%Y-%m-%dT%H:%M:%SZ" > DEPLOYED_AT');
|
||||
});
|
||||
task('artisan:clear', function (): void {
|
||||
run('cd {{release_path}} && php artisan view:clear');
|
||||
run('cd {{release_path}} && php artisan optimize:clear');
|
||||
run('cd {{release_path}} && php artisan config:clear');
|
||||
run('cd {{release_path}} && php artisan route:clear');
|
||||
});
|
||||
task('services:restart', function (): void {
|
||||
run('sudo -n nginx -t');
|
||||
run('sudo -n systemctl reload nginx');
|
||||
run('sudo -n systemctl restart php8.3-fpm');
|
||||
});
|
||||
|
||||
after('deploy:update_code', 'deploy:write_revision_file');
|
||||
after('deploy:vendors', 'build');
|
||||
after('deploy:symlink', 'artisan:clear');
|
||||
after('deploy:symlink', 'queue:restart');
|
||||
after('deploy:failed', 'deploy:unlock');
|
||||
after('deploy:symlink', 'services:restart');
|
||||
Generated
+2246
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"private":true,"type":"module","scripts":{"dev":"vite","build":"vite build"},"devDependencies":{"@tailwindcss/vite":"^4.0.0","axios":"^1.7.9","laravel-vite-plugin":"^2.0.0","tailwindcss":"^4.0.0","vite":"^7.0.0"}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-DkQDD-ks.css",
|
||||
"src": "resources/css/app.css",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
"names": [
|
||||
"app.css"
|
||||
]
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-DMsN-rLE.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
:root{--ink:#070a19;--panel:#0d1228;--purple:#7c3aed;--pink:#ec4899;--cyan:#22d3ee;--gold:#fbbf24}*{box-sizing:border-box}body{margin:0;background:#070a19;color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.shell{max-width:1280px;margin:auto;padding:0 24px}.nav{height:76px;display:flex;align-items:center;justify-content:space-between}.brand{font-size:25px;font-weight:900;color:white;text-decoration:none}.brand b{color:#ec4899}.navlinks{display:flex;gap:28px}.navlinks a{color:#dbeafe;text-decoration:none;font-size:14px}.button{display:inline-flex;padding:13px 20px;border-radius:12px;background:linear-gradient(135deg,var(--purple),var(--pink));color:white;text-decoration:none;border:0;font-weight:800}.button.ghost{background:transparent;border:1px solid #334155}.hero{min-height:610px;background:linear-gradient(90deg,rgba(7,10,25,.97) 0%,rgba(7,10,25,.72) 50%,rgba(7,10,25,.25)),url('https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=2000&q=85') center/cover;border-bottom:1px solid #202743}.hero-copy{padding:105px 0 35px;max-width:760px}.eyebrow{color:#67e8f9;font-weight:800;letter-spacing:.18em;text-transform:uppercase}.hero h1{font-size:72px;line-height:.95;margin:14px 0 22px;letter-spacing:-.05em}.hero h1 span{background:linear-gradient(90deg,#c084fc,#f472b6,#22d3ee);background-clip:text;color:transparent}.hero p{font-size:19px;color:#cbd5e1;max-width:620px}.searchbox{margin-top:34px;padding:10px;background:white;border-radius:18px;display:grid;grid-template-columns:1.2fr 1fr 1fr auto;gap:8px;box-shadow:0 25px 80px #0008}.searchbox input,.searchbox select{border:0;padding:17px;color:#0f172a;background:#f8fafc;border-radius:10px;outline:none}.section{padding:72px 0}.section-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.section h2{font-size:34px;margin:0}.muted{color:#94a3b8}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.card{background:#10162d;border:1px solid #27304d;border-radius:18px;overflow:hidden;box-shadow:0 14px 35px #0004}.card-img{height:235px;background:center/cover}.card-body{padding:18px}.card h3{margin:0 0 8px;font-size:20px}.stars{color:var(--gold)}.price{font-size:20px;font-weight:900;margin:14px 0}.chips{display:flex;gap:7px;flex-wrap:wrap}.chip{font-size:12px;padding:6px 9px;background:#1e293b;border-radius:99px;color:#cbd5e1}.profile-hero{height:350px;background:center/cover;position:relative}.profile-panel{margin:-90px auto 40px;position:relative;background:white;color:#0f172a;border-radius:22px;padding:30px;display:grid;grid-template-columns:130px 1fr 220px;gap:25px;box-shadow:0 30px 70px #0007}.avatar{width:130px;height:130px;border-radius:16px;object-fit:cover}.admin{display:grid;grid-template-columns:240px 1fr;min-height:100vh;background:#f8fafc;color:#0f172a}.sidebar{background:#0b1022;color:white;padding:28px 18px}.sidebar a{display:block;color:#cbd5e1;text-decoration:none;padding:11px;border-radius:8px}.sidebar a:hover{background:#1e293b}.admin-main{padding:35px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.stat{background:white;padding:24px;border-radius:14px;box-shadow:0 3px 15px #0f172a0d}.stat strong{display:block;font-size:30px;color:#7c3aed}.cookie{position:fixed;bottom:18px;left:18px;right:18px;max-width:650px;background:#fff;color:#0f172a;border-radius:16px;padding:18px;box-shadow:0 20px 60px #0008;z-index:50}@media(max-width:900px){.navlinks{display:none}.hero h1{font-size:50px}.searchbox{grid-template-columns:1fr}.cards{grid-template-columns:1fr 1fr}.profile-panel{grid-template-columns:1fr;margin:-40px 16px 30px}.admin{grid-template-columns:1fr}.sidebar{display:none}.stats{grid-template-columns:1fr 1fr}}@media(max-width:600px){.cards,.stats{grid-template-columns:1fr}.hero h1{font-size:42px}.hero-copy{padding-top:70px}}
|
||||
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import axios from 'axios'; window.axios=axios; window.axios.defaults.headers.common['X-Requested-With']='XMLHttpRequest';
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta name="viewport" content="width=device-width">@vite(['resources/css/app.css'])<title>BeatBook Admin</title></head><body><div class="admin"><aside class="sidebar"><div class="brand"><b>◉</b> BeatBook</div><p class="muted">Administration</p>@foreach(['Dashboard','Users','DJs & Businesses','Clients','Bookings','Payments','Reviews','Messages','Notifications','Support','Reports','Content','Roles & Permissions','Privacy','Settings'] as $x)<a href="#">{{ $x }}</a>@endforeach</aside><main class="admin-main"><h1>Dashboard</h1><p style="color:#64748b">Marketplace overview and moderation queue.</p><div class="stats">@foreach($stats as $k=>$v)<div class="stat"><span>{{ ucfirst($k) }}</span><strong>{{ number_format($v) }}</strong></div>@endforeach</div><div class="stat" style="margin-top:22px;min-height:330px"><h2>Operations</h2><p style="color:#64748b">Revenue charts, recent bookings, verification requests, support tickets and review moderation plug into this dashboard.</p><div style="height:180px;background:linear-gradient(180deg,#ede9fe,#fff);border-radius:12px;margin-top:30px;display:flex;align-items:end;padding:25px;gap:10px">@foreach([25,42,36,61,55,78,70,92,86,110,120] as $h)<i style="display:block;width:7%;height:{{ $h }}px;background:#7c3aed;border-radius:5px"></i>@endforeach</div></div></main></div></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('content')<main class="section"><div class="shell"><div class="eyebrow">Your account</div><h1>Client dashboard</h1><div class="cards"><div class="card"><div class="card-body"><h3>Enquiries</h3><p class="muted">Track quotes and conversations.</p></div></div><div class="card"><div class="card-body"><h3>Bookings</h3><p class="muted">Confirmed events and payment status.</p></div></div><div class="card"><div class="card-body"><h3>Favourites</h3><p class="muted">Your saved DJs.</p></div></div><div class="card"><div class="card-body"><h3>Reviews</h3><p class="muted">Review completed bookings.</p></div></div></div></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
<article class="card"><div class="card-img" style="background-image:url('{{ $dj->profile_photo ?: 'https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=700&q=80' }}')"></div><div class="card-body"><h3>{{ $dj->stage_name }} @if($dj->verified_at) ✓ @endif</h3><div class="stars">★★★★★ <span class="muted">{{ number_format($dj->average_rating,1) }} ({{ $dj->review_count }})</span></div><p class="muted">{{ $dj->city }} · {{ $dj->headline }}</p><div class="price">From £{{ number_format($dj->price_from,0) }}</div><a class="button" href="{{ route('djs.show',$dj) }}">View profile</a></div></article>
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('title','Find DJs — BeatBook') @section('content')<main class="section"><div class="shell"><div class="eyebrow">Discover</div><h1 style="font-size:48px">Find your DJ</h1><form class="searchbox" style="margin:25px 0 35px"><input name="location" value="{{ request('location') }}" placeholder="Town or city"><input name="max_price" type="number" value="{{ request('max_price') }}" placeholder="Maximum price"><select><option>Highest rated</option></select><button class="button">Filter</button></form><div class="cards">@foreach($djs as $dj) @include('djs._card') @endforeach</div><div style="margin-top:30px">{{ $djs->links() }}</div></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('title',$dj->stage_name.' — BeatBook') @section('content')<div class="profile-hero" style="background-image:linear-gradient(#0002,#0008),url('{{ $dj->cover_photo ?: $dj->profile_photo }}')"></div><main class="shell"><section class="profile-panel"><img class="avatar" src="{{ $dj->profile_photo }}" alt="{{ $dj->stage_name }}"><div><h1 style="margin:0">{{ $dj->stage_name }} @if($dj->verified_at)<span style="color:#2563eb">✓</span>@endif</h1><div class="stars">★★★★★ <span style="color:#64748b">{{ $dj->average_rating }} · {{ $dj->review_count }} reviews</span></div><p>{{ $dj->headline }}</p><div class="chips">@foreach($dj->genres as $genre)<span class="chip">{{ $genre->name }}</span>@endforeach</div></div><aside><span>From</span><div style="font-size:34px;font-weight:900">£{{ number_format($dj->price_from,0) }}</div><span>per event</span><br><br><a class="button" href="#enquire">Request a quote</a></aside></section><section class="section" style="padding-top:15px"><div style="display:grid;grid-template-columns:2fr 1fr;gap:35px"><div><h2>About {{ $dj->stage_name }}</h2><p class="muted" style="font-size:17px;line-height:1.8">{{ $dj->bio }}</p><h2>Reviews</h2>@foreach($dj->reviews as $review)<div class="card" style="padding:20px;margin-bottom:12px"><div class="stars">{{ str_repeat('★',$review->rating) }}</div><h3>{{ $review->title }}</h3><p>{{ $review->body }}</p></div>@endforeach</div><aside id="enquire" class="card" style="padding:22px;height:max-content"><h2>Check availability</h2>@auth<form method="post" action="{{ route('enquiries.store',$dj) }}">@csrf<input name="event_type" placeholder="Event type" required style="width:100%;padding:12px;margin:5px 0"><input name="event_date" type="date" required style="width:100%;padding:12px;margin:5px 0"><input name="venue" placeholder="Venue / town" required style="width:100%;padding:12px;margin:5px 0"><input name="budget" type="number" placeholder="Budget £" style="width:100%;padding:12px;margin:5px 0"><textarea name="message" required placeholder="Tell the DJ about your event" style="width:100%;min-height:130px;padding:12px;margin:5px 0"></textarea><button class="button">Send enquiry</button></form>@else<p class="muted">Sign in to send a secure enquiry.</p><a class="button" href="/login">Sign in</a>@endauth</aside></div></section></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('content')<section class="hero"><div class="shell hero-copy"><div class="eyebrow">Music creates memories</div><h1>Find your <span>perfect DJ</span></h1><p>Discover trusted DJs for weddings, parties, corporate events and clubs. Compare profiles, reviews and prices, then enquire with confidence.</p><form class="searchbox" action="{{ route('djs.index') }}"><input name="location" placeholder="Town or postcode"><select name="event_type"><option value="">Event type</option><option>Wedding</option><option>Party</option><option>Corporate</option><option>Club</option></select><input type="date" name="date"><button class="button">Search DJs</button></form></div></section><section class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">Top talent</div><h2>Featured DJs</h2></div><a class="button ghost" href="{{ route('djs.index') }}">View all DJs</a></div><div class="cards">@forelse($featured as $dj) @include('djs._card',['dj'=>$dj]) @empty @foreach([['DJ Luna','London',350,'photo-1516280440614-37939bbacd81'],['DJ Maverick','Manchester',400,'photo-1571266028243-d220c9c3b2d2'],['DJ Rhythm','Birmingham',300,'photo-1524368535928-5b5e00ddc76b'],['DJ Serenity','Leeds',320,'photo-1506157786151-b8491531f063']] as $d)<article class="card"><div class="card-img" style="background-image:url('https://images.unsplash.com/{{ $d[3] }}?auto=format&fit=crop&w=700&q=80')"></div><div class="card-body"><h3>{{ $d[0] }}</h3><div class="stars">★★★★★ <span class="muted">4.9</span></div><p class="muted">{{ $d[1] }} · Weddings · Parties</p><div class="price">From £{{ $d[2] }}</div><a class="button" href="{{ route('djs.index') }}">View profile</a></div></article>@endforeach @endforelse</div></div></section><section id="how" class="section" style="background:#0d1228"><div class="shell"><div class="eyebrow">Simple & secure</div><h2>From search to dance floor</h2><div class="cards" style="margin-top:30px">@foreach(['Search'=>'Find DJs by location, date, event and budget.','Compare'=>'Review portfolios, pricing and verified feedback.','Enquire'=>'Send your event details without exposing private contact data.','Book & review'=>'Confirm securely and review after the event.'] as $a=>$b)<div class="card"><div class="card-body"><h3>{{ $a }}</h3><p class="muted">{{ $b }}</p></div></div>@endforeach</div></div></section>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Find and book outstanding DJs for weddings, parties, clubs and corporate events."><title>@yield('title','BeatBook — Find Your Perfect DJ')</title>@vite(['resources/css/app.css','resources/js/app.js'])</head><body><header><div class="shell nav"><a class="brand" href="{{ route('home') }}"><b>◉</b> BeatBook</a><nav class="navlinks"><a href="{{ route('djs.index') }}">Find a DJ</a><a href="/#how">How it works</a><a href="/#for-djs">For DJs</a><a href="/#pricing">Pricing</a></nav><div style="display:flex;gap:9px">@auth<a class="button ghost" href="{{ route('dashboard') }}">Dashboard</a>@else<a class="button ghost" href="/login">Sign in</a><a class="button" href="/register">Join</a>@endauth</div></div></header>@if(session('success'))<div class="shell" style="padding:14px;background:#064e3b;border-radius:12px">{{ session('success') }}</div>@endif>@yield('content')<footer class="section" style="border-top:1px solid #1e293b"><div class="shell"><b class="brand">BeatBook</b><p class="muted">DJs for every moment. Secure enquiries, bookings and verified reviews.</p><p><a href="{{ route('privacy') }}" class="muted">Privacy</a> · <a href="{{ route('cookies') }}" class="muted">Cookies</a> · <a href="{{ route('terms') }}" class="muted">Terms</a></p></div></footer><div class="cookie" id="cookie"><strong>Your privacy matters</strong><p style="margin:6px 0 12px">We use necessary cookies to run BeatBook. Analytics and marketing remain off until you choose them.</p><button class="button" onclick="localStorage.setItem('cookieChoice','necessary');document.getElementById('cookie').remove()">Necessary only</button> <button class="button ghost" style="color:#0f172a" onclick="localStorage.setItem('cookieChoice','all');document.getElementById('cookie').remove()">Accept all</button></div><script>if(localStorage.getItem('cookieChoice'))document.getElementById('cookie')?.remove()</script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('title','Cookie Policy') @section('content')<main class="section"><div class="shell" style="max-width:850px"><h1>Cookie Policy</h1><p class="muted" style="font-size:18px;line-height:1.8">Necessary cookies operate the service. Analytics and marketing cookies require consent. Consent choices must be persisted server-side for production auditing.</p></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('title','Privacy Policy') @section('content')<main class="section"><div class="shell" style="max-width:850px"><h1>Privacy Policy</h1><p class="muted" style="font-size:18px;line-height:1.8">BeatBook is designed for UK GDPR compliance, data minimisation, access/export and deletion workflows. Replace this launch placeholder with counsel-reviewed policy text before production.</p></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('layouts.app') @section('title','Terms of Service') @section('content')<main class="section"><div class="shell" style="max-width:850px"><h1>Terms of Service</h1><p class="muted" style="font-size:18px;line-height:1.8">Marketplace, DJ, client, cancellation, payment, refund and acceptable-use terms must be finalised before production launch.</p></div></main>@endsection
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\DjController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/v1/djs', [DjController::class, 'apiIndex']);
|
||||
Route::middleware('auth:sanctum')->get('/v1/me', fn ($r) => $r->user());
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\DjController;
|
||||
use App\Http\Controllers\EnquiryController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\ReviewController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
Route::get('/djs', [DjController::class, 'index'])->name('djs.index');
|
||||
Route::get('/dj/{djProfile:slug}', [DjController::class, 'show'])->name('djs.show');
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('/dj/{djProfile}/enquire', [EnquiryController::class, 'store'])->name('enquiries.store');
|
||||
Route::post('/bookings/{booking}/reviews', [ReviewController::class, 'store'])->name('reviews.store');
|
||||
Route::view('/dashboard', 'client.dashboard')->name('dashboard');
|
||||
});
|
||||
Route::middleware(['auth', 'role:admin|super-admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', DashboardController::class)->name('dashboard');
|
||||
});
|
||||
Route::view('/privacy', 'legal.privacy')->name('privacy');
|
||||
Route::view('/cookies', 'legal.cookies')->name('cookies');
|
||||
Route::view('/terms', 'legal.terms')->name('terms');
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,10 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
lsp-*.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1 @@
|
||||
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({plugins:[laravel({input:['resources/css/app.css','resources/js/app.js'],refresh:true}),tailwindcss()]});
|
||||
Reference in New Issue
Block a user