Files
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

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="">&nbsp;({{ __('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>