1051 lines
62 KiB
PHP
1051 lines
62 KiB
PHP
<div class="mt-12">
|
|
|
|
<!-- Action buttons -->
|
|
<div class="mb-6">
|
|
<x-jetstream.button wire:click="create" class="bg-theme-brand hover:bg-opacity-80">
|
|
{{ __('New post') }}
|
|
</x-jetstream.button>
|
|
</div>
|
|
|
|
<!-- Search box -->
|
|
<div class="mb-4 flex items-center">
|
|
<!-- Input and Reset Button Container -->
|
|
<div class="relative w-2/3">
|
|
<input class="w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
|
placeholder="{{ __('Search keywords') . '...' }}" type="text"
|
|
wire:keydown.enter="handleSearchEnter" wire:model="search">
|
|
|
|
<!-- Reset Button -->
|
|
@if ($search)
|
|
<button class="absolute inset-y-0 right-0 flex items-center pr-3 text-theme-secondary hover:text-theme-primary focus:outline-none"
|
|
wire:click.prevent="resetSearch">
|
|
<x-icon mini name="backspace" solid />
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Search Button -->
|
|
<x-jetstream.secondary-button class="ml-3 w-32 justify-center" wire:click.prevent="searchPosts">
|
|
{{ __('Search') }}
|
|
</x-jetstream.secondary-button>
|
|
</div>
|
|
|
|
<!-- Filter dropdowns -->
|
|
<div class="mb-4 flex flex-wrap items-center gap-3">
|
|
<!-- Post Type Filter Dropdown -->
|
|
<div>
|
|
<x-select :clearable="true" :searchable="false" class="!w-40" style="width: 10rem !important; min-width: 10rem !important;"
|
|
placeholder="{{ __('Type') }}" wire:model.live="postTypeFilter">
|
|
@foreach ($this->postTypeOptions as $type)
|
|
<x-select.option label="{{ $type['name'] }}" value="{{ $type['id'] }}" />
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
|
|
<!-- Category Filter Dropdown -->
|
|
<div>
|
|
<x-select :clearable="true" :searchable="true" class="!w-80" style="width: 24rem !important; min-width: 24rem !important;"
|
|
placeholder="{{ __('Category') }}" wire:model.live="categoryFilter">
|
|
@foreach ($this->categories as $category)
|
|
<x-select.option label="{{ $category['name'] }}" value="{{ $category['id'] }}" />
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
|
|
<!-- Language Filter Dropdown -->
|
|
<div>
|
|
<x-select :clearable="true" :searchable="true" class="!w-40" style="width: 24rem !important; min-width: 24rem !important;"
|
|
placeholder="{{ __('Language') }}" wire:model.live="languageFilter">
|
|
@foreach ($this->languages as $lang)
|
|
<x-select.option label="{{ $lang['name'] }}" value="{{ $lang['id'] }}" />
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
|
|
<!-- Publication Status Filter Dropdown -->
|
|
<div>
|
|
<x-select :clearable="true" :searchable="false" class="!w-40" style="width: 24rem !important; min-width: 24rem !important;" placeholder="{{ __('Status') }}"
|
|
wire:model.live="publicationStatusFilter">
|
|
@foreach ($this->publicationStatusOptions as $status)
|
|
<x-select.option label="{{ $status['name'] }}" value="{{ $status['id'] }}" />
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup and Restore -->
|
|
@livewire('posts.backup-restore', ['showBackupSelected' => true], key('backup-restore'))
|
|
|
|
<!-- Selection action buttons -->
|
|
<div class="mb-6 flex items-center justify-end space-x-4">
|
|
@if ($publicationStatusFilter === 'deleted')
|
|
<x-jetstream.button
|
|
:disabled="$bulkDisabled"
|
|
onclick="confirm('{{ __('Are you sure you want to restore the selected posts?') }}') || event.stopImmediatePropagation()"
|
|
title="{{ __('Undelete') . ' ' . __('selection') }}"
|
|
wire:click.prevent="undeleteSelected">
|
|
<x-icon class="mr-3 h-5 w-5" name="arrow-path" />
|
|
{{ __('Undelete') }} {{ __('selection') }}
|
|
</x-jetstream.button>
|
|
@else
|
|
<x-jetstream.danger-button
|
|
:disabled="$bulkDisabled"
|
|
onclick="confirm('{{ __('Are you sure?') }}') || event.stopImmediatePropagation()"
|
|
title="{{ __('Delete') . ' ' . __('selection') }}"
|
|
wire:click.prevent="deleteSelected">
|
|
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
|
{{ __('Delete') }} {{ __('selection') }}
|
|
</x-jetstream.danger-button>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="bg-white shadow-sm rounded-lg" x-data="{
|
|
initScrollSync() {
|
|
if (this.$refs.topScroll && this.$refs.bottomScroll && this.$refs.table && this.$refs.topScrollContent) {
|
|
const topScroll = this.$refs.topScroll;
|
|
const bottomScroll = this.$refs.bottomScroll;
|
|
const table = this.$refs.table;
|
|
|
|
// Set the width of the dummy div to match table width
|
|
this.$refs.topScrollContent.style.width = table.scrollWidth + 'px';
|
|
}
|
|
}
|
|
}" x-init="setTimeout(() => initScrollSync(), 100)">
|
|
|
|
<!-- Top scrollbar -->
|
|
<div class="overflow-x-auto border-b border-gray-200" x-ref="topScroll" @scroll="$refs.bottomScroll.scrollLeft = $event.target.scrollLeft">
|
|
<div x-ref="topScrollContent" style="height: 20px;"></div>
|
|
</div>
|
|
|
|
<!-- Table with bottom scrollbar -->
|
|
<div class="overflow-x-auto" x-ref="bottomScroll" @scroll="$refs.topScroll.scrollLeft = $event.target.scrollLeft">
|
|
<table class="min-w-full divide-y divide-gray-200" x-ref="table">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<input
|
|
type="checkbox"
|
|
wire:model.live="selectAll"
|
|
title="{{ __('Select all on current page') }}"
|
|
class="rounded border-gray-300 text-theme-brand focus:border-theme-accent focus:ring-1 focus:ring-theme-accent">
|
|
</th>
|
|
|
|
{{-- Sortable Id column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('id')">
|
|
<div class="flex items-center">
|
|
{{ __('Id') }}
|
|
@if ($sortField === 'id')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable Category column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('category_id')">
|
|
<div class="flex items-center">
|
|
{{ __('Category') }}
|
|
@if ($sortField === 'category_id')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable Language column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('locale')">
|
|
<div class="flex items-center">
|
|
{{ __('Lang.') }}
|
|
@if ($sortField === 'locale')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable Title column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('title')">
|
|
<div class="flex items-center">
|
|
{{ __('Title') }}
|
|
@if ($sortField === 'title')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable Updated column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('updated_at')">
|
|
<div class="flex items-center">
|
|
{{ __('Updated') }}
|
|
@if ($sortField === 'updated_at')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable From column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('from')">
|
|
<div class="flex items-center">
|
|
{{ __('From') }}
|
|
@if ($sortField === 'from')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
{{-- Sortable Till column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('till')">
|
|
<div class="flex items-center">
|
|
{{ __('Till') }}
|
|
@if ($sortField === 'till')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
|
|
@if ($publicationStatusFilter !== 'deleted')
|
|
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-32">{{ __('Actions') }}</th>
|
|
@elseif ($publicationStatusFilter === 'deleted')
|
|
{{-- Sortable Deleted column --}}
|
|
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
wire:click="sortBy('deleted_at')">
|
|
<div class="flex items-center">
|
|
{{ __('Deleted') }}
|
|
@if ($sortField === 'deleted_at')
|
|
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
|
@endif
|
|
</div>
|
|
</th>
|
|
@endif
|
|
</tr>
|
|
</thead>
|
|
|
|
<!-- Table body -->
|
|
<tbody class="bg-white">
|
|
@forelse ($posts as $post)
|
|
@if ($post->translations->count() === 0)
|
|
{{-- Do not show post without any translation --}}
|
|
@else
|
|
@foreach ($post->translations as $loop_index => $translation)
|
|
<tr class="@if($loop_index === 0) border-t border-gray-200 @endif">
|
|
<td class="px-3 py-4 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
value="{{ $translation->id }}"
|
|
wire:model.live="bulkSelected"
|
|
class="rounded border-gray-300 text-theme-brand focus:border-theme-accent focus:ring-1 focus:ring-theme-accent">
|
|
</td>
|
|
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $post->id }}
|
|
</td>
|
|
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 max-w-xs truncate">
|
|
@if ($post->category)
|
|
{{ $post->category->translation ? $post->category->translation->name : __('Untitled category') }}
|
|
@else
|
|
{{ __('No category') }}
|
|
@endif
|
|
</td>
|
|
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $translation->locale }}
|
|
</td>
|
|
<td class="px-3 py-4 whitespace-nowrap max-w-xs">
|
|
<div class="text-sm font-medium text-gray-900 truncate" title="{{ $translation->title ?? __('No title') }}">
|
|
{{ $translation->title ?? __('No title') }}
|
|
</div>
|
|
</td>
|
|
<td class="px-3 py-4">
|
|
<div class="flex items-center space-x-2">
|
|
@if ($translation->updated_by_user)
|
|
<div class="relative block cursor-pointer flex-shrink-0"
|
|
onclick="window.location='{{ url('user/' . $translation->updated_by_user->id) }}'"
|
|
title="{{ $translation->updated_by_user->name }}">
|
|
<img class="h-6 w-6 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600"
|
|
src="{{ $translation->updated_by_user->profile_photo_path ? Storage::url($translation->updated_by_user->profile_photo_path) : Storage::url(timebank_config('profiles.user.profile_photo_path_default')) }}"
|
|
alt="profile">
|
|
</div>
|
|
@endif
|
|
<div class="text-sm text-gray-500 leading-tight">
|
|
<div>{{ $translation->updated_at->translatedFormat('M j') }}</div>
|
|
<div>{{ $translation->updated_at->translatedFormat('Y') }}</div>
|
|
<div class="text-xs">{{ $translation->updated_at->translatedFormat('H:i') }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-3 py-4">
|
|
@if ($translation->from)
|
|
<div class="text-sm text-gray-500 leading-tight">
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->from))->translatedFormat('M j') }}</div>
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->from))->translatedFormat('Y') }}</div>
|
|
<div class="text-xs">{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->from))->translatedFormat('H:i') }}</div>
|
|
</div>
|
|
@else
|
|
<span class="text-sm text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-3 py-4">
|
|
@if ($translation->till)
|
|
<div class="text-sm text-gray-500 leading-tight">
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->till))->translatedFormat('M j') }}</div>
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->till))->translatedFormat('Y') }}</div>
|
|
<div class="text-xs">{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->till))->translatedFormat('H:i') }}</div>
|
|
</div>
|
|
@else
|
|
<span class="text-sm text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
|
|
<!-- Row buttons -->
|
|
@if ($publicationStatusFilter !== 'deleted')
|
|
<td class="px-3 py-4 whitespace-nowrap text-center text-sm font-medium">
|
|
<div class="flex items-center justify-center space-x-1">
|
|
<!-- Start/Stop Publication Button -->
|
|
@if ($translation->from <= \Carbon\Carbon::now() && $translation->from !== null)
|
|
@if ($translation->till > \Carbon\Carbon::now() || $translation->till === null)
|
|
<x-jetstream.danger-button
|
|
title="{{ __('Stop') }}"
|
|
wire:click="openStopPublicationModal({{ $translation->id }})"
|
|
wire:target="openStopPublicationModal({{ $translation->id }})"
|
|
wire:loading.attr="disabled">
|
|
<x-icon class="h-5 w-5" name="stop-circle" solid />
|
|
</x-jetstream.danger-button>
|
|
@else
|
|
<x-jetstream.secondary-button
|
|
title="{{ __('Start') }}"
|
|
wire:click="openStartPublicationModal({{ $translation->id }})"
|
|
wire:target="openStartPublicationModal({{ $translation->id }})"
|
|
wire:loading.attr="disabled">
|
|
<x-icon class="h-5 w-5" name="play-circle" solid />
|
|
</x-jetstream.secondary-button>
|
|
@endif
|
|
@else
|
|
<x-jetstream.secondary-button
|
|
title="{{ __('Start') }}"
|
|
wire:click="openStartPublicationModal({{ $translation->id }})"
|
|
wire:target="openStartPublicationModal({{ $translation->id }})"
|
|
wire:loading.attr="disabled">
|
|
<x-icon class="h-5 w-5" name="play-circle" solid />
|
|
</x-jetstream.secondary-button>
|
|
@endif
|
|
|
|
<!-- Edit Button -->
|
|
<x-jetstream.secondary-button
|
|
title="{{ __('Edit') }}"
|
|
wire:click="edit({{ $translation->id }})"
|
|
wire:target="edit({{ $translation->id }})"
|
|
wire:loading.attr="disabled">
|
|
<x-icon class="h-5 w-5" name="pencil-square" />
|
|
</x-jetstream.secondary-button>
|
|
|
|
<!-- View post Button -->
|
|
@if ($translation->post)
|
|
@php
|
|
$viewUrl = $this->getPostViewUrl($translation->locale, $post->category?->type, $translation->post->id);
|
|
@endphp
|
|
@if ($viewUrl)
|
|
<x-jetstream.secondary-button
|
|
title="{{ __('View post') }}"
|
|
x-on:click="window.open('{{ $viewUrl }}', '_blank')">
|
|
<x-icon class="h-5 w-5" mini name="arrow-top-right-on-square" />
|
|
</x-jetstream.secondary-button>
|
|
@else
|
|
<x-jetstream.secondary-button disabled title="{{ __('View post') }}">
|
|
<x-icon class="h-5 w-5" mini name="arrow-top-right-on-square" />
|
|
</x-jetstream.secondary-button>
|
|
@endif
|
|
@else
|
|
<x-jetstream.secondary-button disabled title="{{ __('View post') }}">
|
|
<x-icon class="h-5 w-5" mini name="arrow-top-right-on-square" />
|
|
</x-jetstream.secondary-button>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
@else
|
|
<!-- Deleted date when showing deleted posts -->
|
|
<td class="px-3 py-4">
|
|
@if ($translation->deleted_at)
|
|
<div class="text-sm text-gray-500 leading-tight">
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->deleted_at))->translatedFormat('M j') }}</div>
|
|
<div>{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->deleted_at))->translatedFormat('Y') }}</div>
|
|
<div class="text-xs">{{ \Carbon\Carbon::createFromTimeStamp(strtotime($translation->deleted_at))->translatedFormat('H:i') }}</div>
|
|
</div>
|
|
@else
|
|
<span class="text-sm text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
@endif
|
|
</tr>
|
|
@endforeach
|
|
@endif
|
|
|
|
@empty
|
|
<tr>
|
|
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
|
{{ __('No results found') }}
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
@if($posts->hasPages())
|
|
<div class="bg-white shadow-sm rounded-lg mt-4">
|
|
<div class="py-3 border-t border-gray-200 flex items-center justify-between">
|
|
<!-- Left Side: perPage Dropdown -->
|
|
<div class="flex items-center">
|
|
<select class="w-20 rounded-md border border-theme-primary bg-theme-background px-3 py-2 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
|
wire:model.live="perPage">
|
|
<option value="10">10</option>
|
|
<option value="20">20</option>
|
|
<option value="50">50</option>
|
|
</select>
|
|
<span class="ml-2 text-sm text-theme-secondary">{{ __('per page') }}</span>
|
|
</div>
|
|
|
|
<!-- Right Side: Paginator -->
|
|
{{ $posts->links('livewire.long-paginator') }}
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!----Start publication modal ---->
|
|
<x-jetstream.dialog-modal wire:model.live="modalStartPublication" wire:key="modalStartPublication" maxWidth="2xl">
|
|
<x-slot name="title">
|
|
{{ __('Start the publication') }}
|
|
</x-slot>
|
|
|
|
<x-slot name="content">
|
|
{{ __('Do you want to start the publication of this post?') }}
|
|
</x-slot>
|
|
|
|
<x-slot name="footer">
|
|
<x-jetstream.secondary-button wire:click="$toggle('modalStartPublication')">
|
|
{{ __('Cancel') }}
|
|
</x-jetstream.secondary-button>
|
|
|
|
<x-jetstream.button
|
|
wire:click.prevent="startPublication({{ $selectedTranslationId }})"
|
|
wire:target="startPublication"
|
|
wire:loading.attr="disabled"
|
|
class="ml-3">
|
|
{{ __('Ok') }}
|
|
</x-jetstream.button>
|
|
</x-slot>
|
|
</x-jetstream.dialog-modal>
|
|
|
|
<!----Stop publication modal ---->
|
|
<x-jetstream.dialog-modal wire:model.live="modalStopPublication" wire:key="modalStopPublication" maxWidth="2xl">
|
|
<x-slot name="title">
|
|
{{ __('Stop the publication') }}
|
|
</x-slot>
|
|
|
|
<x-slot name="content">
|
|
{{ __('Do you want to end the publication of this post?') }}<br>
|
|
{{ __('You can always edit or start the publication again.') }}
|
|
</x-slot>
|
|
|
|
<x-slot name="footer">
|
|
<x-jetstream.secondary-button wire:click="$toggle('modalStopPublication')">
|
|
{{ __('Cancel') }}
|
|
</x-jetstream.secondary-button>
|
|
|
|
<x-jetstream.danger-button
|
|
wire:click.prevent="stopPublication({{ $selectedTranslationId }})"
|
|
wire:target="stopPublication"
|
|
wire:loading.attr="disabled"
|
|
class="ml-3">
|
|
{{ __('Ok') }}
|
|
</x-jetstream.danger-button>
|
|
</x-slot>
|
|
</x-jetstream.dialog-modal>
|
|
|
|
<!-- Edit modal -->
|
|
<x-jetstream.dialog-modal wire:model.live="showModal" wire:key="showModal" maxWidth="4xl" closeButton="true">
|
|
<x-slot name="title">
|
|
<div class="flex items-center">
|
|
<span>
|
|
@if ($postId)
|
|
@if ($createTranslation)
|
|
{{ __('Add translation') . ':' . ' ' . __('messages.' . $language) }}
|
|
@else
|
|
{{ __('Edit post') . ':' . ' ' . __('messages.' . $language) }}
|
|
@endif
|
|
@else
|
|
{{ !$language ? __('Create new post') : __('Create new post') . ':' . ' ' . __('messages.' . $language) }}
|
|
@endif
|
|
</span>
|
|
|
|
<!-- Loading spinner - shows when any action is processing -->
|
|
<span wire:loading class="ml-3">
|
|
<svg class="animate-spin h-5 w-5 text-theme-brand" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
</x-slot>
|
|
|
|
<x-slot name="content">
|
|
<form wire:submit="save" id="post-form">
|
|
<div class="space-y-6">
|
|
<div class="required flex space-x-12" wire:key="category-locale-{{ $postId ?? 'new' }}">
|
|
<livewire:category-selectbox :categorySelected="$categoryId"
|
|
key="category-selectbox-{{ $categoryId }}-{{ $postId ?? 'new' }}" />
|
|
<!-- Use the key to keep track of component that are in a loop -->
|
|
|
|
@if (!$localeIsLocked)
|
|
<livewire:add-translation-selectbox :locale="$locale" :options="$localesOptions"
|
|
key="add-translation-selectbox-{{ $locale }}-{{ $postId ?? 'new' }}" />
|
|
<!-- Use the key to keep track of component that are in a loop -->
|
|
@endif
|
|
</div>
|
|
|
|
<div wire:key="title-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ $this->titleLabel }}
|
|
@if ($language)
|
|
{{ '(' . __($language) . ')' }}
|
|
@endif
|
|
</label>
|
|
<input class="w-full rounded-lg border border-primary-300 py-2 pl-2 pr-4 text-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-base"
|
|
wire:model.live.debounce.800ms="title" />
|
|
@error('title')
|
|
<p class="mt-1 text-sm text-red-600" id="title-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div wire:key="slug-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}">
|
|
<label class="block text-sm text-gray-700 mb-2 flex items-center">
|
|
<span>
|
|
{{ __('Slug') }}
|
|
@if ($language)
|
|
{{ '(' . __($language) . ')' }}
|
|
@endif
|
|
</span>
|
|
<!-- Spinner that shows when title is updating (and generating slug) -->
|
|
<span wire:loading wire:target="title" class="ml-2">
|
|
<svg class="animate-spin h-4 w-4 text-theme-brand" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</span>
|
|
</label>
|
|
<input class="w-full rounded-lg border border-primary-300 py-2 pl-2 pr-4 text-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-base"
|
|
wire:model.blur="post.slug" />
|
|
@error('post.slug')
|
|
<p class="mt-1 text-sm text-red-600" id="slug-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div wire:key="excerpt-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}">
|
|
<x-textarea label="{{ $this->introLabel . ' (' . __($language) . ')' }}"
|
|
maxlength="{{ timebank_config('posts.excerpt_max_input', 500) }}"
|
|
placeholder="" rows="4"
|
|
wire:model.live.debounce.800ms="post.excerpt" />
|
|
@error('post.excerpt')
|
|
<p class="mt-1 text-sm text-red-600" id="excerpt-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!-- Content --- WYSIWYG editor (Quill editor) -->
|
|
<div wire:ignore wire:key="quill-container-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}"
|
|
x-data="{
|
|
quill: null,
|
|
init() {
|
|
this.$nextTick(() => {
|
|
this.quill = new Quill(this.$refs.editor, {
|
|
modules: {
|
|
toolbar: [
|
|
[{ header: [1, 2, false] }],
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
[{'list': 'ordered' }, {'list': 'bullet' }],
|
|
[{ 'align': [] }],
|
|
['link', 'image', 'video'],
|
|
['clean']
|
|
]
|
|
},
|
|
theme: 'snow',
|
|
});
|
|
|
|
// Sync to Livewire on every change
|
|
this.quill.on('text-change', () => {
|
|
@this.set('content', this.quill.root.innerHTML, false);
|
|
});
|
|
});
|
|
}
|
|
}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ $this->contentLabel }}
|
|
@if ($language)
|
|
{{ '(' . __($language) . ')' }}
|
|
@endif
|
|
</label>
|
|
|
|
<div x-ref="editor">{!! \App\Helpers\StringHelper::sanitizeHtml($content) !!}</div>
|
|
|
|
@error('content')
|
|
<p class="mt-1 text-sm text-red-600" id="locale-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!-- Image upload -->
|
|
<div>
|
|
<label class="block text-sm text-gray-700 mb-2">{{ __('Image') }}</label>
|
|
|
|
@if ($image && $imagePreviewable)
|
|
{{-- New upload preview --}}
|
|
<img class="mb-2 w-64 rounded-md border border-gray-300"
|
|
src="{{ $image->temporaryUrl() }}">
|
|
@elseif ($media)
|
|
{{-- Existing post image --}}
|
|
<img class="mb-2 w-64 rounded-md border border-gray-300"
|
|
src="{{ $media }}">
|
|
@elseif ($image && !$imagePreviewable)
|
|
{{-- Not an image file type --}}
|
|
<div
|
|
class="mb-2 flex h-36 w-64 items-center justify-center border border-gray-300">
|
|
<span class="text-red-500">{{ __('Error') }}</span>
|
|
</div>
|
|
@else
|
|
{{-- No image --}}
|
|
<div
|
|
class="mb-2 flex h-36 w-64 items-center justify-center border border-gray-300">
|
|
<span>{{ __('No image') }}</span>
|
|
</div>
|
|
@endif
|
|
<div x-data="{
|
|
isUploading: false,
|
|
progress: 5,
|
|
fileSizeError: null,
|
|
maxSizeMB: 12,
|
|
handleFileSelect(event) {
|
|
this.fileSizeError = null;
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const fileSizeMB = file.size / (1024 * 1024);
|
|
if (fileSizeMB > this.maxSizeMB) {
|
|
this.fileSizeError = '{{ __('File is too large') }}: ' + fileSizeMB.toFixed(2) + ' MB. {{ __('Maximum allowed') }}: ' + this.maxSizeMB + ' MB';
|
|
event.target.value = '';
|
|
return;
|
|
}
|
|
|
|
this.isUploading = true;
|
|
this.progress = 5;
|
|
|
|
@this.upload('image', file,
|
|
() => { this.isUploading = false; this.progress = 5; },
|
|
() => { this.isUploading = false; this.progress = 5; },
|
|
(event) => { this.progress = event.detail.progress; }
|
|
);
|
|
}
|
|
}">
|
|
<!-- File Input styled as button -->
|
|
<div class="space-y-2">
|
|
<div>
|
|
<input accept="image/*" type="file" class="hidden" id="image-upload-input"
|
|
x-on:change="handleFileSelect($event)">
|
|
<x-jetstream.secondary-button type="button" onclick="document.getElementById('image-upload-input').click()">
|
|
{{ __('Browse...') }}
|
|
</x-jetstream.secondary-button>
|
|
</div>
|
|
<!-- File size error message -->
|
|
<div x-show="fileSizeError" x-cloak class="text-sm text-red-600" x-text="fileSizeError"></div>
|
|
@if ($image || $media)
|
|
<div>
|
|
<x-jetstream.danger-button type="button" wire:click="removeImage">
|
|
{{ __('Delete photo') }}
|
|
</x-jetstream.danger-button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<!-- Progress Bar -->
|
|
<div class="flex-start my-6 flex h-4 w-64 overflow-hidden rounded bg-theme-surface font-sans text-xs font-medium"
|
|
x-show.transition="isUploading">
|
|
<progress class="flex h-full items-baseline justify-center overflow-hidden break-all text-white"
|
|
max="100" x-bind:style="`width:${progress}%`"
|
|
x-bind:value="progress"></progress>
|
|
</div>
|
|
</div>
|
|
@error('image')
|
|
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!--- Media owner --->
|
|
<div wire:key="media-owner-{{ $postId ?? 'new' }}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ __('Image ownership') . ' ' . '(' . __('all languages') . ')' }}
|
|
</label>
|
|
<input class="w-full rounded-lg border border-primary-300 py-2 pl-2 pr-4 text-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-base"
|
|
wire:model.blur="mediaOwner" />
|
|
@error('mediaOwner')
|
|
<p class="mt-1 text-sm text-red-600" id="media-caption-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!--- Media caption --->
|
|
<div wire:key="media-caption-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ $this->mediaCaptionLabel }}
|
|
@if ($language)
|
|
{{ '(' . __($language) . ')' }}
|
|
@endif
|
|
</label>
|
|
|
|
<x-textarea class="placeholder-gray-300"
|
|
maxlength="{{ timebank_config('posts.media_caption_max_input', 300) }}"
|
|
placeholder="" rows="3"
|
|
wire:model.blur="mediaCaption" />
|
|
<x-jetstream.input-error class="mt-1" for="mediaCaption" />
|
|
</div>
|
|
|
|
<!-- Event details -->
|
|
@if ($meetingShow)
|
|
<!-- Event date pickers: from and till -->
|
|
<div class="flex flex-wrap gap-6" wire:ignore wire:key="meeting-dates-{{ $postId ?? 'new' }}">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label class="block text-sm text-gray-700 mb-2">{{ __('Start of the event') }}</label>
|
|
<x-flatpickr
|
|
showTime
|
|
dateFormat="Y-m-d"
|
|
timeFormat="H:i"
|
|
altFormat="d-m-Y @ H:i"
|
|
placeholder="{{ __('Select a date and time') }}"
|
|
wire:model.defer="meetingFrom"
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" />
|
|
</div>
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label class="block text-sm text-gray-700 mb-2">{{ __('End of the event') }}</label>
|
|
<x-flatpickr
|
|
showTime
|
|
dateFormat="Y-m-d"
|
|
timeFormat="H:i"
|
|
altFormat="d-m-Y @ H:i"
|
|
placeholder="{{ __('Select a date and time') }}"
|
|
wire:model.defer="meetingTill"
|
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<div wire:key="meeting-venue-{{ $postId ?? 'new' }}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ __('Venue name') }}
|
|
</label>
|
|
<input class="w-full rounded-lg border border-primary-300 py-2 pl-2 pr-4 text-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-base"
|
|
wire:model="meetingVenue" />
|
|
@error('meetingVenue')
|
|
<p class="mt-1 text-sm text-red-600" id="meeting-venue-error">
|
|
{{ $message }}
|
|
</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div wire:key="meeting-address-{{ $postId ?? 'new' }}">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ __('Event address') }}
|
|
</label>
|
|
<input class="w-full rounded-lg border border-primary-300 py-2 pl-2 pr-4 text-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-base"
|
|
wire:model="meetingAddress" />
|
|
@error('meetingAddress')
|
|
<p class="mt-1 text-sm text-red-600" id="meeting-address-error">
|
|
{{ $message }}
|
|
</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!--- Location -->
|
|
<div wire:init="dispatchLocationToChildren">
|
|
<label class="block text-sm text-gray-700 mb-2">
|
|
{{ __('Location ') }} {{ __('Leave empty if not relevant for post') }}
|
|
</label>
|
|
<livewire:locations.locations-dropdown wire:key="locations-dropdown-{{ $postId ?? 'new' }}" />
|
|
@error('country')
|
|
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
@error('division')
|
|
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
@error('city')
|
|
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<!--- Event organizer --->
|
|
<div>
|
|
<livewire:posts.select-organizer wire:key="select-organizer-{{ $postId ?? 'new' }}"> {{-- TODO LATER refactor to profile.select-profile and remove posts.select-organizer --}}
|
|
</div>
|
|
|
|
<!--- Amount and Based on Quantity --->
|
|
<div class="flex flex-wrap items-start gap-6" wire:key="meeting-payment-{{ $postId ?? 'new' }}">
|
|
<div class="min-w-[180px]">
|
|
@livewire('amount', [
|
|
'label' => __('Price'),
|
|
'maxLengthHoursInput' => timebank_config('maxLengthHoursInput.user'),
|
|
'hours' => $hours,
|
|
'minutes' => $minutes,
|
|
'amount' => $amount,
|
|
], key('amount-' . ($postId ?? 'new')))
|
|
@error('amount')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
|
|
<div class="w-64" wire:key="transaction-type-{{ $postId ?? 'new' }}">
|
|
<label class="block text-sm font-medium text-theme-primary mb-1">
|
|
{{ __('Type') }}
|
|
<span wire:loading wire:target="transactionTypeId" class=""> ({{ __('Loading...') }})</span>
|
|
</label>
|
|
<x-select
|
|
placeholder="{{ __('Select type') }}"
|
|
:clearable="true"
|
|
wire:model.live="transactionTypeId"
|
|
>
|
|
@foreach ($this->transactionTypes as $type)
|
|
<x-select.option label="{{ $type['name'] }}" :value="$type['id']" />
|
|
@endforeach
|
|
</x-select>
|
|
@error('transactionTypeId')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
|
|
|
|
@if($transactionTypeId == 1) {{-- 1 === 'work' --}}
|
|
<div class="w-40" wire:key="based-on-quantity-{{ $postId ?? 'new' }}">
|
|
<x-number
|
|
label="{{ __('Based on participants') }}"
|
|
placeholder="0"
|
|
wire:model="basedOnQuantity"
|
|
/>
|
|
@error('basedOnQuantity')
|
|
<div class="mb-3 text-sm text-red-700" role="alert">
|
|
{{ __($message) }}
|
|
</div>
|
|
@enderror
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|
|
@endif
|
|
|
|
<!--- Post author (not shown for meetings) --->
|
|
@if (!$meetingShow)
|
|
<div>
|
|
<livewire:posts.select-author
|
|
wire:key="select-author-{{ $postId ?? 'new' }}"
|
|
:author-id="$author['id'] ?? null"
|
|
:author-model="$author['type'] ?? null">
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Publication from and till -->
|
|
<div x-data="{
|
|
fromDate: @entangle('from').defer,
|
|
tillDate: @entangle('till').defer,
|
|
showWarning: false,
|
|
checkWarning() {
|
|
if (!this.fromDate || this.fromDate === '') {
|
|
this.showWarning = false;
|
|
return;
|
|
}
|
|
const fromStr = this.fromDate.replace(' ', 'T');
|
|
const from = new Date(fromStr);
|
|
const now = new Date();
|
|
if (from < now) {
|
|
if (!this.tillDate || this.tillDate === '') {
|
|
this.showWarning = true;
|
|
} else {
|
|
const tillStr = this.tillDate.replace(' ', 'T');
|
|
const till = new Date(tillStr);
|
|
this.showWarning = till > now;
|
|
}
|
|
} else {
|
|
this.showWarning = false;
|
|
}
|
|
}
|
|
}"
|
|
x-init="$watch('fromDate', () => checkWarning()); $watch('tillDate', () => checkWarning()); checkWarning();">
|
|
<div class="flex flex-wrap gap-6" wire:ignore wire:key="publication-dates-{{ $postId ?? 'new' }}-{{ $locale ?? 'default' }}">
|
|
<div class="flex-1 min-w-[200px]">
|
|
@php
|
|
if ($language) {
|
|
$labelStart = __('Start of publication') . ' (' . __($language) . ')';
|
|
$labelEnd = __('End of publication') . ' (' . __($language) . ')';
|
|
} else {
|
|
$labelStart = __('Start of publication');
|
|
$labelEnd = __('End of publication');
|
|
}
|
|
@endphp
|
|
<label class="block text-sm text-theme-primary mb-2">{{ $labelStart }}</label>
|
|
<x-flatpickr
|
|
showTime
|
|
dateFormat="Y-m-d"
|
|
timeFormat="H:i"
|
|
altFormat="d-m-Y @ H:i"
|
|
placeholder="{{ __('Select a date') }}"
|
|
wire:model.defer="from"
|
|
class="mt-1 block border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm" />
|
|
</div>
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label class="block text-sm text-theme-primary mb-2">{{ $labelEnd }}</label>
|
|
<x-flatpickr
|
|
showTime
|
|
dateFormat="Y-m-d"
|
|
timeFormat="H:i"
|
|
altFormat="d-m-Y @ H:i"
|
|
placeholder="{{ __('Select a date') }}"
|
|
wire:model.defer="till"
|
|
class="mt-1 block border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Publication warning -->
|
|
<div x-show="showWarning" class="mt-2 text-right text-red-600" x-transition>
|
|
{{ __('Warning: post will be published immediately!') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Principles Update Warning -->
|
|
@if ($isPrinciplesPost)
|
|
<div class="mt-6 rounded-md border border-gray-300 bg-gray-50 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm text-gray-800">
|
|
<strong>{{ __('Warning') }}:</strong>
|
|
{{ __('Updating the platform principles will require all authenticated users to review and accept the new version before they can continue.') }}
|
|
</p>
|
|
<p class="text-sm text-gray-600 mt-2">
|
|
{{ __('This action will affect all users who have previously accepted the principles. They will be redirected to the principles page and must accept the updated version.') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 w-1/3">
|
|
<x-input
|
|
label="{!! __('messages.confirm_input') !!}"
|
|
placeholder="{{ __('Confirmation keyword') }}"
|
|
autocomplete="off"
|
|
wire:model="confirmString" />
|
|
</div>
|
|
@endif
|
|
|
|
<!-- List of validation errors -->
|
|
<x-errors />
|
|
</div>
|
|
</form>
|
|
</x-slot>
|
|
|
|
<x-slot name="footer">
|
|
<x-jetstream.secondary-button wire:click="close">
|
|
{{ __('Cancel') }}
|
|
</x-jetstream.secondary-button>
|
|
|
|
<x-jetstream.button type="submit" form="post-form" wire:target="save" class="ml-3 bg-theme-brand">
|
|
@if ($createTranslation === true)
|
|
{{ $postId ? __('Add Translation') : __('Save') }}
|
|
@else
|
|
{{ $postId ? __('Update') : __('Save') }}
|
|
@endif
|
|
</x-jetstream.button>
|
|
</x-slot>
|
|
</x-jetstream.dialog-modal>
|
|
</div>
|
|
|
|
<script>
|
|
// Initialize flatpickr for meeting dates
|
|
function initMeetingFlatpickr() {
|
|
const meetingFromInput = document.querySelector('[wire\\:model\\.defer="meetingFrom"]');
|
|
const meetingTillInput = document.querySelector('[wire\\:model\\.defer="meetingTill"]');
|
|
|
|
[meetingFromInput, meetingTillInput].forEach(input => {
|
|
if (input && !input._flatpickr) {
|
|
// Initialize flatpickr if not already initialized
|
|
if (window.LaravelFlatpickr) {
|
|
window.LaravelFlatpickr.initializeFlatpickr(input);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize flatpickr and hook to Alpine warning state for publication dates
|
|
function initAndHookFlatpickr() {
|
|
const fromInput = document.querySelector('[wire\\:model\\.defer="from"]');
|
|
const tillInput = document.querySelector('[wire\\:model\\.defer="till"]');
|
|
|
|
[fromInput, tillInput].forEach(input => {
|
|
if (input && !input._flatpickr) {
|
|
// Initialize flatpickr if not already initialized
|
|
if (window.LaravelFlatpickr) {
|
|
window.LaravelFlatpickr.initializeFlatpickr(input);
|
|
}
|
|
}
|
|
|
|
// Hook onChange event to Alpine
|
|
if (input && input._flatpickr && !input.dataset.warningHooked) {
|
|
input.dataset.warningHooked = 'true';
|
|
|
|
const fp = input._flatpickr;
|
|
|
|
fp.config.onChange.push(function(selectedDates, dateStr, instance) {
|
|
const alpineEl = input.closest('[x-data]');
|
|
|
|
if (alpineEl && alpineEl._x_dataStack && alpineEl._x_dataStack[0]) {
|
|
const alpineData = alpineEl._x_dataStack[0];
|
|
|
|
const wireName = input.getAttribute('wire:model.defer');
|
|
if (wireName === 'from') {
|
|
alpineData.fromDate = dateStr;
|
|
} else if (wireName === 'till') {
|
|
alpineData.tillDate = dateStr;
|
|
}
|
|
|
|
if (typeof alpineData.checkWarning === 'function') {
|
|
alpineData.checkWarning();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize all flatpickrs
|
|
function initAllFlatpickrs() {
|
|
initMeetingFlatpickr();
|
|
initAndHookFlatpickr();
|
|
}
|
|
|
|
// Initialize on DOMContentLoaded and after Livewire updates
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Run immediately
|
|
setTimeout(initAllFlatpickrs, 100);
|
|
|
|
// Run after Livewire morphs
|
|
Livewire.hook('morph.updated', () => {
|
|
setTimeout(initAllFlatpickrs, 200);
|
|
setTimeout(initAllFlatpickrs, 500);
|
|
});
|
|
|
|
// Listen for modal opening
|
|
Livewire.on('showModal', () => {
|
|
setTimeout(initAllFlatpickrs, 300);
|
|
setTimeout(initAllFlatpickrs, 600);
|
|
});
|
|
});
|
|
</script>
|