updates for admin area
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# BeatBook UI / Backend Rework
|
||||
|
||||
## Completed
|
||||
- Replaced the generic admin placeholder with real section views for users, DJs, clients, bookings, payments, reviews, messages, notifications, support, reports, content, roles, privacy and settings.
|
||||
- Added admin actions for role changes, DJ publish/feature controls, review moderation and cache clearing.
|
||||
- Replaced the non-clickable client dashboard with account routes for overview, enquiries, bookings and editable account details.
|
||||
- Added responsive admin navigation with a mobile drawer.
|
||||
- Added responsive public navigation and mobile menu.
|
||||
- Rebuilt home, DJ directory and DJ profile layouts for desktop, tablet and mobile.
|
||||
- Added sorting to the DJ directory.
|
||||
- Added route coverage tests for account and admin pages.
|
||||
- Made auth tests explicitly bypass request-forgery middleware during automated tests only.
|
||||
- Restored a clean Laravel .gitignore and ensured composer.lock is not ignored.
|
||||
- Updated Laravel 13 request-forgery middleware API usage.
|
||||
|
||||
## Notes
|
||||
Messages, notifications and support now have proper destinations and admin presentation, but the project currently has no dedicated conversation/support database schema. Their pages therefore show deliberate empty states until those workflows/models are added.
|
||||
|
||||
After applying the files run:
|
||||
|
||||
composer install
|
||||
npm install
|
||||
npm run build
|
||||
php artisan optimize:clear
|
||||
php artisan test
|
||||
./vendor/bin/pint
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return view('account.dashboard', [
|
||||
'user' => $user,
|
||||
'enquiries' => $user->enquiries()->with('djProfile')->latest()->take(5)->get(),
|
||||
'bookings' => $user->bookings()->with('djProfile')->latest('event_date')->take(5)->get(),
|
||||
'stats' => [
|
||||
'enquiries' => $user->enquiries()->count(),
|
||||
'bookings' => $user->bookings()->count(),
|
||||
'upcoming' => $user->bookings()->whereDate('event_date', '>=', today())->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function enquiries(Request $request): View
|
||||
{
|
||||
return view('account.enquiries', [
|
||||
'enquiries' => $request->user()->enquiries()->with('djProfile')->latest()->paginate(12),
|
||||
]);
|
||||
}
|
||||
|
||||
public function bookings(Request $request): View
|
||||
{
|
||||
return view('account.bookings', [
|
||||
'bookings' => $request->user()->bookings()->with(['djProfile', 'review'])->latest('event_date')->paginate(12),
|
||||
]);
|
||||
}
|
||||
|
||||
public function profile(Request $request): View
|
||||
{
|
||||
return view('account.profile', ['user' => $request->user()]);
|
||||
}
|
||||
|
||||
public function updateProfile(Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'email' => ['required', 'email', 'max:190', Rule::unique('users')->ignore($user->id)],
|
||||
'phone' => ['nullable', 'string', 'max:40'],
|
||||
'marketing_opt_in' => ['nullable', 'boolean'],
|
||||
]);
|
||||
$data['marketing_opt_in'] = $request->boolean('marketing_opt_in');
|
||||
$user->update($data);
|
||||
|
||||
return back()->with('success', 'Your account details have been updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DjProfile;
|
||||
use App\Models\Review;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
class ActionController extends Controller
|
||||
{
|
||||
public function userRole(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['role' => ['required', 'in:super-admin,admin,dj,client,moderator,support']]);
|
||||
$role = Role::firstOrCreate(['name' => $data['role'], 'guard_name' => 'web']);
|
||||
$user->syncRoles([$role]);
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
return back()->with('success', "{$user->name}'s role has been updated.");
|
||||
}
|
||||
|
||||
public function toggleDjPublished(DjProfile $djProfile): RedirectResponse
|
||||
{
|
||||
$djProfile->update(['is_published' => ! $djProfile->is_published]);
|
||||
|
||||
return back()->with('success', 'DJ publication status updated.');
|
||||
}
|
||||
|
||||
public function toggleDjFeatured(DjProfile $djProfile): RedirectResponse
|
||||
{
|
||||
$djProfile->update(['is_featured' => ! $djProfile->is_featured]);
|
||||
|
||||
return back()->with('success', 'DJ featured status updated.');
|
||||
}
|
||||
|
||||
public function reviewStatus(Request $request, Review $review): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['status' => ['required', 'in:pending,published,rejected']]);
|
||||
$review->update($data);
|
||||
|
||||
return back()->with('success', 'Review moderation status updated.');
|
||||
}
|
||||
|
||||
public function clearCaches(): RedirectResponse
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
return back()->with('success', 'Application cache cleared.');
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,25 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Booking;
|
||||
use App\Models\DjProfile;
|
||||
use App\Models\Enquiry;
|
||||
use App\Models\Review;
|
||||
use App\Models\User;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
public function __invoke(): View
|
||||
{
|
||||
return view('admin.dashboard', ['stats' => ['users' => User::count(), 'djs' => DjProfile::count(), 'bookings' => Booking::count(), 'reviews' => Review::where('status', 'pending')->count()]]);
|
||||
return view('admin.dashboard', [
|
||||
'stats' => [
|
||||
'users' => User::count(),
|
||||
'djs' => DjProfile::count(),
|
||||
'bookings' => Booking::count(),
|
||||
'pending reviews' => Review::where('status', 'pending')->count(),
|
||||
],
|
||||
'recentUsers' => User::latest()->take(6)->get(),
|
||||
'recentEnquiries' => Enquiry::with(['user', 'djProfile'])->latest()->take(6)->get(),
|
||||
'recentBookings' => Booking::with(['client', 'djProfile'])->latest()->take(6)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,29 +9,51 @@ use App\Models\DjProfile;
|
||||
use App\Models\Enquiry;
|
||||
use App\Models\Review;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class SectionController extends Controller
|
||||
{
|
||||
public function __invoke(string $section): View
|
||||
private const SECTIONS = [
|
||||
'users', 'djs', 'clients', 'bookings', 'payments', 'reviews', 'messages',
|
||||
'notifications', 'support', 'reports', 'content', 'roles', 'privacy', 'settings',
|
||||
];
|
||||
|
||||
public function __invoke(Request $request, string $section): View
|
||||
{
|
||||
$allowed = [
|
||||
'users', 'djs', 'clients', 'bookings', 'payments', 'reviews', 'messages',
|
||||
'notifications', 'support', 'reports', 'content', 'roles', 'privacy', 'settings',
|
||||
abort_unless(in_array($section, self::SECTIONS, true), 404);
|
||||
|
||||
$data = [
|
||||
'section' => $section,
|
||||
'counts' => [
|
||||
'users' => User::count(),
|
||||
'djs' => DjProfile::count(),
|
||||
'businesses' => Business::count(),
|
||||
'clients' => User::whereHas('roles', fn ($query) => $query->where('name', 'client'))->count(),
|
||||
'bookings' => Booking::count(),
|
||||
'enquiries' => Enquiry::count(),
|
||||
'reviews' => Review::count(),
|
||||
],
|
||||
];
|
||||
|
||||
abort_unless(in_array($section, $allowed, true), 404);
|
||||
$data += match ($section) {
|
||||
'users' => ['users' => User::with('roles')->latest()->paginate(20)],
|
||||
'djs' => ['djs' => DjProfile::with(['user', 'business'])->latest()->paginate(20)],
|
||||
'clients' => ['clients' => User::whereHas('roles', fn ($query) => $query->where('name', 'client'))->withCount(['enquiries', 'bookings'])->latest()->paginate(20)],
|
||||
'bookings' => ['bookings' => Booking::with(['client', 'djProfile'])->latest('event_date')->paginate(20)],
|
||||
'payments' => ['bookings' => Booking::with(['client', 'djProfile'])->latest()->paginate(20)],
|
||||
'reviews' => ['reviews' => Review::with(['user', 'djProfile'])->latest()->paginate(20)],
|
||||
'roles' => ['roles' => Role::withCount('users')->orderBy('name')->get()],
|
||||
'reports' => ['report' => [
|
||||
'booking_value' => Booking::sum('total'),
|
||||
'deposits' => Booking::sum('deposit'),
|
||||
'published_djs' => DjProfile::where('is_published', true)->count(),
|
||||
'pending_reviews' => Review::where('status', 'pending')->count(),
|
||||
]],
|
||||
default => [],
|
||||
};
|
||||
|
||||
$counts = [
|
||||
'users' => User::count(),
|
||||
'djs' => DjProfile::count(),
|
||||
'businesses' => Business::count(),
|
||||
'clients' => User::role('client')->count(),
|
||||
'bookings' => Booking::count(),
|
||||
'enquiries' => Enquiry::count(),
|
||||
'reviews' => Review::count(),
|
||||
];
|
||||
|
||||
return view('admin.section', compact('section', 'counts'));
|
||||
return view("admin.sections.{$section}", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,40 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DjProfile;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DjController extends Controller
|
||||
{
|
||||
public function index(Request $r)
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$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'));
|
||||
$query = DjProfile::query()->where('is_published', true)->with('genres');
|
||||
|
||||
if ($request->filled('location')) {
|
||||
$query->where('city', 'like', '%'.$request->string('location').'%');
|
||||
}
|
||||
|
||||
return view('djs.index', ['djs' => $q->orderByDesc('is_featured')->orderByDesc('average_rating')->paginate(12)->withQueryString()]);
|
||||
if ($request->filled('max_price')) {
|
||||
$query->where('price_from', '<=', $request->integer('max_price'));
|
||||
}
|
||||
|
||||
$query->orderByDesc('is_featured');
|
||||
|
||||
match ($request->string('sort')->toString()) {
|
||||
'price_low' => $query->orderBy('price_from'),
|
||||
'price_high' => $query->orderByDesc('price_from'),
|
||||
default => $query->orderByDesc('average_rating'),
|
||||
};
|
||||
|
||||
return view('djs.index', ['djs' => $query->paginate(12)->withQueryString()]);
|
||||
}
|
||||
|
||||
public function show(DjProfile $djProfile)
|
||||
public function show(DjProfile $djProfile): View
|
||||
{
|
||||
abort_unless($djProfile->is_published, 404);
|
||||
|
||||
return view('djs.show', ['dj' => $djProfile->load(['genres', 'reviews' => fn ($q) => $q->where('status', 'published')->latest()])]);
|
||||
return view('djs.show', [
|
||||
'dj' => $djProfile->load(['genres', 'reviews' => fn ($query) => $query->where('status', 'published')->latest()]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function apiIndex()
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->validateCsrfTokens(except: ['stripe/*']);
|
||||
$middleware->preventRequestForgery(except: ['stripe/*']);
|
||||
|
||||
$middleware->alias([
|
||||
'role' => RoleMiddleware::class,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-4ylyCDk1.css",
|
||||
"file": "assets/app-B873fQy4.css",
|
||||
"src": "resources/css/app.css",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
@@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-DMsN-rLE.js",
|
||||
"file": "assets/app-fQMWg8Nx.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
|
||||
+15
-3
@@ -1,4 +1,16 @@
|
||||
@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}}
|
||||
.nav-actions{display:flex;gap:9px;align-items:center}.inline-form{display:inline;margin:0}.footer{border-top:1px solid #1e293b}.flash{margin-top:14px;padding:14px 18px;border-radius:12px}.flash.success{background:#064e3b}.cookie p{margin:6px 0 12px}.cookie-secondary{color:#0f172a}.auth-page{min-height:calc(100vh - 160px);padding:70px 24px;background:radial-gradient(circle at 20% 15%,#3b076455,transparent 35%),radial-gradient(circle at 85% 35%,#164e6355,transparent 32%),#070a19}.auth-card{width:min(100%,520px);margin:0 auto;background:#10162d;border:1px solid #27304d;border-radius:24px;padding:34px;box-shadow:0 28px 80px #0008}.auth-card h1{margin:10px 0 8px;font-size:36px}.auth-form{display:grid;gap:16px;margin-top:26px}.auth-form label{display:grid;gap:8px;font-weight:700;color:#e2e8f0}.auth-form input{width:100%;border:1px solid #334155;background:#0b1022;color:#f8fafc;border-radius:12px;padding:14px 15px;font:inherit;outline:none}.auth-form input:focus{border-color:#a855f7;box-shadow:0 0 0 3px #a855f722}.auth-form .check-row{display:flex;grid-template-columns:auto 1fr;align-items:center;font-weight:500}.auth-form .check-row input{width:auto}.button.full{justify-content:center;width:100%;cursor:pointer}.auth-links{display:flex;flex-direction:column;gap:10px;margin-top:22px;color:#94a3b8}.auth-links a{color:#67e8f9;text-decoration:none}.form-errors{margin-top:18px;padding:14px 16px;border:1px solid #7f1d1d;background:#450a0a;color:#fecaca;border-radius:12px;display:grid;gap:5px}
|
||||
@media(max-width:900px){.nav-actions{gap:6px}.nav-actions .button{padding:10px 12px}.auth-page{padding:40px 16px}}@media(max-width:600px){.nav{height:auto;min-height:70px;gap:12px}.nav-actions{flex-wrap:wrap;justify-content:flex-end}.auth-card{padding:24px}.auth-card h1{font-size:30px}}
|
||||
|
||||
:root{--bg:#070914;--panel:#101526;--panel-2:#151c31;--line:#26304a;--text:#f8fafc;--muted:#9aa8bd;--purple:#7c3aed;--pink:#ec4899;--cyan:#22d3ee;--gold:#fbbf24;--white:#fff;--admin-bg:#f5f7fb;--admin-ink:#172033;--admin-muted:#65738a;--admin-line:#e4e9f1;--shadow:0 18px 55px rgba(0,0,0,.24)}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}a{color:inherit}.shell{width:min(100% - 40px,1240px);margin-inline:auto}.muted{color:var(--muted)}
|
||||
.site-header{height:76px;border-bottom:1px solid rgba(255,255,255,.07);background:rgba(7,9,20,.88);backdrop-filter:blur(18px);position:sticky;top:0;z-index:40}.nav{height:100%;display:flex;align-items:center;gap:32px}.brand{display:inline-flex;align-items:center;gap:8px;font-size:24px;font-weight:900;letter-spacing:-.04em;text-decoration:none;white-space:nowrap}.brand b{color:var(--pink)}.nav-panel{margin-left:auto;display:flex;align-items:center;gap:34px}.navlinks{display:flex;align-items:center;gap:26px}.navlinks a,.text-button{font-size:14px;font-weight:700;color:#dce5f4;text-decoration:none;background:none;border:0;padding:8px 0;cursor:pointer;font:inherit}.navlinks a:hover,.text-button:hover{color:#fff}.nav-actions{display:flex;align-items:center;gap:12px}.nav-account-link{display:flex;align-items:center;gap:8px;color:#fff;text-decoration:none;font-weight:800;font-size:14px}.mini-avatar{display:grid;place-items:center;width:31px;height:31px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--pink));font-size:12px}.mobile-menu-button{display:none;margin-left:auto;background:transparent;border:1px solid #34405d;color:#fff;border-radius:10px;width:42px;height:42px;font-size:21px}.inline-form{margin:0}.button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:46px;padding:0 19px;border:0;border-radius:12px;background:linear-gradient(135deg,var(--purple),var(--pink));color:#fff;font:inherit;font-size:14px;font-weight:850;text-decoration:none;cursor:pointer;box-shadow:0 9px 24px rgba(124,58,237,.24);transition:transform .18s ease,filter .18s ease}.button:hover{transform:translateY(-1px);filter:brightness(1.06)}.button.ghost{background:transparent;border:1px solid #3a4663;box-shadow:none}.button.small{min-height:40px;padding:0 15px}.button.full{width:100%}
|
||||
.hero{min-height:640px;position:relative;display:flex;align-items:center;background:url('https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=2200&q=84') center 38%/cover no-repeat}.hero-shade{position:absolute;inset:0;background:linear-gradient(90deg,rgba(7,9,20,.98) 0%,rgba(7,9,20,.91) 44%,rgba(7,9,20,.47) 72%,rgba(7,9,20,.18) 100%)}.hero-layout{position:relative;z-index:1}.hero-copy{max-width:790px;padding:82px 0}.eyebrow{color:#67e8f9;font-size:12px;font-weight:900;letter-spacing:.18em;text-transform:uppercase}.hero h1{max-width:760px;margin:14px 0 22px;font-size:clamp(46px,6vw,78px);line-height:.97;letter-spacing:-.06em}.hero h1 span{background:linear-gradient(90deg,#c084fc,#f472b6,#67e8f9);-webkit-background-clip:text;background-clip:text;color:transparent}.hero-copy>p{max-width:650px;color:#c3cede;font-size:18px;line-height:1.65}.searchbox,.filterbar{display:grid;gap:8px;background:#fff;border-radius:18px;padding:9px;box-shadow:0 24px 70px rgba(0,0,0,.42)}.searchbox{grid-template-columns:1.2fr 1fr 1fr auto;margin-top:30px}.searchbox label,.filterbar label{display:grid;gap:3px;padding:8px 12px;border-radius:11px;background:#f6f8fb}.searchbox label span,.filterbar label span{font-size:10px;font-weight:900;color:#64748b;text-transform:uppercase;letter-spacing:.08em}.searchbox input,.searchbox select,.filterbar input,.filterbar select{min-width:0;width:100%;border:0;outline:0;background:transparent;color:#172033;font:inherit;font-size:14px}.hero-proof{display:flex;gap:22px;flex-wrap:wrap;margin-top:18px;color:#cbd5e1;font-size:13px;font-weight:700}
|
||||
.section{padding:78px 0}.section-alt{background:#0c1120;border-block:1px solid rgba(255,255,255,.06)}.section-head{display:flex;justify-content:space-between;align-items:end;gap:24px;margin-bottom:28px}.section-head h2,.section-intro h2,.split-feature h2,.pricing-box h2{font-size:clamp(30px,4vw,44px);letter-spacing:-.04em;margin:6px 0 8px}.section-head p,.section-intro p,.split-feature .lead,.pricing-box p{color:var(--muted);line-height:1.65}.cards{display:grid;gap:20px}.dj-cards{grid-template-columns:repeat(4,minmax(0,1fr))}.card{background:var(--panel);border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:0 12px 34px rgba(0,0,0,.18)}.dj-card{min-width:0}.card-img{display:block;aspect-ratio:4/3;background-position:center;background-size:cover}.card-body{padding:18px}.card-title-line{display:flex;align-items:center;gap:8px}.card-title-line h3{margin:0;font-size:19px;letter-spacing:-.02em}.verified{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;background:#2563eb;color:#fff;font-size:12px;font-weight:900}.verified.large{width:25px;height:25px}.stars{color:var(--gold);font-size:14px;font-weight:800}.stars span{color:var(--muted);font-weight:600}.card-body>p{min-height:42px;margin:10px 0;color:var(--muted);font-size:14px;line-height:1.5}.card-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;padding-top:14px;border-top:1px solid var(--line)}.card-foot strong{font-size:17px}.card-foot a{color:#a5f3fc;text-decoration:none;font-size:13px;font-weight:800}.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:30px}.step-card{padding:26px;border:1px solid var(--line);border-radius:18px;background:linear-gradient(180deg,#11182b,#0e1425)}.step-card>span{font-size:12px;font-weight:900;color:#c084fc}.step-card h3{margin:26px 0 8px;font-size:21px}.step-card p{margin:0;color:var(--muted);line-height:1.55}.split-feature{display:grid;grid-template-columns:.9fr 1.1fr;gap:70px;align-items:center}.feature-list{display:grid;grid-template-columns:1fr 1fr;gap:14px}.feature-list>div{display:grid;gap:6px;padding:22px;background:var(--panel);border:1px solid var(--line);border-radius:16px}.feature-list span{color:var(--muted);line-height:1.5;font-size:14px}.pricing-section{padding-top:0}.pricing-box{display:flex;align-items:center;justify-content:space-between;gap:40px;padding:42px;border-radius:24px;background:linear-gradient(135deg,#1b1141,#10223b);border:1px solid #3a3f68}.pricing-box>div{max-width:760px}
|
||||
.directory-page{padding:68px 0 82px}.directory-heading{display:flex;justify-content:space-between;align-items:end;gap:20px}.directory-heading h1{font-size:clamp(40px,6vw,62px);margin:6px 0 6px;letter-spacing:-.05em}.directory-heading p{color:var(--muted)}.results-count,.metric-chip{display:inline-flex;align-items:center;padding:8px 12px;border-radius:999px;background:#171e31;border:1px solid var(--line);font-size:12px;font-weight:800;color:#cbd5e1}.filterbar{grid-template-columns:1.1fr 1fr 1fr auto;margin:28px 0 34px}.directory-empty{grid-column:1/-1}.pagination-wrap{margin-top:32px}.pagination-wrap nav{color:#fff}
|
||||
.profile-hero{height:360px;background:center/cover}.profile-shell{padding-bottom:80px}.profile-panel{position:relative;margin-top:-82px;background:#fff;color:#172033;border-radius:22px;padding:26px;display:grid;grid-template-columns:126px 1fr 210px;gap:24px;align-items:center;box-shadow:0 24px 70px rgba(0,0,0,.38)}.avatar{width:126px;height:126px;border-radius:16px;object-fit:cover}.profile-title{display:flex;align-items:center;gap:10px}.profile-title h1{margin:0;font-size:36px;letter-spacing:-.04em}.profile-summary p{color:#64748b}.chips{display:flex;gap:7px;flex-wrap:wrap}.chip{padding:6px 9px;border-radius:999px;background:#eef2f7;color:#526078;font-size:11px;font-weight:800}.profile-price{display:flex;flex-direction:column;align-items:flex-start;padding-left:22px;border-left:1px solid #e2e8f0}.profile-price>span,.profile-price small{color:#64748b}.profile-price strong{font-size:36px;line-height:1.1}.profile-price .button{margin-top:14px;width:100%}.profile-grid{display:grid;grid-template-columns:minmax(0,1fr) 370px;gap:34px;padding-top:38px}.profile-section{padding:4px 0 24px}.profile-section h2,.enquiry-card h2{font-size:27px;letter-spacing:-.03em}.profile-bio{color:#aab6c8;font-size:16px;line-height:1.8}.review-card{padding:20px 0;border-top:1px solid var(--line)}.review-card h3{margin:8px 0}.review-card p{color:var(--muted);line-height:1.65}.enquiry-card{height:max-content;position:sticky;top:100px;padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:19px}.enquiry-card>p{color:var(--muted)}.enquiry-form,.settings-form{display:grid;gap:14px}.enquiry-form label,.settings-form label{display:grid;gap:7px}.enquiry-form label span,.settings-form label span{font-size:12px;font-weight:800;color:#b8c5d8}.enquiry-form input,.enquiry-form textarea,.settings-form input,.settings-form select{width:100%;border:1px solid #34405c;background:#0b1020;color:#fff;border-radius:10px;padding:12px 13px;font:inherit}.enquiry-form textarea{min-height:120px;resize:vertical}
|
||||
.flash{margin-top:14px;padding:13px 16px;border-radius:12px}.flash.success{background:#064e3b;border:1px solid #047857}.site-footer{border-top:1px solid #1e293b;background:#080b16;padding:56px 0 24px}.footer-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:50px}.footer-grid>div{display:flex;flex-direction:column;gap:10px}.footer-grid p{max-width:470px;color:var(--muted);line-height:1.6}.footer-grid a:not(.brand){color:#9aa8bd;text-decoration:none;font-size:14px}.footer-bottom{margin-top:40px;padding-top:22px;border-top:1px solid #1b2235;color:#68768c;font-size:12px}.cookie{position:fixed;left:20px;right:20px;bottom:20px;z-index:60;max-width:720px;padding:18px;display:flex;align-items:center;justify-content:space-between;gap:20px;background:#fff;color:#172033;border-radius:16px;box-shadow:0 22px 70px rgba(0,0,0,.5)}.cookie p{margin:5px 0 0;color:#64748b;font-size:13px}.cookie-actions{display:flex;gap:8px;flex-shrink:0}.cookie .button.ghost{color:#172033;border-color:#cbd5e1}
|
||||
.auth-page{min-height:calc(100vh - 76px);padding:64px 20px;background:radial-gradient(circle at 20% 15%,rgba(124,58,237,.25),transparent 35%),radial-gradient(circle at 85% 35%,rgba(34,211,238,.13),transparent 32%),var(--bg)}.auth-card{width:min(100%,510px);margin:auto;background:var(--panel);border:1px solid var(--line);border-radius:22px;padding:32px;box-shadow:var(--shadow)}.auth-card h1{font-size:34px;margin:10px 0 8px}.auth-form{display:grid;gap:16px;margin-top:24px}.auth-form label{display:grid;gap:7px;font-weight:700;color:#dbe4f2}.auth-form input{width:100%;border:1px solid #34405d;background:#0a0f1d;color:#fff;border-radius:11px;padding:13px 14px;font:inherit}.auth-form .check-row{display:flex;align-items:center;gap:9px;font-weight:500}.auth-form .check-row input{width:auto}.auth-links{display:flex;flex-direction:column;gap:8px;margin-top:20px;color:var(--muted)}.auth-links a{color:#67e8f9;text-decoration:none}.form-errors{padding:12px 14px;border:1px solid #7f1d1d;background:#450a0a;color:#fecaca;border-radius:10px}
|
||||
.account-page{padding:54px 0 78px;min-height:70vh}.account-shell{display:grid;grid-template-columns:240px minmax(0,1fr);gap:28px}.account-sidebar{height:max-content;position:sticky;top:100px;background:var(--panel);border:1px solid var(--line);border-radius:18px;overflow:hidden}.account-person{display:flex;align-items:center;gap:11px;padding:18px;border-bottom:1px solid var(--line)}.account-avatar{width:42px;height:42px;display:grid;place-items:center;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--pink));font-weight:900}.account-person div:last-child{display:grid;min-width:0}.account-person span{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis}.account-nav{padding:9px}.account-nav a{display:block;padding:11px 12px;border-radius:9px;color:#bac6d8;text-decoration:none;font-size:14px;font-weight:700}.account-nav a:hover,.account-nav a.active{background:#1b2440;color:#fff}.account-content{min-width:0}.page-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;margin-bottom:24px}.page-heading h1{font-size:clamp(34px,4vw,48px);margin:6px 0;letter-spacing:-.045em}.page-heading p{margin:0;color:var(--muted)}.account-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px}.account-stat{padding:20px;background:var(--panel);border:1px solid var(--line);border-radius:15px}.account-stat span{display:block;color:var(--muted);font-size:12px;font-weight:800}.account-stat strong{display:block;margin-top:4px;font-size:30px}.dashboard-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}.panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:20px;min-width:0}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:14px}.panel-head h2{margin:0;font-size:19px}.panel-head a,.panel-head span{color:#67e8f9;text-decoration:none;font-size:12px;font-weight:800}.list-row{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:15px 0;border-top:1px solid var(--line);text-decoration:none}.list-row:first-of-type{margin-top:10px}.list-row div{display:grid;gap:3px}.list-row span{color:var(--muted);font-size:12px}.status-pill{display:inline-flex;align-items:center;width:max-content;padding:5px 8px;border-radius:999px;background:#e9eef6;color:#475569;font-size:11px!important;font-weight:800;white-space:nowrap}.empty-state{padding:28px 12px;color:var(--muted);text-align:center;line-height:1.6}.table-panel{padding:0;overflow:hidden}.responsive-table{width:100%;overflow-x:auto}.responsive-table table{width:100%;border-collapse:collapse;min-width:720px}.responsive-table th,.responsive-table td{padding:14px 16px;text-align:left;border-bottom:1px solid rgba(255,255,255,.08);font-size:13px;vertical-align:middle}.responsive-table th{color:#8f9db2;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.responsive-table td small{display:block;color:var(--muted);margin-top:3px}.form-panel{max-width:720px}.settings-form input{background:#0b1020}.check-line{display:flex!important;grid-template-columns:auto 1fr;align-items:center}.check-line input{width:auto!important}
|
||||
.admin-body{background:var(--admin-bg);color:var(--admin-ink)}.admin-shell{min-height:100vh;display:grid;grid-template-columns:248px minmax(0,1fr)}.admin-sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:20px 14px;background:#0b1020;color:#fff;overflow-y:auto;z-index:80}.admin-sidebar-head{display:flex;align-items:center;justify-content:space-between;padding:0 8px 18px}.admin-kicker{padding:0 10px 8px;color:#66748c;font-size:10px;font-weight:900;text-transform:uppercase;letter-spacing:.12em}.admin-nav{display:grid;gap:2px}.admin-nav a,.admin-sidebar-foot a,.admin-sidebar-foot button{display:block;width:100%;padding:9px 10px;border:0;border-radius:8px;background:transparent;color:#aab7ca;text-align:left;text-decoration:none;font:inherit;font-size:13px;font-weight:700;cursor:pointer}.admin-nav a:hover,.admin-nav a.active,.admin-sidebar-foot a:hover,.admin-sidebar-foot button:hover{background:#17203a;color:#fff}.admin-sidebar-foot{margin-top:auto;padding-top:16px;border-top:1px solid #202a42}.admin-content{min-width:0}.admin-topbar{height:66px;padding:0 28px;display:flex;align-items:center;justify-content:space-between;gap:15px;background:#fff;border-bottom:1px solid var(--admin-line);position:sticky;top:0;z-index:30}.admin-topbar>div{display:grid}.admin-topbar span{font-size:11px;color:var(--admin-muted)}.admin-page{padding:30px}.admin-heading h1{color:var(--admin-ink)}.admin-heading p{color:var(--admin-muted)}.admin-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:18px}.admin-stat{display:block;padding:20px;background:#fff;border:1px solid var(--admin-line);border-radius:14px;color:inherit;text-decoration:none;box-shadow:0 4px 18px rgba(31,45,75,.04)}.admin-stat span{display:block;color:var(--admin-muted);font-size:11px;font-weight:800;text-transform:uppercase}.admin-stat strong{display:block;margin:6px 0 4px;font-size:29px;letter-spacing:-.04em}.admin-stat small{color:#7c3aed;font-weight:800}.admin-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.admin-card{padding:20px;background:#fff;border:1px solid var(--admin-line);border-radius:14px;box-shadow:0 4px 18px rgba(31,45,75,.04);min-width:0}.admin-card-wide{grid-column:1/-1}.admin-card .panel-head h2{color:var(--admin-ink)}.admin-list-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 0;border-top:1px solid var(--admin-line);font-size:12px}.admin-list-row:first-of-type{margin-top:10px}.admin-list-row>div{display:grid}.admin-list-row span{color:var(--admin-muted)}.admin-card .responsive-table th,.admin-card .responsive-table td{border-color:var(--admin-line);color:var(--admin-ink)}.admin-card .responsive-table th{color:#7a879c}.table-card{padding:0;overflow:hidden}.admin-button,.toggle-button{display:inline-flex;align-items:center;justify-content:center;min-height:33px;padding:0 11px;border:0;border-radius:8px;background:#172033;color:#fff;font:inherit;font-size:11px;font-weight:800;text-decoration:none;cursor:pointer}.toggle-button{background:#e9eef6;color:#5a677c}.toggle-button.on{background:#dcfce7;color:#166534}.inline-controls{display:flex;align-items:center;gap:7px}.inline-controls select{min-width:110px;padding:7px 9px;border:1px solid #d5dce7;border-radius:8px;background:#fff;color:#172033;font:inherit;font-size:11px}.admin-muted{color:var(--admin-muted);line-height:1.6}.admin-flash{margin:18px 30px 0;padding:12px 15px;border-radius:10px;background:#dcfce7;color:#166534;border:1px solid #bbf7d0;font-size:13px}.settings-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.settings-list>div{display:grid;grid-template-columns:140px 1fr auto;gap:14px;padding:12px 0;border-top:1px solid var(--admin-line);font-size:12px}.settings-list>div:first-child{margin-top:10px}.settings-list span{color:var(--admin-muted)}.action-card{display:block;color:inherit;text-decoration:none}.action-card:hover{border-color:#b8a5ff}.action-card h2{margin-top:0}.action-card p{color:var(--admin-muted);line-height:1.6}.action-card>span{color:#6d28d9;font-size:12px;font-weight:900}.role-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.role-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:#ede9fe;color:#6d28d9;font-size:11px;font-weight:900}.role-count{display:block;font-size:30px;margin-top:16px}.feature-empty{text-align:center;padding:38px 15px}.feature-icon{width:54px;height:54px;margin:0 auto 14px;display:grid;place-items:center;border-radius:15px;background:#ede9fe;color:#6d28d9;font-size:24px;font-weight:900}.feature-empty p{max-width:560px;margin:0 auto 20px;color:var(--admin-muted);line-height:1.6}.icon-button{display:none;border:0;background:transparent;font-size:22px;cursor:pointer}.admin-overlay{display:none}
|
||||
@media(max-width:1100px){.dj-cards{grid-template-columns:repeat(3,minmax(0,1fr))}.steps{grid-template-columns:repeat(2,1fr)}.profile-grid{grid-template-columns:minmax(0,1fr) 330px}.admin-stats{grid-template-columns:repeat(2,1fr)}.role-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:900px){.shell{width:min(100% - 28px,1240px)}.site-header{height:68px}.nav{height:68px}.mobile-menu-button{display:block}.nav-panel{display:none;position:absolute;top:68px;left:14px;right:14px;margin:0;padding:14px;flex-direction:column;align-items:stretch;gap:12px;background:#0e1425;border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.nav-panel.open{display:flex}.navlinks,.nav-actions{align-items:stretch;flex-direction:column;gap:2px}.navlinks a,.nav-actions>a,.nav-actions .text-button{padding:11px 10px;text-align:left}.nav-actions{padding-top:8px;border-top:1px solid var(--line)}.nav-actions .button{width:100%}.hero{min-height:auto}.hero-copy{padding:74px 0}.searchbox,.filterbar{grid-template-columns:1fr 1fr}.searchbox .button,.filterbar .button{grid-column:1/-1}.dj-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.split-feature{grid-template-columns:1fr;gap:36px}.profile-panel{grid-template-columns:100px 1fr}.profile-price{grid-column:1/-1;padding:18px 0 0;border-left:0;border-top:1px solid #e2e8f0;display:grid;grid-template-columns:auto auto 1fr auto;gap:8px;align-items:center}.profile-price .button{margin:0}.profile-grid{grid-template-columns:1fr}.enquiry-card{position:static}.account-shell{grid-template-columns:1fr}.account-sidebar{position:static}.account-nav{display:flex;gap:4px;overflow-x:auto}.account-nav a{white-space:nowrap}.dashboard-grid{grid-template-columns:1fr}.admin-shell{display:block}.admin-sidebar{position:fixed;left:0;top:0;width:270px;transform:translateX(-105%);transition:transform .2s ease}.admin-sidebar.open{transform:none}.admin-overlay{position:fixed;inset:0;background:rgba(11,16,32,.55);z-index:70}.admin-overlay.open{display:block}.admin-menu,.admin-close{display:inline-grid;place-items:center}.admin-topbar{padding:0 18px}.admin-page{padding:22px 18px}.admin-flash{margin:14px 18px 0}.admin-grid{grid-template-columns:1fr}.settings-grid{grid-template-columns:1fr}}
|
||||
@media(max-width:640px){.shell{width:min(100% - 22px,1240px)}.brand{font-size:21px}.hero-shade{background:linear-gradient(180deg,rgba(7,9,20,.78),rgba(7,9,20,.97))}.hero-copy{padding:58px 0}.hero h1{font-size:44px}.hero-copy>p{font-size:16px}.searchbox,.filterbar{grid-template-columns:1fr;padding:8px}.searchbox .button,.filterbar .button{grid-column:auto}.hero-proof{gap:9px 16px;font-size:11px}.section{padding:58px 0}.section-head,.directory-heading,.page-heading,.pricing-box{align-items:flex-start;flex-direction:column}.section-head .button{width:100%}.dj-cards{grid-template-columns:1fr}.card-img{aspect-ratio:16/10}.steps{grid-template-columns:1fr}.feature-list{grid-template-columns:1fr}.pricing-box{padding:28px}.pricing-box .button{width:100%}.footer-grid{grid-template-columns:1fr;gap:28px}.cookie{left:10px;right:10px;bottom:10px;flex-direction:column;align-items:stretch}.cookie-actions{display:grid;grid-template-columns:1fr 1fr}.profile-hero{height:230px}.profile-shell{width:100%}.profile-panel{margin:-34px 11px 0;padding:18px;grid-template-columns:72px 1fr;gap:14px;border-radius:17px}.avatar{width:72px;height:72px}.profile-title h1{font-size:27px}.profile-summary p{font-size:13px}.profile-price{grid-template-columns:auto 1fr}.profile-price strong{font-size:30px}.profile-price small{display:none}.profile-price .button{grid-column:1/-1}.profile-grid{padding:28px 11px 0}.directory-page{padding:44px 0 60px}.directory-heading h1{font-size:42px}.account-page{padding:36px 0 58px}.account-person{display:none}.account-sidebar{border-radius:12px}.account-stats{grid-template-columns:1fr 1fr}.account-stat:last-child{grid-column:1/-1}.panel{padding:16px}.admin-topbar .button{display:none}.admin-page{padding:18px 12px}.admin-stats{grid-template-columns:1fr 1fr;gap:10px}.admin-stat{padding:15px}.admin-stat strong{font-size:24px}.settings-list>div{grid-template-columns:1fr}.role-grid{grid-template-columns:1fr}.inline-controls{align-items:stretch;flex-direction:column}.inline-controls select,.inline-controls .admin-button{width:100%}.auth-card{padding:23px}.auth-page{padding:38px 12px}}
|
||||
|
||||
+30
-1
@@ -1 +1,30 @@
|
||||
import './bootstrap';
|
||||
import './bootstrap';
|
||||
|
||||
const menuButton = document.querySelector('[data-menu-toggle]');
|
||||
const menuPanel = document.querySelector('[data-menu-panel]');
|
||||
|
||||
menuButton?.addEventListener('click', () => {
|
||||
const open = menuPanel?.classList.toggle('open') ?? false;
|
||||
menuButton.setAttribute('aria-expanded', String(open));
|
||||
menuButton.textContent = open ? '×' : '☰';
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-menu-panel] a').forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
menuPanel?.classList.remove('open');
|
||||
menuButton?.setAttribute('aria-expanded', 'false');
|
||||
if (menuButton) menuButton.textContent = '☰';
|
||||
});
|
||||
});
|
||||
|
||||
const adminSidebar = document.querySelector('#adminSidebar');
|
||||
const adminOverlay = document.querySelector('[data-admin-overlay]');
|
||||
const setAdminMenu = (open) => {
|
||||
adminSidebar?.classList.toggle('open', open);
|
||||
adminOverlay?.classList.toggle('open', open);
|
||||
document.body.classList.toggle('admin-menu-open', open);
|
||||
};
|
||||
|
||||
document.querySelector('[data-admin-open]')?.addEventListener('click', () => setAdminMenu(true));
|
||||
document.querySelector('[data-admin-close]')?.addEventListener('click', () => setAdminMenu(false));
|
||||
adminOverlay?.addEventListener('click', () => setAdminMenu(false));
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.account')
|
||||
@section('title','My bookings — BeatBook')
|
||||
@section('account-content')
|
||||
<div class="page-heading"><div><div class="eyebrow">Your account</div><h1>Bookings</h1><p>Your confirmed and pending events in one place.</p></div></div>
|
||||
<div class="panel table-panel"><div class="responsive-table"><table><thead><tr><th>DJ</th><th>Event date</th><th>Total</th><th>Deposit</th><th>Payment</th><th>Status</th></tr></thead><tbody>
|
||||
@forelse($bookings as $item)<tr><td><strong>{{ $item->djProfile?->stage_name ?? 'DJ' }}</strong></td><td>{{ optional($item->event_date)->format('j M Y') }}</td><td>£{{ number_format((float)$item->total,2) }}</td><td>£{{ number_format((float)$item->deposit,2) }}</td><td>{{ $item->paid_at ? 'Paid' : 'Outstanding' }}</td><td><span class="status-pill">{{ ucfirst($item->status) }}</span></td></tr>@empty<tr><td colspan="6"><div class="empty-state">No bookings yet.</div></td></tr>@endforelse
|
||||
</tbody></table></div></div>{{ $bookings->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,16 @@
|
||||
@extends('layouts.account')
|
||||
@section('title','Dashboard — BeatBook')
|
||||
@section('account-content')
|
||||
<div class="page-heading"><div><div class="eyebrow">Your account</div><h1>Welcome back, {{ Str::before($user->name, ' ') }}</h1><p>Manage your enquiries, bookings and account details.</p></div><a class="button" href="{{ route('djs.index') }}">Find a DJ</a></div>
|
||||
<div class="account-stats">
|
||||
@foreach($stats as $label => $value)<div class="account-stat"><span>{{ ucfirst($label) }}</span><strong>{{ $value }}</strong></div>@endforeach
|
||||
</div>
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel"><div class="panel-head"><h2>Recent enquiries</h2><a href="{{ route('account.enquiries') }}">View all</a></div>
|
||||
@forelse($enquiries as $enquiry)<a class="list-row" href="{{ route('account.enquiries') }}"><div><strong>{{ $enquiry->djProfile?->stage_name ?? 'DJ' }}</strong><span>{{ $enquiry->event_type }} · {{ optional($enquiry->event_date)->format('j M Y') }}</span></div><span class="status-pill">{{ ucfirst($enquiry->status) }}</span></a>@empty<div class="empty-state">No enquiries yet. Find a DJ and send your first enquiry.</div>@endforelse
|
||||
</section>
|
||||
<section class="panel"><div class="panel-head"><h2>Bookings</h2><a href="{{ route('account.bookings') }}">View all</a></div>
|
||||
@forelse($bookings as $booking)<a class="list-row" href="{{ route('account.bookings') }}"><div><strong>{{ $booking->djProfile?->stage_name ?? 'DJ' }}</strong><span>{{ optional($booking->event_date)->format('j M Y') }} · £{{ number_format((float)$booking->total,2) }}</span></div><span class="status-pill">{{ ucfirst($booking->status) }}</span></a>@empty<div class="empty-state">You do not have any bookings yet.</div>@endforelse
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.account')
|
||||
@section('title','My enquiries — BeatBook')
|
||||
@section('account-content')
|
||||
<div class="page-heading"><div><div class="eyebrow">Your account</div><h1>Enquiries</h1><p>Track every enquiry you have sent to a DJ.</p></div><a class="button" href="{{ route('djs.index') }}">New enquiry</a></div>
|
||||
<div class="panel table-panel"><div class="responsive-table"><table><thead><tr><th>DJ</th><th>Event</th><th>Date</th><th>Venue</th><th>Budget</th><th>Status</th></tr></thead><tbody>
|
||||
@forelse($enquiries as $item)<tr><td><strong>{{ $item->djProfile?->stage_name ?? 'DJ' }}</strong></td><td>{{ $item->event_type }}</td><td>{{ optional($item->event_date)->format('j M Y') }}</td><td>{{ $item->venue }}</td><td>{{ $item->budget ? '£'.number_format((float)$item->budget,2) : '—' }}</td><td><span class="status-pill">{{ ucfirst($item->status) }}</span></td></tr>@empty<tr><td colspan="6"><div class="empty-state">No enquiries yet.</div></td></tr>@endforelse
|
||||
</tbody></table></div></div>{{ $enquiries->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,13 @@
|
||||
@extends('layouts.account')
|
||||
@section('title','Account details — BeatBook')
|
||||
@section('account-content')
|
||||
<div class="page-heading"><div><div class="eyebrow">Your account</div><h1>Account details</h1><p>Keep your contact information up to date.</p></div></div>
|
||||
<div class="panel form-panel"><form method="post" action="{{ route('account.profile.update') }}" class="settings-form">@csrf @method('PATCH')
|
||||
<label><span>Name</span><input name="name" value="{{ old('name',$user->name) }}" required></label>
|
||||
<label><span>Email</span><input type="email" name="email" value="{{ old('email',$user->email) }}" required></label>
|
||||
<label><span>Phone</span><input name="phone" value="{{ old('phone',$user->phone) }}"></label>
|
||||
<label class="check-line"><input type="checkbox" name="marketing_opt_in" value="1" @checked(old('marketing_opt_in',$user->marketing_opt_in))><span>Send me relevant BeatBook news and offers.</span></label>
|
||||
@if($errors->any())<div class="form-errors">@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach</div>@endif
|
||||
<div><button class="button" type="submit">Save changes</button></div>
|
||||
</form></div>
|
||||
@endsection
|
||||
@@ -1,41 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>BeatBook Admin</title>
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="{{ route('admin.dashboard') }}"><b>◉</b> BeatBook</a>
|
||||
<p class="muted">Administration</p>
|
||||
<a href="{{ route('admin.dashboard') }}">Dashboard</a>
|
||||
@foreach([
|
||||
'users' => 'Users', 'djs' => 'DJs & Businesses', 'clients' => 'Clients',
|
||||
'bookings' => 'Bookings', 'payments' => 'Payments', 'reviews' => 'Reviews',
|
||||
'messages' => 'Messages', 'notifications' => 'Notifications', 'support' => 'Support',
|
||||
'reports' => 'Reports', 'content' => 'Content', 'roles' => 'Roles & Permissions',
|
||||
'privacy' => 'Privacy', 'settings' => 'Settings'
|
||||
] as $key => $label)
|
||||
<a href="{{ route('admin.section', $key) }}">{{ $label }}</a>
|
||||
@endforeach
|
||||
<a href="{{ route('home') }}">Back to site</a>
|
||||
</aside>
|
||||
<main class="admin-main">
|
||||
<h1>Dashboard</h1>
|
||||
<p style="color:#64748b">Marketplace overview and moderation queue.</p>
|
||||
<div class="stats">
|
||||
@foreach($stats as $key => $value)
|
||||
<div class="stat"><span>{{ ucfirst($key) }}</span><strong>{{ number_format($value) }}</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>
|
||||
</main>
|
||||
@extends('layouts.admin')
|
||||
@section('title','Dashboard')
|
||||
@section('page-title','Dashboard')
|
||||
@section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Marketplace control centre</div><h1>Dashboard</h1><p>Live overview of users, DJs, bookings and moderation.</p></div></div>
|
||||
<div class="admin-stats">@foreach($stats as $key=>$value)<a class="admin-stat" href="{{ $key === 'pending reviews' ? route('admin.section','reviews') : ($key === 'djs' ? route('admin.section','djs') : ($key === 'bookings' ? route('admin.section','bookings') : route('admin.section','users'))) }}"><span>{{ ucfirst($key) }}</span><strong>{{ number_format($value) }}</strong><small>Open section →</small></a>@endforeach</div>
|
||||
<div class="admin-grid">
|
||||
<section class="admin-card"><div class="panel-head"><h2>Newest users</h2><a href="{{ route('admin.section','users') }}">Manage</a></div>@forelse($recentUsers as $user)<div class="admin-list-row"><div><strong>{{ $user->name }}</strong><span>{{ $user->email }}</span></div><span>{{ $user->created_at?->diffForHumans() }}</span></div>@empty<div class="empty-state">No users yet.</div>@endforelse</section>
|
||||
<section class="admin-card"><div class="panel-head"><h2>Latest enquiries</h2><a href="{{ route('admin.section','clients') }}">Clients</a></div>@forelse($recentEnquiries as $item)<div class="admin-list-row"><div><strong>{{ $item->user?->name }}</strong><span>{{ $item->djProfile?->stage_name }} · {{ $item->event_type }}</span></div><span class="status-pill">{{ ucfirst($item->status) }}</span></div>@empty<div class="empty-state">No enquiries yet.</div>@endforelse</section>
|
||||
<section class="admin-card admin-card-wide"><div class="panel-head"><h2>Recent bookings</h2><a href="{{ route('admin.section','bookings') }}">View all</a></div><div class="responsive-table"><table><thead><tr><th>Client</th><th>DJ</th><th>Date</th><th>Total</th><th>Status</th></tr></thead><tbody>@forelse($recentBookings as $item)<tr><td>{{ $item->client?->name }}</td><td>{{ $item->djProfile?->stage_name }}</td><td>{{ optional($item->event_date)->format('j M Y') }}</td><td>£{{ number_format((float)$item->total,2) }}</td><td><span class="status-pill">{{ ucfirst($item->status) }}</span></td></tr>@empty<tr><td colspan="5">No bookings yet.</td></tr>@endforelse</tbody></table></div></section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Bookings') @section('page-title','Bookings') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Events</div><h1>Bookings</h1><p>All marketplace bookings and event values.</p></div><span class="metric-chip">{{ number_format($counts['bookings']) }} bookings</span></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>Client</th><th>DJ</th><th>Event date</th><th>Total</th><th>Deposit</th><th>Status</th></tr></thead><tbody>@forelse($bookings as $item)<tr><td>{{ $item->client?->name }}</td><td>{{ $item->djProfile?->stage_name }}</td><td>{{ optional($item->event_date)->format('j M Y') }}</td><td>£{{ number_format((float)$item->total,2) }}</td><td>£{{ number_format((float)$item->deposit,2) }}</td><td><span class="status-pill">{{ ucfirst($item->status) }}</span></td></tr>@empty<tr><td colspan="6">No bookings yet.</td></tr>@endforelse</tbody></table></div></div>{{ $bookings->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Clients') @section('page-title','Clients') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Customers</div><h1>Clients</h1><p>Review client activity across enquiries and bookings.</p></div><span class="metric-chip">{{ number_format($counts['clients']) }} clients</span></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>Client</th><th>Phone</th><th>Enquiries</th><th>Bookings</th><th>Joined</th></tr></thead><tbody>@forelse($clients as $client)<tr><td><strong>{{ $client->name }}</strong><small>{{ $client->email }}</small></td><td>{{ $client->phone ?: '—' }}</td><td>{{ $client->enquiries_count }}</td><td>{{ $client->bookings_count }}</td><td>{{ $client->created_at?->format('j M Y') }}</td></tr>@empty<tr><td colspan="5">No clients yet.</td></tr>@endforelse</tbody></table></div></div>{{ $clients->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Content') @section('page-title','Content') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Website</div><h1>Content</h1><p>Public-site content and marketplace presentation.</p></div></div>
|
||||
<div class="settings-grid"><a class="admin-card action-card" href="{{ route('home') }}"><h2>Homepage</h2><p>Review hero, featured DJs, how it works and DJ proposition.</p><span>Open page →</span></a><a class="admin-card action-card" href="{{ route('djs.index') }}"><h2>DJ directory</h2><p>Review public listings, filtering and profile presentation.</p><span>Open directory →</span></a><a class="admin-card action-card" href="{{ route('privacy') }}"><h2>Legal content</h2><p>Review privacy, cookie and terms content.</p><span>Open privacy →</span></a></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','DJs & businesses') @section('page-title','DJs & businesses') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Marketplace supply</div><h1>DJs & businesses</h1><p>Control visibility and featured placement.</p></div><span class="metric-chip">{{ number_format($counts['djs']) }} profiles</span></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>DJ</th><th>Owner</th><th>City</th><th>Price</th><th>Published</th><th>Featured</th></tr></thead><tbody>@forelse($djs as $dj)<tr><td><strong>{{ $dj->stage_name }}</strong><small>{{ $dj->business?->name ?: 'Independent' }}</small></td><td>{{ $dj->user?->email }}</td><td>{{ $dj->city }}</td><td>{{ $dj->price_from ? '£'.number_format((float)$dj->price_from) : '—' }}</td><td><form method="post" action="{{ route('admin.djs.published',$dj) }}">@csrf @method('PATCH')<button class="toggle-button {{ $dj->is_published ? 'on' : '' }}" type="submit">{{ $dj->is_published ? 'Published' : 'Hidden' }}</button></form></td><td><form method="post" action="{{ route('admin.djs.featured',$dj) }}">@csrf @method('PATCH')<button class="toggle-button {{ $dj->is_featured ? 'on' : '' }}" type="submit">{{ $dj->is_featured ? 'Featured' : 'Standard' }}</button></form></td></tr>@empty<tr><td colspan="6">No DJ profiles yet.</td></tr>@endforelse</tbody></table></div></div>{{ $djs->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Messages') @section('page-title','Messages') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Communication</div><h1>Messages</h1><p>Marketplace messaging will appear here once conversations are created.</p></div></div>
|
||||
<div class="admin-card"><div class="feature-empty"><div class="feature-icon">✉</div><h2>No conversations yet</h2><p>Enquiry messaging can be added here without exposing private contact details between clients and DJs.</p><a class="admin-button" href="{{ route('admin.section','clients') }}">View clients</a></div></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Notifications') @section('page-title','Notifications') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Delivery</div><h1>Notifications</h1><p>System alerts and delivery health.</p></div></div>
|
||||
<div class="settings-grid"><div class="admin-card"><h2>Notification channels</h2><div class="settings-list"><div><strong>Email</strong><span>Laravel mail channel</span><span class="status-pill">Configured</span></div><div><strong>Database</strong><span>In-app notifications</span><span class="status-pill">Ready</span></div></div></div><div class="admin-card"><h2>Recommended events</h2><p class="admin-muted">Booking confirmation, new enquiry, review request, payment receipt and support replies can be delivered from this area.</p></div></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Payments') @section('page-title','Payments') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Finance</div><h1>Payments</h1><p>Payment state and booking value overview.</p></div></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>Booking</th><th>Client</th><th>DJ</th><th>Total</th><th>Deposit</th><th>Payment</th></tr></thead><tbody>@forelse($bookings as $item)<tr><td>{{ Str::limit($item->id,10,'…') }}</td><td>{{ $item->client?->name }}</td><td>{{ $item->djProfile?->stage_name }}</td><td>£{{ number_format((float)$item->total,2) }}</td><td>£{{ number_format((float)$item->deposit,2) }}</td><td><span class="status-pill">{{ $item->paid_at ? 'Paid' : 'Outstanding' }}</span></td></tr>@empty<tr><td colspan="6">No payment records yet.</td></tr>@endforelse</tbody></table></div></div>{{ $bookings->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Privacy') @section('page-title','Privacy') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Compliance</div><h1>Privacy</h1><p>Quick access to the public legal policies and privacy controls.</p></div></div>
|
||||
<div class="settings-grid"><a class="admin-card action-card" href="{{ route('privacy') }}"><h2>Privacy policy</h2><p>Review the current public privacy policy.</p><span>Open policy →</span></a><a class="admin-card action-card" href="{{ route('cookies') }}"><h2>Cookie policy</h2><p>Review necessary, analytics and marketing cookie information.</p><span>Open policy →</span></a><a class="admin-card action-card" href="{{ route('terms') }}"><h2>Terms</h2><p>Review marketplace terms shown to users.</p><span>Open terms →</span></a></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Reports') @section('page-title','Reports') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Performance</div><h1>Reports</h1><p>Marketplace totals calculated from live BeatBook data.</p></div></div>
|
||||
<div class="admin-stats"><div class="admin-stat"><span>Booking value</span><strong>£{{ number_format((float)$report['booking_value'],2) }}</strong></div><div class="admin-stat"><span>Deposits</span><strong>£{{ number_format((float)$report['deposits'],2) }}</strong></div><div class="admin-stat"><span>Published DJs</span><strong>{{ $report['published_djs'] }}</strong></div><div class="admin-stat"><span>Pending reviews</span><strong>{{ $report['pending_reviews'] }}</strong></div></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Reviews') @section('page-title','Reviews') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Trust & safety</div><h1>Reviews</h1><p>Moderate feedback before it appears publicly.</p></div><span class="metric-chip">{{ number_format($counts['reviews']) }} reviews</span></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>Reviewer</th><th>DJ</th><th>Rating</th><th>Review</th><th>Status</th><th>Moderate</th></tr></thead><tbody>@forelse($reviews as $review)<tr><td>{{ $review->user?->name }}</td><td>{{ $review->djProfile?->stage_name }}</td><td>★ {{ $review->rating }}/5</td><td><strong>{{ $review->title ?: 'Review' }}</strong><small>{{ Str::limit($review->body,80) }}</small></td><td><span class="status-pill">{{ ucfirst($review->status) }}</span></td><td><form class="inline-controls" method="post" action="{{ route('admin.reviews.status',$review) }}">@csrf @method('PATCH')<select name="status"><option value="pending" @selected($review->status==='pending')>Pending</option><option value="published" @selected($review->status==='published')>Published</option><option value="rejected" @selected($review->status==='rejected')>Rejected</option></select><button class="admin-button">Save</button></form></td></tr>@empty<tr><td colspan="6">No reviews yet.</td></tr>@endforelse</tbody></table></div></div>{{ $reviews->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Roles & permissions') @section('page-title','Roles & permissions') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Access control</div><h1>Roles & permissions</h1><p>Roles currently available to BeatBook accounts.</p></div></div>
|
||||
<div class="role-grid">@foreach($roles as $role)<div class="admin-card"><span class="role-badge">{{ $role->name }}</span><strong class="role-count">{{ $role->users_count }}</strong><p>{{ Str::plural('user',$role->users_count) }}</p></div>@endforeach</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Settings') @section('page-title','Settings') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Application</div><h1>Settings</h1><p>Operational controls for the BeatBook installation.</p></div></div>
|
||||
<div class="settings-grid"><div class="admin-card"><h2>Environment</h2><div class="settings-list"><div><strong>Application</strong><span>{{ config('app.name') }}</span></div><div><strong>Environment</strong><span>{{ app()->environment() }}</span></div><div><strong>URL</strong><span>{{ config('app.url') }}</span></div></div></div><div class="admin-card"><h2>Maintenance</h2><p class="admin-muted">Clear application cache after configuration or content changes.</p><form method="post" action="{{ route('admin.settings.clear-cache') }}">@csrf<button class="admin-button" type="submit">Clear application cache</button></form></div></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Support') @section('page-title','Support') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">Customer care</div><h1>Support</h1><p>Central area for customer and DJ support cases.</p></div></div>
|
||||
<div class="admin-card"><div class="feature-empty"><div class="feature-icon">?</div><h2>No open support cases</h2><p>The support queue is ready for a ticket model and inbound support form when you enable that workflow.</p></div></div>
|
||||
@endsection
|
||||
@@ -0,0 +1,4 @@
|
||||
@extends('layouts.admin') @section('title','Users') @section('page-title','Users') @section('content')
|
||||
<div class="page-heading admin-heading"><div><div class="eyebrow">People</div><h1>Users</h1><p>Manage accounts and assign marketplace roles.</p></div><span class="metric-chip">{{ number_format($counts['users']) }} users</span></div>
|
||||
<div class="admin-card table-card"><div class="responsive-table"><table><thead><tr><th>User</th><th>Role</th><th>Joined</th><th>Change role</th></tr></thead><tbody>@foreach($users as $user)<tr><td><strong>{{ $user->name }}</strong><small>{{ $user->email }}</small></td><td>{{ $user->getRoleNames()->join(', ') ?: 'No role' }}</td><td>{{ $user->created_at?->format('j M Y') }}</td><td><form class="inline-controls" method="post" action="{{ route('admin.users.role',$user) }}">@csrf @method('PATCH')<select name="role">@foreach(['client','dj','support','moderator','admin','super-admin'] as $role)<option value="{{ $role }}" @selected($user->hasRole($role))>{{ ucfirst($role) }}</option>@endforeach</select><button class="admin-button" type="submit">Save</button></form></td></tr>@endforeach</tbody></table></div></div>{{ $users->links() }}
|
||||
@endsection
|
||||
@@ -1 +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>
|
||||
<article class="card dj-card"><a class="card-img" href="{{ route('djs.show',$dj) }}" style="background-image:url('{{ $dj->profile_photo ?: 'https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=900&q=82' }}')"></a><div class="card-body"><div class="card-title-line"><h3>{{ $dj->stage_name }}</h3>@if($dj->verified_at)<span class="verified" title="Verified">✓</span>@endif</div><div class="stars">★★★★★ <span>{{ number_format($dj->average_rating,1) }} ({{ $dj->review_count }})</span></div><p>{{ $dj->city }}@if($dj->headline) · {{ Str::limit($dj->headline,48) }}@endif</p><div class="card-foot"><strong>{{ $dj->price_from ? 'From £'.number_format($dj->price_from,0) : 'Ask for a quote' }}</strong><a href="{{ route('djs.show',$dj) }}">View profile →</a></div></div></article>
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
@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
|
||||
@extends('layouts.app')
|
||||
@section('title','Find DJs — BeatBook')
|
||||
@section('content')
|
||||
<main class="directory-page"><div class="shell"><div class="directory-heading"><div><div class="eyebrow">Discover</div><h1>Find your DJ</h1><p>Search trusted DJs by location and budget.</p></div><span class="results-count">{{ $djs->total() }} {{ Str::plural('DJ',$djs->total()) }}</span></div>
|
||||
<form class="filterbar" method="get"><label><span>Location</span><input name="location" value="{{ request('location') }}" placeholder="Town or city"></label><label><span>Maximum price</span><input name="max_price" type="number" min="0" value="{{ request('max_price') }}" placeholder="Any budget"></label><label><span>Sort</span><select name="sort"><option value="rating" @selected(request('sort')==='rating')>Highest rated</option><option value="price_low" @selected(request('sort')==='price_low')>Price: low to high</option><option value="price_high" @selected(request('sort')==='price_high')>Price: high to low</option></select></label><button class="button" type="submit">Apply filters</button></form>
|
||||
<div class="cards dj-cards directory-cards">@forelse($djs as $dj) @include('djs._card') @empty <div class="empty-state directory-empty"><h2>No DJs matched your search</h2><p>Try removing a filter or searching a nearby town.</p><a class="button ghost" href="{{ route('djs.index') }}">Clear filters</a></div>@endforelse</div><div class="pagination-wrap">{{ $djs->withQueryString()->links() }}</div></div></main>
|
||||
@endsection
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
@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="{{ route('login') }}">Sign in</a>@endauth</aside></div></section></main>@endsection
|
||||
@extends('layouts.app')
|
||||
@section('title',$dj->stage_name.' — BeatBook')
|
||||
@section('content')
|
||||
<div class="profile-hero" style="background-image:linear-gradient(180deg,rgba(7,10,25,.1),rgba(7,10,25,.78)),url('{{ $dj->cover_photo ?: $dj->profile_photo }}')"></div>
|
||||
<main class="shell profile-shell"><section class="profile-panel"><img class="avatar" src="{{ $dj->profile_photo }}" alt="{{ $dj->stage_name }}"><div class="profile-summary"><div class="profile-title"><h1>{{ $dj->stage_name }}</h1>@if($dj->verified_at)<span class="verified large">✓</span>@endif</div><div class="stars">★★★★★ <span>{{ $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 class="profile-price"><span>From</span><strong>£{{ number_format($dj->price_from,0) }}</strong><small>per event</small><a class="button" href="#enquire">Request a quote</a></aside></section>
|
||||
<div class="profile-grid"><section class="profile-main"><div class="profile-section"><h2>About {{ $dj->stage_name }}</h2><p class="profile-bio">{{ $dj->bio }}</p></div><div class="profile-section"><div class="panel-head"><h2>Reviews</h2><span>{{ $dj->review_count }} total</span></div>@forelse($dj->reviews->where('status','published') as $review)<article class="review-card"><div class="stars">{{ str_repeat('★',$review->rating) }}</div><h3>{{ $review->title }}</h3><p>{{ $review->body }}</p></article>@empty<div class="empty-state">No published reviews yet.</div>@endforelse</div></section>
|
||||
<aside id="enquire" class="enquiry-card"><h2>Check availability</h2><p>Tell {{ $dj->stage_name }} about your event.</p>@auth<form class="enquiry-form" method="post" action="{{ route('enquiries.store',$dj) }}">@csrf<label><span>Event type</span><input name="event_type" placeholder="Wedding, party…" required></label><label><span>Event date</span><input name="event_date" type="date" required></label><label><span>Venue / town</span><input name="venue" required></label><label><span>Budget</span><input name="budget" type="number" placeholder="£"></label><label><span>Event details</span><textarea name="message" required placeholder="Guests, timings, music, venue details…"></textarea></label><button class="button full" type="submit">Send enquiry</button></form>@else<p class="muted">Sign in to send a secure enquiry.</p><a class="button full" href="{{ route('login') }}">Sign in to enquire</a>@endauth</aside></div></main>
|
||||
@endsection
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
@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><section id="for-djs" class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">For professionals</div><h2>Grow your DJ business</h2></div></div><div class="cards"><div class="card"><div class="card-body"><h3>Professional profile</h3><p class="muted">Show your sound, experience, service area and pricing in one trusted profile.</p></div></div><div class="card"><div class="card-body"><h3>Qualified enquiries</h3><p class="muted">Receive structured event details so you can quote quickly and accurately.</p></div></div><div class="card"><div class="card-body"><h3>Verified reviews</h3><p class="muted">Build reputation from completed BeatBook bookings.</p></div></div><div class="card"><div class="card-body"><h3>Business tools</h3><p class="muted">Manage bookings, payments and customer communication from your dashboard.</p></div></div></div></div></section><section id="pricing" class="section" style="background:#0d1228"><div class="shell"><div class="eyebrow">Straightforward pricing</div><h2>Built for clients and DJs</h2><p class="muted" style="max-width:720px">Browsing and creating a client account is free. DJ subscription and payment options can be configured from the BeatBook billing area as the marketplace launches.</p></div></section>@endsection
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="hero"><div class="hero-shade"></div><div class="shell hero-layout"><div class="hero-copy"><div class="eyebrow">Music creates memories</div><h1>Find the DJ that <span>fits your night.</span></h1><p>Discover trusted professionals for weddings, parties, corporate events and clubs. Compare profiles, reviews and prices, then enquire directly.</p><form class="searchbox" action="{{ route('djs.index') }}"><label><span>Where?</span><input name="location" placeholder="Town or postcode"></label><label><span>Event</span><select name="event_type"><option value="">Any event</option><option>Wedding</option><option>Party</option><option>Corporate</option><option>Club</option></select></label><label><span>Date</span><input type="date" name="date"></label><button class="button" type="submit">Search DJs</button></form><div class="hero-proof"><span>✓ Verified profiles</span><span>✓ Genuine reviews</span><span>✓ Secure enquiries</span></div></div></div></section>
|
||||
<section class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">Top talent</div><h2>Featured DJs</h2><p>Standout professionals ready for your next event.</p></div><a class="button ghost" href="{{ route('djs.index') }}">Browse all DJs</a></div><div class="cards dj-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 dj-card"><div class="card-img" style="background-image:url('https://images.unsplash.com/{{ $d[3] }}?auto=format&fit=crop&w=900&q=82')"></div><div class="card-body"><div class="card-title-line"><h3>{{ $d[0] }}</h3><span class="verified">✓</span></div><div class="stars">★★★★★ <span>4.9</span></div><p>{{ $d[1] }} · Weddings · Parties</p><div class="card-foot"><strong>From £{{ $d[2] }}</strong><a href="{{ route('djs.index') }}">View profile →</a></div></div></article>@endforeach @endforelse</div></div></section>
|
||||
<section id="how" class="section section-alt"><div class="shell"><div class="section-intro"><div class="eyebrow">Simple & secure</div><h2>From search to dance floor</h2><p>BeatBook keeps finding and booking entertainment straightforward.</p></div><div class="steps">@foreach([['01','Search','Find DJs by location, event and budget.'],['02','Compare','See experience, pricing and verified feedback.'],['03','Enquire','Share your event details securely.'],['04','Book','Confirm your event and review afterwards.']] as $step)<div class="step-card"><span>{{ $step[0] }}</span><h3>{{ $step[1] }}</h3><p>{{ $step[2] }}</p></div>@endforeach</div></div></section>
|
||||
<section id="for-djs" class="section"><div class="shell split-feature"><div><div class="eyebrow">For professionals</div><h2>Turn more enquiries into bookings.</h2><p class="lead">Build a trusted profile, receive structured leads and manage your work from one place.</p><a class="button" href="{{ route('register') }}">Join as a DJ</a></div><div class="feature-list"><div><strong>Professional presence</strong><span>Show your music, experience, service area and pricing.</span></div><div><strong>Better enquiries</strong><span>Get the event details you need before you quote.</span></div><div><strong>Reputation</strong><span>Build trust with reviews from completed bookings.</span></div><div><strong>One dashboard</strong><span>Keep enquiries, bookings and account details together.</span></div></div></div></section>
|
||||
<section id="pricing" class="section pricing-section"><div class="shell pricing-box"><div><div class="eyebrow">Straightforward pricing</div><h2>Free for clients. Built to grow with DJs.</h2><p>Clients can browse and enquire without a subscription. DJ plans and payment services can be introduced as the marketplace grows.</p></div><a class="button ghost" href="{{ route('register') }}">Create an account</a></div></section>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<main class="account-page">
|
||||
<div class="shell account-shell">
|
||||
<aside class="account-sidebar">
|
||||
<div class="account-person">
|
||||
<div class="account-avatar">{{ strtoupper(substr(auth()->user()->name,0,1)) }}</div>
|
||||
<div><strong>{{ auth()->user()->name }}</strong><span>{{ auth()->user()->email }}</span></div>
|
||||
</div>
|
||||
<nav class="account-nav">
|
||||
<a class="{{ request()->routeIs('dashboard') ? 'active' : '' }}" href="{{ route('dashboard') }}">Overview</a>
|
||||
<a class="{{ request()->routeIs('account.enquiries') ? 'active' : '' }}" href="{{ route('account.enquiries') }}">Enquiries</a>
|
||||
<a class="{{ request()->routeIs('account.bookings') ? 'active' : '' }}" href="{{ route('account.bookings') }}">Bookings</a>
|
||||
<a class="{{ request()->routeIs('account.profile*') ? 'active' : '' }}" href="{{ route('account.profile') }}">Account details</a>
|
||||
@if(auth()->user()->hasAnyRole(['admin','super-admin']))
|
||||
<a href="{{ route('admin.dashboard') }}">Administration</a>
|
||||
@endif
|
||||
</nav>
|
||||
</aside>
|
||||
<section class="account-content">@yield('account-content')</section>
|
||||
</div>
|
||||
</main>
|
||||
@endsection
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Administration') — BeatBook</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="admin-shell">
|
||||
<aside class="admin-sidebar" id="adminSidebar">
|
||||
<div class="admin-sidebar-head">
|
||||
<a class="brand" href="{{ route('admin.dashboard') }}"><b>◉</b> BeatBook</a>
|
||||
<button class="icon-button admin-close" type="button" data-admin-close aria-label="Close menu">×</button>
|
||||
</div>
|
||||
<div class="admin-kicker">Administration</div>
|
||||
<nav class="admin-nav">
|
||||
<a class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}">Overview</a>
|
||||
@foreach([
|
||||
'users' => 'Users', 'djs' => 'DJs & businesses', 'clients' => 'Clients', 'bookings' => 'Bookings',
|
||||
'payments' => 'Payments', 'reviews' => 'Reviews', 'messages' => 'Messages', 'notifications' => 'Notifications',
|
||||
'support' => 'Support', 'reports' => 'Reports', 'content' => 'Content', 'roles' => 'Roles & permissions',
|
||||
'privacy' => 'Privacy', 'settings' => 'Settings'
|
||||
] as $key => $label)
|
||||
<a class="{{ request()->is('admin/'.$key) ? 'active' : '' }}" href="{{ route('admin.section', $key) }}">{{ $label }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
<div class="admin-sidebar-foot">
|
||||
<a href="{{ route('home') }}">View public site</a>
|
||||
<a href="{{ route('dashboard') }}">My account</a>
|
||||
<form method="post" action="{{ route('logout') }}">@csrf<button type="submit">Sign out</button></form>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="admin-overlay" data-admin-overlay></div>
|
||||
<main class="admin-content">
|
||||
<header class="admin-topbar">
|
||||
<button class="icon-button admin-menu" type="button" data-admin-open aria-label="Open menu">☰</button>
|
||||
<div><strong>@yield('page-title', 'Administration')</strong><span>{{ auth()->user()->name }}</span></div>
|
||||
<a class="button small" href="{{ route('home') }}">Public site</a>
|
||||
</header>
|
||||
@if(session('success'))<div class="admin-flash">{{ session('success') }}</div>@endif
|
||||
<div class="admin-page">@yield('content')</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,69 +2,45 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<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>
|
||||
<header class="site-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="{{ route('home') }}#how">How it works</a>
|
||||
<a href="{{ route('home') }}#for-djs">For DJs</a>
|
||||
<a href="{{ route('home') }}#pricing">Pricing</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
@auth
|
||||
@if(auth()->user()->hasAnyRole(['admin', 'super-admin']))
|
||||
<a class="button ghost" href="{{ route('admin.dashboard') }}">Admin</a>
|
||||
@endif
|
||||
<a class="button ghost" href="{{ route('dashboard') }}">Dashboard</a>
|
||||
<form method="post" action="{{ route('logout') }}" class="inline-form">
|
||||
@csrf
|
||||
<button class="button" type="submit">Sign out</button>
|
||||
</form>
|
||||
@else
|
||||
<a class="button ghost" href="{{ route('login') }}">Sign in</a>
|
||||
<a class="button" href="{{ route('register') }}">Join</a>
|
||||
@endauth
|
||||
<button class="mobile-menu-button" type="button" data-menu-toggle aria-label="Open navigation" aria-expanded="false">☰</button>
|
||||
<div class="nav-panel" data-menu-panel>
|
||||
<nav class="navlinks">
|
||||
<a href="{{ route('djs.index') }}">Find a DJ</a>
|
||||
<a href="{{ route('home') }}#how">How it works</a>
|
||||
<a href="{{ route('home') }}#for-djs">For DJs</a>
|
||||
<a href="{{ route('home') }}#pricing">Pricing</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
@auth
|
||||
<a class="nav-account-link" href="{{ route('dashboard') }}"><span class="mini-avatar">{{ strtoupper(substr(auth()->user()->name,0,1)) }}</span><span>My account</span></a>
|
||||
@if(auth()->user()->hasAnyRole(['admin', 'super-admin']))<a class="button ghost small" href="{{ route('admin.dashboard') }}">Admin</a>@endif
|
||||
<form method="post" action="{{ route('logout') }}" class="inline-form">@csrf<button class="text-button" type="submit">Sign out</button></form>
|
||||
@else
|
||||
<a class="text-button" href="{{ route('login') }}">Sign in</a>
|
||||
<a class="button small" href="{{ route('register') }}">Join BeatBook</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="shell flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('status'))
|
||||
<div class="shell flash success">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))<div class="shell flash success">{{ session('success') }}</div>@endif
|
||||
@if(session('status'))<div class="shell flash success">{{ session('status') }}</div>@endif
|
||||
@yield('content')
|
||||
|
||||
<footer class="section footer">
|
||||
<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 class="site-footer">
|
||||
<div class="shell footer-grid"><div><a class="brand" href="{{ route('home') }}"><b>◉</b> BeatBook</a><p>DJs for every moment. Search, compare and book with confidence.</p></div><div><strong>Discover</strong><a href="{{ route('djs.index') }}">Find a DJ</a><a href="{{ route('home') }}#how">How it works</a><a href="{{ route('home') }}#for-djs">For DJs</a></div><div><strong>Legal</strong><a href="{{ route('privacy') }}">Privacy</a><a href="{{ route('cookies') }}">Cookies</a><a href="{{ route('terms') }}">Terms</a></div></div>
|
||||
<div class="shell footer-bottom">© {{ date('Y') }} BeatBook. All rights reserved.</div>
|
||||
</footer>
|
||||
|
||||
<div class="cookie" id="cookie">
|
||||
<strong>Your privacy matters</strong>
|
||||
<p>We use necessary cookies to run BeatBook. Analytics and marketing remain off until you choose them.</p>
|
||||
<button class="button" type="button" onclick="localStorage.setItem('cookieChoice','necessary');document.getElementById('cookie').remove()">Necessary only</button>
|
||||
<button class="button ghost cookie-secondary" type="button" onclick="localStorage.setItem('cookieChoice','all');document.getElementById('cookie').remove()">Accept all</button>
|
||||
</div>
|
||||
<script>
|
||||
if (localStorage.getItem('cookieChoice')) document.getElementById('cookie')?.remove();
|
||||
</script>
|
||||
<div class="cookie" id="cookie"><div><strong>Your privacy matters</strong><p>Necessary cookies keep BeatBook working. Optional analytics and marketing stay off unless you accept them.</p></div><div class="cookie-actions"><button class="button small" type="button" onclick="localStorage.setItem('cookieChoice','necessary');document.getElementById('cookie').remove()">Necessary only</button><button class="button ghost small" type="button" onclick="localStorage.setItem('cookieChoice','all');document.getElementById('cookie').remove()">Accept all</button></div></div>
|
||||
<script>if(localStorage.getItem('cookieChoice')) document.getElementById('cookie')?.remove();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-1
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Account\DashboardController as AccountDashboardController;
|
||||
use App\Http\Controllers\Account\PageController as AccountPageController;
|
||||
use App\Http\Controllers\Admin\ActionController as AdminActionController;
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\SectionController;
|
||||
use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||
@@ -31,12 +34,22 @@ Route::middleware('auth')->group(function () {
|
||||
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
|
||||
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::get('/dashboard', AccountDashboardController::class)->name('dashboard');
|
||||
Route::get('/account/enquiries', [AccountPageController::class, 'enquiries'])->name('account.enquiries');
|
||||
Route::get('/account/bookings', [AccountPageController::class, 'bookings'])->name('account.bookings');
|
||||
Route::get('/account/profile', [AccountPageController::class, 'profile'])->name('account.profile');
|
||||
Route::patch('/account/profile', [AccountPageController::class, 'updateProfile'])->name('account.profile.update');
|
||||
});
|
||||
|
||||
Route::middleware(['auth', 'role:admin|super-admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', DashboardController::class)->name('dashboard');
|
||||
Route::get('/{section}', SectionController::class)->name('section');
|
||||
Route::patch('/users/{user}/role', [AdminActionController::class, 'userRole'])->name('users.role');
|
||||
Route::patch('/djs/{djProfile}/published', [AdminActionController::class, 'toggleDjPublished'])->name('djs.published');
|
||||
Route::patch('/djs/{djProfile}/featured', [AdminActionController::class, 'toggleDjFeatured'])->name('djs.featured');
|
||||
Route::patch('/reviews/{review}/status', [AdminActionController::class, 'reviewStatus'])->name('reviews.status');
|
||||
Route::post('/settings/clear-cache', [AdminActionController::class, 'clearCaches'])->name('settings.clear-cache');
|
||||
});
|
||||
|
||||
Route::view('/privacy', 'legal.privacy')->name('privacy');
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AccountAndAdminRoutesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_authenticated_account_pages_render(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
foreach (['/dashboard', '/account/enquiries', '/account/bookings', '/account/profile'] as $uri) {
|
||||
$this->actingAs($user)->get($uri)->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_all_admin_pages_render_for_admin(): void
|
||||
{
|
||||
Role::create(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('admin');
|
||||
|
||||
$sections = ['users', 'djs', 'clients', 'bookings', 'payments', 'reviews', 'messages', 'notifications', 'support', 'reports', 'content', 'roles', 'privacy', 'settings'];
|
||||
|
||||
$this->actingAs($admin)->get('/admin')->assertSuccessful();
|
||||
foreach ($sections as $section) {
|
||||
$this->actingAs($admin)->get('/admin/'.$section)->assertSuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
@@ -11,6 +12,12 @@ class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(PreventRequestForgery::class);
|
||||
}
|
||||
|
||||
public function test_user_can_register(): void
|
||||
{
|
||||
Role::create(['name' => 'client', 'guard_name' => 'web']);
|
||||
|
||||
Reference in New Issue
Block a user