feat: Phase 3 - public registration pages and Mailwizz config

This commit is contained in:
2026-04-03 21:42:19 +02:00
parent cf026f46b0
commit a1d570254e
17 changed files with 1236 additions and 44 deletions

View File

@@ -13,34 +13,143 @@ class MailwizzApiController extends Controller
{
public function lists(Request $request): JsonResponse
{
$request->validate(['api_key' => 'required|string']);
$request->validate(['api_key' => ['required', 'string']]);
$response = Http::withHeaders([
'X-Api-Key' => $request->api_key,
'X-Api-Key' => $request->string('api_key')->toString(),
])->get('https://www.mailwizz.nl/api/lists');
if ($response->failed()) {
return response()->json(['error' => 'Invalid API key or connection failed'], 422);
return response()->json(['message' => __('Invalid API key or connection failed.')], 422);
}
return response()->json($response->json());
$json = $response->json();
if (is_array($json) && ($json['status'] ?? null) === 'error') {
return response()->json(['message' => __('Invalid API key or connection failed.')], 422);
}
return response()->json([
'lists' => $this->normalizeListsPayload(is_array($json) ? $json : []),
]);
}
public function fields(Request $request): JsonResponse
{
$request->validate([
'api_key' => 'required|string',
'list_uid' => 'required|string',
'api_key' => ['required', 'string'],
'list_uid' => ['required', 'string'],
]);
$listUid = $request->string('list_uid')->toString();
$response = Http::withHeaders([
'X-Api-Key' => $request->api_key,
])->get("https://www.mailwizz.nl/api/lists/{$request->list_uid}/fields");
'X-Api-Key' => $request->string('api_key')->toString(),
])->get("https://www.mailwizz.nl/api/lists/{$listUid}/fields");
if ($response->failed()) {
return response()->json(['error' => 'Failed to fetch list fields'], 422);
return response()->json(['message' => __('Failed to fetch list fields.')], 422);
}
return response()->json($response->json());
$json = $response->json();
if (is_array($json) && ($json['status'] ?? null) === 'error') {
return response()->json(['message' => __('Failed to fetch list fields.')], 422);
}
return response()->json([
'fields' => $this->normalizeFieldsPayload(is_array($json) ? $json : []),
]);
}
/**
* @return list<array{list_uid: string, name: string}>
*/
private function normalizeListsPayload(array $json): array
{
$out = [];
$records = data_get($json, 'data.records');
if (! is_array($records)) {
return $out;
}
foreach ($records as $row) {
if (! is_array($row)) {
continue;
}
$uid = data_get($row, 'general.list_uid') ?? data_get($row, 'list_uid');
$name = data_get($row, 'general.name') ?? data_get($row, 'name');
if (is_string($uid) && $uid !== '' && is_string($name) && $name !== '') {
$out[] = ['list_uid' => $uid, 'name' => $name];
}
}
return $out;
}
/**
* @return list<array{tag: string, label: string, type_identifier: string, options: array<string, string>}>
*/
private function normalizeFieldsPayload(array $json): array
{
$out = [];
$records = data_get($json, 'data.records');
if (! is_array($records)) {
return $out;
}
foreach ($records as $row) {
if (! is_array($row)) {
continue;
}
$tag = data_get($row, 'tag');
$label = data_get($row, 'label');
$typeId = data_get($row, 'type.identifier');
if (! is_string($tag) || $tag === '' || ! is_string($label)) {
continue;
}
$typeIdentifier = is_string($typeId) ? $typeId : '';
$rawOptions = data_get($row, 'options');
$options = $this->normalizeFieldOptions($rawOptions);
$out[] = [
'tag' => $tag,
'label' => $label,
'type_identifier' => $typeIdentifier,
'options' => $options,
];
}
return $out;
}
/**
* @return array<string, string>
*/
private function normalizeFieldOptions(mixed $rawOptions): array
{
if ($rawOptions === null || $rawOptions === '') {
return [];
}
if (is_string($rawOptions)) {
$decoded = json_decode($rawOptions, true);
if (is_array($decoded)) {
$rawOptions = $decoded;
} else {
return [];
}
}
if (! is_array($rawOptions)) {
return [];
}
$out = [];
foreach ($rawOptions as $key => $value) {
if (is_string($key) || is_int($key)) {
$k = (string) $key;
$out[$k] = is_scalar($value) ? (string) $value : '';
}
}
return $out;
}
}

View File

