feat: Phase 2 - admin layout, dashboard, page CRUD, subscribers, user management
This commit is contained in:
@@ -5,13 +5,23 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\DashboardStatisticsService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
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'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class AuthenticatedSessionController extends Controller
|
|||||||
|
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
return redirect()->intended(route('admin.dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ class ConfirmablePasswordController extends Controller
|
|||||||
|
|
||||||
$request->session()->put('auth.password_confirmed_at', time());
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
return redirect()->intended(route('admin.dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class EmailVerificationNotificationController extends Controller
|
|||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
if ($request->user()->hasVerifiedEmail()) {
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
return redirect()->intended(route('admin.dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->user()->sendEmailVerificationNotification();
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class EmailVerificationPromptController extends Controller
|
|||||||
public function __invoke(Request $request): RedirectResponse|View
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
{
|
{
|
||||||
return $request->user()->hasVerifiedEmail()
|
return $request->user()->hasVerifiedEmail()
|
||||||
? redirect()->intended(route('dashboard', absolute: false))
|
? redirect()->intended(route('admin.dashboard', absolute: false))
|
||||||
: view('auth.verify-email');
|
: view('auth.verify-email');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,6 @@ class RegisteredUserController extends Controller
|
|||||||
|
|
||||||
Auth::login($user);
|
Auth::login($user);
|
||||||
|
|
||||||
return redirect(route('dashboard', absolute: false));
|
return redirect(route('admin.dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ class VerifyEmailController extends Controller
|
|||||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
if ($request->user()->hasVerifiedEmail()) {
|
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()) {
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
event(new Verified($request->user()));
|
event(new Verified($request->user()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
return redirect()->intended(route('admin.dashboard', absolute: false).'?verified=1');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$request->user()->save();
|
$request->user()->save();
|
||||||
|
|
||||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
return Redirect::route('admin.profile.edit')->with('status', 'profile-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
41
app/Services/DashboardStatisticsService.php
Normal file
41
app/Services/DashboardStatisticsService.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
final class DashboardStatisticsService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{total_pages: int, total_subscribers: int, active_pages: int}
|
||||||
|
*/
|
||||||
|
public function forUser(User $user): array
|
||||||
|
{
|
||||||
|
$pagesQuery = PreregistrationPage::query();
|
||||||
|
if (! $user->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
92
resources/views/admin/dashboard.blade.php
Normal file
92
resources/views/admin/dashboard.blade.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('Dashboard'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('Dashboard'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Dashboard') }}</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-600">{{ __('Overview of your pre-registration activity.') }}</p>
|
||||||
|
</div>
|
||||||
|
@can('create', \App\Models\PreregistrationPage::class)
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.create') }}"
|
||||||
|
class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('New page') }}
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-100 text-indigo-700">
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-slate-500">{{ __('Total pages') }}</p>
|
||||||
|
<p class="text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($totalPages) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-slate-500">
|
||||||
|
@if (auth()->user()->isSuperadmin())
|
||||||
|
{{ __('All pre-registration pages in the system.') }}
|
||||||
|
@else
|
||||||
|
{{ __('Pages you own.') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-100 text-emerald-700">
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-slate-500">{{ __('Total subscribers') }}</p>
|
||||||
|
<p class="text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($totalSubscribers) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-slate-500">{{ __('Across all pages in this overview.') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:col-span-2 lg:col-span-1">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-100 text-amber-800">
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-slate-500">{{ __('Active pages') }}</p>
|
||||||
|
<p class="text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($activePages) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs text-slate-500">{{ __('Currently within the start and end date window.') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@can('create', \App\Models\PreregistrationPage::class)
|
||||||
|
<div class="mt-8 rounded-xl border border-dashed border-slate-300 bg-white/80 p-6 text-center">
|
||||||
|
<p class="text-sm text-slate-600">{{ __('Ready to launch a new campaign?') }}</p>
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.create') }}"
|
||||||
|
class="mt-3 inline-flex text-sm font-semibold text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
{{ __('Create a pre-registration page') }} →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/pages/create.blade.php
Normal file
12
resources/views/admin/pages/create.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('New page'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('New page'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Create page') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 8.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/pages/edit.blade.php
Normal file
12
resources/views/admin/pages/edit.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('Edit') . ' — ' . $page->title)
|
||||||
|
|
||||||
|
@section('mobile_title', __('Edit'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Edit page') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 8.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/pages/index.blade.php
Normal file
12
resources/views/admin/pages/index.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('Pages'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('Pages'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Pre-registration pages') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Page list and CRUD will be added in the next step.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/pages/show.blade.php
Normal file
12
resources/views/admin/pages/show.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', $page->title)
|
||||||
|
|
||||||
|
@section('mobile_title', $page->title)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ $page->title }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Detail view placeholder.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/users/create.blade.php
Normal file
12
resources/views/admin/users/create.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('New user'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('New user'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Create user') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 10.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
13
resources/views/admin/users/edit.blade.php
Normal file
13
resources/views/admin/users/edit.blade.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('Edit user'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('Edit user'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Edit user') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ $user->email }}</p>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Form will be added in Step 10.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
12
resources/views/admin/users/index.blade.php
Normal file
12
resources/views/admin/users/index.blade.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', __('Users'))
|
||||||
|
|
||||||
|
@section('mobile_title', __('Users'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900">{{ __('Users') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('User management will be completed in a later step.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
15
resources/views/components/admin/sidebar-link.blade.php
Normal file
15
resources/views/components/admin/sidebar-link.blade.php
Normal file
@@ -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
|
||||||
|
|
||||||
|
<a href="{{ $href }}" {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</a>
|
||||||
128
resources/views/layouts/admin.blade.php
Normal file
128
resources/views/layouts/admin.blade.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>@hasSection('title')@yield('title') — @endif{{ config('app.name', 'PreRegister') }}</title>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="font-sans antialiased bg-slate-100 text-slate-900" x-data="{ sidebarOpen: false }">
|
||||||
|
{{-- Mobile overlay --}}
|
||||||
|
<div
|
||||||
|
x-show="sidebarOpen"
|
||||||
|
x-transition.opacity
|
||||||
|
class="fixed inset-0 z-40 bg-slate-900/60 lg:hidden"
|
||||||
|
style="display: none;"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="min-h-screen lg:flex">
|
||||||
|
{{-- Sidebar --}}
|
||||||
|
<aside
|
||||||
|
class="fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-slate-800 bg-slate-900 shadow-xl transition-transform duration-200 lg:static lg:translate-x-0"
|
||||||
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
|
||||||
|
aria-label="{{ __('Main navigation') }}"
|
||||||
|
>
|
||||||
|
<div class="flex h-16 shrink-0 items-center justify-between border-b border-slate-800 px-4">
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="text-lg font-semibold tracking-tight text-white">
|
||||||
|
{{ config('app.name', 'PreRegister') }}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg p-2 text-slate-400 hover:bg-slate-800 hover:text-white lg:hidden"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
aria-label="{{ __('Close menu') }}"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex flex-1 flex-col gap-1 p-3">
|
||||||
|
<x-admin.sidebar-link
|
||||||
|
:href="route('admin.dashboard')"
|
||||||
|
:active="request()->routeIs('admin.dashboard')"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 shrink-0 opacity-70 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('Dashboard') }}
|
||||||
|
</x-admin.sidebar-link>
|
||||||
|
|
||||||
|
<x-admin.sidebar-link
|
||||||
|
:href="route('admin.pages.index')"
|
||||||
|
:active="request()->routeIs('admin.pages.*')"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 shrink-0 opacity-70 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('Pages') }}
|
||||||
|
</x-admin.sidebar-link>
|
||||||
|
|
||||||
|
@if (auth()->user()->isSuperadmin())
|
||||||
|
<x-admin.sidebar-link
|
||||||
|
:href="route('admin.users.index')"
|
||||||
|
:active="request()->routeIs('admin.users.*')"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 shrink-0 opacity-70 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('Users') }}
|
||||||
|
</x-admin.sidebar-link>
|
||||||
|
@endif
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="border-t border-slate-800 p-3">
|
||||||
|
<div class="mb-2 truncate px-3 text-xs text-slate-500">{{ auth()->user()->email }}</div>
|
||||||
|
<x-admin.sidebar-link :href="route('admin.profile.edit')" :active="request()->routeIs('admin.profile.*')" class="mb-1">
|
||||||
|
{{ __('Profile') }}
|
||||||
|
</x-admin.sidebar-link>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm font-medium text-slate-300 transition hover:bg-slate-800/80 hover:text-white"
|
||||||
|
>
|
||||||
|
{{ __('Log out') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{{-- Main column --}}
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
<header class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-slate-200 bg-white px-4 shadow-sm lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg p-2 text-slate-600 hover:bg-slate-100"
|
||||||
|
@click="sidebarOpen = true"
|
||||||
|
aria-label="{{ __('Open menu') }}"
|
||||||
|
>
|
||||||
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="truncate font-semibold text-slate-900">@yield('mobile_title', config('app.name', 'PreRegister'))</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 p-4 sm:p-6 lg:p-8">
|
||||||
|
@if (session('status'))
|
||||||
|
<div class="mb-6 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800" role="status">
|
||||||
|
{{ session('status') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,14 +5,14 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="shrink-0 flex items-center">
|
<div class="shrink-0 flex items-center">
|
||||||
<a href="{{ route('dashboard') }}">
|
<a href="{{ route('admin.dashboard') }}">
|
||||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-nav-link>
|
</x-nav-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="content">
|
<x-slot name="content">
|
||||||
<x-dropdown-link :href="route('profile.edit')">
|
<x-dropdown-link :href="route('admin.profile.edit')">
|
||||||
{{ __('Profile') }}
|
{{ __('Profile') }}
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<!-- Responsive Navigation Menu -->
|
<!-- Responsive Navigation Menu -->
|
||||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
<div class="pt-2 pb-3 space-y-1">
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 space-y-1">
|
<div class="mt-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('profile.edit')">
|
<x-responsive-nav-link :href="route('admin.profile.edit')">
|
||||||
{{ __('Profile') }}
|
{{ __('Profile') }}
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
>{{ __('Delete Account') }}</x-danger-button>
|
>{{ __('Delete Account') }}</x-danger-button>
|
||||||
|
|
||||||
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
||||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
|
<form method="post" action="{{ route('admin.profile.destroy') }}" class="p-6">
|
||||||
@csrf
|
@csrf
|
||||||
@method('delete')
|
@method('delete')
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
@csrf
|
@csrf
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
|
<form method="post" action="{{ route('admin.profile.update') }}" class="mt-6 space-y-6">
|
||||||
@csrf
|
@csrf
|
||||||
@method('patch')
|
@method('patch')
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AuthenticationTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
$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
|
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class EmailVerificationTest extends TestCase
|
|||||||
|
|
||||||
Event::assertDispatched(Verified::class);
|
Event::assertDispatched(Verified::class);
|
||||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
$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
|
public function test_email_is_not_verified_with_invalid_hash(): void
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class PasswordUpdateTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->from('/profile')
|
->from('/admin/profile')
|
||||||
->put('/password', [
|
->put('/password', [
|
||||||
'current_password' => 'password',
|
'current_password' => 'password',
|
||||||
'password' => 'new-password',
|
'password' => 'new-password',
|
||||||
@@ -26,7 +26,7 @@ class PasswordUpdateTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertSessionHasNoErrors()
|
->assertSessionHasNoErrors()
|
||||||
->assertRedirect('/profile');
|
->assertRedirect('/admin/profile');
|
||||||
|
|
||||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ class PasswordUpdateTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->from('/profile')
|
->from('/admin/profile')
|
||||||
->put('/password', [
|
->put('/password', [
|
||||||
'current_password' => 'wrong-password',
|
'current_password' => 'wrong-password',
|
||||||
'password' => 'new-password',
|
'password' => 'new-password',
|
||||||
@@ -46,6 +46,6 @@ class PasswordUpdateTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertSessionHasErrorsIn('updatePassword', 'current_password')
|
->assertSessionHasErrorsIn('updatePassword', 'current_password')
|
||||||
->assertRedirect('/profile');
|
->assertRedirect('/admin/profile');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ class RegistrationTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
$this->assertAuthenticated();
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
$response->assertRedirect(route('admin.dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ProfileTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->get('/profile');
|
->get('/admin/profile');
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
}
|
}
|
||||||
@@ -27,14 +27,14 @@ class ProfileTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->patch('/profile', [
|
->patch('/admin/profile', [
|
||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => 'test@example.com',
|
'email' => 'test@example.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertSessionHasNoErrors()
|
->assertSessionHasNoErrors()
|
||||||
->assertRedirect('/profile');
|
->assertRedirect('/admin/profile');
|
||||||
|
|
||||||
$user->refresh();
|
$user->refresh();
|
||||||
|
|
||||||
@@ -49,14 +49,14 @@ class ProfileTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->patch('/profile', [
|
->patch('/admin/profile', [
|
||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertSessionHasNoErrors()
|
->assertSessionHasNoErrors()
|
||||||
->assertRedirect('/profile');
|
->assertRedirect('/admin/profile');
|
||||||
|
|
||||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ class ProfileTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->delete('/profile', [
|
->delete('/admin/profile', [
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -85,14 +85,14 @@ class ProfileTest extends TestCase
|
|||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->from('/profile')
|
->from('/admin/profile')
|
||||||
->delete('/profile', [
|
->delete('/admin/profile', [
|
||||||
'password' => 'wrong-password',
|
'password' => 'wrong-password',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertSessionHasErrorsIn('userDeletion', 'password')
|
->assertSessionHasErrorsIn('userDeletion', 'password')
|
||||||
->assertRedirect('/profile');
|
->assertRedirect('/admin/profile');
|
||||||
|
|
||||||
$this->assertNotNull($user->fresh());
|
$this->assertNotNull($user->fresh());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user