diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 2cfdcbf..27191df 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -5,13 +5,23 @@ declare(strict_types=1); namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Services\DashboardStatisticsService; use Illuminate\Http\Request; use Illuminate\View\View; class DashboardController extends Controller { - public function __invoke(Request $request): View + public function __invoke(Request $request, DashboardStatisticsService $statistics): View { - return view('admin.dashboard'); + $user = $request->user(); + abort_if($user === null, 401); + + $stats = $statistics->forUser($user); + + return view('admin.dashboard', [ + 'totalPages' => $stats['total_pages'], + 'totalSubscribers' => $stats['total_subscribers'], + 'activePages' => $stats['active_pages'], + ]); } } diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 613bcd9..a572ab7 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -28,7 +28,7 @@ class AuthenticatedSessionController extends Controller $request->session()->regenerate(); - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route('admin.dashboard', absolute: false)); } /** diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php index 712394a..018a63e 100644 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -35,6 +35,6 @@ class ConfirmablePasswordController extends Controller $request->session()->put('auth.password_confirmed_at', time()); - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route('admin.dashboard', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php index f64fa9b..a8f8f06 100644 --- a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -14,7 +14,7 @@ class EmailVerificationNotificationController extends Controller public function store(Request $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route('admin.dashboard', absolute: false)); } $request->user()->sendEmailVerificationNotification(); diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php index ee3cb6f..1b4447f 100644 --- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -15,7 +15,7 @@ class EmailVerificationPromptController extends Controller public function __invoke(Request $request): RedirectResponse|View { return $request->user()->hasVerifiedEmail() - ? redirect()->intended(route('dashboard', absolute: false)) + ? redirect()->intended(route('admin.dashboard', absolute: false)) : view('auth.verify-email'); } } diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 44a3930..2cb3036 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -46,6 +46,6 @@ class RegisteredUserController extends Controller Auth::login($user); - return redirect(route('dashboard', absolute: false)); + return redirect(route('admin.dashboard', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index 784765e..49fb0b2 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -15,13 +15,13 @@ class VerifyEmailController extends Controller public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('admin.dashboard', absolute: false).'?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('admin.dashboard', absolute: false).'?verified=1'); } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index a48eb8d..3e5185e 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -34,7 +34,7 @@ class ProfileController extends Controller $request->user()->save(); - return Redirect::route('profile.edit')->with('status', 'profile-updated'); + return Redirect::route('admin.profile.edit')->with('status', 'profile-updated'); } /** diff --git a/app/Services/DashboardStatisticsService.php b/app/Services/DashboardStatisticsService.php new file mode 100644 index 0000000..aa8e829 --- /dev/null +++ b/app/Services/DashboardStatisticsService.php @@ -0,0 +1,41 @@ +isSuperadmin()) { + $pagesQuery->where('user_id', $user->id); + } + + $now = Carbon::now(); + $totalPages = (clone $pagesQuery)->count(); + $pageIds = (clone $pagesQuery)->pluck('id'); + $totalSubscribers = $pageIds->isEmpty() + ? 0 + : Subscriber::whereIn('preregistration_page_id', $pageIds)->count(); + $activePages = (clone $pagesQuery) + ->where('start_date', '<=', $now) + ->where('end_date', '>=', $now) + ->count(); + + return [ + 'total_pages' => $totalPages, + 'total_subscribers' => $totalSubscribers, + 'active_pages' => $activePages, + ]; + } +} diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php new file mode 100644 index 0000000..fe86d83 --- /dev/null +++ b/resources/views/admin/dashboard.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.admin') + +@section('title', __('Dashboard')) + +@section('mobile_title', __('Dashboard')) + +@section('content') +
+
+
+

{{ __('Dashboard') }}

+

{{ __('Overview of your pre-registration activity.') }}

+
+ @can('create', \App\Models\PreregistrationPage::class) + + + {{ __('New page') }} + + @endcan +
+ +
+
+
+
+ +
+
+

{{ __('Total pages') }}

+

{{ number_format($totalPages) }}

+
+
+

+ @if (auth()->user()->isSuperadmin()) + {{ __('All pre-registration pages in the system.') }} + @else + {{ __('Pages you own.') }} + @endif +

+
+ +
+
+
+ +
+
+

{{ __('Total subscribers') }}

+

{{ number_format($totalSubscribers) }}

+
+
+

{{ __('Across all pages in this overview.') }}

+
+ +
+
+
+ +
+
+

{{ __('Active pages') }}

+

{{ number_format($activePages) }}

+
+
+

{{ __('Currently within the start and end date window.') }}

+
+
+ + @can('create', \App\Models\PreregistrationPage::class) +
+

{{ __('Ready to launch a new campaign?') }}

+ + {{ __('Create a pre-registration page') }} → + +
+ @endcan +
+@endsection diff --git a/resources/views/admin/pages/create.blade.php b/resources/views/admin/pages/create.blade.php new file mode 100644 index 0000000..f4d6cbd --- /dev/null +++ b/resources/views/admin/pages/create.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', __('New page')) + +@section('mobile_title', __('New page')) + +@section('content') +
+

{{ __('Create page') }}

+

{{ __('Form will be added in Step 8.') }}

+
+@endsection diff --git a/resources/views/admin/pages/edit.blade.php b/resources/views/admin/pages/edit.blade.php new file mode 100644 index 0000000..f15864e --- /dev/null +++ b/resources/views/admin/pages/edit.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', __('Edit') . ' — ' . $page->title) + +@section('mobile_title', __('Edit')) + +@section('content') +
+

{{ __('Edit page') }}

+

{{ __('Form will be added in Step 8.') }}

+
+@endsection diff --git a/resources/views/admin/pages/index.blade.php b/resources/views/admin/pages/index.blade.php new file mode 100644 index 0000000..8fad587 --- /dev/null +++ b/resources/views/admin/pages/index.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', __('Pages')) + +@section('mobile_title', __('Pages')) + +@section('content') +
+

{{ __('Pre-registration pages') }}

+

{{ __('Page list and CRUD will be added in the next step.') }}

+
+@endsection diff --git a/resources/views/admin/pages/show.blade.php b/resources/views/admin/pages/show.blade.php new file mode 100644 index 0000000..a5ed663 --- /dev/null +++ b/resources/views/admin/pages/show.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', $page->title) + +@section('mobile_title', $page->title) + +@section('content') +
+

{{ $page->title }}

+

{{ __('Detail view placeholder.') }}

+
+@endsection diff --git a/resources/views/admin/users/create.blade.php b/resources/views/admin/users/create.blade.php new file mode 100644 index 0000000..c3ad2f9 --- /dev/null +++ b/resources/views/admin/users/create.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', __('New user')) + +@section('mobile_title', __('New user')) + +@section('content') +
+

{{ __('Create user') }}

+

{{ __('Form will be added in Step 10.') }}

+
+@endsection diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php new file mode 100644 index 0000000..d851d0e --- /dev/null +++ b/resources/views/admin/users/edit.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.admin') + +@section('title', __('Edit user')) + +@section('mobile_title', __('Edit user')) + +@section('content') +
+

{{ __('Edit user') }}

+

{{ $user->email }}

+

{{ __('Form will be added in Step 10.') }}

+
+@endsection diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php new file mode 100644 index 0000000..6808d62 --- /dev/null +++ b/resources/views/admin/users/index.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.admin') + +@section('title', __('Users')) + +@section('mobile_title', __('Users')) + +@section('content') +
+

{{ __('Users') }}

+

{{ __('User management will be completed in a later step.') }}

+
+@endsection diff --git a/resources/views/components/admin/sidebar-link.blade.php b/resources/views/components/admin/sidebar-link.blade.php new file mode 100644 index 0000000..8baa369 --- /dev/null +++ b/resources/views/components/admin/sidebar-link.blade.php @@ -0,0 +1,15 @@ +@props([ + 'href', + 'active' => false, +]) + +@php + $base = 'group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition'; + $classes = $active + ? $base . ' bg-slate-800 text-white' + : $base . ' text-slate-300 hover:bg-slate-800/80 hover:text-white'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php new file mode 100644 index 0000000..1fc7e04 --- /dev/null +++ b/resources/views/layouts/admin.blade.php @@ -0,0 +1,128 @@ + + + + + + + + @hasSection('title')@yield('title') — @endif{{ config('app.name', 'PreRegister') }} + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + {{-- Mobile overlay --}} + + +
+ {{-- Sidebar --}} + + + {{-- Main column --}} +
+
+ + @yield('mobile_title', config('app.name', 'PreRegister')) +
+ +
+ @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + @yield('content') +
+
+
+ + diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c2d3a65..524e626 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -5,14 +5,14 @@
- +
@@ -34,7 +34,7 @@ - + {{ __('Profile') }} @@ -67,7 +67,7 @@
- + {{ __('Profile') }} diff --git a/resources/views/profile/partials/delete-user-form.blade.php b/resources/views/profile/partials/delete-user-form.blade.php index edeeb4a..a72b647 100644 --- a/resources/views/profile/partials/delete-user-form.blade.php +++ b/resources/views/profile/partials/delete-user-form.blade.php @@ -15,7 +15,7 @@ >{{ __('Delete Account') }} -
+ @csrf @method('delete') diff --git a/resources/views/profile/partials/update-profile-information-form.blade.php b/resources/views/profile/partials/update-profile-information-form.blade.php index 5ae3d35..b507a66 100644 --- a/resources/views/profile/partials/update-profile-information-form.blade.php +++ b/resources/views/profile/partials/update-profile-information-form.blade.php @@ -13,7 +13,7 @@ @csrf
-
+ @csrf @method('patch') diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 13dcb7c..378e8c3 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -27,7 +27,7 @@ class AuthenticationTest extends TestCase ]); $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); + $response->assertRedirect(route('admin.dashboard', absolute: false)); } public function test_users_can_not_authenticate_with_invalid_password(): void diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 705570b..144fb70 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -38,7 +38,7 @@ class EmailVerificationTest extends TestCase Event::assertDispatched(Verified::class); $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + $response->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1'); } public function test_email_is_not_verified_with_invalid_hash(): void diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php index ca28c6c..d59da2b 100644 --- a/tests/Feature/Auth/PasswordUpdateTest.php +++ b/tests/Feature/Auth/PasswordUpdateTest.php @@ -17,7 +17,7 @@ class PasswordUpdateTest extends TestCase $response = $this ->actingAs($user) - ->from('/profile') + ->from('/admin/profile') ->put('/password', [ 'current_password' => 'password', 'password' => 'new-password', @@ -26,7 +26,7 @@ class PasswordUpdateTest extends TestCase $response ->assertSessionHasNoErrors() - ->assertRedirect('/profile'); + ->assertRedirect('/admin/profile'); $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); } @@ -37,7 +37,7 @@ class PasswordUpdateTest extends TestCase $response = $this ->actingAs($user) - ->from('/profile') + ->from('/admin/profile') ->put('/password', [ 'current_password' => 'wrong-password', 'password' => 'new-password', @@ -46,6 +46,6 @@ class PasswordUpdateTest extends TestCase $response ->assertSessionHasErrorsIn('updatePassword', 'current_password') - ->assertRedirect('/profile'); + ->assertRedirect('/admin/profile'); } } diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 1489d0e..9ac4cbf 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -26,6 +26,6 @@ class RegistrationTest extends TestCase ]); $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); + $response->assertRedirect(route('admin.dashboard', absolute: false)); } } diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index 252fdcc..f1e33f0 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -16,7 +16,7 @@ class ProfileTest extends TestCase $response = $this ->actingAs($user) - ->get('/profile'); + ->get('/admin/profile'); $response->assertOk(); } @@ -27,14 +27,14 @@ class ProfileTest extends TestCase $response = $this ->actingAs($user) - ->patch('/profile', [ + ->patch('/admin/profile', [ 'name' => 'Test User', 'email' => 'test@example.com', ]); $response ->assertSessionHasNoErrors() - ->assertRedirect('/profile'); + ->assertRedirect('/admin/profile'); $user->refresh(); @@ -49,14 +49,14 @@ class ProfileTest extends TestCase $response = $this ->actingAs($user) - ->patch('/profile', [ + ->patch('/admin/profile', [ 'name' => 'Test User', 'email' => $user->email, ]); $response ->assertSessionHasNoErrors() - ->assertRedirect('/profile'); + ->assertRedirect('/admin/profile'); $this->assertNotNull($user->refresh()->email_verified_at); } @@ -67,7 +67,7 @@ class ProfileTest extends TestCase $response = $this ->actingAs($user) - ->delete('/profile', [ + ->delete('/admin/profile', [ 'password' => 'password', ]); @@ -85,14 +85,14 @@ class ProfileTest extends TestCase $response = $this ->actingAs($user) - ->from('/profile') - ->delete('/profile', [ + ->from('/admin/profile') + ->delete('/admin/profile', [ 'password' => 'wrong-password', ]); $response ->assertSessionHasErrorsIn('userDeletion', 'password') - ->assertRedirect('/profile'); + ->assertRedirect('/admin/profile'); $this->assertNotNull($user->fresh()); }