Expand settings beyond QR into a full Events preferences page.
Deploy Ladill Events / deploy (push) Successful in 51s
Deploy Ladill Events / deploy (push) Successful in 51s
Add event notification toggles, new-event defaults that pre-fill creation, and tuck QR styling into a secondary section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -69,6 +69,7 @@ class AccountController extends Controller
|
||||
return view('qr.account.settings', [
|
||||
'account' => $account,
|
||||
'settings' => $settings,
|
||||
'eventDefaults' => $settings->resolvedEventDefaults(),
|
||||
'style' => $style,
|
||||
'moduleStyles' => QrModuleStyleCatalog::visible(),
|
||||
'cornerOuterStyles' => QrCornerStyleCatalog::outerStyles(),
|
||||
@@ -85,6 +86,17 @@ class AccountController extends Controller
|
||||
'notify_email' => ['nullable', 'email', 'max:255'],
|
||||
'product_updates' => ['nullable', 'boolean'],
|
||||
'low_balance_alerts' => ['nullable', 'boolean'],
|
||||
'notify_registrations' => ['nullable', 'boolean'],
|
||||
'notify_payouts' => ['nullable', 'boolean'],
|
||||
'event_defaults' => ['nullable', 'array'],
|
||||
'event_defaults.currency' => ['nullable', 'in:GHS,USD,NGN,KES'],
|
||||
'event_defaults.mode' => ['nullable', 'in:ticketing,contributions,free'],
|
||||
'event_defaults.badge_size' => ['nullable', 'in:4x3,4x6,cr80'],
|
||||
'event_defaults.brand_color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'event_defaults.organizer' => ['nullable', 'string', 'max:120'],
|
||||
'event_defaults.registration_open' => ['nullable', 'boolean'],
|
||||
'event_defaults.badge_fields' => ['nullable', 'array'],
|
||||
'event_defaults.badge_fields.*' => ['nullable', 'string', 'max:40'],
|
||||
'default_style' => ['nullable', 'array'],
|
||||
'default_style.foreground' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.background' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
@@ -102,12 +114,18 @@ class AccountController extends Controller
|
||||
'default_style.gradient_rotation' => ['nullable', 'integer', 'min:0', 'max:360'],
|
||||
]);
|
||||
|
||||
$eventDefaults = $data['event_defaults'] ?? [];
|
||||
$eventDefaults['registration_open'] = $request->boolean('event_defaults.registration_open');
|
||||
|
||||
QrSetting::updateOrCreate(
|
||||
['user_id' => $account->id],
|
||||
[
|
||||
'notify_email' => $data['notify_email'] ?? null,
|
||||
'product_updates' => $request->boolean('product_updates'),
|
||||
'low_balance_alerts' => $request->boolean('low_balance_alerts'),
|
||||
'notify_registrations' => $request->boolean('notify_registrations'),
|
||||
'notify_payouts' => $request->boolean('notify_payouts'),
|
||||
'event_defaults' => QrSetting::sanitizeEventDefaults($eventDefaults),
|
||||
'default_style' => QrSetting::sanitizeDefaultStyle($data['default_style'] ?? null),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -89,6 +89,7 @@ class QrCodeController extends Controller
|
||||
'ladillWalletBalance' => $this->platformBilling->balanceMinor($account->public_id) / 100,
|
||||
'topupUrl' => 'https://'.config('app.account_domain').'/wallet',
|
||||
'accountDefaultStyle' => $qrSettings->resolvedDefaultStyle(),
|
||||
'accountEventDefaults' => $qrSettings->resolvedEventDefaults(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use App\Support\Qr\QrTypeCatalog;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/** Per-account QR Plus preferences (notifications and new-code defaults). */
|
||||
/** Per-account Ladill Events preferences (notifications, event defaults, QR style). */
|
||||
class QrSetting extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
@@ -15,16 +15,36 @@ class QrSetting extends Model
|
||||
'notify_email',
|
||||
'product_updates',
|
||||
'low_balance_alerts',
|
||||
'notify_registrations',
|
||||
'notify_payouts',
|
||||
'default_type',
|
||||
'default_style',
|
||||
'event_defaults',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'product_updates' => 'boolean',
|
||||
'low_balance_alerts' => 'boolean',
|
||||
'notify_registrations' => 'boolean',
|
||||
'notify_payouts' => 'boolean',
|
||||
'default_style' => 'array',
|
||||
'event_defaults' => 'array',
|
||||
];
|
||||
|
||||
/** @return list<string> */
|
||||
public static function storableEventDefaultKeys(): array
|
||||
{
|
||||
return [
|
||||
'currency',
|
||||
'mode',
|
||||
'badge_size',
|
||||
'brand_color',
|
||||
'organizer',
|
||||
'registration_open',
|
||||
'badge_fields',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public static function storableStyleKeys(): array
|
||||
{
|
||||
@@ -58,6 +78,73 @@ class QrSetting extends Model
|
||||
return in_array($type, QrTypeCatalog::eventsTypes(), true) ? $type : QrCode::TYPE_EVENT;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function resolvedEventDefaults(): array
|
||||
{
|
||||
$defaults = [
|
||||
'currency' => 'GHS',
|
||||
'mode' => 'ticketing',
|
||||
'badge_size' => '4x3',
|
||||
'brand_color' => '#4f46e5',
|
||||
'organizer' => '',
|
||||
'registration_open' => true,
|
||||
'badge_fields' => ['Company', 'Role'],
|
||||
];
|
||||
|
||||
$stored = collect($this->event_defaults ?? [])
|
||||
->only(self::storableEventDefaultKeys())
|
||||
->filter(fn ($value) => $value !== null && $value !== '')
|
||||
->all();
|
||||
|
||||
if (isset($stored['badge_fields']) && is_array($stored['badge_fields'])) {
|
||||
$stored['badge_fields'] = array_values(array_filter(
|
||||
array_map('strval', $stored['badge_fields']),
|
||||
fn (string $field) => trim($field) !== '',
|
||||
));
|
||||
if ($stored['badge_fields'] === []) {
|
||||
unset($stored['badge_fields']);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($defaults, $stored);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $input */
|
||||
public static function sanitizeEventDefaults(?array $input): ?array
|
||||
{
|
||||
if ($input === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$allowedModes = ['ticketing', 'contributions', 'free'];
|
||||
$allowedSizes = ['4x3', '4x6', 'cr80'];
|
||||
$allowedCurrencies = ['GHS', 'USD', 'NGN', 'KES'];
|
||||
|
||||
$mode = (string) ($input['mode'] ?? 'ticketing');
|
||||
$badgeSize = (string) ($input['badge_size'] ?? '4x3');
|
||||
$currency = strtoupper(trim((string) ($input['currency'] ?? 'GHS')));
|
||||
|
||||
$badgeFields = array_values(array_filter(
|
||||
array_map(fn ($field) => mb_substr(trim((string) $field), 0, 40), (array) ($input['badge_fields'] ?? [])),
|
||||
fn (string $field) => $field !== '',
|
||||
));
|
||||
|
||||
$brandColor = (string) ($input['brand_color'] ?? '#4f46e5');
|
||||
if (! preg_match('/^#[0-9a-fA-F]{6}$/', $brandColor)) {
|
||||
$brandColor = '#4f46e5';
|
||||
}
|
||||
|
||||
return [
|
||||
'currency' => in_array($currency, $allowedCurrencies, true) ? $currency : 'GHS',
|
||||
'mode' => in_array($mode, $allowedModes, true) ? $mode : 'ticketing',
|
||||
'badge_size' => in_array($badgeSize, $allowedSizes, true) ? $badgeSize : '4x3',
|
||||
'brand_color' => $brandColor,
|
||||
'organizer' => mb_substr(trim((string) ($input['organizer'] ?? '')), 0, 120),
|
||||
'registration_open' => filter_var($input['registration_open'] ?? true, FILTER_VALIDATE_BOOL),
|
||||
'badge_fields' => $badgeFields !== [] ? $badgeFields : ['Company', 'Role'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function resolvedDefaultStyle(): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('qr_settings', function (Blueprint $table) {
|
||||
$table->boolean('notify_registrations')->default(true)->after('low_balance_alerts');
|
||||
$table->boolean('notify_payouts')->default(true)->after('notify_registrations');
|
||||
$table->json('event_defaults')->nullable()->after('default_style');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('qr_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['notify_registrations', 'notify_payouts', 'event_defaults']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
@php
|
||||
$ed = $accountEventDefaults ?? [];
|
||||
@endphp
|
||||
|
||||
{{-- URL --}}
|
||||
<div x-show="type === 'url'" x-cloak>
|
||||
<label class="block text-xs font-semibold text-slate-600">Destination URL</label>
|
||||
@@ -381,10 +385,10 @@
|
||||
{{-- Event --}}
|
||||
<div x-show="type === 'event'" x-cloak class="space-y-4"
|
||||
x-data="{
|
||||
mode: @js(old('mode', 'ticketing')),
|
||||
mode: @js(old('mode', $ed['mode'] ?? 'ticketing')),
|
||||
tiers: {{ Illuminate\Support\Js::from(old('tiers', [['name' => 'General Admission', 'price' => '', 'capacity' => '']])) }},
|
||||
categories: {{ Illuminate\Support\Js::from(array_values(array_filter(old('contribution_categories', ['Contribution'])))) }},
|
||||
badgeFields: {{ Illuminate\Support\Js::from(array_values(array_filter(old('badge_fields', ['Company', 'Role'])))) }},
|
||||
badgeFields: {{ Illuminate\Support\Js::from(array_values(array_filter(old('badge_fields', $ed['badge_fields'] ?? ['Company', 'Role'])))) }},
|
||||
addTier() { this.tiers.push({ name: '', price: '', capacity: '' }); },
|
||||
removeTier(i) { this.tiers.splice(i, 1); },
|
||||
addCategory() { this.categories.push(''); },
|
||||
@@ -414,7 +418,7 @@
|
||||
<input type="text" name="location" value="{{ old('location') }}" placeholder="Venue / location"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="text" name="organizer" value="{{ old('organizer') }}" placeholder="Organizer name"
|
||||
<input type="text" name="organizer" value="{{ old('organizer', $ed['organizer'] ?? '') }}" placeholder="Organizer name"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<input type="url" name="website" value="{{ old('website') }}" placeholder="Website URL"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
@@ -442,7 +446,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Brand Color</label>
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', '#4f46e5') }}"
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', $ed['brand_color'] ?? '#4f46e5') }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,13 +554,13 @@
|
||||
<label class="block text-[11px] font-medium text-slate-500">Badge size</label>
|
||||
<select name="badge_size" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2.5 text-sm text-slate-700 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
@foreach(['4x3' => '4 × 3 in (landscape badge)', '4x6' => '4 × 6 in (lanyard badge)', 'cr80' => 'CR80 card (3.375 × 2.125 in)'] as $bs => $bl)
|
||||
<option value="{{ $bs }}" @selected(old('badge_size', '4x3') === $bs)>{{ $bl }}</option>
|
||||
<option value="{{ $bs }}" @selected(old('badge_size', $ed['badge_size'] ?? '4x3') === $bs)>{{ $bl }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex cursor-pointer items-center justify-between rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm sm:mt-5">
|
||||
<span class="text-sm font-semibold text-slate-900" x-text="mode === 'contributions' ? 'Accepting contributions' : 'Registration open'">Registration open</span>
|
||||
<input type="checkbox" name="registration_open" value="1" @checked(old('registration_open', '1'))
|
||||
<input type="checkbox" name="registration_open" value="1" @checked(old('registration_open', ($ed['registration_open'] ?? true) ? '1' : ''))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
</div>
|
||||
@@ -603,7 +607,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Brand Color</label>
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', '#b45309') }}"
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', $ed['brand_color'] ?? '#b45309') }}"
|
||||
class="mt-1 h-10 w-16 cursor-pointer rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Settings</x-slot>
|
||||
|
||||
@php
|
||||
$ed = $eventDefaults;
|
||||
$badgeFieldLabels = old('event_defaults.badge_fields', $ed['badge_fields'] ?? ['Company', 'Role']);
|
||||
@endphp
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Settings</h1>
|
||||
<p class="mt-0.5 text-sm text-slate-500">QR Plus notifications and defaults for new codes.</p>
|
||||
<p class="mt-0.5 text-sm text-slate-500">Notifications, defaults for new events, and QR preferences.</p>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
|
||||
@@ -15,7 +20,7 @@
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Notifications</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Where we send QR Plus billing reminders and product updates.</p>
|
||||
<p class="mt-1 text-xs text-slate-500">Choose what we email you about your events and account.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="notify_email" class="block text-[11px] font-medium text-slate-500">Notifications email</label>
|
||||
@@ -27,8 +32,28 @@
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Low balance alerts</span>
|
||||
<span class="block text-xs text-slate-400">Email when your wallet is too low to create a new code.</span>
|
||||
<span class="block text-sm font-medium text-slate-800">New registrations</span>
|
||||
<span class="block text-xs text-slate-400">Email when someone registers or buys a ticket.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="notify_registrations" value="1"
|
||||
@checked(old('notify_registrations', $settings->notify_registrations ?? true))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Payouts & settlements</span>
|
||||
<span class="block text-xs text-slate-400">Email when ticket revenue is ready to withdraw.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="notify_payouts" value="1"
|
||||
@checked(old('notify_payouts', $settings->notify_payouts ?? true))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Low QR balance</span>
|
||||
<span class="block text-xs text-slate-400">Email when your wallet is too low to publish a new event QR.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="low_balance_alerts" value="1"
|
||||
@checked(old('low_balance_alerts', $settings->low_balance_alerts ?? true))
|
||||
@@ -37,8 +62,8 @@
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Product updates</span>
|
||||
<span class="block text-xs text-slate-400">Occasional emails about new QR Plus features.</span>
|
||||
<span class="block text-sm font-medium text-slate-800">Ladill Events updates</span>
|
||||
<span class="block text-xs text-slate-400">Occasional emails about new features and improvements.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="product_updates" value="1"
|
||||
@checked(old('product_updates', $settings->product_updates ?? true))
|
||||
@@ -47,93 +72,181 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">New code defaults</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Pre-fill the create screen so every new QR starts with your brand look.</p>
|
||||
<h2 class="text-sm font-semibold text-slate-900">New event defaults</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Pre-fill the create flow so every new event starts with your usual setup.</p>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="foreground" class="block text-[11px] font-medium text-slate-500">Foreground</label>
|
||||
<input type="color" id="foreground" name="default_style[foreground]"
|
||||
value="{{ old('default_style.foreground', $style['foreground']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="background" class="block text-[11px] font-medium text-slate-500">Background</label>
|
||||
<input type="color" id="background" name="default_style[background]"
|
||||
value="{{ old('default_style.background', $style['background']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="module_style" class="block text-[11px] font-medium text-slate-500">Module style</label>
|
||||
<select id="module_style" name="default_style[module_style]"
|
||||
<label for="default_currency" class="block text-[11px] font-medium text-slate-500">Currency</label>
|
||||
<select id="default_currency" name="event_defaults[currency]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($moduleStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.module_style', $style['module_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@foreach (['GHS' => 'GHS (Ghanaian Cedi)', 'USD' => 'USD (US Dollar)', 'NGN' => 'NGN (Nigerian Naira)', 'KES' => 'KES (Kenyan Shilling)'] as $code => $label)
|
||||
<option value="{{ $code }}" @selected(old('event_defaults.currency', $ed['currency']) === $code)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="error_correction" class="block text-[11px] font-medium text-slate-500">Error correction</label>
|
||||
<select id="error_correction" name="default_style[error_correction]"
|
||||
<label for="default_mode" class="block text-[11px] font-medium text-slate-500">Collection type</label>
|
||||
<select id="default_mode" name="event_defaults[mode]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach (['L' => 'Low', 'M' => 'Medium', 'Q' => 'Quartile', 'H' => 'High'] as $key => $label)
|
||||
<option value="{{ $key }}" @selected(old('default_style.error_correction', $style['error_correction']) === $key)>{{ $label }} ({{ $key }})</option>
|
||||
@endforeach
|
||||
<option value="ticketing" @selected(old('event_defaults.mode', $ed['mode']) === 'ticketing')>Ticketed event</option>
|
||||
<option value="contributions" @selected(old('event_defaults.mode', $ed['mode']) === 'contributions')>Contributions / gifts</option>
|
||||
<option value="free" @selected(old('event_defaults.mode', $ed['mode']) === 'free')>Free registration</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="finder_outer" class="block text-[11px] font-medium text-slate-500">Corner outer</label>
|
||||
<select id="finder_outer" name="default_style[finder_outer]"
|
||||
<label for="default_badge_size" class="block text-[11px] font-medium text-slate-500">Badge size</label>
|
||||
<select id="default_badge_size" name="event_defaults[badge_size]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerOuterStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_outer', $style['finder_outer']) === $key)>{{ $meta['label'] }}</option>
|
||||
@foreach (['4x3' => '4 × 3 in (landscape)', '4x6' => '4 × 6 in (lanyard)', 'cr80' => 'CR80 card'] as $size => $label)
|
||||
<option value="{{ $size }}" @selected(old('event_defaults.badge_size', $ed['badge_size']) === $size)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="finder_inner" class="block text-[11px] font-medium text-slate-500">Corner inner</label>
|
||||
<select id="finder_inner" name="default_style[finder_inner]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerInnerStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_inner', $style['finder_inner']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="frame_style" class="block text-[11px] font-medium text-slate-500">Frame</label>
|
||||
<select id="frame_style" name="default_style[frame_style]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($frameStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.frame_style', $style['frame_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="frame_color" class="block text-[11px] font-medium text-slate-500">Frame color</label>
|
||||
<input type="color" id="frame_color" name="default_style[frame_color]"
|
||||
value="{{ old('default_style.frame_color', $style['frame_color']) }}"
|
||||
<label for="default_brand_color" class="block text-[11px] font-medium text-slate-500">Brand color</label>
|
||||
<input type="color" id="default_brand_color" name="event_defaults[brand_color]"
|
||||
value="{{ old('event_defaults.brand_color', $ed['brand_color']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="frame_text" class="block text-[11px] font-medium text-slate-500">Frame label</label>
|
||||
<input type="text" id="frame_text" name="default_style[frame_text]" maxlength="100"
|
||||
value="{{ old('default_style.frame_text', $style['frame_text']) }}"
|
||||
placeholder="SCAN ME"
|
||||
<label for="default_organizer" class="block text-[11px] font-medium text-slate-500">Default organizer name</label>
|
||||
<input type="text" id="default_organizer" name="event_defaults[organizer]" maxlength="120"
|
||||
value="{{ old('event_defaults.organizer', $ed['organizer']) }}"
|
||||
placeholder="e.g. CAPBuSS Events"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
</div>
|
||||
|
||||
<div class="mt-4" x-data="{ fields: @js(array_values($badgeFieldLabels)) }">
|
||||
<label class="block text-[11px] font-medium text-slate-500">Default badge fields</label>
|
||||
<p class="mt-0.5 text-[11px] text-slate-400">Extra attendee fields shown on badges (e.g. Company, Role).</p>
|
||||
<div class="mt-2 space-y-2">
|
||||
<template x-for="(field, i) in fields" :key="i">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" :name="'event_defaults[badge_fields]['+i+']'" x-model="fields[i]" maxlength="40"
|
||||
class="flex-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
<button type="button" x-show="fields.length > 1" @click="fields.splice(i, 1)"
|
||||
class="rounded-lg border border-slate-200 px-2 py-1 text-xs text-slate-500 hover:bg-slate-50">Remove</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button type="button" @click="fields.push('')"
|
||||
class="mt-2 text-xs font-semibold text-indigo-600 hover:text-indigo-800">+ Add field</button>
|
||||
</div>
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4 rounded-xl border border-slate-100 bg-slate-50/80 px-4 py-3">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Registration open by default</span>
|
||||
<span class="block text-xs text-slate-400">New events accept sign-ups unless you turn this off.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="event_defaults[registration_open]" value="1"
|
||||
@checked(old('event_defaults.registration_open', $ed['registration_open']))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<details class="group rounded-2xl border border-slate-200 bg-white">
|
||||
<summary class="cursor-pointer list-none p-5 marker:content-none">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-slate-900">QR code defaults</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Style applied when you design event QR codes.</p>
|
||||
</div>
|
||||
<svg class="h-5 w-5 shrink-0 text-slate-400 transition group-open:rotate-180" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 border-t border-slate-100 px-5 pb-5 pt-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="foreground" class="block text-[11px] font-medium text-slate-500">Foreground</label>
|
||||
<input type="color" id="foreground" name="default_style[foreground]"
|
||||
value="{{ old('default_style.foreground', $style['foreground']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="background" class="block text-[11px] font-medium text-slate-500">Background</label>
|
||||
<input type="color" id="background" name="default_style[background]"
|
||||
value="{{ old('default_style.background', $style['background']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="module_style" class="block text-[11px] font-medium text-slate-500">Module style</label>
|
||||
<select id="module_style" name="default_style[module_style]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($moduleStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.module_style', $style['module_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="error_correction" class="block text-[11px] font-medium text-slate-500">Error correction</label>
|
||||
<select id="error_correction" name="default_style[error_correction]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach (['L' => 'Low', 'M' => 'Medium', 'Q' => 'Quartile', 'H' => 'High'] as $key => $label)
|
||||
<option value="{{ $key }}" @selected(old('default_style.error_correction', $style['error_correction']) === $key)>{{ $label }} ({{ $key }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="finder_outer" class="block text-[11px] font-medium text-slate-500">Corner outer</label>
|
||||
<select id="finder_outer" name="default_style[finder_outer]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerOuterStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_outer', $style['finder_outer']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="finder_inner" class="block text-[11px] font-medium text-slate-500">Corner inner</label>
|
||||
<select id="finder_inner" name="default_style[finder_inner]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerInnerStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_inner', $style['finder_inner']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="frame_style" class="block text-[11px] font-medium text-slate-500">Frame</label>
|
||||
<select id="frame_style" name="default_style[frame_style]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($frameStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.frame_style', $style['frame_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="frame_color" class="block text-[11px] font-medium text-slate-500">Frame color</label>
|
||||
<input type="color" id="frame_color" name="default_style[frame_color]"
|
||||
value="{{ old('default_style.frame_color', $style['frame_color']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="frame_text" class="block text-[11px] font-medium text-slate-500">Frame label</label>
|
||||
<input type="text" id="frame_text" name="default_style[frame_text]" maxlength="100"
|
||||
value="{{ old('default_style.frame_text', $style['frame_text']) }}"
|
||||
placeholder="SCAN ME"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700">
|
||||
Save settings
|
||||
</button>
|
||||
|
||||
@@ -31,7 +31,9 @@ class QrSettingsTest extends TestCase
|
||||
->get(route('account.settings'))
|
||||
->assertOk()
|
||||
->assertSee('favicon.ico', false)
|
||||
->assertSee('New code defaults')
|
||||
->assertSee('New event defaults')
|
||||
->assertSee('QR code defaults')
|
||||
->assertSee('New registrations')
|
||||
->assertSee('Notifications');
|
||||
}
|
||||
|
||||
@@ -44,6 +46,17 @@ class QrSettingsTest extends TestCase
|
||||
'notify_email' => 'alerts@example.com',
|
||||
'product_updates' => '1',
|
||||
'low_balance_alerts' => '0',
|
||||
'notify_registrations' => '1',
|
||||
'notify_payouts' => '0',
|
||||
'event_defaults' => [
|
||||
'currency' => 'USD',
|
||||
'mode' => 'free',
|
||||
'badge_size' => '4x6',
|
||||
'brand_color' => '#aabbcc',
|
||||
'organizer' => 'CAPBuSS',
|
||||
'registration_open' => '1',
|
||||
'badge_fields' => ['Company', 'Title'],
|
||||
],
|
||||
'default_style' => [
|
||||
'foreground' => '#112233',
|
||||
'background' => '#ffffff',
|
||||
@@ -61,6 +74,11 @@ class QrSettingsTest extends TestCase
|
||||
$this->assertSame('alerts@example.com', $settings->notify_email);
|
||||
$this->assertTrue($settings->product_updates);
|
||||
$this->assertFalse($settings->low_balance_alerts);
|
||||
$this->assertTrue($settings->notify_registrations);
|
||||
$this->assertFalse($settings->notify_payouts);
|
||||
$this->assertSame('USD', $settings->resolvedEventDefaults()['currency']);
|
||||
$this->assertSame('free', $settings->resolvedEventDefaults()['mode']);
|
||||
$this->assertSame('CAPBuSS', $settings->resolvedEventDefaults()['organizer']);
|
||||
$this->assertSame('#112233', $settings->resolvedDefaultStyle()['foreground']);
|
||||
$this->assertSame('scan_me', $settings->resolvedDefaultStyle()['frame_style']);
|
||||
}
|
||||
@@ -72,6 +90,7 @@ class QrSettingsTest extends TestCase
|
||||
'user_id' => $user->id,
|
||||
'default_type' => QrCode::TYPE_EVENT,
|
||||
'default_style' => ['foreground' => '#aabbcc', 'module_style' => 'dots'],
|
||||
'event_defaults' => ['mode' => 'contributions', 'organizer' => 'Test Org', 'brand_color' => '#ff00aa'],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
@@ -83,6 +102,7 @@ class QrSettingsTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('favicon.ico', false)
|
||||
->assertSee('#aabbcc', false)
|
||||
->assertSee('Test Org', false)
|
||||
->assertSee('"event"', false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user