@@ -5,23 +5,51 @@ declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateMailwizzConfigRequest;
use App\Models\PreregistrationPage;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class MailwizzController extends Controller
{
public function edit(PreregistrationPage $page): \Illuminate\View\View
public function edit(PreregistrationPage $page): View
{
$this->authorize('update', $page);
$page->load('mailwizzConfig');
return view('admin.mailwizz.edit', compact('page'));
}
public function update(Request $request, PreregistrationPage $page): \Illuminate\Http\RedirectResponse
public function update(UpdateMailwizzConfigRequest $request, PreregistrationPage $page): RedirectResponse
{
return redirect()->route('admin.pages.mailwizz.edit', $page);
$validated = $request->validated();
if (($validated['api_key'] ?? '') === '' && $page->mailwizzConfig !== null) {
unset($validated['api_key']);
}
DB::transaction(function () use ($page, $validated): void {
$page->mailwizzConfig()->updateOrCreate(
['preregistration_page_id' => $page->id],
array_merge($validated, ['preregistration_page_id' => $page->id])
);
});
return redirect()
->route('admin.pages.mailwizz.edit', $page)
->with('status', __('Mailwizz configuration saved.'));
}
public function destroy(PreregistrationPage $page): \Illuminate\Http\RedirectResponse
public function destroy(PreregistrationPage $page): RedirectResponse
{
return redirect()->route('admin.pages.mailwizz.edit', $page);
$this->authorize('update', $page);
$page->mailwizzConfig()?->delete();
return redirect()
->route('admin.pages.mailwizz.edit', $page)
->with('status', __('Mailwizz integration removed.'));
}
}

View File

@@ -4,55 +4,45 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\SubscribePublicPageRequest;
use App\Models\PreregistrationPage;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PublicPageController extends Controller
{
public function show(string $slug): View
public function show(PreregistrationPage $publicPage): View
{
$page = PreregistrationPage::where('slug', $slug)
->where('is_active', true)
->firstOrFail();
return view('public.page', compact('page'));
return view('public.page', ['page' => $publicPage]);
}
public function subscribe(Request $request, string $slug): JsonResponse
public function subscribe(SubscribePublicPageRequest $request, PreregistrationPage $publicPage): JsonResponse
{
$page = PreregistrationPage::where('slug', $slug)
->where('is_active', true)
->firstOrFail();
abort_if(now()->lt($publicPage->start_date) || now()->gt($publicPage->end_date), 403);
abort_if(now()->lt($page->start_date) || now()->gt($page->end_date), 403);
$validated = $request->validated();
$validated = $request->validate([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => $page->phone_enabled ? 'required|string|max:20' : 'nullable',
]);
$exists = $publicPage->subscribers()
->where('email', $validated['email'])
->exists();
$exists = $page->subscribers()->where('email', $validated['email'])->exists();
if ($exists) {
return response()->json([
'success' => false,
'message' => 'This email address is already registered.',
'message' => __('You are already registered for this event.'),
], 422);
}
$subscriber = $page->subscribers()->create($validated);
$publicPage->subscribers()->create($validated);
// Mailwizz sync will be wired up in Phase 4
if ($page->mailwizzConfig) {
if ($publicPage->mailwizzConfig) {
// SyncSubscriberToMailwizz::dispatch($subscriber);
}
return response()->json([
'success' => true,
'message' => $page->thank_you_message ?? 'Thank you for registering!',
'message' => $publicPage->thank_you_message ?? __('Thank you for registering!'),
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Admin;
use App\Models\PreregistrationPage;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateMailwizzConfigRequest extends FormRequest
{
public function authorize(): bool
{
$page = $this->route('page');
if (! $page instanceof PreregistrationPage) {
return false;
}
return $this->user()?->can('update', $page) ?? false;
}
/**
* @return array<string, array<int, ValidationRule|string>>
*/
public function rules(): array
{
/** @var PreregistrationPage $page */
$page = $this->route('page');
return [
'api_key' => [
Rule::requiredIf(fn (): bool => $page->mailwizzConfig === null),
'nullable',
'string',
'max:512',
],
'list_uid' => ['required', 'string', 'max:255'],
'list_name' => ['nullable', 'string', 'max:255'],
'field_email' => ['required', 'string', 'max:255'],
'field_first_name' => ['required', 'string', 'max:255'],
'field_last_name' => ['required', 'string', 'max:255'],
'field_phone' => $page->phone_enabled
? ['required', 'string', 'max:255']
: ['nullable', 'string', 'max:255'],
'tag_field' => ['required', 'string', 'max:255'],
'tag_value' => ['required', 'string', 'max:255'],
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use App\Models\PreregistrationPage;
use Illuminate\Foundation\Http\FormRequest;
class SubscribePublicPageRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var PreregistrationPage $page */
$page = $this->route('publicPage');
return [
'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'phone' => $page->phone_enabled
? ['required', 'string', 'max:20']
: ['nullable', 'string', 'max:20'],
];
}
protected function prepareForValidation(): void
{
$email = $this->input('email');
if (is_string($email)) {
$this->merge([
'email' => strtolower(trim($email)),
]);
}
}
}

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\Providers;
use App\Models\PreregistrationPage;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -23,5 +25,12 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
Paginator::useTailwind();
Route::bind('publicPage', function (string $value): PreregistrationPage {
return PreregistrationPage::query()
->where('slug', $value)
->where('is_active', true)
->firstOrFail();
});
}
}