feat: Phase 2 - page CRUD, subscriber management, user management
This commit is contained in:
121
resources/views/admin/pages/_form.blade.php
Normal file
121
resources/views/admin/pages/_form.blade.php
Normal file
@@ -0,0 +1,121 @@
|
||||
@php
|
||||
/** @var \App\Models\PreregistrationPage|null $page */
|
||||
$page = $page ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="grid max-w-3xl gap-6">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-slate-700">{{ __('Title') }}</label>
|
||||
<input type="text" name="title" id="title" value="{{ old('title', $page?->title) }}" required maxlength="255"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
|
||||
@error('title')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="heading" class="block text-sm font-medium text-slate-700">{{ __('Heading') }}</label>
|
||||
<input type="text" name="heading" id="heading" value="{{ old('heading', $page?->heading) }}" required maxlength="255"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
|
||||
@error('heading')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="intro_text" class="block text-sm font-medium text-slate-700">{{ __('Intro text') }}</label>
|
||||
<textarea name="intro_text" id="intro_text" rows="4"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('intro_text', $page?->intro_text) }}</textarea>
|
||||
@error('intro_text')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="thank_you_message" class="block text-sm font-medium text-slate-700">{{ __('Thank you message') }}</label>
|
||||
<textarea name="thank_you_message" id="thank_you_message" rows="3"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('thank_you_message', $page?->thank_you_message) }}</textarea>
|
||||
@error('thank_you_message')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="expired_message" class="block text-sm font-medium text-slate-700">{{ __('Expired message') }}</label>
|
||||
<textarea name="expired_message" id="expired_message" rows="3"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('expired_message', $page?->expired_message) }}</textarea>
|
||||
@error('expired_message')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="ticketshop_url" class="block text-sm font-medium text-slate-700">{{ __('Ticket shop URL') }}</label>
|
||||
<input type="text" name="ticketshop_url" id="ticketshop_url" inputmode="url" autocomplete="url"
|
||||
value="{{ old('ticketshop_url', $page?->ticketshop_url) }}"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="https://…" />
|
||||
@error('ticketshop_url')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium text-slate-700">{{ __('Start date') }}</label>
|
||||
<input type="datetime-local" name="start_date" id="start_date" required
|
||||
value="{{ old('start_date', $page?->start_date?->format('Y-m-d\TH:i')) }}"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
|
||||
@error('start_date')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block text-sm font-medium text-slate-700">{{ __('End date') }}</label>
|
||||
<input type="datetime-local" name="end_date" id="end_date" required
|
||||
value="{{ old('end_date', $page?->end_date?->format('Y-m-d\TH:i')) }}"
|
||||
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
|
||||
@error('end_date')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" name="phone_enabled" value="1" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
||||
@checked(old('phone_enabled', $page?->phone_enabled ?? false)) />
|
||||
<span class="text-sm font-medium text-slate-700">{{ __('Phone enabled') }}</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" name="is_active" value="1" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
||||
@checked(old('is_active', $page?->is_active ?? true)) />
|
||||
<span class="text-sm font-medium text-slate-700">{{ __('Active') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="background_image" class="block text-sm font-medium text-slate-700">{{ __('Background image') }}</label>
|
||||
<p class="mt-0.5 text-xs text-slate-500">{{ __('JPG, PNG or WebP. Max 5 MB.') }}</p>
|
||||
<input type="file" name="background_image" id="background_image" accept="image/jpeg,image/png,image/webp"
|
||||
class="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-slate-100 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-slate-700 hover:file:bg-slate-200" />
|
||||
@error('background_image')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@if ($page?->background_image)
|
||||
<p class="mt-2 text-xs text-slate-600">{{ __('Current file:') }} <a href="/storage/{{ $page->background_image }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ __('View') }}</a></p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="logo_image" class="block text-sm font-medium text-slate-700">{{ __('Logo image') }}</label>
|
||||
<p class="mt-0.5 text-xs text-slate-500">{{ __('JPG, PNG, WebP or SVG. Max 2 MB.') }}</p>
|
||||
<input type="file" name="logo_image" id="logo_image" accept="image/jpeg,image/png,image/webp,image/svg+xml,.svg"
|
||||
class="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-slate-100 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-slate-700 hover:file:bg-slate-200" />
|
||||
@error('logo_image')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@if ($page?->logo_image)
|
||||
<p class="mt-2 text-xs text-slate-600">{{ __('Current file:') }} <a href="/storage/{{ $page->logo_image }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ __('View') }}</a></p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,8 +5,32 @@
|
||||
@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 class="mx-auto max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<a href="{{ route('admin.pages.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Back to pages') }}</a>
|
||||
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Create pre-registration page') }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">{{ __('After saving, use the pages list to copy the public URL.') }}</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.pages.store') }}" method="post" enctype="multipart/form-data" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" novalidate>
|
||||
@csrf
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
|
||||
<p class="font-medium">{{ __('Please fix the following:') }}</p>
|
||||
<ul class="mt-2 list-inside list-disc space-y-1">
|
||||
@foreach ($errors->all() as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@include('admin.pages._form', ['page' => null])
|
||||
<div class="mt-8 flex gap-3">
|
||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
|
||||
{{ __('Create page') }}
|
||||
</button>
|
||||
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -2,11 +2,38 @@
|
||||
|
||||
@section('title', __('Edit') . ' — ' . $page->title)
|
||||
|
||||
@section('mobile_title', __('Edit'))
|
||||
@section('mobile_title', __('Edit page'))
|
||||
|
||||
@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 class="mx-auto max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<a href="{{ route('admin.pages.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Back to pages') }}</a>
|
||||
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Edit page') }}</h1>
|
||||
<p class="mt-2 rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-700">
|
||||
{{ __('Public URL') }}: <a href="{{ route('public.page', ['slug' => $page->slug]) }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ url('/r/'.$page->slug) }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.pages.update', $page) }}" method="post" enctype="multipart/form-data" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" novalidate>
|
||||
@csrf
|
||||
@method('PUT')
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
|
||||
<p class="font-medium">{{ __('Please fix the following:') }}</p>
|
||||
<ul class="mt-2 list-inside list-disc space-y-1">
|
||||
@foreach ($errors->all() as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@include('admin.pages._form', ['page' => $page])
|
||||
<div class="mt-8 flex gap-3">
|
||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
|
||||
{{ __('Save changes') }}
|
||||
</button>
|
||||
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -5,8 +5,106 @@
|
||||
@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 class="mx-auto max-w-7xl">
|
||||
<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">{{ __('Pre-registration pages') }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">{{ __('Manage landing pages and subscriber lists.') }}</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 hover:bg-indigo-500">
|
||||
{{ __('New page') }}
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
@if (auth()->user()->isSuperadmin())
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Owner') }}</th>
|
||||
@endif
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Title') }}</th>
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Status') }}</th>
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Start') }}</th>
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('End') }}</th>
|
||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Subscribers') }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right font-semibold text-slate-700">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse ($pages as $page)
|
||||
@php
|
||||
$publicUrl = route('public.page', ['slug' => $page->slug]);
|
||||
$key = $page->statusKey();
|
||||
$statusClasses = match ($key) {
|
||||
'before_start' => 'bg-slate-100 text-slate-800',
|
||||
'active' => 'bg-emerald-100 text-emerald-800',
|
||||
'expired' => 'bg-rose-100 text-rose-800',
|
||||
default => 'bg-slate-100 text-slate-800',
|
||||
};
|
||||
$statusLabel = match ($key) {
|
||||
'before_start' => __('Before start'),
|
||||
'active' => __('Active'),
|
||||
'expired' => __('Expired'),
|
||||
default => $key,
|
||||
};
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50/80">
|
||||
@if (auth()->user()->isSuperadmin())
|
||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->user?->email ?? '—' }}</td>
|
||||
@endif
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ $page->title }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium {{ $statusClasses }}">{{ $statusLabel }}</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->start_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->end_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 tabular-nums text-slate-600">{{ number_format($page->subscribers_count) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
@can('update', $page)
|
||||
<a href="{{ route('admin.pages.edit', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Edit') }}</a>
|
||||
@endcan
|
||||
@can('view', $page)
|
||||
<a href="{{ route('admin.pages.subscribers.index', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Subscribers') }}</a>
|
||||
@endcan
|
||||
<button
|
||||
type="button"
|
||||
x-data="{ copied: false }"
|
||||
x-on:click="navigator.clipboard.writeText({{ json_encode($publicUrl) }}); copied = true; setTimeout(() => copied = false, 2000)"
|
||||
class="text-sm text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
<span x-show="!copied">{{ __('Copy URL') }}</span>
|
||||
<span x-show="copied" x-cloak class="text-emerald-600">{{ __('Copied!') }}</span>
|
||||
</button>
|
||||
@can('delete', $page)
|
||||
<form action="{{ route('admin.pages.destroy', $page) }}" method="post" class="inline"
|
||||
onsubmit="return confirm(@js(__('Delete this page and all subscribers? This cannot be undone.')));">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-500">{{ __('Delete') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ auth()->user()->isSuperadmin() ? 7 : 6 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
{{ __('No pages yet.') }}
|
||||
@can('create', \App\Models\PreregistrationPage::class)
|
||||
<a href="{{ route('admin.pages.create') }}" class="font-medium text-indigo-600 hover:text-indigo-500">{{ __('Create one') }}</a>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user