Commit ce5c6e89 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Upgrade event form builder with 20+ new features — radio, uniqueness, maps, conditionals

- Add radio, national_id, location, section_divider, terms field types to FormFieldType enum
- Add field uniqueness constraint (blocks duplicate registrations per unique field)
- Add Google Maps locations support (JSONB column + map embed on public page)
- Add conditional field visibility (show field only when another field equals a value)
- Add min/max length, min/max value, regex pattern validation per field
- Add allow_other option for select/radio/multi_select fields
- Add field description/help text, placeholder, default value
- Add duplicate field action in form builder
- Add section dividers for visual grouping
- Add terms acceptance field type with URL link
- Sanitize form fields before save (auto-fill values, strip invalid options)
- Public registration form uses Alpine.js for conditional visibility
- Character counters shown when max_length is set
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent a26be026
......@@ -12,8 +12,13 @@
case Date = 'date';
case Select = 'select';
case MultiSelect = 'multi_select';
case Radio = 'radio';
case Checkbox = 'checkbox';
case FileUpload = 'file_upload';
case NationalId = 'national_id';
case Location = 'location';
case SectionDivider = 'section_divider';
case Terms = 'terms';
public function label(): string
{
......@@ -24,10 +29,15 @@ public function label(): string
self::Email => 'بريد إلكتروني',
self::Phone => 'هاتف',
self::Date => 'تاريخ',
self::Select => 'قائمة اختيار',
self::Select => 'قائمة منسدلة (اختيار واحد)',
self::MultiSelect => 'اختيار متعدد',
self::Radio => 'أزرار اختيار (إجابة واحدة)',
self::Checkbox => 'خانة اختيار',
self::FileUpload => 'رفع ملف',
self::NationalId => 'الرقم القومي',
self::Location => 'موقع خريطة',
self::SectionDivider => 'فاصل / عنوان قسم',
self::Terms => 'الموافقة على الشروط',
};
}
......@@ -42,8 +52,13 @@ public function icon(): string
self::Date => 'calendar',
self::Select => 'chevron-down',
self::MultiSelect => 'list-bullet',
self::Checkbox => 'check-circle',
self::Radio => 'check-circle',
self::Checkbox => 'check',
self::FileUpload => 'arrow-up-tray',
self::NationalId => 'identification',
self::Location => 'map-pin',
self::SectionDivider => 'minus',
self::Terms => 'shield-check',
};
}
......@@ -58,8 +73,28 @@ public function baseValidation(): string
self::Date => 'date',
self::Select => 'string',
self::MultiSelect => 'array',
self::Checkbox => 'boolean',
self::Radio => 'string',
self::Checkbox => 'accepted',
self::FileUpload => 'file|max:5120',
self::NationalId => 'string|size:14|regex:/^[0-9]{14}$/',
self::Location => 'string',
self::SectionDivider => '',
self::Terms => 'accepted',
};
}
public function hasOptions(): bool
{
return in_array($this, [self::Select, self::MultiSelect, self::Radio]);
}
public function isInputField(): bool
{
return $this !== self::SectionDivider;
}
public function supportsUnique(): bool
{
return in_array($this, [self::Text, self::Email, self::Phone, self::NationalId, self::Number]);
}
}
......@@ -34,6 +34,7 @@ class Event extends Model
'facility_id',
'location_name',
'location_address',
'map_locations',
'starts_at',
'ends_at',
'registration_opens_at',
......@@ -59,6 +60,7 @@ class Event extends Model
'registrations_count' => 'integer',
'form_fields' => 'array',
'settings' => 'array',
'map_locations' => 'array',
];
public function facility(): BelongsTo
......
......@@ -23,6 +23,8 @@ public function register(Event $event, array $formData, array $meta): EventRegis
throw new DomainException('تم اكتمال العدد المسموح به');
}
$this->checkUniqueness($event, $formData);
return DB::transaction(function () use ($event, $formData, $meta) {
$personId = $this->matchPerson($meta['phone'], $meta['email'] ?? null, $event->academy_id);
......@@ -126,40 +128,130 @@ public function validateFormData(Event $event, array $submitted): array
$messages = [];
foreach ($event->form_fields as $field) {
$type = FormFieldType::tryFrom($field['type']);
if (! $type || ! $type->isInputField()) {
continue;
}
if ($this->isFieldHiddenByCondition($field, $submitted, $event->form_fields)) {
continue;
}
$key = "fields.{$field['key']}";
$fieldRules = [];
if ($field['is_required']) {
$fieldRules[] = 'required';
if ($field['is_required'] ?? false) {
$fieldRules[] = $field['type'] === 'terms' ? 'accepted' : 'required';
$messages["{$key}.required"] = "حقل {$field['label']} مطلوب";
$messages["{$key}.accepted"] = "يجب الموافقة على {$field['label']}";
} else {
$fieldRules[] = 'nullable';
}
$type = FormFieldType::tryFrom($field['type']);
if ($type) {
if ($type && $type->baseValidation()) {
foreach (explode('|', $type->baseValidation()) as $rule) {
if ($rule === 'accepted' && !($field['is_required'] ?? false)) {
continue;
}
$fieldRules[] = $rule;
}
}
if (in_array($field['type'], ['select']) && ! empty($field['options'])) {
if ($type && $type->hasOptions() && !empty($field['options'])) {
$values = array_column($field['options'], 'value');
$fieldRules[] = 'in:' . implode(',', $values);
if ($field['allow_other'] ?? false) {
// Don't restrict to options if "Other" is allowed
} elseif ($type === FormFieldType::MultiSelect) {
$fieldRules[] = 'array';
$rules["{$key}.*"] = 'in:' . implode(',', $values);
} else {
$fieldRules[] = 'in:' . implode(',', $values);
}
}
$rules[$key] = implode('|', $fieldRules);
if (!empty($field['min_length'])) {
$fieldRules[] = 'min:' . (int) $field['min_length'];
}
if (!empty($field['max_length'])) {
$fieldRules[] = 'max:' . (int) $field['max_length'];
}
if (!empty($field['min_value']) && $type === FormFieldType::Number) {
$fieldRules[] = 'min:' . (float) $field['min_value'];
}
if (!empty($field['max_value']) && $type === FormFieldType::Number) {
$fieldRules[] = 'max:' . (float) $field['max_value'];
}
if (!empty($field['pattern'])) {
$fieldRules[] = 'regex:/' . $field['pattern'] . '/';
$messages["{$key}.regex"] = $field['pattern_message'] ?? "تنسيق {$field['label']} غير صحيح";
}
$rules[$key] = implode('|', array_filter($fieldRules));
}
return ['rules' => $rules, 'messages' => $messages];
}
public function checkUniqueness(Event $event, array $formData): void
{
foreach ($event->form_fields as $field) {
if (empty($field['is_unique'])) {
continue;
}
$value = $formData[$field['key']] ?? null;
if (empty($value)) {
continue;
}
$exists = EventRegistration::withoutGlobalScopes()
->where('event_id', $event->id)
->where('status', '!=', 'cancelled')
->whereRaw("form_data->>? = ?", [$field['key'], $value])
->exists();
if ($exists) {
throw new DomainException("تم التسجيل مسبقاً بنفس {$field['label']}");
}
}
}
private function isFieldHiddenByCondition(array $field, array $submitted, array $allFields): bool
{
if (empty($field['condition_field']) || empty($field['condition_value'])) {
return false;
}
$conditionFieldKey = $field['condition_field'];
$conditionValue = $field['condition_value'];
$submittedValue = $submitted[$conditionFieldKey] ?? null;
if ($submittedValue === null) {
return true;
}
if (is_array($submittedValue)) {
return !in_array($conditionValue, $submittedValue);
}
return (string) $submittedValue !== (string) $conditionValue;
}
public function matchPerson(string $phone, ?string $email, int $academyId): ?int
{
if (empty($phone) && empty($email)) {
return null;
}
$person = Person::withoutGlobalScopes()
->where('academy_id', $academyId)
->where(function ($q) use ($phone, $email) {
$q->where('phone', $phone);
if ($phone) {
$q->where('phone', $phone);
}
if ($email) {
$q->orWhere('email', $email);
}
......
......@@ -38,6 +38,7 @@ class CreateEventWizard extends Component
public ?int $facilityId = null;
public string $locationName = '';
public string $locationAddress = '';
public array $mapLocations = [];
public string $startsAt = '';
public string $endsAt = '';
public string $registrationOpensAt = '';
......@@ -75,6 +76,7 @@ private function fillFromEvent(Event $event): void
$this->facilityId = $event->facility_id;
$this->locationName = $event->location_name ?? '';
$this->locationAddress = $event->location_address ?? '';
$this->mapLocations = $event->map_locations ?: [];
$this->startsAt = $event->starts_at?->format('Y-m-d\TH:i');
$this->endsAt = $event->ends_at?->format('Y-m-d\TH:i');
$this->registrationOpensAt = $event->registration_opens_at?->format('Y-m-d\TH:i') ?? '';
......@@ -86,42 +88,40 @@ private function fillFromEvent(Event $event): void
private function getDefaultFields(): array
{
return [
[
'key' => 'name',
'type' => 'text',
'label' => 'الاسم الكامل',
'label_en' => 'Full Name',
'placeholder' => '',
'is_required' => true,
'sort_order' => 1,
'options' => [],
'width' => 'full',
],
[
'key' => 'phone',
'type' => 'phone',
'label' => 'رقم الهاتف',
'label_en' => 'Phone',
'placeholder' => '01XXXXXXXXX',
'is_required' => true,
'sort_order' => 2,
'options' => [],
'width' => 'half',
],
[
'key' => 'email',
'type' => 'email',
'label' => 'البريد الإلكتروني',
'label_en' => 'Email',
'placeholder' => '',
'is_required' => false,
'sort_order' => 3,
'options' => [],
'width' => 'half',
],
$this->makeField('name', 'text', 'الاسم الكامل', true, 'full', ['label_en' => 'Full Name']),
$this->makeField('phone', 'phone', 'رقم الهاتف', true, 'half', ['label_en' => 'Phone', 'placeholder' => '01XXXXXXXXX', 'is_unique' => true]),
$this->makeField('email', 'email', 'البريد الإلكتروني', false, 'half', ['label_en' => 'Email']),
];
}
private function makeField(string $key, string $type, string $label, bool $required, string $width, array $extra = []): array
{
return array_merge([
'key' => $key,
'type' => $type,
'label' => $label,
'label_en' => '',
'placeholder' => '',
'description' => '',
'is_required' => $required,
'is_unique' => false,
'sort_order' => 0,
'options' => [],
'width' => $width,
'allow_other' => false,
'min_length' => null,
'max_length' => null,
'min_value' => null,
'max_value' => null,
'default_value' => '',
'pattern' => '',
'pattern_message' => '',
'condition_field' => '',
'condition_value' => '',
'terms_url' => '',
], $extra);
}
public function nextStep(): void
{
$this->validateStep();
......@@ -173,21 +173,25 @@ private function validateStep(): void
}
}
// Form Builder Actions
// ========== Form Builder Actions ==========
public function addField(): void
{
$order = count($this->formFields) + 1;
$this->formFields[] = [
'key' => 'field_' . $order,
'type' => 'text',
'label' => '',
'label_en' => '',
'placeholder' => '',
'is_required' => false,
'sort_order' => $order,
'options' => [],
'width' => 'full',
];
$this->formFields[] = $this->makeField('field_' . $order, 'text', '', false, 'full');
$this->reorderFields();
}
public function duplicateField(int $index): void
{
if (!isset($this->formFields[$index])) return;
$clone = $this->formFields[$index];
$clone['key'] = $clone['key'] . '_copy';
$clone['label'] = $clone['label'] . ' (نسخة)';
array_splice($this->formFields, $index + 1, 0, [$clone]);
$this->reorderFields();
}
public function removeField(int $index): void
......@@ -219,11 +223,13 @@ public function moveFieldDown(int $index): void
public function addOption(int $fieldIndex): void
{
if (!isset($this->formFields[$fieldIndex])) return;
$this->formFields[$fieldIndex]['options'][] = ['value' => '', 'label' => ''];
}
public function removeOption(int $fieldIndex, int $optionIndex): void
{
if (!isset($this->formFields[$fieldIndex]['options'][$optionIndex])) return;
unset($this->formFields[$fieldIndex]['options'][$optionIndex]);
$this->formFields[$fieldIndex]['options'] = array_values($this->formFields[$fieldIndex]['options']);
}
......@@ -232,13 +238,36 @@ private function reorderFields(): void
{
foreach ($this->formFields as $i => &$field) {
$field['sort_order'] = $i + 1;
$field['key'] = $field['key'] ?: 'field_' . ($i + 1);
if (empty($field['key'])) {
$field['key'] = 'field_' . ($i + 1);
}
}
}
// ========== Map Locations ==========
public function addMapLocation(): void
{
$this->mapLocations[] = [
'title' => '',
'lat' => '',
'lng' => '',
'address' => '',
];
}
public function removeMapLocation(int $index): void
{
unset($this->mapLocations[$index]);
$this->mapLocations = array_values($this->mapLocations);
}
// ========== Save ==========
public function save(EventService $eventService, MediaService $mediaService): void
{
$this->validateStep();
$this->sanitizeFormFields();
$academyId = app('current_academy')->id;
......@@ -253,6 +282,7 @@ public function save(EventService $eventService, MediaService $mediaService): vo
'facility_id' => $this->locationType === 'facility' ? $this->facilityId : null,
'location_name' => $this->locationType === 'external' ? $this->locationName : null,
'location_address' => $this->locationType === 'external' ? $this->locationAddress : null,
'map_locations' => array_filter($this->mapLocations, fn ($loc) => !empty($loc['lat']) && !empty($loc['lng'])),
'starts_at' => $this->startsAt,
'ends_at' => $this->endsAt,
'registration_opens_at' => $this->registrationOpensAt ?: null,
......@@ -268,7 +298,6 @@ public function save(EventService $eventService, MediaService $mediaService): vo
$event = $eventService->create($data, auth()->user());
}
// Upload cover
if ($this->coverPhoto) {
if ($event->cover) {
$mediaService->replace($event->cover, $this->coverPhoto);
......@@ -282,7 +311,6 @@ public function save(EventService $eventService, MediaService $mediaService): vo
}
}
// Upload gallery
foreach ($this->galleryPhotos as $photo) {
$mediaService->upload(
$photo,
......@@ -302,6 +330,39 @@ public function save(EventService $eventService, MediaService $mediaService): vo
}
}
private function sanitizeFormFields(): void
{
foreach ($this->formFields as $i => &$field) {
$field['key'] = $field['key'] ?: 'field_' . ($i + 1);
$field['key'] = preg_replace('/[^a-z0-9_]/', '_', strtolower($field['key']));
$type = FormFieldType::tryFrom($field['type']);
if (!$type || !$type->hasOptions()) {
$field['options'] = [];
$field['allow_other'] = false;
}
if (!$type || !$type->supportsUnique()) {
$field['is_unique'] = false;
}
if ($type === FormFieldType::SectionDivider) {
$field['is_required'] = false;
$field['is_unique'] = false;
}
// Auto-fill option values from labels if empty
foreach ($field['options'] as $oi => &$opt) {
if (empty($opt['value']) && !empty($opt['label'])) {
$opt['value'] = preg_replace('/\s+/', '_', trim($opt['label']));
}
}
// Remove empty options
$field['options'] = array_values(array_filter($field['options'], fn ($o) => !empty($o['label'])));
}
}
public function render()
{
return view('livewire.events.create-event-wizard', [
......
<?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('events', function (Blueprint $table) {
$table->jsonb('map_locations')->default('[]')->after('location_address');
});
}
public function down(): void
{
Schema::table('events', function (Blueprint $table) {
$table->dropColumn('map_locations');
});
}
};
......@@ -141,6 +141,36 @@ class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2
@endif
</div>
{{-- Google Maps Locations --}}
<div class="border-t pt-4">
<div class="flex items-center justify-between mb-3">
<div>
<label class="block text-sm font-medium text-gray-700">{{ __('مواقع على الخريطة') }}</label>
<p class="text-xs text-gray-500">{{ __('أضف إحداثيات ليتم عرضها على خرائط جوجل في صفحة الحدث') }}</p>
</div>
<button wire:click="addMapLocation" type="button" class="text-xs text-blue-600 hover:text-blue-800 font-medium">+ {{ __('إضافة موقع') }}</button>
</div>
@foreach($mapLocations as $locIndex => $loc)
<div class="flex items-start gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200" wire:key="map-loc-{{ $locIndex }}">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-4 gap-2">
<input type="text" wire:model="mapLocations.{{ $locIndex }}.title" placeholder="{{ __('عنوان الموقع') }}"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="text" wire:model="mapLocations.{{ $locIndex }}.lat" placeholder="{{ __('خط العرض (Lat)') }}" dir="ltr"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="text" wire:model="mapLocations.{{ $locIndex }}.lng" placeholder="{{ __('خط الطول (Lng)') }}" dir="ltr"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="text" wire:model="mapLocations.{{ $locIndex }}.address" placeholder="{{ __('العنوان (اختياري)') }}"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
</div>
<button wire:click="removeMapLocation({{ $locIndex }})" type="button" class="p-1.5 text-red-400 hover:text-red-600 mt-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
@endforeach
<p class="text-xs text-gray-400 mt-1">{{ __('يمكنك الحصول على الإحداثيات من Google Maps بالنقر بزر الماوس الأيمن على الموقع') }}</p>
</div>
{{-- Description --}}
<div class="border-t pt-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('وصف الحدث') }}</label>
......@@ -155,9 +185,9 @@ class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-lg font-semibold text-gray-800">{{ __('نموذج التسجيل') }}</h2>
<p class="text-sm text-gray-500">{{ __('حدد الحقول التي سيملأها المسجلون') }}</p>
<p class="text-sm text-gray-500">{{ __('حدد الحقول التي سيملأها المسجلون — الحقول الفريدة تمنع التسجيل المكرر') }}</p>
</div>
<button wire:click="addField" class="inline-flex items-center gap-1.5 px-3 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
<button wire:click="addField" type="button" class="inline-flex items-center gap-1.5 px-3 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('إضافة حقل') }}
</button>
......@@ -170,14 +200,15 @@ class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2
@else
<div class="space-y-4">
@foreach($formFields as $index => $field)
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50" wire:key="field-{{ $index }}">
<div class="flex items-start justify-between gap-4">
{{-- Field Config --}}
<div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50 {{ $field['type'] === 'section_divider' ? 'border-dashed border-blue-300 bg-blue-50/30' : '' }}" wire:key="field-{{ $index }}">
{{-- Row 1: Main config --}}
<div class="flex items-start justify-between gap-3">
<div class="flex-1 grid grid-cols-1 md:grid-cols-4 gap-3">
<div class="md:col-span-2">
<label class="block text-xs font-medium text-gray-500 mb-1">{{ __('العنوان') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="formFields.{{ $index }}.label"
placeholder="{{ __('مثال: الاسم الكامل') }}"
placeholder="{{ $field['type'] === 'section_divider' ? __('عنوان القسم') : __('مثال: الاسم الكامل') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
......@@ -200,44 +231,167 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
</div>
{{-- Actions --}}
<div class="flex items-center gap-1 pt-5">
<label class="flex items-center gap-1 text-xs text-gray-500 me-2">
<input type="checkbox" wire:model="formFields.{{ $index }}.is_required" class="rounded text-blue-600 focus:ring-blue-500">
{{ __('مطلوب') }}
</label>
<button wire:click="moveFieldUp({{ $index }})" @if($index === 0) disabled @endif
<div class="flex items-center gap-0.5 pt-5">
<button wire:click="duplicateField({{ $index }})" type="button" title="{{ __('نسخ') }}" class="p-1.5 text-gray-400 hover:text-blue-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
</button>
<button wire:click="moveFieldUp({{ $index }})" type="button" @if($index === 0) disabled @endif
class="p-1.5 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>
</button>
<button wire:click="moveFieldDown({{ $index }})" @if($index === count($formFields) - 1) disabled @endif
<button wire:click="moveFieldDown({{ $index }})" type="button" @if($index === count($formFields) - 1) disabled @endif
class="p-1.5 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<button wire:click="removeField({{ $index }})" class="p-1.5 text-red-400 hover:text-red-600">
<button wire:click="removeField({{ $index }})" type="button" class="p-1.5 text-red-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
{{-- Options for select/multi_select --}}
@if(in_array($field['type'], ['select', 'multi_select']))
{{-- Row 2: Toggles (skip for section_divider) --}}
@if($field['type'] !== 'section_divider')
<div class="flex flex-wrap items-center gap-4 mt-3 pt-3 border-t border-gray-200">
<label class="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer">
<input type="checkbox" wire:model="formFields.{{ $index }}.is_required" class="rounded text-blue-600 focus:ring-blue-500">
{{ __('مطلوب') }}
</label>
@if(in_array($field['type'], ['text', 'email', 'phone', 'national_id', 'number']))
<label class="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer">
<input type="checkbox" wire:model="formFields.{{ $index }}.is_unique" class="rounded text-orange-600 focus:ring-orange-500">
{{ __('فريد (لا يتكرر)') }}
</label>
@endif
@if(in_array($field['type'], ['select', 'multi_select', 'radio']))
<label class="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer">
<input type="checkbox" wire:model="formFields.{{ $index }}.allow_other" class="rounded text-purple-600 focus:ring-purple-500">
{{ __('السماح بـ "أخرى"') }}
</label>
@endif
</div>
{{-- Row 3: Expandable advanced settings --}}
<div x-data="{ showAdvanced: false }" class="mt-2">
<button @click="showAdvanced = !showAdvanced" type="button" class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1">
<svg class="w-3 h-3 transition-transform" :class="showAdvanced ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
{{ __('إعدادات متقدمة') }}
</button>
<div x-show="showAdvanced" x-transition class="mt-2 grid grid-cols-1 md:grid-cols-3 gap-3 p-3 bg-white rounded-lg border border-gray-100">
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('وصف / مساعدة') }}</label>
<input type="text" wire:model="formFields.{{ $index }}.description" placeholder="{{ __('نص يظهر أسفل الحقل') }}"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('نص توضيحي (placeholder)') }}</label>
<input type="text" wire:model="formFields.{{ $index }}.placeholder" placeholder="{{ __('مثال: أدخل اسمك') }}"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('قيمة افتراضية') }}</label>
<input type="text" wire:model="formFields.{{ $index }}.default_value" placeholder="{{ __('قيمة مسبقة') }}"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
@if(in_array($field['type'], ['text', 'textarea']))
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('الحد الأدنى (حروف)') }}</label>
<input type="number" wire:model="formFields.{{ $index }}.min_length" dir="ltr" min="0"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('الحد الأقصى (حروف)') }}</label>
<input type="number" wire:model="formFields.{{ $index }}.max_length" dir="ltr" min="0"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
@endif
@if($field['type'] === 'number')
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('أقل قيمة') }}</label>
<input type="number" wire:model="formFields.{{ $index }}.min_value" dir="ltr"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('أعلى قيمة') }}</label>
<input type="number" wire:model="formFields.{{ $index }}.max_value" dir="ltr"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
@endif
@if(in_array($field['type'], ['text', 'phone', 'national_id']))
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('نمط التحقق (Regex)') }}</label>
<input type="text" wire:model="formFields.{{ $index }}.pattern" dir="ltr" placeholder="^[0-9]{11}$"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs font-mono focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('رسالة خطأ النمط') }}</label>
<input type="text" wire:model="formFields.{{ $index }}.pattern_message" placeholder="{{ __('يجب أن يكون 11 رقم') }}"
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
@endif
@if($field['type'] === 'terms')
<div class="md:col-span-3">
<label class="block text-xs text-gray-500 mb-1">{{ __('رابط الشروط والأحكام') }}</label>
<input type="url" wire:model="formFields.{{ $index }}.terms_url" dir="ltr" placeholder="https://..."
class="w-full px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
@endif
{{-- Conditional Visibility --}}
<div class="md:col-span-3 border-t border-gray-100 pt-2 mt-1">
<label class="block text-xs text-gray-500 mb-1">{{ __('إظهار هذا الحقل فقط إذا...') }}</label>
<div class="grid grid-cols-2 gap-2">
<select wire:model="formFields.{{ $index }}.condition_field"
class="px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
<option value="">{{ __('بدون شرط (يظهر دائماً)') }}</option>
@foreach($formFields as $ci => $cf)
@if($ci !== $index && $cf['type'] !== 'section_divider')
<option value="{{ $cf['key'] }}">{{ $cf['label'] ?: 'حقل ' . ($ci + 1) }}</option>
@endif
@endforeach
</select>
<input type="text" wire:model="formFields.{{ $index }}.condition_value" placeholder="{{ __('يساوي هذه القيمة') }}"
class="px-3 py-1.5 border border-gray-200 rounded text-xs focus:ring-1 focus:ring-blue-500">
</div>
</div>
</div>
</div>
@endif
{{-- Options for select/multi_select/radio --}}
@if(in_array($field['type'], ['select', 'multi_select', 'radio']))
<div class="mt-3 pt-3 border-t border-gray-200">
<label class="block text-xs font-medium text-gray-500 mb-2">{{ __('الخيارات') }}</label>
<label class="block text-xs font-medium text-gray-500 mb-2">
{{ __('الخيارات') }}
@if($field['type'] === 'radio')
<span class="text-gray-400">({{ __('إجابة واحدة فقط') }})</span>
@elseif($field['type'] === 'multi_select')
<span class="text-gray-400">({{ __('يمكن اختيار أكثر من إجابة') }})</span>
@endif
</label>
<div class="space-y-2">
@foreach($field['options'] ?? [] as $optIndex => $option)
<div class="flex items-center gap-2">
@if($field['type'] === 'radio')
<span class="w-4 h-4 rounded-full border-2 border-gray-300 shrink-0"></span>
@elseif($field['type'] === 'multi_select')
<span class="w-4 h-4 rounded border-2 border-gray-300 shrink-0"></span>
@endif
<input type="text" wire:model="formFields.{{ $index }}.options.{{ $optIndex }}.label"
placeholder="{{ __('نص الخيار') }}"
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<input type="text" wire:model="formFields.{{ $index }}.options.{{ $optIndex }}.value"
placeholder="{{ __('القيمة') }}" dir="ltr"
class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button wire:click="removeOption({{ $index }}, {{ $optIndex }})" class="p-1 text-red-400 hover:text-red-600">
placeholder="{{ __('القيمة (تلقائي)') }}" dir="ltr"
class="w-28 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button wire:click="removeOption({{ $index }}, {{ $optIndex }})" type="button" class="p-1 text-red-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
@endforeach
<button wire:click="addOption({{ $index }})" class="text-xs text-blue-600 hover:text-blue-800 font-medium">
<button wire:click="addOption({{ $index }})" type="button" class="text-xs text-blue-600 hover:text-blue-800 font-medium">
+ {{ __('إضافة خيار') }}
</button>
</div>
......@@ -300,6 +454,9 @@ class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 f
<div><span class="text-gray-500">{{ __('النهاية') }}:</span> <span class="font-medium" dir="ltr">{{ $endsAt }}</span></div>
<div><span class="text-gray-500">{{ __('السعة') }}:</span> <span class="font-medium">{{ $maxCapacity ?? __('غير محدود') }}</span></div>
<div><span class="text-gray-500">{{ __('حقول النموذج') }}:</span> <span class="font-medium">{{ count($formFields) }} {{ __('حقل') }}</span></div>
@if(!empty($mapLocations))
<div><span class="text-gray-500">{{ __('مواقع الخريطة') }}:</span> <span class="font-medium">{{ count($mapLocations) }}</span></div>
@endif
</div>
@if(!empty($formFields))
......@@ -307,9 +464,10 @@ class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 f
<h3 class="text-sm font-medium text-gray-700 mb-2">{{ __('حقول التسجيل') }}</h3>
<div class="flex flex-wrap gap-2">
@foreach($formFields as $field)
<span class="px-2.5 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
<span class="px-2.5 py-1 bg-gray-100 text-gray-700 rounded-full text-xs inline-flex items-center gap-1">
{{ $field['label'] ?: __('بدون عنوان') }}
@if($field['is_required']) <span class="text-red-500">*</span> @endif
@if($field['is_required'] ?? false) <span class="text-red-500">*</span> @endif
@if($field['is_unique'] ?? false) <span class="text-orange-500 text-[10px]">(فريد)</span> @endif
</span>
@endforeach
</div>
......@@ -321,7 +479,7 @@ class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 f
{{-- Navigation --}}
<div class="flex items-center justify-between mt-6">
@if($currentStep > 1)
<button wire:click="previousStep" class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">
<button wire:click="previousStep" type="button" class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">
{{ __('السابق') }}
</button>
@else
......@@ -329,12 +487,12 @@ class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 f
@endif
@if($currentStep < $totalSteps)
<button wire:click="nextStep" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
<button wire:click="nextStep" type="button" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
{{ __('التالي') }}
</button>
@else
<button wire:click="save" wire:loading.attr="disabled" wire:target="save"
class="px-6 py-2.5 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
<button wire:click="save" wire:loading.attr="disabled" wire:target="save" type="button"
class="px-6 py-2.5 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ $editing ? __('حفظ التعديلات') : __('إنشاء الحدث') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
......
......@@ -14,35 +14,86 @@
</div>
@else
{{-- Registration Form --}}
<form wire:submit="submit" class="space-y-5">
<form wire:submit="submit" class="space-y-5" x-data="eventForm(@js($event->form_fields), @js($fields))">
@error('form')
<div class="p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">{{ $message }}</div>
@enderror
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($event->form_fields as $field)
<div class="{{ $field['width'] === 'full' ? 'md:col-span-2' : '' }}">
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $field['label'] }}
@if($field['is_required']) <span class="text-red-500">*</span> @endif
</label>
@foreach($event->form_fields as $fieldIndex => $field)
@php
$hasCondition = !empty($field['condition_field']) && !empty($field['condition_value']);
$colSpan = ($field['width'] ?? 'full') === 'full' || $field['type'] === 'section_divider' ? 'md:col-span-2' : '';
@endphp
{{-- Section Divider --}}
@if($field['type'] === 'section_divider')
<div class="{{ $colSpan }} pt-4 pb-1"
@if($hasCondition) x-show="isVisible('{{ $field['key'] }}')" x-transition @endif>
<h4 class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-2">{{ $field['label'] }}</h4>
@if(!empty($field['description']))
<p class="text-xs text-gray-500 mt-1">{{ $field['description'] }}</p>
@endif
</div>
@continue
@endif
<div class="{{ $colSpan }}"
@if($hasCondition) x-show="isVisible('{{ $field['key'] }}')" x-transition @endif>
{{-- Label --}}
@if($field['type'] !== 'terms')
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $field['label'] }}
@if($field['is_required'] ?? false) <span class="text-red-500">*</span> @endif
@if($field['is_unique'] ?? false)
<span class="text-xs text-orange-500 ms-1">({{ __('فريد') }})</span>
@endif
</label>
@endif
{{-- Description --}}
@if(!empty($field['description']) && $field['type'] !== 'section_divider')
<p class="text-xs text-gray-400 mb-1.5">{{ $field['description'] }}</p>
@endif
@switch($field['type'])
@case('text')
@case('email')
@case('phone')
@case('number')
<input type="{{ $field['type'] === 'phone' ? 'tel' : $field['type'] }}"
@case('national_id')
<input type="{{ $field['type'] === 'phone' ? 'tel' : ($field['type'] === 'email' ? 'email' : 'text') }}"
wire:model="fields.{{ $field['key'] }}"
placeholder="{{ $field['placeholder'] ?? '' }}"
@if(in_array($field['type'], ['email', 'phone', 'number'])) dir="ltr" @endif
@if(in_array($field['type'], ['email', 'phone', 'national_id'])) dir="ltr" @endif
@if(!empty($field['max_length'])) maxlength="{{ $field['max_length'] }}" @endif
@if(!empty($field['pattern'])) pattern="{{ $field['pattern'] }}" @endif
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
@if(!empty($field['max_length']))
<p class="text-xs text-gray-400 mt-1 text-end" dir="ltr">
<span x-text="($wire.fields['{{ $field['key'] }}'] || '').length"></span>/{{ $field['max_length'] }}
</p>
@endif
@break
@case('number')
<input type="number" wire:model="fields.{{ $field['key'] }}" dir="ltr"
placeholder="{{ $field['placeholder'] ?? '' }}"
@if(!empty($field['min_value'])) min="{{ $field['min_value'] }}" @endif
@if(!empty($field['max_value'])) max="{{ $field['max_value'] }}" @endif
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
@break
@case('textarea')
<textarea wire:model="fields.{{ $field['key'] }}" rows="3"
placeholder="{{ $field['placeholder'] ?? '' }}"
@if(!empty($field['max_length'])) maxlength="{{ $field['max_length'] }}" @endif
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror"></textarea>
@if(!empty($field['max_length']))
<p class="text-xs text-gray-400 mt-1 text-end" dir="ltr">
<span x-text="($wire.fields['{{ $field['key'] }}'] || '').length"></span>/{{ $field['max_length'] }}
</p>
@endif
@break
@case('date')
......@@ -51,32 +102,100 @@ class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 f
@break
@case('select')
<select wire:model="fields.{{ $field['key'] }}"
<select wire:model.live="fields.{{ $field['key'] }}"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
<option value="">{{ $field['placeholder'] ?? __('اختر...') }}</option>
@foreach($field['options'] ?? [] as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
@if($field['allow_other'] ?? false)
<option value="__other__">{{ __('أخرى') }}</option>
@endif
</select>
@if($field['allow_other'] ?? false)
<div x-show="$wire.fields['{{ $field['key'] }}'] === '__other__'" x-transition class="mt-2">
<input type="text" wire:model="fields.{{ $field['key'] }}_other" placeholder="{{ __('حدد...') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@endif
@break
@case('radio')
<div class="space-y-2 @error('fields.'.$field['key']) ring-1 ring-red-400 rounded-xl p-2 @enderror">
@foreach($field['options'] ?? [] as $option)
<label class="flex items-center gap-2.5 cursor-pointer p-2 rounded-lg hover:bg-gray-50 transition-colors">
<input type="radio" wire:model.live="fields.{{ $field['key'] }}" value="{{ $option['value'] }}"
class="text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $option['label'] }}</span>
</label>
@endforeach
@if($field['allow_other'] ?? false)
<label class="flex items-center gap-2.5 cursor-pointer p-2 rounded-lg hover:bg-gray-50 transition-colors">
<input type="radio" wire:model.live="fields.{{ $field['key'] }}" value="__other__"
class="text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ __('أخرى') }}</span>
</label>
<div x-show="$wire.fields['{{ $field['key'] }}'] === '__other__'" x-transition class="ms-6">
<input type="text" wire:model="fields.{{ $field['key'] }}_other" placeholder="{{ __('حدد...') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@endif
</div>
@break
@case('multi_select')
<div class="space-y-2 border border-gray-300 rounded-xl p-3 @error('fields.'.$field['key']) border-red-500 @enderror">
@foreach($field['options'] ?? [] as $option)
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" value="{{ $option['value'] }}" class="rounded text-blue-600 focus:ring-blue-500">
<label class="flex items-center gap-2.5 cursor-pointer p-1.5 rounded-lg hover:bg-gray-50 transition-colors">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" value="{{ $option['value'] }}"
class="rounded text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $option['label'] }}</span>
</label>
@endforeach
@if($field['allow_other'] ?? false)
<label class="flex items-center gap-2.5 cursor-pointer p-1.5 rounded-lg hover:bg-gray-50 transition-colors">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" value="__other__"
class="rounded text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ __('أخرى') }}</span>
</label>
<div x-show="($wire.fields['{{ $field['key'] }}'] || []).includes('__other__')" x-transition class="ms-6">
<input type="text" wire:model="fields.{{ $field['key'] }}_other" placeholder="{{ __('حدد...') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@endif
</div>
@break
@case('checkbox')
<label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-2.5 cursor-pointer">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" class="rounded text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $field['placeholder'] ?? $field['label'] }}</span>
</label>
@break
@case('terms')
<label class="flex items-start gap-2.5 cursor-pointer">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" class="rounded text-blue-600 focus:ring-blue-500 mt-0.5">
<span class="text-sm text-gray-700">
{{ $field['label'] }}
@if($field['is_required'] ?? false) <span class="text-red-500">*</span> @endif
@if(!empty($field['terms_url']))
<a href="{{ $field['terms_url'] }}" target="_blank" rel="noopener" class="text-blue-600 underline hover:text-blue-800">{{ __('اقرأ الشروط') }}</a>
@endif
</span>
</label>
@break
@case('location')
<input type="text" wire:model="fields.{{ $field['key'] }}" dir="ltr"
placeholder="{{ $field['placeholder'] ?? __('أدخل رابط الموقع أو الإحداثيات') }}"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
@break
@case('file_upload')
<input type="file" wire:model="fields.{{ $field['key'] }}"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 file:me-3 file:py-1 file:px-3 file:rounded-lg file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 @error('fields.'.$field['key']) border-red-500 @enderror">
@break
@endswitch
@error('fields.'.$field['key']) <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
......@@ -90,5 +209,21 @@ class="w-full py-3.5 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue
<span wire:loading wire:target="submit">{{ __('جارٍ التسجيل...') }}</span>
</button>
</form>
<script>
function eventForm(formFields, fields) {
return {
formFields: formFields,
isVisible(fieldKey) {
const field = this.formFields.find(f => f.key === fieldKey);
if (!field || !field.condition_field || !field.condition_value) return true;
const condVal = this.$wire.fields[field.condition_field];
if (condVal === null || condVal === undefined || condVal === '') return false;
if (Array.isArray(condVal)) return condVal.includes(field.condition_value);
return String(condVal) === String(field.condition_value);
}
};
}
</script>
@endif
</div>
......@@ -50,11 +50,11 @@ class="w-full h-full object-cover">
@endphp
<div class="flex flex-wrap items-center gap-3 mb-4">
<span class="inline-block px-3 py-1 text-xs font-bold rounded-full {{ $statusColors[$event->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusLabels[$event->status] ?? $event->status }}
<span class="inline-block px-3 py-1 text-xs font-bold rounded-full {{ $statusColors[$event->status->value ?? $event->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusLabels[$event->status->value ?? $event->status] ?? $event->status }}
</span>
<span class="inline-block px-3 py-1 text-xs font-medium rounded-full bg-[var(--site-accent)]/10 text-[var(--site-accent)]">
{{ $typeLabels[$event->type] ?? $event->type }}
{{ $typeLabels[$event->type->value ?? $event->type] ?? $event->type }}
</span>
</div>
......@@ -156,6 +156,33 @@ class="w-full h-full object-cover">
</div>
</div>
@endif
{{-- Map Locations --}}
@if(!empty($event->map_locations))
<div>
<h3 class="text-lg font-bold text-gray-900 mb-4">{{ __('الموقع على الخريطة') }}</h3>
<div class="space-y-4">
@foreach($event->map_locations as $loc)
@if(!empty($loc['lat']) && !empty($loc['lng']))
<div class="rounded-xl overflow-hidden border border-gray-200">
@if(!empty($loc['title']))
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<p class="text-sm font-medium text-gray-700">{{ $loc['title'] }}</p>
@if(!empty($loc['address']))
<p class="text-xs text-gray-500">{{ $loc['address'] }}</p>
@endif
</div>
@endif
<iframe
src="https://maps.google.com/maps?q={{ $loc['lat'] }},{{ $loc['lng'] }}&z=15&output=embed"
width="100%" height="250" style="border:0;" allowfullscreen loading="lazy"
referrerpolicy="no-referrer-when-downgrade" class="w-full"></iframe>
</div>
@endif
@endforeach
</div>
</div>
@endif
</div>
{{-- Sidebar: Registration Form --}}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment