Snapshot of main at 9f39a6e1e (2026-11-02)
No files matched your search
@@ -0,0 +1,111 @@
|
||||
<div>
|
||||
@auth
|
||||
@if($hasAccepted && !$needsReaccept)
|
||||
{{-- User has accepted and version is current --}}
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg mb-3">
|
||||
<div class="px-3 sm:px-0">
|
||||
<div class="max-w-4xl mx-auto my-12 p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="h-6 w-6 text-theme-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-theme-primary">{{ __('Principles Accepted') }}</h3>
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ __('You accepted these principles on') }} {{ \Carbon\Carbon::parse($acceptedData['accepted_at'])->isoFormat('LL') }}
|
||||
</p>
|
||||
<p class="text-xs text-theme-muted mt-1">
|
||||
{{ __('Language') }}: {{ strtoupper($acceptedData['locale']) }} |
|
||||
{{ __('Version updated') }}: {{ \Carbon\Carbon::parse($acceptedData['updated_at'])->isoFormat('LL') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@elseif($hasAccepted && $needsReaccept)
|
||||
{{-- User has accepted but needs to re-accept newer version --}}
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg mb-3">
|
||||
<div class="px-3 sm:px-0">
|
||||
<div class="max-w-4xl mx-auto my-12 p-4">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/20 border border-gray-300 dark:border-gray-700 rounded-lg mb-6">
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-800 dark:text-gray-200">{{ __('Updated Principles') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mt-1">
|
||||
{{ __('Our principles have been updated since you last accepted them. Please review the changes and accept the new version.') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
{{ __('You previously accepted on') }}: {{ \Carbon\Carbon::parse($acceptedData['accepted_at'])->isoFormat('LL') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-6 text-xl font-semibold text-theme-primary">{{ __('Accept Updated Principles') }}</h3>
|
||||
|
||||
<form wire:submit.prevent="accept">
|
||||
<div class="mb-6">
|
||||
<label class="flex items-start space-x-3">
|
||||
<x-jetstream.checkbox wire:model="agreed" />
|
||||
<span class="text-theme-primary">
|
||||
{{ __('I have read and accept the updated platform principles described above.') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<x-jetstream.button type="submit" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="accept">
|
||||
{{ __('Accept Updated Principles') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="accept">
|
||||
{{ __('Saving...') }}
|
||||
</span>
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- User needs to accept --}}
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg mb-3">
|
||||
<div class="px-3 sm:px-0">
|
||||
<div class="max-w-4xl mx-auto my-12 p-4">
|
||||
<h3 class="mb-6 text-xl font-semibold text-theme-primary">{{ __('Accept Principles') }}</h3>
|
||||
|
||||
<form wire:submit.prevent="accept">
|
||||
<div class="mb-6">
|
||||
<label class="flex items-start space-x-3">
|
||||
<x-jetstream.checkbox wire:model="agreed" />
|
||||
<span class="text-theme-primary">
|
||||
{{ __('I have read and accept the platform principles described above.') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<x-jetstream.button type="submit" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="accept">
|
||||
{{ __('Accept Principles') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="accept">
|
||||
{{ __('Saving...') }}
|
||||
</span>
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
@guest
|
||||
{{-- Not logged in - no action needed --}}
|
||||
@endguest
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<div>
|
||||
<x-jetstream.dialog-modal wire:model="show" maxWidth="lg" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Account info') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if (count($accounts) > 0)
|
||||
<div>
|
||||
@foreach ($accounts as $account)
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<span class="text-sm text-theme-primary">{{ $account['name'] }}</span>
|
||||
<span class="text-sm font-medium text-theme-primary">{{ $account['balanceFormatted'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between border-t border-theme-border pt-4">
|
||||
<span class="text-sm font-semibold text-theme-primary">{{ __('Total available') }}</span>
|
||||
<span class="text-sm font-semibold text-theme-primary">{{ $totalBalanceFormatted }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex items-center gap-3">
|
||||
<x-toggle wire:model.live="decimalFormat" id="decimalFormat" name="decimalFormat" />
|
||||
<span class="text-xs text-theme-light">{{ __('Show in decimals') }} ({{ $totalBalanceDecimal }} {{ __('h.') }})</span>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-theme-light">{{ __('No active accounts found.') }}</p>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="close">
|
||||
{{ __('Close') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<div>
|
||||
@if ($hasTransactions)
|
||||
|
||||
<div class="my-6">
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-gray text-sm">
|
||||
{{ __('Account usage') }}
|
||||
<button
|
||||
wire:click="$dispatch('openAccountUsageInfoModal')"
|
||||
class="ml-1 underline hover:text-theme-accent underline cursor-pointer">
|
||||
{{ __('info') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<svg class="rc-progress-line" preserveAspectRatio="none" viewBox="0 0 100 1">
|
||||
<path class="rc-progress-line-trail" d="M 0.5,0.5 L 99.5,0.5" fill-opacity="0" stroke-linecap="round" stroke-width="1" stroke="rgb(var(--color-secondary))"></path>
|
||||
|
||||
<!-- Path for the part up to 80% -->
|
||||
<path class="rc-progress-line-path" d="M 0.5,0.5 L {{ min($balancePct, 80) }}.5,0.5" fill-opacity="0" stroke-linecap="round" stroke-width="1" stroke="rgb(var(--color-brand))"
|
||||
style="stroke-dasharray: {{ min($balancePct, 80) }}px, 100px; stroke-dashoffset: 0px; transition: stroke-dasharray 0.5s ease, stroke 0.5s ease; ">
|
||||
</path>
|
||||
|
||||
<!-- Path for the part over 80% -->
|
||||
@if ($balancePct > 80)
|
||||
<path class="rc-progress-line-path" d="M 80.5,0.5 L {{ $balancePct }}.5,0.5" fill-opacity="0" stroke-linecap="round" stroke-width="1" stroke="#ef4444"
|
||||
style="stroke-dasharray: {{ $balancePct - 80 }}px, 100px; stroke-dashoffset: 0px; transition: stroke-dasharray 0.5s ease, stroke 0.5s ease; transition-delay: 0.5s ;
|
||||
">
|
||||
</path>
|
||||
@endif
|
||||
|
||||
</svg>
|
||||
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-theme-muted text-sm">
|
||||
{{__('Balance limit of this account')}}:
|
||||
{{ ' ' . tbFormat($selectedAccount['limitMax']) }}
|
||||
@if ($profileLimitMax > 0 && $profileAccountCount > 1)
|
||||
, {{ __('and your profile has an overall limit of') }} {{ tbFormat($profileLimitMax) }}
|
||||
@endif
|
||||
</span>
|
||||
@if ($balancePct > 80)
|
||||
<span class="text-theme-muted text-sm">{{ tbFormat($selectedAccount['available']) . ' ' . __('available') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Account Usage Info Modal -->
|
||||
<livewire:account-usage-info-modal />
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<div>
|
||||
<x-jetstream.dialog-modal wire:model="show" maxWidth="2xl" closeButton="true" >
|
||||
<x-slot name="title">
|
||||
{{ $post && $post->translations->isNotEmpty() ? $post->translations[0]->title : $fallbackTitle }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if ($post && $post->translations->isNotEmpty())
|
||||
<div class="post">
|
||||
@if ($post->translations[0]->excerpt)
|
||||
<div class="my-6 text-md">
|
||||
{{ $post->translations[0]->excerpt }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($image)
|
||||
<div class="images my-6">
|
||||
@php
|
||||
$tooltipParts = array_filter([$imageCaption, $imageOwner]);
|
||||
$tooltip = implode(', ', $tooltipParts);
|
||||
@endphp
|
||||
<img
|
||||
src="{{ $image }}"
|
||||
alt="{{ $imageCaption ?? '' }}"
|
||||
title="{{ $tooltip }}"
|
||||
class="w-full h-auto mb-4">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="text-sm">
|
||||
{!! \App\Helpers\StringHelper::sanitizeHtml($post->translations[0]->content ?? '') !!}
|
||||
</div>
|
||||
</div>
|
||||
@include('livewire.posts.manage-actions', ['post' => $post])
|
||||
@else
|
||||
<div class="text-sm">
|
||||
{{ $fallbackDescription }}
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="close">
|
||||
{{ __('Close') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div>
|
||||
<x-select
|
||||
label="{{ __('Language') }} *"
|
||||
placeholder="{{ __('Select language') }}"
|
||||
:options="$options"
|
||||
option-label="name"
|
||||
option-value="lang_code"
|
||||
wire:model.live="localeSelected"
|
||||
:empty-message="$emptyMessage"
|
||||
/>
|
||||
@error('locale')
|
||||
<div class="mt-2 text-sm text-red-600" id="locale-error">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Be like water. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="scroll-py-3">
|
||||
<div class="mb-3">
|
||||
<h3 class="text-lg font-semibold text-theme-text-primary">{{ $logTitle }}</h3>
|
||||
|
||||
@if ($message)
|
||||
<div class="mt-2">
|
||||
{!! $message !!}
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-2 text-xs text-theme-muted">{{ __('Recent log output') }}</div>
|
||||
@endif
|
||||
|
||||
@if ($fileSize)
|
||||
<div class="mt-1 text-xs text-theme-muted">
|
||||
<span>{{ __('File size') }}: <span class="text-theme-text-primary">{{ $fileSize }}</span></span>
|
||||
<span class="ml-4">{{ __('Last modified') }}: <span class="text-theme-text-primary">{{ $lastModified }}</span></span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($logContent)
|
||||
<div class="box-border max-h-[200px] overflow-x-auto overflow-y-auto rounded-lg bg-black pb-6 pl-6 pr-6 font-mono text-xs text-gray-200"
|
||||
id="log-scroll-{{ $logFilename }}" style="border-radius: 0.5rem;">
|
||||
<div class="m-0 w-full whitespace-pre-wrap">
|
||||
{{ $logContent }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex w-full justify-between">
|
||||
<x-jetstream.secondary-button type="button" wire:click="refreshLog" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="refreshLog">{{ __('Refresh log') }}</span>
|
||||
<span wire:loading wire:target="refreshLog">{{ __('Refreshing...') }}</span>
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button type="button" wire:click="downloadLog" wire:loading.attr="disabled">
|
||||
{{ __('Download log') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<style>
|
||||
#log-scroll-{{ $logFilename }} {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
#log-scroll-{{ $logFilename }}::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#log-scroll-{{ $logFilename }}::-webkit-scrollbar-track {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#log-scroll-{{ $logFilename }}::-webkit-scrollbar-thumb {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var logDiv = document.getElementById('log-scroll-{{ $logFilename }}');
|
||||
if (logDiv) {
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for refresh event
|
||||
window.addEventListener('logRefreshed', function() {
|
||||
setTimeout(function() {
|
||||
var logDiv = document.getElementById('log-scroll-{{ $logFilename }}');
|
||||
if (logDiv) {
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,81 @@
|
||||
<div class="scroll-py-3">
|
||||
<div class="mb-3">
|
||||
<h3 class="text-lg font-semibold text-theme-text-primary">{{ __('Application Log') }}</h3>
|
||||
|
||||
@if ($message)
|
||||
<div class="mt-2">
|
||||
{!! $message !!}
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-2 text-xs text-theme-muted">{{ __('Recent log output') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-1 text-xs text-theme-muted">
|
||||
<span>
|
||||
<span class="text-theme-muted">{{ __('Disk') }}:</span>
|
||||
<span class="{{ $diskUsageClass }}"> {{ $diskUsage }}</span>
|
||||
</span>
|
||||
<span class="ml-4">
|
||||
<span class="text-theme-muted">{{ __('RAM memory') }}:</span>
|
||||
<span class="{{ $availableRamClass }}"> {{ $availableRam }}</span>
|
||||
</span>
|
||||
<span class="ml-4">
|
||||
{{ __('Queue workers running') }}:
|
||||
<span class="{{ $queueWorkersCount > 0 ? 'text-green-500' : 'text-red-500' }}">
|
||||
{{ $queueWorkersCount }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="ml-4">
|
||||
{{ __('Reverb server') }}:
|
||||
<span class="{{ $reverbConnected ? 'text-green-500' : 'text-red-500' }}">
|
||||
{{ $reverbConnected ? __('Connected') : __('Not connected') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($queueWorkersCount > 0)
|
||||
<div class="mb-2 text-xs text-theme-muted">
|
||||
<pre class="whitespace-pre-wrap">{{ implode("\n", $queueWorkers) }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="box-border max-h-[200px] overflow-x-auto overflow-y-auto rounded-lg bg-black pb-6 pl-6 pr-6 font-mono text-xs text-gray-200"
|
||||
id="log-scroll" style="border-radius: 0.5rem;">
|
||||
<div class="m-0 w-full whitespace-pre-wrap">
|
||||
{{ $logContent }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex w-full justify-end">
|
||||
<x-jetstream.button type="button" wire:click="downloadLog" wire:loading.attr="disabled">
|
||||
{{ __('Download log') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#log-scroll {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
#log-scroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#log-scroll::-webkit-scrollbar-track {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#log-scroll::-webkit-scrollbar-thumb {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var logDiv = document.getElementById('log-scroll');
|
||||
if (logDiv) {
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div>
|
||||
@if($maintenanceMode)
|
||||
<div class="bg-theme-background">
|
||||
<div class="max-w-full sm:mx-auto mr-16 ml-2 bg-theme-background text-2xs sm:text-sm text-center text-red-700 py-2">
|
||||
<span class="font-semibold">
|
||||
<x-icon class="mr-1 inline h-5 w-5" name="exclamation-triangle" />
|
||||
{{ __('Site is currently in maintenance mode') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="bg-theme-background p-6 sm:px-20">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="mb-2 text-lg font-medium">
|
||||
{{ __('Maintenance Mode') }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
@if ($maintenanceMode)
|
||||
<div class="text-sm font-medium">
|
||||
{{ __('Site is currently in maintenance mode') }}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-theme-secondary">
|
||||
{{ __('Regular users cannot log in. Only administrators can login.') }}
|
||||
</p>
|
||||
@else
|
||||
<div class="text-sm font-medium">
|
||||
{{ __('Site is currently accessible to all users') }}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-theme-secondary">
|
||||
{{ __('Enable maintenance mode to restrict access to users with administrator permissions only.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="ml-6">
|
||||
@if ($maintenanceMode)
|
||||
<x-jetstream.button wire:click="openModal">
|
||||
{{ __('Disable') }}
|
||||
</x-jetstream.button>
|
||||
@else
|
||||
<x-jetstream.button wire:click="openModal" class="bg-theme-brand">
|
||||
{{ __('Enable') }}
|
||||
</x-jetstream.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showModal">
|
||||
<x-slot name="title">
|
||||
{{ __('Maintenance Mode') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if ($maintenanceMode)
|
||||
<p>{{ __('Site is currently in maintenance mode') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-600">{{ __('Regular users cannot log in. Only administrators can login.') }}</p>
|
||||
@else
|
||||
<p>{{ __('Site is currently accessible to all users') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-600">{{ __('Enable maintenance mode to restrict access to users with administrator permissions only.') }}</p>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeModal">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
@if ($maintenanceMode)
|
||||
<x-jetstream.danger-button wire:click="toggleMaintenanceMode" wire:loading.attr="disabled" class="ml-3">
|
||||
<span wire:loading.remove wire:target="toggleMaintenanceMode">{{ __('Disable') }}</span>
|
||||
<span wire:loading wire:target="toggleMaintenanceMode">{{ __('Loading...') }}</span>
|
||||
</x-jetstream.danger-button>
|
||||
@else
|
||||
<x-jetstream.danger-button wire:click="toggleMaintenanceMode" wire:loading.attr="disabled" class="ml-3 bg-theme-brand">
|
||||
<span wire:loading.remove wire:target="toggleMaintenanceMode">{{ __('Enable') }}</span>
|
||||
<span wire:loading wire:target="toggleMaintenanceMode">{{ __('Loading...') }}</span>
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-theme-text-primary">{{ __('Search Metrics') }}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<select wire:model.live="period" class="rounded border border-theme-border bg-transparent py-1 text-xs text-theme-text-primary focus:outline-none focus:ring-1 focus:ring-theme-brand focus:border-theme-brand">
|
||||
@foreach ($periods as $value => $label)
|
||||
<option value="{{ $value }}">{{ __($label) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<x-jetstream.secondary-button type="button" wire:click="refresh" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="refresh">{{ __('Refresh') }}</span>
|
||||
<span wire:loading wire:target="refresh">{{ __('Refreshing...') }}</span>
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div class="rounded-lg border border-theme-brand p-3 text-center">
|
||||
<div class="text-2xl font-bold text-theme-text-primary">{{ $metrics['searches_last_24h'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-xs">{{ __('Searches Last 24h') }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-brand p-3 text-center">
|
||||
<div class="text-2xl font-bold text-theme-text-primary">{{ $metrics['avg_results'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-xs">{{ __('Avg Results') }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-brand p-3 text-center">
|
||||
<div class="text-2xl font-bold text-theme-text-primary">{{ $metrics['avg_execution_time'] ?? 0 }}s</div>
|
||||
<div class="mt-1 text-xs">{{ __('Avg Exec Time') }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-brand p-3 text-center">
|
||||
<div class="text-2xl font-bold text-theme-text-primary">{{ $metrics['avg_searches_per_day'] ?? 0 }}</div>
|
||||
<div class="mt-1 text-xs">{{ __('Avg Searches / Day') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!empty($metrics['popular_categories']))
|
||||
<!-- Popular Categories -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 text-xs font-semibold uppercase tracking-wide">{{ __('Top Categories') }}</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($metrics['popular_categories'] as $categoryName => $count)
|
||||
<a href="{{ route('search.show') }}?category={{ $metrics['popular_category_ids'][$categoryName] ?? '' }}"
|
||||
class="bg-{{ $metrics['popular_category_colors'][$categoryName] ?? 'gray' }}-400 inline-flex items-center rounded-md px-2 py-1 text-sm font-normal text-black hover:opacity-80">
|
||||
{{ $categoryName }} ×{{ $count }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!empty($recentSearches))
|
||||
<!-- Recent Searches Table -->
|
||||
<div class="mb-2 text-xs font-semibold uppercase tracking-wide">{{ __('Recent Search Log') }}</div>
|
||||
<div class="box-border max-h-[200px] overflow-x-auto overflow-y-auto rounded-lg bg-black pb-2 font-mono text-xs text-gray-200">
|
||||
<table class="w-full">
|
||||
<thead class="sticky top-0 bg-black">
|
||||
<tr class="border-b border-gray-700 text-left text-gray-400">
|
||||
<th class="px-3 py-2">{{ __('Date') }}</th>
|
||||
<th class="px-3 py-2">{{ __('Query') }}</th>
|
||||
<th class="px-3 py-2">{{ __('Categories') }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ __('Results') }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ __('Exec (s)') }}</th>
|
||||
<th class="px-3 py-2">{{ __('Lang.') }}</th>
|
||||
<th class="px-3 py-2">{{ __('Type') }}</th>
|
||||
<th class="px-3 py-2">{{ __('Location') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($recentSearches as $search)
|
||||
<tr class="border-b border-gray-800 hover:bg-white/5">
|
||||
<td class="px-3 py-1 text-gray-400">
|
||||
{{ \Carbon\Carbon::parse($search['timestamp'])->format('Y-m-d') }}
|
||||
</td>
|
||||
<td class="px-3 py-1">
|
||||
{{ $search['search_string'] ?? '—' ?: '—' }}
|
||||
</td>
|
||||
<td class="px-3 py-1">
|
||||
{{ implode(', ', array_column($search['category_tags'] ?? [], 'name')) ?: '—' }}
|
||||
</td>
|
||||
<td class="px-3 py-1 text-right">{{ $search['total_results'] ?? 0 }}</td>
|
||||
<td class="px-3 py-1 text-right">{{ number_format($search['execution_time'] ?? 0, 4) }}</td>
|
||||
<td class="px-3 py-1">{{ $search['locale'] ?? '—' }}</td>
|
||||
<td class="px-3 py-1">{{ $search['user_type'] ?? 'guest' }}</td>
|
||||
<td class="px-3 py-1 text-gray-400">{{ $search['location_label'] ?? '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 flex w-full justify-end">
|
||||
<x-jetstream.button type="button" wire:click="downloadLog" wire:loading.attr="disabled">
|
||||
{{ __('Download log') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@else
|
||||
<div class="py-4 text-center text-xs">
|
||||
{{ __('No recent search data available.') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<div>
|
||||
<style>
|
||||
/* Amount component unified focus styles */
|
||||
.amount-wrapper:focus-within {
|
||||
border-color: rgb(var(--color-accent)) !important;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px rgb(var(--color-accent) / 0.5);
|
||||
}
|
||||
|
||||
.amount-wrapper input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
@isset($label)
|
||||
<x-jetstream.label :value="$label" for="hours" />
|
||||
@else
|
||||
<x-jetstream.label for="hours" value="{{ __('Amount of time') }}" />
|
||||
@endisset
|
||||
|
||||
<!-- Unified border wrapper with proper ARIA labeling -->
|
||||
<div class="amount-wrapper mt-1 flex items-center rounded-md border border-theme-primary bg-theme-background shadow-sm overflow-hidden transition-colors duration-200 {{ $maxLengthHoursInput > 4 ? 'w-full' : 'w-40' }}"
|
||||
role="group"
|
||||
aria-labelledby="hours-label">
|
||||
|
||||
@if(!platform_trans('platform_currency_position_end', null, false))
|
||||
<!-- Currency symbol prefix (when position_end is false) -->
|
||||
<span class="bg-theme-background px-3 py-2 text-sm text-theme-secondary select-none" aria-hidden="true">{{ platform_currency_symbol() }}</span>
|
||||
@endif
|
||||
|
||||
<!-- Hours input -->
|
||||
<input id="hours"
|
||||
class="@error('hours') is-invalid @enderror {{ $maxLengthHoursInput > 4 ? 'flex-1' : 'w-14' }} bg-transparent border-0 px-2 py-2 text-right placeholder-theme-light focus:ring-0 sm:text-sm"
|
||||
maxlength="{{ $maxLengthHoursInput }}" min="0" name="hours"
|
||||
oninput="this.value = this.value.replace(/[^0-9]/g, '').slice(0, {{ $maxLengthHoursInput }})"
|
||||
placeholder="{{ __('hh') }}" step="1" type="text" value="{{ old('hours') }}"
|
||||
aria-label="{{ trans_with_platform('Hours') }}"
|
||||
wire:model.blur.number="hours">
|
||||
|
||||
<!-- Colon separator -->
|
||||
<span class="bg-theme-background px-1 py-2 text-sm text-theme-secondary select-none" aria-hidden="true">:</span>
|
||||
|
||||
<!-- Minutes input -->
|
||||
<input id="minutes"
|
||||
class="@error('minutes') is-invalid @enderror flex-1 bg-transparent border-0 px-2 py-2 text-left placeholder-theme-light focus:ring-0 sm:text-sm"
|
||||
maxlength="4" min="0" name="minutes"
|
||||
oninput="this.value = this.value.replace(/[^0-9]/g, '').slice(0, 4)"
|
||||
placeholder="{{ __('mm') }}" step="1" type="text" value="{{ old('minutes') }}"
|
||||
aria-label="{{ __('Minutes') }}"
|
||||
wire:model.blur.number="minutes">
|
||||
|
||||
@if(platform_trans('platform_currency_position_end', null, false))
|
||||
<!-- Currency symbol suffix (when position_end is true) -->
|
||||
<span class="bg-theme-background px-3 py-2 text-sm text-theme-secondary select-none" aria-hidden="true">{{ platform_currency_symbol() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,180 @@
|
||||
<div>
|
||||
{{-- Tagify single-tag picker --}}
|
||||
<div x-data="callTagPicker">
|
||||
<label class="block text-sm text-theme-primary mb-2">
|
||||
{{ __('Requested activity or skill') }}
|
||||
</label>
|
||||
<div wire:ignore>
|
||||
<input x-ref="callTagsInput"
|
||||
type="text"
|
||||
data-suggestions='@json($suggestions)'
|
||||
placeholder="{{ __('Select a label, or create a new one') }}"
|
||||
class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- New Tag Creation Modal --}}
|
||||
@if ($modalVisible)
|
||||
<form wire:submit.prevent="createTag">
|
||||
<x-jetstream.dialog-modal wire:model.live="modalVisible">
|
||||
<x-slot name="title">
|
||||
{{ str_replace('@PLATFORM_NAME@', platform_name(), __('Add a new activity or skill to @PLATFORM_NAME@')) }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="my-3 text-xl">
|
||||
<span class="bg-{{ $categoryColor }}-300 inline-flex items-center rounded-md px-3 py-2 text-sm font-normal">
|
||||
{{ $newTag['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-6">
|
||||
<x-input
|
||||
label="{{ __('Activity tag (min. 2 words)') }}"
|
||||
placeholder="{{ __('Accurate and unique name for this activity, avoid vague or general keywords') }}"
|
||||
wire:model.live="newTag.name" />
|
||||
</div>
|
||||
|
||||
@if (!$sessionLanguageOk)
|
||||
<div class="mt-3 grid grid-cols-1 gap-6">
|
||||
@php
|
||||
$locale = app()->getLocale();
|
||||
$localeName = \Locale::getDisplayName($locale, $locale);
|
||||
@endphp
|
||||
<x-checkbox :disabled="$sessionLanguageOk" id="sessionLang-ignore-checkbox"
|
||||
label="{{ __('This tag is in :locale.', ['locale' => $localeName]) }}"
|
||||
wire:model.live="sessionLanguageIgnored" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($translationPossible && $translationAllowed)
|
||||
<div class="mt-6 grid grid-cols-1 gap-6">
|
||||
<x-checkbox id="checkbox"
|
||||
label="{{ __('Attach a translation to this tag (recommended)') }}"
|
||||
wire:model.live="translationVisible" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-6" wire:loading wire:target="translationVisible">
|
||||
<x-mini-button flat icon="" primary rounded spinner />
|
||||
<span>{{ __('Loading...') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
@if ($translationVisible)
|
||||
<div class="my-6 grid grid-cols-1 gap-6 pl-8 md:grid-cols-2" id="select-translation-language">
|
||||
<x-select :options="$translationLanguages" class="placeholder-theme-light"
|
||||
id="translation-language" label="{{ __('Translation language') }}"
|
||||
option-label="name" option-value="lang_code"
|
||||
placeholder="{{ __('Select a translation language') }}"
|
||||
wire:model.live="selectTranslationLanguage" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 pl-8" wire:loading wire:target="selectTranslationLanguage">
|
||||
<x-mini-button flat icon="" primary rounded spinner />
|
||||
<span>{{ __('Loading...') }}</span>
|
||||
</div>
|
||||
|
||||
@if ($selectTranslationLanguage)
|
||||
@php
|
||||
$translationLang = \App\Models\Language::where('lang_code', $selectTranslationLanguage)->first()?->name ?? $selectTranslationLanguage;
|
||||
@endphp
|
||||
|
||||
<hr class="border-t border-theme-primary" />
|
||||
<x-radio id="radio-select"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Select an existing, untranslated activity tag in @LANGUAGE@')) }}"
|
||||
value="select" wire:model.live="translateRadioButton" />
|
||||
|
||||
<div class="my-6 grid grid-cols-1 gap-6 pl-8 md:grid-cols-2" id="select-translation">
|
||||
@if (count($translationOptions) > 0)
|
||||
<x-select
|
||||
:disabled="$translateRadioButton === 'input'"
|
||||
:options="$translationOptions"
|
||||
class="placeholder-theme-light"
|
||||
id="translation" label="" option-label="name" option-value="tag_id"
|
||||
placeholder="{{ __('Select a translation') }}"
|
||||
wire:model.live="selectTagTranslation" />
|
||||
@else
|
||||
<x-select
|
||||
:disabled="true"
|
||||
:options="[['tag_id' => '', 'name' => __('No translations available'), 'disabled' => true]]"
|
||||
class="placeholder-theme-light"
|
||||
id="translation" label="" option-label="name" option-value="tag_id"
|
||||
placeholder="{{ __('No existing translation available') }}"
|
||||
wire:model.live="selectTagTranslation" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<hr class="border-t border-theme-primary" />
|
||||
<x-radio id="radio-input"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Or create a new Activity tag in @LANGUAGE@')) }}"
|
||||
value="input" wire:model.live="translateRadioButton" />
|
||||
|
||||
<div id="input-translation">
|
||||
<div class="mt-6 grid grid-cols-1 gap-6 pl-8">
|
||||
<x-input :disabled="$translateRadioButton === 'select'"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Activity tag in @LANGUAGE@ (min. 2 words)')) }}"
|
||||
placeholder="{{ !empty($newTag['name']) ? '\'' . $newTag['name'] . '\' ' . __('in') . ' ' . $translationLang : __('Activity tag name in') . ' ' . $translationLang }}"
|
||||
wire:key="nameInput" wire:model.live="inputTagTranslation.name" />
|
||||
|
||||
@if (!$transLanguageOk)
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
@php
|
||||
$localeTranslation = $selectTranslationLanguage ?? '';
|
||||
$localeNameTranslation = \Locale::getDisplayName($localeTranslation, $localeTranslation);
|
||||
@endphp
|
||||
<x-checkbox :disabled="$translateRadioButton === 'select'"
|
||||
id="transLang-ignore-checkbox"
|
||||
label="{{ __('This tag is in :localeTranslation.', ['localeTranslation' => $localeNameTranslation]) }}"
|
||||
wire:model.live="transLanguageIgnored" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<x-select :disabled="$translateRadioButton === 'select'"
|
||||
:options="$categoryOptions"
|
||||
class="placeholder-theme-light" id="category"
|
||||
label="{{ __('Category') }}" option-label="name"
|
||||
option-value="category_id"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
wire:model.live="newTagCategory" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (!$translationVisible || !$translationAllowed)
|
||||
<div class="mt-3 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<x-select :options="$categoryOptions" class="placeholder-theme-light" id="category"
|
||||
label="{{ __('Category') }}" option-label="name" option-value="category_id"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
wire:model.live="newTagCategory" />
|
||||
</div>
|
||||
@error('newTagCategory')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@endif
|
||||
|
||||
<div class="pt-10 my-3 grid grid-cols-1">
|
||||
<x-skill-tag-warning />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="cancelCreateTag" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.secondary-button
|
||||
class="ml-3"
|
||||
wire:click="createTag"
|
||||
wire:key="createTagButton"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,215 @@
|
||||
<div>
|
||||
<x-jetstream.button
|
||||
wire:click.prevent="openModal()"
|
||||
class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('New Call') }}
|
||||
</x-jetstream.button>
|
||||
|
||||
{{-- No credits modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showNoCreditsModal" wire:key="showCallNoCreditsModal" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ trans_with_platform('Post a @PLATFORM_NAME@ call') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@if ($spendableBalance !== null)
|
||||
<div class="mb-4 text-sm text-theme-secondary">
|
||||
{{ __('Current balance total') }}: <span class="font-semibold text-theme-primary">{{ tbFormat($spendableBalance) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@livewire('static-post', ['type' => 'SiteContents\\Call\\NotAllowed', 'limit' => 1, 'hideAuthor' => true, 'fallbackText' => trans_with_platform('You need @PLATFORM_CURRENCY_NAME_PLURAL@ to post a call.')])
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.button wire:click="$set('showNoCreditsModal', false)">
|
||||
{{ __('OK') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
@if ($showModal)
|
||||
<x-jetstream.dialog-modal wire:model="showModal" wire:key="showCallModal" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ trans_with_platform('Post a @PLATFORM_NAME@ call') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
{{-- Tag picker (dedicated component) --}}
|
||||
<livewire:calls.call-skill-input wire:key="call-skill-input" />
|
||||
|
||||
@error('tagId')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
{{-- Content --}}
|
||||
@php $contentMax = timebank_config('calls.content_max_input', 200); @endphp
|
||||
<div wire:key="call-content"
|
||||
x-data="{ remaining: {{ $contentMax - mb_strlen($content) }} }"
|
||||
x-init="$nextTick(() => { const ta = $el.querySelector('textarea'); if (ta) { ta.addEventListener('input', () => { remaining = {{ $contentMax }} - ta.value.length; }); } })">
|
||||
<label class="block text-sm text-theme-primary mb-2">
|
||||
{{ __('Description') }}
|
||||
<span class="text-theme-secondary text-xs ml-1" x-text="remaining + ' {{ __('characters left') }}'"></span>
|
||||
</label>
|
||||
<x-textarea
|
||||
placeholder="{{ __('Describe your request in more detail...') }}"
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model.blur="content" />
|
||||
@error('content')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Description translations (only if the selected tag has other-language translations) --}}
|
||||
@if(!empty($this->remainingTagLocales) || !empty($translationContents))
|
||||
<div wire:key="call-translations" class="space-y-4">
|
||||
@foreach($translationContents as $locale => $text)
|
||||
<div wire:key="call-translation-{{ $locale }}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm text-theme-primary">
|
||||
{{ __('Description in :language', ['language' => __('messages.' . $locale)]) }}
|
||||
</label>
|
||||
<button type="button" wire:click="removeTranslation('{{ $locale }}')" class="text-xs text-red-600 hover:underline">
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</div>
|
||||
<x-textarea
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model="translationContents.{{ $locale }}" />
|
||||
@error('translationContents.' . $locale)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if(!empty($this->remainingTagLocales))
|
||||
<div>
|
||||
<label class="block text-sm text-theme-primary mb-1">{{ __('Add translation') }}</label>
|
||||
<p class="text-xs text-theme-secondary mb-2">
|
||||
{{ __('Only languages the selected activity tag has been translated into can be selected.') }}
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-select :clearable="false" :searchable="false" class="!w-48"
|
||||
placeholder="{{ __('Select a language') }}"
|
||||
wire:model="newTranslationLocale">
|
||||
@foreach($this->remainingTagLocales as $locale)
|
||||
<x-select.option label="{{ __('messages.' . $locale) }}" value="{{ $locale }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
<x-jetstream.secondary-button wire:click="addTranslation()">
|
||||
{{ __('Add') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Location --}}
|
||||
<div wire:key="call-location">
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Exchange location') }}</label>
|
||||
<livewire:locations.locations-dropdown
|
||||
:hide-label="true"
|
||||
:country="$country"
|
||||
:city="$city"
|
||||
:division="$division"
|
||||
:district="$district"
|
||||
wire:key="call-location-dropdown-{{ $country }}-{{ $city }}" />
|
||||
@error('country')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Expiry date --}}
|
||||
<div wire:key="call-till">
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Expires on') }}</label>
|
||||
@php
|
||||
$tillMinDate = now()->addDay()->format('Y-m-d');
|
||||
$activeProfileType = session('activeProfileType');
|
||||
$tillMaxDays = ($activeProfileType && $activeProfileType !== \App\Models\User::class)
|
||||
? timebank_config('calls.till_max_days_non_user')
|
||||
: timebank_config('calls.till_max_days');
|
||||
$tillMaxDate = $tillMaxDays !== null ? now()->addDays($tillMaxDays)->format('Y-m-d') : null;
|
||||
@endphp
|
||||
<div wire:ignore>
|
||||
<x-flatpickr
|
||||
dateFormat="Y-m-d"
|
||||
altFormat="d-m-Y"
|
||||
:minDate="$tillMinDate"
|
||||
:maxDate="$tillMaxDate"
|
||||
placeholder="{{ __('Select a date') }}"
|
||||
wire:model.defer="till"
|
||||
:config="['altInputClass' => 'w-full mt-1 block border border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm px-3 py-2 text-sm bg-white']"
|
||||
class="mt-1 block w-full border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm" />
|
||||
</div>
|
||||
@error('till')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Visibility --}}
|
||||
<div wire:key="call-public">
|
||||
<x-checkbox
|
||||
id="call-is-public"
|
||||
label="{{ __('Public, visible for search engines and sharable on social media') }}"
|
||||
wire:model.live="isPublic" />
|
||||
@if ($isPublic)
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
{{ str_replace(':username', $profileName, __('This exposes your username (:username), your profile photo and your profile and work locations!')) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div wire:loading wire:target="save">
|
||||
<x-mini-button flat primary rounded spinner />
|
||||
</div>
|
||||
<x-jetstream.secondary-button
|
||||
class="ml-3 w-32 justify-center"
|
||||
wire:click="$set('showModal', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
class="ml-3 w-32 justify-center"
|
||||
wire:click.prevent="save()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Publish') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initCallFlatpickr() {
|
||||
const tillInput = document.querySelector('[wire\\:model\\.defer="till"]');
|
||||
if (tillInput && !tillInput._flatpickr) {
|
||||
if (window.LaravelFlatpickr) {
|
||||
window.LaravelFlatpickr.initializeFlatpickr(tillInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(initCallFlatpickr, 100);
|
||||
|
||||
Livewire.hook('morph.updated', () => {
|
||||
setTimeout(initCallFlatpickr, 200);
|
||||
setTimeout(initCallFlatpickr, 500);
|
||||
});
|
||||
|
||||
Livewire.on('showModal', () => {
|
||||
setTimeout(initCallFlatpickr, 300);
|
||||
setTimeout(initCallFlatpickr, 600);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
<div>
|
||||
@if ($compact)
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click.prevent="openModal()"
|
||||
wire:target="openModal"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@else
|
||||
<x-jetstream.button wire:click.prevent="openModal()">
|
||||
{{ __('Edit Call') }}
|
||||
</x-jetstream.button>
|
||||
@endif
|
||||
|
||||
@if ($showModal)
|
||||
<x-jetstream.dialog-modal wire:model="showModal" wire:key="editCallModal-{{ $call->id }}" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ trans_with_platform('Edit @PLATFORM_NAME@ call') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
{{-- Tag picker (dedicated component) --}}
|
||||
<livewire:calls.call-skill-input :initial-tag-id="$tagId" wire:key="edit-call-skill-input-{{ $call->id }}" />
|
||||
|
||||
@error('tagId')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
{{-- Content --}}
|
||||
@php $contentMax = timebank_config('calls.content_max_input', 200); @endphp
|
||||
<div wire:key="edit-call-content-{{ $call->id }}"
|
||||
x-data="{ remaining: {{ $contentMax - mb_strlen($content) }} }"
|
||||
x-init="$nextTick(() => { const ta = $el.querySelector('textarea'); if (ta) { ta.addEventListener('input', () => { remaining = {{ $contentMax }} - ta.value.length; }); } })">
|
||||
<label class="block text-sm text-theme-primary mb-2">
|
||||
{{ __('Description') }}
|
||||
<span class="text-theme-secondary text-xs ml-1" x-text="remaining + ' {{ __('characters left') }}'"></span>
|
||||
</label>
|
||||
<x-textarea
|
||||
placeholder="{{ __('Describe your request in more detail...') }}"
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model.blur="content" />
|
||||
@error('content')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Description translations (only if the selected tag has other-language translations) --}}
|
||||
@if(!empty($this->remainingTagLocales) || !empty($translationContents))
|
||||
<div wire:key="edit-call-translations-{{ $call->id }}" class="space-y-4">
|
||||
@foreach($translationContents as $locale => $text)
|
||||
<div wire:key="edit-call-translation-{{ $call->id }}-{{ $locale }}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm text-theme-primary">
|
||||
{{ __('Description in :language', ['language' => __('messages.' . $locale)]) }}
|
||||
</label>
|
||||
<button type="button" wire:click="removeTranslation('{{ $locale }}')" class="text-xs text-red-600 hover:underline">
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</div>
|
||||
<x-textarea
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model="translationContents.{{ $locale }}" />
|
||||
@error('translationContents.' . $locale)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if(!empty($this->remainingTagLocales))
|
||||
<div>
|
||||
<label class="block text-sm text-theme-primary mb-1">{{ __('Add translation') }}</label>
|
||||
<p class="text-xs text-theme-secondary mb-2">
|
||||
{{ __('Only languages the selected activity tag has been translated into can be selected.') }}
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-select :clearable="false" :searchable="false" class="!w-48"
|
||||
placeholder="{{ __('Select a language') }}"
|
||||
wire:model="newTranslationLocale">
|
||||
@foreach($this->remainingTagLocales as $locale)
|
||||
<x-select.option label="{{ __('messages.' . $locale) }}" value="{{ $locale }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
<x-jetstream.secondary-button wire:click="addTranslation()">
|
||||
{{ __('Add') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Location --}}
|
||||
<div wire:key="edit-call-location-{{ $call->id }}">
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Exchange location') }}</label>
|
||||
<livewire:locations.locations-dropdown
|
||||
:hide-label="true"
|
||||
:country="$country"
|
||||
:city="$city"
|
||||
:division="$division"
|
||||
:district="$district"
|
||||
wire:key="edit-call-location-dropdown-{{ $country }}-{{ $city }}" />
|
||||
@error('country')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Expiry date --}}
|
||||
<div wire:key="edit-call-till-{{ $call->id }}">
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Expires on') }}</label>
|
||||
@php
|
||||
$tillMinDate = now()->addDay()->format('Y-m-d');
|
||||
$callableType = $call->callable_type ?? session('activeProfileType');
|
||||
$tillMaxDays = ($callableType && $callableType !== \App\Models\User::class)
|
||||
? timebank_config('calls.till_max_days_non_user')
|
||||
: timebank_config('calls.till_max_days');
|
||||
$tillMaxDate = $tillMaxDays !== null ? now()->addDays($tillMaxDays)->format('Y-m-d') : null;
|
||||
@endphp
|
||||
<div wire:ignore>
|
||||
<x-flatpickr
|
||||
dateFormat="Y-m-d"
|
||||
altFormat="d-m-Y"
|
||||
:minDate="$tillMinDate"
|
||||
:maxDate="$tillMaxDate"
|
||||
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>
|
||||
@error('till')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Visibility --}}
|
||||
<div wire:key="edit-call-public-{{ $call->id }}">
|
||||
<x-checkbox
|
||||
id="edit-call-is-public-{{ $call->id }}"
|
||||
label="{{ __('Public, visible for search engines and sharable on social media') }}"
|
||||
wire:model.live="isPublic" />
|
||||
@if ($isPublic)
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
{{ str_replace(':username', $profileName, __('This exposes your username (:username), your profile photo and your profile and work locations!')) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<x-jetstream.danger-button
|
||||
wire:click="confirmDelete()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div wire:loading wire:target="save">
|
||||
<x-mini-button flat primary rounded spinner />
|
||||
</div>
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showModal', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click.prevent="save()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
{{-- Delete confirmation modal --}}
|
||||
<x-jetstream.dialog-modal wire:model="showDeleteConfirm" wire:key="deleteCallModal-{{ $call->id }}">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete Call') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">{{ __('Are you sure you want to delete this call? You can undelete this call later.') }}</p>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showDeleteConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button
|
||||
class="ml-3"
|
||||
wire:click="delete()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Yes, delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initEditCallFlatpickr() {
|
||||
const tillInput = document.querySelector('[wire\\:key="edit-call-till-{{ $call->id }}"] [wire\\:model\\.defer="till"]');
|
||||
if (tillInput && !tillInput._flatpickr) {
|
||||
if (window.LaravelFlatpickr) {
|
||||
window.LaravelFlatpickr.initializeFlatpickr(tillInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(initEditCallFlatpickr, 100);
|
||||
|
||||
Livewire.hook('morph.updated', () => {
|
||||
setTimeout(initEditCallFlatpickr, 200);
|
||||
setTimeout(initEditCallFlatpickr, 500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,854 @@
|
||||
<div class="mt-12">
|
||||
|
||||
{{-- Action buttons --}}
|
||||
@if (!$isAdminView)
|
||||
<div class="mb-6">
|
||||
@livewire('calls.create', key('calls-create-manage'))
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Search box --}}
|
||||
<div class="mb-4 flex items-center">
|
||||
<div class="relative w-1/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="$refresh" wire:model="search">
|
||||
@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="$set('search', '')">
|
||||
<x-icon mini name="backspace" solid />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<x-jetstream.secondary-button class="ml-3 w-32 justify-center" wire:click="$refresh">
|
||||
{{ __('Search') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
|
||||
{{-- Filter dropdowns --}}
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
{{-- Status filter --}}
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="false" class="!w-40" style="width: 10rem !important; min-width: 10rem !important;"
|
||||
placeholder="{{ __('Status') }}" wire:model.live="statusFilter">
|
||||
<x-select.option label="{{ __('Active') }}" value="active" />
|
||||
<x-select.option label="{{ __('Paused') }}" value="paused" />
|
||||
<x-select.option label="{{ __('Expired') }}" value="expired" />
|
||||
<x-select.option label="{{ __('Deleted') }}" value="deleted" />
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
@if ($isAdminView)
|
||||
{{-- Callable type filter --}}
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="false" class="!w-44" style="width: 11rem !important; min-width: 11rem !important;"
|
||||
placeholder="{{ __('Profile type') }}" wire:model.live="callableFilter">
|
||||
<x-select.option label="{{ __('User') }}" value="user" />
|
||||
<x-select.option label="{{ __('Organization') }}" value="organization" />
|
||||
<x-select.option label="{{ __('Bank') }}" value="bank" />
|
||||
</x-select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Language filter --}}
|
||||
@if (count($availableLocales) > 1)
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="false" class="!w-36" style="width: 9rem !important; min-width: 9rem !important;"
|
||||
placeholder="{{ __('Language') }}" wire:model.live="localeFilter">
|
||||
@foreach ($availableLocales as $lang)
|
||||
<x-select.option label="{{ $lang['name'] }}" value="{{ $lang['id'] }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Bulk action buttons --}}
|
||||
<div class="mb-6 flex items-center justify-end space-x-4">
|
||||
{{-- Print selection --}}
|
||||
<x-jetstream.secondary-button
|
||||
:disabled="$bulkDisabled"
|
||||
wire:click.prevent="confirmBulkPrint">
|
||||
<x-icon class="mr-3 h-5 w-5" name="printer" />
|
||||
{{ __('Print') }} {{ __('selection') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
@if ($statusFilter === 'deleted')
|
||||
@can('update calls')
|
||||
<x-jetstream.button
|
||||
:disabled="$bulkDisabled"
|
||||
onclick="confirm('{{ __('Are you sure you want to restore the selected calls?') }}') || event.stopImmediatePropagation()"
|
||||
wire:click.prevent="undeleteSelected">
|
||||
<x-icon class="mr-3 h-5 w-5" name="arrow-path" />
|
||||
{{ __('Undelete') }} {{ __('selection') }}
|
||||
</x-jetstream.button>
|
||||
@endcan
|
||||
@else
|
||||
@can('delete calls')
|
||||
@if ($isAdminView)
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$bulkDisabled"
|
||||
wire:click.prevent="confirmAdminDelete">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
@else
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$bulkDisabled"
|
||||
wire:click.prevent="confirmBulkDelete">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="bg-white shadow-sm rounded-lg overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<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>
|
||||
|
||||
@if ($isAdminView)
|
||||
{{-- ID --}}
|
||||
<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>
|
||||
@endif
|
||||
|
||||
{{-- Lang --}}
|
||||
<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>
|
||||
|
||||
@if ($isAdminView)
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{{ __('Profile') }}
|
||||
</th>
|
||||
@endif
|
||||
|
||||
{{-- Tag --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{{ __('Tag') }}
|
||||
</th>
|
||||
|
||||
{{-- Description --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-full">
|
||||
{{ __('Description') }}
|
||||
</th>
|
||||
|
||||
{{-- Public --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
||||
{{ __('Public') }}
|
||||
</th>
|
||||
|
||||
{{-- Expires --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap cursor-pointer"
|
||||
wire:click="sortBy('till')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Expires') }}
|
||||
@if ($sortField === 'till')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap" style="min-width: 9rem;">
|
||||
{{ __('Actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse ($calls as $call)
|
||||
@php $dimClass = $call->trashed() ? 'opacity-50' : ''; @endphp
|
||||
<tr wire:key="calls-row-{{ $call->id }}">
|
||||
<td class="px-3 py-4 whitespace-nowrap {{ $dimClass }}">
|
||||
<input type="checkbox" value="{{ $call->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>
|
||||
|
||||
@if ($isAdminView)
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 {{ $dimClass }}">
|
||||
{{ $call->id }}
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium text-gray-700 uppercase {{ $dimClass }}">
|
||||
{{ $call->translations->pluck('locale')->implode(', ') ?: '-' }}
|
||||
</td>
|
||||
|
||||
@if ($isAdminView)
|
||||
<td class="px-3 py-4 whitespace-nowrap {{ $dimClass }}">
|
||||
@if ($call->callable)
|
||||
<div class="relative block cursor-pointer flex-shrink-0"
|
||||
onclick="window.location='{{ $call->callable->profile_url }}'"
|
||||
title="{{ $call->callable->name }} ({{ class_basename($call->callable_type) }})">
|
||||
<img src="{{ $call->callable->profile_photo_url }}"
|
||||
alt="{{ $call->callable->name }}"
|
||||
class="h-6 w-6 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600">
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<td class="px-3 py-4 whitespace-nowrap {{ $dimClass }}">
|
||||
@if ($call->tag)
|
||||
@php
|
||||
$tagContext = $call->tag->contexts->first();
|
||||
$tagColor = $tagContext?->category?->relatedColor ?? 'gray';
|
||||
$tagCategoryId = $tagContext?->category?->id;
|
||||
@endphp
|
||||
<a href="{{ route('search.show') }}?category={{ $tagCategoryId }}"
|
||||
class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-{{ $tagColor }}-400 hover:opacity-80">
|
||||
{{ $call->tag->translation?->name ?? $call->tag->name }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-sm text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Description --}}
|
||||
<td class="px-3 py-4 w-full max-w-0 {{ $dimClass }}">
|
||||
@php
|
||||
$desc = $call->translations->firstWhere('locale', app()->getLocale())?->content
|
||||
?? $call->translations->firstWhere('locale', config('app.fallback_locale'))?->content
|
||||
?? $call->translations->first()?->content;
|
||||
@endphp
|
||||
<div class="text-sm text-gray-700 truncate" title="{{ $desc }}">
|
||||
{{ $desc ?? '-' }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- Public --}}
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 {{ $dimClass }}">
|
||||
{{ $call->is_public ? __('Yes') : __('No') }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4 whitespace-nowrap {{ $dimClass }}">
|
||||
@if ($call->till)
|
||||
<div class="text-sm leading-tight {{ $call->till < now() ? 'text-red-500' : 'text-gray-500' }}">
|
||||
<div>{{ $call->till->translatedFormat('M j') }}</div>
|
||||
<div>{{ $call->till->translatedFormat('Y') }}</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4 whitespace-nowrap text-center text-sm font-medium" style="min-width: 9rem;">
|
||||
@if (!$call->trashed())
|
||||
<div class="flex items-center justify-center gap-1 flex-nowrap"
|
||||
x-data="{ loading: false }"
|
||||
x-on:click="loading = true"
|
||||
x-on:admin-action-ready.window="loading = false"
|
||||
x-on:pause-publish-done.window="loading = false"
|
||||
x-on:edit-done.window="loading = false">
|
||||
{{-- Pause / Publish / Blocked indicator --}}
|
||||
@if (!$isAdminView && $call->is_suppressed)
|
||||
<x-jetstream.danger-button title="{{ __('Publication blocked due to policy violation') }}" disabled>
|
||||
<x-icon class="h-5 w-5" name="exclamation-triangle" solid />
|
||||
</x-jetstream.danger-button>
|
||||
@elseif ($isAdminView)
|
||||
{{-- Admin: toggle is_paused, never touch till --}}
|
||||
@can('publish calls')
|
||||
@if ($call->is_paused)
|
||||
<x-jetstream.secondary-button
|
||||
no-spinner
|
||||
title="{{ __('Publish') }}"
|
||||
wire:click="confirmAdminAction({{ $call->id }}, 'publish')"
|
||||
x-bind:disabled="loading">
|
||||
<span x-show="!loading"><x-icon class="h-5 w-5" name="play-circle" solid /></span>
|
||||
<span x-show="loading" class="flex items-center justify-center"><svg class="animate-spin h-5 w-5" 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>
|
||||
</x-jetstream.secondary-button>
|
||||
@else
|
||||
<x-jetstream.secondary-button
|
||||
no-spinner
|
||||
title="{{ __('Pause') }}"
|
||||
wire:click="confirmAdminAction({{ $call->id }}, 'pause')"
|
||||
x-bind:disabled="loading">
|
||||
<span x-show="!loading"><x-icon class="h-5 w-5" name="pause-circle" solid /></span>
|
||||
<span x-show="loading" class="flex items-center justify-center"><svg class="animate-spin h-5 w-5" 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>
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
@endcan
|
||||
@elseif (!$call->is_paused && ($call->till === null || $call->till > now()))
|
||||
{{-- Non-admin: pause button sets is_paused = true --}}
|
||||
@can('unpublish calls')
|
||||
<x-jetstream.secondary-button
|
||||
no-spinner
|
||||
title="{{ __('Pause') }}"
|
||||
wire:click="pause({{ $call->id }})"
|
||||
x-bind:disabled="loading">
|
||||
<span x-show="!loading"><x-icon class="h-5 w-5" name="pause-circle" solid /></span>
|
||||
<span x-show="loading" class="flex items-center justify-center"><svg class="animate-spin h-5 w-5" 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>
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@else
|
||||
{{-- Non-admin: publish button (shown when paused or expired) --}}
|
||||
@can('publish calls')
|
||||
<x-jetstream.secondary-button
|
||||
no-spinner
|
||||
title="{{ $canPublish ? __('Publish') : trans_with_platform(__('You need @PLATFORM_CURRENCY_NAME_PLURAL@ to post a call.')) }}"
|
||||
wire:click="publish({{ $call->id }})"
|
||||
x-bind:disabled="{{ $canPublish ? 'false' : 'true' }} || loading">
|
||||
<span x-show="!loading"><x-icon class="h-5 w-5" name="play-circle" solid /></span>
|
||||
<span x-show="loading" class="flex items-center justify-center"><svg class="animate-spin h-5 w-5" 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>
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
@if ($isAdminView)
|
||||
{{-- Block / Unblock --}}
|
||||
@can('manage calls')
|
||||
@if ($call->is_suppressed)
|
||||
<x-jetstream.danger-button
|
||||
title="{{ __('Unblock') }}"
|
||||
wire:click="unblock({{ $call->id }})"
|
||||
wire:target="unblock({{ $call->id }})"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="exclamation-triangle" solid />
|
||||
</x-jetstream.danger-button>
|
||||
@else
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Block publication') }}"
|
||||
wire:click="block({{ $call->id }})"
|
||||
wire:target="block({{ $call->id }})"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="exclamation-triangle" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
@endcan
|
||||
@else
|
||||
{{-- Edit --}}
|
||||
@can('update calls')
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Edit') }}"
|
||||
x-on:click.stop="$wire.call('openEdit', {{ $call->id }})"
|
||||
wire:target="openEdit({{ $call->id }})"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
{{-- View --}}
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('View') }}"
|
||||
x-on:click.stop="window.open('{{ route('call.show', ['id' => $call->id]) }}', '_blank')">
|
||||
<x-icon class="h-5 w-5" mini name="arrow-top-right-on-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
{{-- Delete --}}
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
title="{{ __('Delete') }}"
|
||||
wire:click="confirmDeleteRow({{ $call->id }})"
|
||||
wire:target="confirmDeleteRow({{ $call->id }})"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="trash" />
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
@else
|
||||
{{-- Deleted: View + Restore --}}
|
||||
<div class="flex items-center justify-center gap-1 flex-nowrap">
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('View') }}"
|
||||
x-on:click.stop="window.open('{{ route('call.show', ['id' => $call->id]) }}', '_blank')">
|
||||
<x-icon class="h-5 w-5" mini name="arrow-top-right-on-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@can('update calls')
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Restore call') }}"
|
||||
wire:click="confirmRestoreRow({{ $call->id }})"
|
||||
wire:target="confirmRestoreRow({{ $call->id }})"
|
||||
wire:loading.attr="disabled">
|
||||
<x-icon class="h-5 w-5" name="arrow-path" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $isAdminView ? 9 : 7 }}" class="px-6 py-12 text-center text-gray-500">
|
||||
{{ __('No results found') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if ($calls->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 px-4">
|
||||
<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>
|
||||
{{ $calls->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Inline Edit Modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showEditModal" wire:key="manage-edit-modal" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ trans_with_platform('Edit @PLATFORM_NAME@ call') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
|
||||
{{-- Tag picker --}}
|
||||
<livewire:calls.call-skill-input :initial-tag-id="$editTagId" wire:key="manage-edit-tag-{{ $editCallId }}" />
|
||||
@error('editTagId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
|
||||
{{-- Content --}}
|
||||
@php $contentMax = timebank_config('calls.content_max_input', 200); @endphp
|
||||
<div x-data="{ remaining: {{ $contentMax - mb_strlen($editContent) }} }"
|
||||
x-init="$nextTick(() => { const ta = $el.querySelector('textarea'); if (ta) { ta.addEventListener('input', () => { remaining = {{ $contentMax }} - ta.value.length; }); } })">
|
||||
<label class="block text-sm text-theme-primary mb-2">
|
||||
{{ __('Description') }}
|
||||
<span class="text-theme-secondary text-xs ml-1" x-text="remaining + ' {{ __('characters left') }}'"></span>
|
||||
</label>
|
||||
<x-textarea
|
||||
placeholder="{{ __('Describe your request in more detail...') }}"
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model.blur="editContent" />
|
||||
@error('editContent') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Description translations (only if the selected tag has other-language translations) --}}
|
||||
@if(!empty($this->editRemainingTagLocales) || !empty($editTranslationContents))
|
||||
<div wire:key="manage-edit-translations-{{ $editCallId }}" class="space-y-4">
|
||||
@foreach($editTranslationContents as $locale => $text)
|
||||
<div wire:key="manage-edit-translation-{{ $editCallId }}-{{ $locale }}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm text-theme-primary">
|
||||
{{ __('Description in :language', ['language' => __('messages.' . $locale)]) }}
|
||||
</label>
|
||||
<button type="button" wire:click="removeEditTranslation('{{ $locale }}')" class="text-xs text-red-600 hover:underline">
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</div>
|
||||
<x-textarea
|
||||
rows="4"
|
||||
maxlength="{{ $contentMax }}"
|
||||
class="!text-base"
|
||||
wire:model="editTranslationContents.{{ $locale }}" />
|
||||
@error('editTranslationContents.' . $locale)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if(!empty($this->editRemainingTagLocales))
|
||||
<div>
|
||||
<label class="block text-sm text-theme-primary mb-1">{{ __('Add translation') }}</label>
|
||||
<p class="text-xs text-theme-secondary mb-2">
|
||||
{{ __('Only languages the selected activity tag has been translated into can be selected.') }}
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-select :clearable="false" :searchable="false" class="!w-48"
|
||||
placeholder="{{ __('Select a language') }}"
|
||||
wire:model="editNewTranslationLocale">
|
||||
@foreach($this->editRemainingTagLocales as $locale)
|
||||
<x-select.option label="{{ __('messages.' . $locale) }}" value="{{ $locale }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
<x-jetstream.secondary-button wire:click="addEditTranslation()">
|
||||
{{ __('Add') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Location --}}
|
||||
<div>
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Exchange location') }}</label>
|
||||
<livewire:locations.locations-dropdown
|
||||
:hide-label="true"
|
||||
:country="$editCountry"
|
||||
:city="$editCity"
|
||||
:division="$editDivision"
|
||||
:district="$editDistrict"
|
||||
wire:key="manage-edit-location-dropdown-{{ $editCountry }}-{{ $editCity }}" />
|
||||
@error('editCountry') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Expiry date --}}
|
||||
<div>
|
||||
<label class="block text-sm text-theme-primary mb-2">{{ __('Expires on') }}</label>
|
||||
@php
|
||||
$tillMinDate = now()->addDay()->format('Y-m-d');
|
||||
$callableType = ($editCall?->callable_type) ?? session('activeProfileType');
|
||||
$tillMaxDays = ($callableType && $callableType !== \App\Models\User::class)
|
||||
? timebank_config('calls.till_max_days_non_user')
|
||||
: timebank_config('calls.till_max_days');
|
||||
$tillMaxDate = $tillMaxDays !== null ? now()->addDays($tillMaxDays)->format('Y-m-d') : null;
|
||||
@endphp
|
||||
<div wire:ignore>
|
||||
<x-flatpickr
|
||||
dateFormat="Y-m-d"
|
||||
altFormat="d-m-Y"
|
||||
:minDate="$tillMinDate"
|
||||
:maxDate="$tillMaxDate"
|
||||
:value="$editTill"
|
||||
placeholder="{{ __('Select a date') }}"
|
||||
wire:model.defer="editTill"
|
||||
:config="['altInputClass' => 'w-full mt-1 block border border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm px-3 py-2 text-sm bg-white']"
|
||||
class="mt-1 block w-full border-theme-border focus:border-theme-accent focus:ring-1 focus:ring-theme-accent rounded-md shadow-sm" />
|
||||
</div>
|
||||
@error('editTill') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Visibility --}}
|
||||
<div>
|
||||
<x-checkbox
|
||||
id="manage-edit-is-public"
|
||||
label="{{ __('Public, visible for search engines and sharable on social media') }}"
|
||||
wire:model.live="editIsPublic" />
|
||||
@if ($editIsPublic)
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
{{ str_replace(':username', $editCall?->callable?->name ?? '', __('Heads up: this exposes your username (:username) and exchange location.')) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex w-full flex-wrap items-center justify-between gap-y-3">
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
wire:click="confirmDelete()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showEditModal', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click.prevent="saveEdit()"
|
||||
wire:loading.attr="disabled"
|
||||
wire:loading.class="opacity-75">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
{{-- Delete confirmation modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showDeleteConfirm" wire:key="manage-delete-confirm">
|
||||
<x-slot name="title">{{ __('Delete Call') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">{{ __('Are you sure you want to delete this call? You can undelete this call later.') }}</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showDeleteConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
class="ml-3"
|
||||
wire:click="deleteCall()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Yes, delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
{{-- Non-admin bulk delete confirmation modal --}}
|
||||
@if (!$isAdminView)
|
||||
<x-jetstream.dialog-modal wire:model.live="showDeleteConfirmModal" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete selection?') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">
|
||||
{{ trans_choice('Are you sure you want to delete :count call? This can always be undone later.|Are you sure you want to delete :count calls? This can always be undone later.', count($bulkSelected), ['count' => count($bulkSelected)]) }}
|
||||
</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<div class="flex items-center gap-3 justify-end">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showDeleteConfirmModal', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
wire:click="deleteSelected()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ trans_choice('Delete :count call|Delete :count calls', count($bulkSelected), ['count' => count($bulkSelected)]) }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
{{-- Admin bulk delete confirmation modal --}}
|
||||
@if ($isAdminView)
|
||||
<x-jetstream.dialog-modal wire:model.live="showAdminDeleteConfirm" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete selection?') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">
|
||||
{{ trans_choice('Delete :count call?|Delete :count calls?', count($bulkSelected), ['count' => count($bulkSelected)]) }}
|
||||
</p>
|
||||
<p class="mt-3 text-theme-primary">
|
||||
{{ __('Do you have permission of :names to take this action?', ['names' => $adminDeleteCallableNames]) }}
|
||||
</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<div class="flex items-center gap-3 justify-end">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showAdminDeleteConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
wire:click="deleteSelected()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ trans_choice('Delete :count call|Delete :count calls', count($bulkSelected), ['count' => count($bulkSelected)]) }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
{{-- Per-row delete confirmation modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showDeleteRowConfirm" wire:key="manage-delete-row-confirm">
|
||||
<x-slot name="title">{{ __('Delete Call') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">{{ __('Are you sure you want to delete this call? You can undelete this call later.') }}</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showDeleteRowConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@can('delete calls')
|
||||
<x-jetstream.danger-button
|
||||
class="ml-3"
|
||||
wire:click="deleteRowCall()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Yes, delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
{{-- Per-row restore confirmation modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showRestoreRowConfirm" wire:key="manage-restore-row-confirm">
|
||||
<x-slot name="title">{{ __('Restore call') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">{{ __('Are you sure you want to restore this call?') }}</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showRestoreRowConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
class="ml-3"
|
||||
wire:click="restoreRowCall()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Yes, restore') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
{{-- Admin pause/publish confirmation modal --}}
|
||||
@if ($isAdminView)
|
||||
<x-jetstream.dialog-modal wire:model.live="showAdminActionConfirm" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ $adminActionType === 'pause' ? __('Pause call') : __('Publish call') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<p class="text-theme-primary">
|
||||
{{ $adminActionType === 'pause'
|
||||
? __('Pause this call on behalf of :name?', ['name' => $adminActionCallableName])
|
||||
: __('Publish this call on behalf of :name?', ['name' => $adminActionCallableName]) }}
|
||||
</p>
|
||||
<p class="mt-3 text-theme-primary">
|
||||
{{ __('Do you have permission of :name to take this action?', ['name' => $adminActionCallableName]) }}
|
||||
</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<div class="flex items-center gap-3 justify-end">
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="$set('showAdminActionConfirm', false)"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click="executeAdminAction()"
|
||||
wire:loading.attr="disabled">
|
||||
{{ $adminActionType === 'pause' ? __('Pause call') : __('Publish call') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
{{-- Bulk print modal --}}
|
||||
<x-jetstream.dialog-modal wire:model.live="showBulkPrintModal" wire:key="bulk-print-modal" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Print selected calls') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
{{-- Calls per page --}}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Calls per page') }}</p>
|
||||
<div class="flex items-center gap-6">
|
||||
@foreach([1, 2, 4] as $option)
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio"
|
||||
wire:model.live="bulkPrintPerPage"
|
||||
value="{{ $option }}"
|
||||
class="text-theme-brand focus:ring-theme-brand">
|
||||
<span class="text-sm text-theme-primary">{{ $option }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Color --}}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Color') }}</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio"
|
||||
wire:model.live="bulkPrintColor"
|
||||
value="bw"
|
||||
class="text-theme-brand focus:ring-theme-brand">
|
||||
<span class="text-sm text-theme-primary">{{ __('Black & white') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio"
|
||||
wire:model.live="bulkPrintColor"
|
||||
value="color"
|
||||
class="text-theme-brand focus:ring-theme-brand">
|
||||
<span class="text-sm text-theme-primary">{{ __('Color') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ trans_choice(':count call selected|:count calls selected', count($bulkSelected), ['count' => count($bulkSelected)]) }}
|
||||
</p>
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('showBulkPrintModal', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click="executeBulkPrint"
|
||||
wire:loading.attr="disabled"
|
||||
class="ml-3 bg-theme-brand">
|
||||
<x-icon class="mr-2 h-4 w-4" name="printer" />
|
||||
{{ __('Generate PDF') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initManageCallFlatpickr() {
|
||||
const tillInput = document.querySelector('[wire\\:model\\.defer="editTill"]');
|
||||
if (tillInput && !tillInput._flatpickr && window.LaravelFlatpickr) {
|
||||
window.LaravelFlatpickr.initializeFlatpickr(tillInput);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize on first page load (modal is always in DOM)
|
||||
setTimeout(initManageCallFlatpickr, 100);
|
||||
|
||||
Livewire.hook('morph.updated', () => {
|
||||
setTimeout(initManageCallFlatpickr, 200);
|
||||
});
|
||||
|
||||
// Update flatpickr value/min/max imperatively when a call is opened for editing
|
||||
Livewire.on('manage-edit-opened', (params) => {
|
||||
const data = Array.isArray(params) ? params[0] : params;
|
||||
setTimeout(() => {
|
||||
const tillInput = document.querySelector('[wire\\:model\\.defer="editTill"]');
|
||||
if (!tillInput) return;
|
||||
|
||||
// Initialize if not yet done
|
||||
if (!tillInput._flatpickr) {
|
||||
window.LaravelFlatpickr && window.LaravelFlatpickr.initializeFlatpickr(tillInput);
|
||||
}
|
||||
|
||||
const fp = tillInput._flatpickr;
|
||||
if (!fp) return;
|
||||
|
||||
if (data.minDate) fp.set('minDate', data.minDate);
|
||||
if (data.maxDate) fp.set('maxDate', data.maxDate);
|
||||
fp.setDate(data.value || '', false);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Open bulk PDF in new tab
|
||||
Livewire.on('open-bulk-pdf', (params) => {
|
||||
const data = Array.isArray(params) ? params[0] : params;
|
||||
if (data.url) window.open(data.url, '_blank');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="flex items-end ml-auto">
|
||||
@php
|
||||
$activeProfile = getActiveProfile();
|
||||
$isOwnCall = $callable && $activeProfile &&
|
||||
get_class($activeProfile) === get_class($callable) &&
|
||||
$activeProfile->id === $callable->id;
|
||||
$isDeleted = $call->trashed();
|
||||
$isExpired = $call->till !== null && $call->till->isPast();
|
||||
$isSuppressed = $call->is_suppressed;
|
||||
$isPaused = $call->is_paused;
|
||||
$callableRemoved = $callable?->isRemoved();
|
||||
$unavailableReason = match(true) {
|
||||
$isDeleted => __('This call has been deleted.'),
|
||||
$isSuppressed => __('This call has been blocked.'),
|
||||
$isPaused => __('This call is currently paused.'),
|
||||
$isExpired => __('This call has expired.'),
|
||||
$callableRemoved => __('This user is no longer available.'),
|
||||
default => null,
|
||||
};
|
||||
@endphp
|
||||
@if ($isOwnCall)
|
||||
@livewire('calls.edit', ['call' => $call], key('edit-call-' . $call->id))
|
||||
@elseif ($unavailableReason)
|
||||
<p class="text-sm text-white/70 italic max-w-48 text-right">{{ $unavailableReason }}</p>
|
||||
@else
|
||||
<x-jetstream.button wire:click="createConversation">
|
||||
{{ __('Respond') }}
|
||||
</x-jetstream.button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div x-data="{ previewName: @entangle('previewName') }"
|
||||
x-on:updatePreviewName.window="previewName = $event.detail || '{{ __('Category name') }}'">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $label }}
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
wire:model.live="selectedColor">
|
||||
@foreach ($this->availableColors as $color)
|
||||
<option value="{{ $color['value'] }}">{{ $color['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<!-- Color swatch preview -->
|
||||
<div class="pointer-events-none absolute inset-y-0 right-10 flex items-center pr-2">
|
||||
<span class="inline-block h-6 w-6 rounded border border-gray-300 bg-{{ $selectedColor }}-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Large color preview -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
<span class="bg-{{ $selectedColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black" x-text="previewName"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Care about people's approval and you will be their prisoner. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,806 @@
|
||||
<div class="mt-12">
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
@can('create categories')
|
||||
<x-jetstream.button wire:click="openCreateCategoryModal" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('New Category') }}
|
||||
</x-jetstream.button>
|
||||
@endcan
|
||||
</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="searchCategories">
|
||||
{{ __('Search') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<!-- Parent Filter Dropdown -->
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="true" class="!w-40" style="width: 24rem !important; min-width: 24rem !important;"
|
||||
placeholder="{{ __('Parent') }}" wire:model.live="parentFilter">
|
||||
@foreach ($this->parents as $parent)
|
||||
<x-select.option label="{{ $parent['name'] }}" value="{{ $parent['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>
|
||||
</div>
|
||||
|
||||
<!-- Bulk action buttons -->
|
||||
<div class="mb-6 flex items-center justify-end space-x-4">
|
||||
@can('delete categories')
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$this->bulkDisabled"
|
||||
title="{{ __('Delete') . ' ' . __('selection') }}"
|
||||
wire:click.prevent="openBulkDeleteTranslationsModal()">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
|
||||
@if (!$this->bulkDisabled)
|
||||
<x-jetstream.secondary-button wire:click="resetBulkSelection">
|
||||
{{ __('Clear Selection') }}
|
||||
</x-jetstream.secondary-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>
|
||||
{{-- Checkbox column --}}
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></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 Name column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('name')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Name') }}
|
||||
@if ($sortField === 'name')
|
||||
<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 Tags column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('tags_count')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Tags') }}
|
||||
@if ($sortField === 'tags_count')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Parent column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('parent')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Parent') }}
|
||||
@if ($sortField === 'parent')
|
||||
<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>
|
||||
|
||||
{{-- Non-sortable Action column --}}
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-32">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="bg-white">
|
||||
@forelse ($categories as $category)
|
||||
@php
|
||||
// When sorting by translation fields, we have joined data instead of eager loaded translations
|
||||
$sortingByTranslation = in_array($sortField, ['name', 'locale', 'updated_at']);
|
||||
|
||||
if ($sortingByTranslation && isset($category->joined_translation_id)) {
|
||||
// Use joined translation data
|
||||
$translations = [
|
||||
(object)[
|
||||
'id' => $category->joined_translation_id,
|
||||
'locale' => $category->joined_locale,
|
||||
'name' => $category->joined_name,
|
||||
'updated_at' => $category->joined_updated_at ? \Carbon\Carbon::parse($category->joined_updated_at) : null,
|
||||
]
|
||||
];
|
||||
} else {
|
||||
// Use regular eager loaded translations
|
||||
$translations = $category->translations;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if (count($translations) === 0)
|
||||
{{-- Skip categories without translations --}}
|
||||
@else
|
||||
@foreach ($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="{{ $category->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">
|
||||
{{ $category->id }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm">
|
||||
@php
|
||||
// Use parent's color if it has a parent, otherwise use category's own color
|
||||
$nameColor = $category->parent_id && $category->parent
|
||||
? ($category->parent->color ?? 'gray')
|
||||
: ($category->color ?? 'gray');
|
||||
@endphp
|
||||
<a href="{{ route('search.show') }}?category={{ $category->id }}"
|
||||
class="bg-{{ $nameColor }}-400 inline-flex items-center rounded-md px-2 py-1 text-sm font-normal text-black hover:opacity-80">
|
||||
{{ $translation->name }}
|
||||
</a>
|
||||
</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 text-sm text-gray-500">
|
||||
{{ $category->tags_count }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm">
|
||||
@if ($category->parent && $category->parent->translation)
|
||||
@php
|
||||
$parentColor = $category->parent->color ?? 'gray';
|
||||
@endphp
|
||||
<a href="{{ route('search.show') }}?category={{ $category->parent->id }}"
|
||||
class="bg-{{ $parentColor }}-400 inline-flex items-center rounded-md px-2 py-1 text-sm font-normal text-black hover:opacity-80">
|
||||
{{ $category->parent->translation->name }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
@if ($translation->updated_at)
|
||||
<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>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Action Buttons --}}
|
||||
<td class="px-3 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex items-center justify-center space-x-1">
|
||||
@can('update categories')
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click="openEditCategoryModal({{ $translation->id }})">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
|
||||
@can('delete categories')
|
||||
<x-jetstream.danger-button
|
||||
title="{{ __('Delete') }}"
|
||||
wire:click="openDeleteCategoryModal({{ $translation->id }})">
|
||||
<x-icon class="h-5 w-5" name="trash" />
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
|
||||
{{ __('No results found') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($categories->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="50"> 50 </option>
|
||||
<option value="100"> 100 </option>
|
||||
</select>
|
||||
<span class="ml-2 text-sm text-theme-secondary">{{ __('per page') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Paginator -->
|
||||
{{ $categories->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Delete single translation modal -->
|
||||
@if ($modalDeleteTranslation)
|
||||
<x-jetstream.dialog-modal wire:key="modalDeleteTranslation" wire:model.live="modalDeleteTranslation" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Are you sure?') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Do you want to permanently delete this category and all its translations?') }}
|
||||
|
||||
<table class="mt-6 table min-w-full border-t-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Id') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Category ID') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Language') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Name') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach ($selectedTranslations as $translation)
|
||||
<tr class="border-theme-background" wire:key="delete-translation-{{ $translation->id }}">
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->category_id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->locale }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($affectedTagsCount > 0)
|
||||
<div class="mt-6 rounded-md border border-gray-200 p-4">
|
||||
<div class="flex">
|
||||
<div class="w-1/3 font-medium">{{ __('Affected Tags') }}:</div>
|
||||
<div class="flex-1 text-red-600 font-semibold">{{ $affectedTagsCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($needsCategoryReassignment)
|
||||
<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>{{ __('Important') }}:</strong>
|
||||
{{ __('This category has') }} {{ $affectedTagsCount }} {{ __('associated tags') }}.
|
||||
{{ __('Please select a category to reassign these tags to') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-select
|
||||
:clearable="false"
|
||||
:searchable="true"
|
||||
label="{{ __('Reassign tags to category') }}"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
:options="$availableCategories"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model="targetCategoryId" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-md border-l-4 border-red-400 bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-red-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-700">
|
||||
<strong>{{ __('Warning') }}:</strong>
|
||||
{{ __('This can not be undone!') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-1/3">
|
||||
<x-input label="{!! __('messages.confirm_input') !!}" placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalDeleteTranslation', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteCategory"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Bulk delete modal -->
|
||||
@if ($modalBulkDeleteTranslations)
|
||||
<x-jetstream.dialog-modal wire:key="modalBulkDeleteTranslations" wire:model.live="modalBulkDeleteTranslations" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Are you sure?') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Do you want to permanently delete these translations?') }}
|
||||
|
||||
<table class="mt-6 table min-w-full border-t-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Id') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Category ID') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Language') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Name') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach ($selectedTranslations as $translation)
|
||||
<tr class="border-theme-background" wire:key="bulk-delete-translation-{{ $translation->id }}">
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->category_id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->locale }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($affectedTagsCount > 0)
|
||||
<div class="mt-6 rounded-md border border-gray-200 p-4">
|
||||
<div class="flex">
|
||||
<div class="w-1/3 font-medium">{{ __('Total Affected Tags') }}:</div>
|
||||
<div class="flex-1 text-red-600 font-semibold">{{ $affectedTagsCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (count($categoriesWithTags) > 0)
|
||||
<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>{{ __('Important') }}:</strong>
|
||||
{{ __('The following categories have associated tags. Please select a target category for each to reassign their tags') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
@foreach ($categoriesWithTags as $categoryData)
|
||||
<div class="rounded-md border border-gray-300 p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="font-medium text-gray-700">
|
||||
{{ $categoryData['name'] }}
|
||||
</div>
|
||||
<div class="text-sm text-red-600">
|
||||
{{ $categoryData['tags_count'] }} {{ __('tags') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-select
|
||||
:clearable="false"
|
||||
:searchable="true"
|
||||
label="{{ __('Reassign tags to category') }}"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
:options="$availableCategories"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model="categoryReassignments.{{ $categoryData['category_id'] }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-md border-l-4 border-red-400 bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-red-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-700">
|
||||
<strong>{{ __('Warning') }}:</strong>
|
||||
{{ __('This can not be undone!') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-1/3">
|
||||
<x-input label="{!! __('messages.confirm_input') !!}" placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalBulkDeleteTranslations', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteSelected"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Edit category modal -->
|
||||
@if ($modalEditCategory)
|
||||
<x-jetstream.dialog-modal wire:key="modalEditCategory" wire:model.live="modalEditCategory" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit Category') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-4">
|
||||
<!-- Translation info -->
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium">{{ __('Category ID') }}:</span>
|
||||
<span class="ml-2">{{ $editCategoryId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">{{ __('Language') }}:</span>
|
||||
<span class="ml-2">{{ $editLocale }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name field (translation-specific) -->
|
||||
<div>
|
||||
<x-input
|
||||
label="{{ __('Name') }}"
|
||||
placeholder="{{ __('Category name') }}"
|
||||
wire:model="editName" />
|
||||
</div>
|
||||
|
||||
<!-- Parent field (category-level - affects all translations) -->
|
||||
<div>
|
||||
<x-select
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
label="{{ __('Parent Category') }} ({{ __('affects all translations') }})"
|
||||
placeholder="{{ __('Select parent category') }}"
|
||||
:options="$this->parents"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model.live="editParentId" />
|
||||
</div>
|
||||
|
||||
<!-- Color field (category-level - affects all translations) -->
|
||||
@if (!$editParentId || $editParentId === 'none')
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ __('Color') }} ({{ __('affects all translations') }})
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
wire:model.live="editColor">
|
||||
@foreach ($availableColors as $color)
|
||||
<option value="{{ $color['value'] }}">{{ $color['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<!-- Color swatch preview -->
|
||||
<div class="pointer-events-none absolute inset-y-0 right-10 flex items-center pr-2">
|
||||
<span class="inline-block h-6 w-6 rounded border border-gray-300 bg-{{ $editColor }}-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Color will be inherited from parent category') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Large color preview (always visible) -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
<span class="bg-{{ $this->editPreviewColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black">
|
||||
{{ $editName ?: __('Category name') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Affected tags warning -->
|
||||
@if ($editAffectedTagsCount > 0)
|
||||
<div class="rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-icon class="h-5 w-5 text-red-500" name="information-circle" />
|
||||
<span class="font-medium text-red-700">{{ __('Affected Tags') }}:</span>
|
||||
</div>
|
||||
<div class="text-red-600 font-semibold">{{ $editAffectedTagsCount }}</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
{{ __('Changes to parent or color will affect how tags in this category are displayed') }}.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Warning box -->
|
||||
<div class="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>{{ __('Important') }}:</strong>
|
||||
{{ __('Name changes only affect this translation') }}.
|
||||
{{ __('Parent and color changes affect all translations of this category') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation input -->
|
||||
<div class="w-1/2">
|
||||
<x-input
|
||||
label="{!! __('messages.confirm_input') !!}"
|
||||
placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalEditCategory', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3" wire:click.prevent="updateCategory" wire:loading.attr="disabled">
|
||||
{{ __('Update') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Create category modal -->
|
||||
@if ($modalCreateCategory)
|
||||
<x-jetstream.dialog-modal wire:key="modalCreateCategory" wire:model.live="modalCreateCategory" maxWidth="3xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Create New Category') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
<!-- Info box -->
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-500" name="information-circle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Please provide category names in all supported languages') }}.
|
||||
{{ __('All fields are required') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Translation fields -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-medium text-gray-700">{{ __('Category Names') }}</h3>
|
||||
|
||||
@php
|
||||
$appLocale = app()->getLocale();
|
||||
@endphp
|
||||
|
||||
@foreach ($this->supportedLocales as $index => $locale)
|
||||
@if($locale === $appLocale)
|
||||
<div x-data="{}"
|
||||
x-on:input.debounce.300ms="$dispatch('updatePreviewName', $event.target.value)"
|
||||
x-on:change="$dispatch('updatePreviewName', $event.target.value)">
|
||||
<x-input
|
||||
label="{{ __('messages.' . $locale) }}"
|
||||
placeholder="{{ __('Category name in') }} {{ __('messages.' . $locale) }}"
|
||||
wire:model.defer="createTranslations.{{ $locale }}" />
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
<x-input
|
||||
label="{{ __('messages.' . $locale) }}"
|
||||
placeholder="{{ __('Category name in') }} {{ __('messages.' . $locale) }}"
|
||||
wire:model.defer="createTranslations.{{ $locale }}" />
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Parent category selection -->
|
||||
<div>
|
||||
<x-select
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
label="{{ __('Parent Category') }} ({{ __('optional') }})"
|
||||
placeholder="{{ __('Select parent category') }}"
|
||||
:options="$this->parents"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model.live="createParentId" />
|
||||
</div>
|
||||
|
||||
<!-- Color picker - only shown if no parent selected -->
|
||||
@if (!$createParentId || $createParentId === 'none')
|
||||
<div>
|
||||
@livewire('categories.color-picker', [
|
||||
'color' => $createColor,
|
||||
'label' => __('Color'),
|
||||
'required' => true
|
||||
])
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Color will be inherited from parent category') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Large color preview (always visible) -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
@php $appLocale = app()->getLocale(); @endphp
|
||||
<span class="bg-{{ $this->createPreviewColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black">
|
||||
{{ $createTranslations[$appLocale] ?? __('Category name') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation input -->
|
||||
<div>
|
||||
<x-input
|
||||
label="{!! __('messages.confirm_input') !!}"
|
||||
placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalCreateCategory', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="storeCategory" wire:loading.attr="disabled">
|
||||
{{ __('Create Category') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('scroll-to-top', event => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div>
|
||||
<x-select
|
||||
label="{{ __('Category') }} * "
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
:options="$categoryOptions"
|
||||
option-label="name"
|
||||
option-value="category_id"
|
||||
wire:model.live="categorySelected"
|
||||
/>
|
||||
@error('categoryId')
|
||||
<div class="mt-2 text-sm text-red-600" id="category-error">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,169 @@
|
||||
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="p-6 sm:p-8">
|
||||
|
||||
{{-- Success Message --}}
|
||||
@if($showSuccessMessage)
|
||||
<div class="mb-6 rounded-md bg-green-50 border border-green-200 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="text-sm text-green-800">
|
||||
<p class="font-medium">{{ __('Message sent') }}</p>
|
||||
<p class="mt-1">{{ __('We received your message successfully and will get back to you shortly!') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto pl-3">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('showSuccessMessage', false)"
|
||||
class="inline-flex rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none transition">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Contact Form --}}
|
||||
<form wire:submit.prevent="submitForm">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-4 space-y-6">
|
||||
|
||||
{{-- Name Field (only shown when not authenticated) --}}
|
||||
@guest
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Name') }}
|
||||
</label>
|
||||
<x-input
|
||||
wire:model="name"
|
||||
type="text"
|
||||
placeholder="{{ __('Your full name') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- Email Field (only shown when not authenticated) --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Email') }}
|
||||
</label>
|
||||
<x-input
|
||||
wire:model="email"
|
||||
type="email"
|
||||
placeholder="{{ __('your.email@example.com') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@endguest
|
||||
|
||||
{{-- Subject Field (optional, shown for certain contexts) --}}
|
||||
@if(in_array($context, ['contact', 'report-issue']))
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Subject') }}
|
||||
</label>
|
||||
<x-input
|
||||
wire:model="subject"
|
||||
type="text"
|
||||
placeholder="{{ __('Brief description of your message') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- URL Field (for error reporting only) --}}
|
||||
@if($context === 'report-error')
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Page URL') }}
|
||||
</label>
|
||||
<x-input
|
||||
wire:model="url"
|
||||
type="url"
|
||||
placeholder="{{ __('URL where the error occurred') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
{{ __('Copy and paste the web address from your browser.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Message Field --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Message') }}
|
||||
</label>
|
||||
<x-textarea
|
||||
wire:model="message"
|
||||
rows="6"
|
||||
placeholder="{{ __('Please provide as much detail as possible...') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
{{ __('Minimum 10 characters') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Context-specific info boxes --}}
|
||||
@if($context === 'delete-profile')
|
||||
<div class="p-3 bg-yellow-50 rounded-md border border-yellow-200">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<strong>{{ __('Important') }}:</strong> {{ __('Profile deletion is permanent and cannot be undone. All your data will be removed from our system.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($context === 'report-error')
|
||||
<div class="p-3 bg-gray-50 rounded-md border border-gray-300">
|
||||
<p class="text-sm text-gray-700">
|
||||
<strong>{{ __('Tip') }}:</strong> {{ __('Include details like what you were trying to do, what happened, and any error messages you saw.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Submit Button --}}
|
||||
<div class="text-right mt-6">
|
||||
<x-button
|
||||
type="submit"
|
||||
primary
|
||||
class="bg-theme-brand hover:bg-opacity-80"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="submitForm"
|
||||
>
|
||||
<span wire:loading.remove wire:target="submitForm">
|
||||
{{ $this->submitButtonText }}
|
||||
</span>
|
||||
<span wire:loading wire:target="submitForm" class="flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" 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>
|
||||
{{ __('Sending...') }}
|
||||
</span>
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,315 @@
|
||||
<div class="my-4">
|
||||
|
||||
<!-- Search/Filter Section -->
|
||||
<form wire:submit.prevent="applySearch" class="mb-6">
|
||||
<div class="flex flex-col md:flex-row md:space-x-12">
|
||||
<div class="w-full md:w-2/4 md:flex-none space-y-4">
|
||||
<div>
|
||||
<x-jetstream.label for="search" value="{{ __('Keywords') }}" />
|
||||
<x-jetstream.input :clearable="true" class="text-theme-primary placeholder-theme-light text-sm"
|
||||
placeholder="{{ __('Search by name or location') }}" right-icon="search"
|
||||
wire:model="searchInput" />
|
||||
@error('search')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-jetstream.label for="filterType" value="{{ __('Select by interaction') }}" />
|
||||
<x-select wire:model="filterTypeInput" multiselect placeholder="{{ __('All interactions') }}">
|
||||
<x-select.option label="{{ __('Stars') }}" value="stars" />
|
||||
<x-select.option label="{{ __('Bookmarks') }}" value="bookmarks" />
|
||||
<x-select.option label="{{ __('Transactions') }}" value="transactions" />
|
||||
<x-select.option label="{{ __('Conversations') }}" value="conversations" />
|
||||
@if($showRoleColumn)
|
||||
<x-select.option label="{{ __('Linked members') }}" value="linked" />
|
||||
@endif
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
@if($showRoleColumn && count($availableRoles) > 0)
|
||||
<div>
|
||||
<x-jetstream.label for="filterRoleInput" value="{{ __(ucfirst(strtolower(getActiveProfileType())) . ' role') }}" />
|
||||
<x-select wire:model="filterRoleInput" :clearable="true" placeholder="{{ __('Select role') }}">
|
||||
<x-select.option label="{{ __('Any role') }}" value="any" />
|
||||
@foreach($availableRoles as $roleKey => $roleLabel)
|
||||
<x-select.option label="{{ $roleLabel }}" value="{{ $roleKey }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 mb-6 md:mb-12 lg:mb-18">
|
||||
<x-jetstream.secondary-button class="my-3" type="submit" wire:loading.attr="disabled" wire:target="applySearch">
|
||||
<span wire:loading.remove wire:target="applySearch">{{ __('Search') }}</span>
|
||||
<span wire:loading wire:target="applySearch">{{ __('Searching...') }}</span>
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.secondary-button class="my-3 ml-4" wire:click.prevent="resetSearch" wire:loading.attr="disabled" wire:target="resetSearch">
|
||||
{{ __('Clear all') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- General error section -->
|
||||
@if (session('error'))
|
||||
<div class="relative mt-6 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700" role="alert">
|
||||
<strong class="font-bold">{{ __('Error') }}!</strong>
|
||||
<span class="block sm:inline">{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Results table -->
|
||||
<div class="relative mb-6 mt-6">
|
||||
<div wire:loading>
|
||||
<span> {{ __('Loading...') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-sm rounded-lg" x-data="{
|
||||
initScrollSync() {
|
||||
if (this.$refs.topScroll && this.$refs.bottomScroll && this.$refs.table && this.$refs.topScrollContent) {
|
||||
this.$refs.topScrollContent.style.width = this.$refs.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" id="contacts" x-ref="table">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 tracking-wider align-middle">
|
||||
<button wire:click="sortBy('name')" class="uppercase flex items-center hover:text-gray-700 focus:outline-none">
|
||||
{{ __('Contact') }}
|
||||
@if($sortField === 'name')
|
||||
<x-icon class="ml-1 h-4 w-4" name="{{ $sortAsc ? 'chevron-up' : 'chevron-down' }}" />
|
||||
@endif
|
||||
</button>
|
||||
</th>
|
||||
@if($showRoleColumn)
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 tracking-wider align-middle">
|
||||
<button wire:click="sortBy('org_role')" class="uppercase flex items-center hover:text-gray-700 focus:outline-none">
|
||||
{{ __('Role') }}
|
||||
@if($sortField === 'org_role')
|
||||
<x-icon class="ml-1 h-4 w-4" name="{{ $sortAsc ? 'chevron-up' : 'chevron-down' }}" />
|
||||
@endif
|
||||
</button>
|
||||
</th>
|
||||
@endif
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 tracking-wider align-middle">
|
||||
<button wire:click="sortBy('has_star')" class="uppercase flex items-center hover:text-gray-700 focus:outline-none">
|
||||
{{ __('Saved') }}
|
||||
@if($sortField === 'has_star')
|
||||
<x-icon class="ml-1 h-4 w-4" name="{{ $sortAsc ? 'chevron-up' : 'chevron-down' }}" />
|
||||
@endif
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 tracking-wider align-middle">
|
||||
<button wire:click="sortBy('transaction_count')" class="uppercase flex items-center hover:text-gray-700 focus:outline-none">
|
||||
{{ __('Exchanges') }}
|
||||
@if($sortField === 'transaction_count')
|
||||
<x-icon class="ml-1 h-4 w-4" name="{{ $sortAsc ? 'chevron-up' : 'chevron-down' }}" />
|
||||
@endif
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-right text-xs font-medium text-gray-500 tracking-wider align-middle hidden xl:table-cell">
|
||||
<button wire:click="sortBy('last_interaction')" class="uppercase flex items-center justify-end ml-auto hover:text-gray-700 focus:outline-none">
|
||||
{{ __('Last interaction') }}
|
||||
@if($sortField === 'last_interaction')
|
||||
<x-icon class="ml-1 h-4 w-4" name="{{ $sortAsc ? 'chevron-up' : 'chevron-down' }}" />
|
||||
@endif
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@if ($contacts && $contacts->count() > 0)
|
||||
@foreach ($contacts as $contact)
|
||||
<tr onclick="window.location='{{ url($contact['profile_path']) }}'"
|
||||
class="cursor-pointer hover:bg-gray-50">
|
||||
<td class="px-3 py-4 text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<img alt="profile"
|
||||
class="h-10 w-10 md:h-12 md:w-12 lg:h-16 lg:w-16 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600"
|
||||
src="{{ Storage::url($contact['profile_photo']) }}" />
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 truncate">
|
||||
{{ $contact['name'] }}
|
||||
</div>
|
||||
@if ($contact['full_name'] != $contact['name'])
|
||||
<div class="text-xs text-gray-500 font-normal truncate">
|
||||
{{ $contact['full_name'] }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-xs text-gray-500 font-normal truncate">
|
||||
{{ $contact['location'] }}
|
||||
</div>
|
||||
@if($contact['profile_type_name'] === 'Organization')
|
||||
<div class="text-xs font-normal mt-1">
|
||||
<span class="bg-theme-brand px-1 text-gray-100">
|
||||
{{ __('Organization') }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif($contact['profile_type_name'] === 'Bank')
|
||||
<div class="text-xs font-normal mt-1">
|
||||
<span class="bg-theme-brand px-1 text-gray-100">
|
||||
{{ __('Bank') }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif($contact['profile_type_name'] === 'Admin')
|
||||
<div class="text-xs font-normal mt-1">
|
||||
<span class="bg-theme-brand px-1 text-gray-100">
|
||||
{{ __('Admin') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@if($showRoleColumn)
|
||||
<td class="px-3 py-4 text-sm text-gray-500" onclick="event.stopPropagation()">
|
||||
@if(!empty($contact['org_role']))
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
|
||||
{{ $roleLabels[$contact['org_role']] ?? $contact['org_role'] }}
|
||||
@if($authIsManager && str_ends_with($contact['org_role'], '-coordinator'))
|
||||
<button type="button"
|
||||
wire:click="confirmRemoveCoordinator({{ (int) $contact['profile_id'] }})"
|
||||
class="ml-0.5 text-gray-400 hover:text-red-600 focus:outline-none"
|
||||
title="{{ __('Remove coordinator') }}">
|
||||
<x-icon name="x-circle" class="w-3.5 h-3.5" mini />
|
||||
</button>
|
||||
@endif
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-300">—</span>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
<div class="flex justify-start items-center space-x-2">
|
||||
@if ($contact['has_star'])
|
||||
<x-icon class="h-5 w-5 text-yellow-500" name="star" solid
|
||||
title="{{ __('Has stars') }}" />
|
||||
@endif
|
||||
@if ($contact['has_bookmark'])
|
||||
<x-icon class="h-5 w-5 text-yellow-500" name="bookmark" solid
|
||||
title="{{ __('Has bookmarks') }}" />
|
||||
@endif
|
||||
@if (!$contact['has_star'] && !$contact['has_bookmark'])
|
||||
<span class="text-gray-300">—</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
<div class="flex justify-start items-center space-x-3 text-xs">
|
||||
@if ($contact['transaction_count'] > 0)
|
||||
<span class="text-theme-primary font-medium">
|
||||
<x-icon class="h-4 w-4 inline text-theme-primary" name="clock" />
|
||||
{{ $contact['transaction_count'] }}
|
||||
</span>
|
||||
@endif
|
||||
@if ($contact['message_count'] > 0)
|
||||
<span class="text-theme-primary font-medium">
|
||||
<x-icon class="h-4 w-4 inline text-theme-primary" name="chat-bubble-left-right" />
|
||||
{{ $contact['message_count'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-right text-sm text-gray-500 hidden xl:table-cell">
|
||||
@if($contact['last_interaction'])
|
||||
<div class="leading-tight">
|
||||
<div>{{ \Carbon\Carbon::parse($contact['last_interaction'])->translatedFormat('d M') }}</div>
|
||||
<div>{{ \Carbon\Carbon::parse($contact['last_interaction'])->translatedFormat('Y') }}</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-300">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="{{ $showRoleColumn ? 5 : 4 }}" class="px-6 py-12 text-center text-gray-500">
|
||||
<span wire:loading>
|
||||
{{ __('One moment, collecting all your contacts...') }}
|
||||
</span>
|
||||
<span wire:loading.remove>
|
||||
{{ __('No contacts found') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- end overflow-x-auto bottom scroll -->
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($contacts && $contacts->hasPages())
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $contacts->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Results count and per-page selector -->
|
||||
@if ($contacts && $contacts->total() > 0)
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<select class="w-20 cursor-pointer rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring focus:ring-primary-500 sm:text-sm"
|
||||
wire:model.live="perPage">
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span class="ml-2 text-sm text-gray-500">{{ __('per page') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ trans_choice('messages.contacts_found', $contacts->total(), ['count' => $contacts->total()]) }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Remove coordinator confirmation modal -->
|
||||
<x-jetstream.dialog-modal wire:model="confirmingRemoveCoordinator">
|
||||
<x-slot name="title">
|
||||
{{ __('Remove coordinator') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
{{ __('Are you sure you want to remove :name as a coordinator?', ['name' => $removingProfileName]) }}
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('confirmingRemoveCoordinator', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click="removeCoordinator" wire:loading.attr="disabled" wire:target="removeCoordinator">
|
||||
<span wire:loading.remove wire:target="removeCoordinator">{{ __('Remove') }}</span>
|
||||
<span wire:loading wire:target="removeCoordinator">{{ __('Removing...') }}</span>
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
@push('scripts')
|
||||
{{-- Scroll to top when clicking paginator --}}
|
||||
<script>
|
||||
document.addEventListener('scroll-to-top', event => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div>
|
||||
@if($value)
|
||||
<x-icons.check-circle class="text-green-600 mx-auto" />
|
||||
@else
|
||||
<x-icons.x-circle class="text-red-300 mx-auto" />
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="flex justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model.live="selected"
|
||||
value="{{ $value }}"
|
||||
@if (in_array($value, $this->pinnedRecords)) checked @endif
|
||||
class="w-4 h-4 mt-1 text-black form-checkbox transition duration-150 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="space-y-4">
|
||||
@foreach($rules as $index => $rule)
|
||||
@php $key = $parentIndex !== null ? $parentIndex . '.' . $index : $index; @endphp
|
||||
<div wire:key="{{ $key }}">
|
||||
@if($rule['type'] === 'rule')
|
||||
@include('datatables::complex-query-rule', ['parentIndex' => $key, 'rule' => $rule])
|
||||
@elseif($rule['type'] === 'group')
|
||||
<div x-data="{
|
||||
key: '{{ collect(explode('.', $key))->join(".content.") . ".content" }}',
|
||||
source: () => document.querySelector('[dragging]'),
|
||||
dragstart: (e, key) => {
|
||||
e.target.setAttribute('dragging', key)
|
||||
e.target.classList.add('bg-opacity-20', 'bg-white')
|
||||
},
|
||||
dragend: (e) => {
|
||||
e.target.removeAttribute('dragging')
|
||||
e.target.classList.remove('bg-opacity-20', 'bg-white')
|
||||
},
|
||||
dragenter(e) {
|
||||
if (e.target.closest('[drag-target]') !== this.source().closest('[drag-target]')) {
|
||||
this.$refs.placeholder.appendChild(this.source())
|
||||
}
|
||||
},
|
||||
drop(e) {
|
||||
$wire.call('moveRule', this.source().getAttribute('dragging'), this.key)
|
||||
},
|
||||
}" drag-target
|
||||
x-on:dragenter.prevent="dragenter"
|
||||
x-on:dragleave.prevent
|
||||
x-on:dragover.prevent
|
||||
x-on:drop="drop"
|
||||
class="p-4 space-y-4 bg-blue-500 bg-opacity-10 rounded-lg text-gray-{{ strlen($parentIndex) > 6 ? 1 : 9 }}00 border border-blue-400"
|
||||
>
|
||||
<span class="flex justify-center space-x-4">
|
||||
<button wire:click="addRule('{{ collect(explode('.', $key))->join(".content.") . ".content" }}')" class="flex items-center space-x-2 px-3 py-2 border border-blue-400 rounded-md bg-white text-blue-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-blue-200 focus:outline-none">ADD RULE</button>
|
||||
<button wire:click="addGroup('{{ collect(explode('.', $key))->join(".content.") . ".content" }}')" class="flex items-center space-x-2 px-3 py-2 border border-blue-400 rounded-md bg-white text-blue-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-blue-200 focus:outline-none">ADD GROUP</button>
|
||||
</span>
|
||||
<div class="block sm:flex items-center">
|
||||
<div class="flex justify-center sm:block">
|
||||
@if(count($rule['content']) > 1)
|
||||
<div class="mr-8">
|
||||
<label class="block uppercase tracking-wide text-xs font-bold py-1 rounded flex justify-between">Logic</label>
|
||||
<select
|
||||
wire:model.live="rules.{{ collect(explode('.', $key))->join(".content.") }}.logic"
|
||||
class="w-24 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
>
|
||||
<option value="and">AND</option>
|
||||
<option value="or">OR</option>
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div x-ref="placeholder" class="flex-grow space-y-4">
|
||||
<div>
|
||||
@include('datatables::complex-query-group', [
|
||||
'parentIndex' => $key,
|
||||
'rules' => $rule['content'],
|
||||
'logic' => $rule['logic']
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-end">
|
||||
@unless($key === 0)
|
||||
<button wire:click="removeRule('{{ collect(explode('.', $key))->join(".content.") . ".content" }}')" class="px-3 py-2 rounded bg-red-600 text-white"><x-icons.trash /></button>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
<div class="w-full">
|
||||
@php $key = collect(explode('.', $parentIndex))->join(".content.") . ".content" @endphp
|
||||
<div
|
||||
draggable="true"
|
||||
x-on:dragstart="dragstart($event, '{{ $key }}')"
|
||||
x-on:dragend="dragend"
|
||||
key="{{ $key }}"
|
||||
class="px-3 py-2 -my-1 sm:flex space-x-4 items-end hover:bg-opacity-20 hover:bg-white hover:shadow-xl"
|
||||
>
|
||||
<div class="sm:flex flex-grow sm:space-x-4">
|
||||
<div class="sm:w-1/3">
|
||||
<label
|
||||
class="block uppercase tracking-wide text-xs font-bold py-1 rounded flex justify-between">Column</label>
|
||||
<div class="relative">
|
||||
<select wire:model.live="rules.{{ $key }}.column" name="selectedColumn"
|
||||
class="w-full my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option value=""></option>
|
||||
@foreach ($columns as $i => $column)
|
||||
<option value="{{ $i }}">{{ Str::ucfirst($column['label']) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($options = $this->getOperands($key))
|
||||
<div class="sm:w-1/3">
|
||||
<label
|
||||
class="block uppercase tracking-wide text-xs font-bold py-1 rounded flex justify-between">Operand</label>
|
||||
<div class="relative">
|
||||
<select name="operand" wire:model.live="rules.{{ $key }}.operand"
|
||||
class="w-full my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option selected></option>
|
||||
@foreach ($options as $operand)
|
||||
<option value="{{ $operand }}">{{ $operand }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!in_array($rule['content']['operand'], ['is empty', 'is not empty']))
|
||||
<div class="sm:w-1/3">
|
||||
@if ($column = $this->getRuleColumn($key))
|
||||
<label
|
||||
class="block uppercase tracking-wide text-xs font-bold py-1 rounded flex justify-between">Value</label>
|
||||
<div class="relative">
|
||||
@if (is_array($column['filterable']))
|
||||
<select name="value" wire:model.live="rules.{{ $key }}.value"
|
||||
class="w-full my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option selected></option>
|
||||
@foreach ($column['filterable'] as $value => $label)
|
||||
@if (is_object($label))
|
||||
<option value="{{ $label->id }}">{{ $label->name }}</option>
|
||||
@elseif(is_array($label))
|
||||
<option value="{{ $label['id'] }}">{{ $label['name'] }}</option>
|
||||
@elseif(is_numeric($value))
|
||||
<option value="{{ $label }}">{{ $label }}</option>
|
||||
@else
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
@elseif($column['type'] === 'boolean')
|
||||
<select name="value" wire:model.live="rules.{{ $key }}.value"
|
||||
class="w-full my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option selected></option>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
@elseif($column['type'] === 'date')
|
||||
<input type="date" name="value" wire:model.blur="rules.{{ $key }}.value"
|
||||
class="w-full px-3 py-2 border my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" />
|
||||
@elseif($column['type'] === 'time')
|
||||
<input type="time" name="value" wire:model.blur="rules.{{ $key }}.value"
|
||||
class="w-full px-3 py-2 border my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" />
|
||||
@else
|
||||
<input name="value" wire:model.blur="rules.{{ $key }}.value"
|
||||
class="w-full px-3 py-2 border my-1 text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex justify-center sm:justify-end">
|
||||
<button wire:click="duplicateRule('{{ $key }}')"
|
||||
class="mb-px w-9 h-9 flex items-center justify-center rounded text-green-600 hover:text-green-400">
|
||||
<x-icons.copy />
|
||||
</button>
|
||||
<button wire:click="removeRule('{{ $key }}')"
|
||||
class="mb-px w-9 h-9 flex items-center justify-center rounded text-red-600 hover:text-red-400">
|
||||
<x-icons.trash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
<div x-data="{
|
||||
rules: @if($persistKey) $persist('').as('{{ $persistKey }}') @else '' @endif,
|
||||
init() {
|
||||
Livewire.on('complexQuery', rules => this.rules = rules)
|
||||
if (this.rules && this.rules !== '') {
|
||||
$wire.set('rules', this.rules)
|
||||
$wire.runQuery()
|
||||
}
|
||||
}
|
||||
}" class=""
|
||||
>
|
||||
<div class="my-4 flex justify-between text-xl uppercase tracking-wide font-medium leading-none">
|
||||
<span>Query Builder</span>
|
||||
<span>@if($errors->any())
|
||||
<div class="text-red-500">You have missing values in your rules</div>
|
||||
@endif</span>
|
||||
</div>
|
||||
|
||||
@if(count($this->rules[0]['content']))
|
||||
<div class="my-4 px-4 py-2 bg-gray-500 whitespace-pre-wrap @if($errors->any())text-red-200 @else text-green-100 @endif rounded">{{ $this->rulesString }}@if($errors->any()) Invalid rules @endif</div>
|
||||
@endif
|
||||
|
||||
<div>@include('datatables::complex-query-group', ['rules' => $rules, 'parentIndex' => null])</div>
|
||||
|
||||
@if(count($this->rules[0]['content']))
|
||||
@unless($errors->any())
|
||||
<div class="pt-2 sm:flex w-full justify-between">
|
||||
<div>
|
||||
{{-- <button class="bg-blue-500 px-3 py-2 rounded text-white" wire:click="runQuery">Apply Query</button> --}}
|
||||
</div>
|
||||
<div class="mt-2 sm:mt-0 sm:flex sm:space-x-2">
|
||||
@isset($savedQueries)
|
||||
<div class="flex items-center space-x-2" x-data="{
|
||||
name: null,
|
||||
saveQuery() {
|
||||
$wire.call('saveQuery', this.name)
|
||||
this.name = null
|
||||
}
|
||||
}">
|
||||
<input x-model="name" wire:loading.attr="disabled" x-on:keydown.enter="saveQuery" placeholder="save as..." class="flex-grow px-3 py-3 border text-sm text-theme-primary leading-4 block rounded-md border-theme-border shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" />
|
||||
<button x-bind:disabled="! name" x-show="rules" x-on:click="saveQuery" class="flex items-center space-x-2 px-3 py-0.5 border border-green-400 disabled:border-theme-border rounded-md bg-white text-green-500 disabled:text-theme-muted text-xs leading-4 font-medium uppercase tracking-wider hover:bg-green-200 disabled:hover:bg-white focus:outline-none disabled:pointer-events-none">
|
||||
<span>{{ __('Save') }}</span>
|
||||
<span wire:loading.remove><x-icons.check-circle class="m-2" /></span>
|
||||
<span wire:loading><x-icons.cog class="animate-spin m-2" /></span>
|
||||
</button>
|
||||
</div>
|
||||
@endisset
|
||||
<button x-show="rules" wire:click="resetQuery" class="flex items-center space-x-2 px-3 border border-red-400 rounded-md bg-white text-red-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-red-200 focus:outline-none">
|
||||
<span>{{ __('Reset') }}</span>
|
||||
<x-icons.x-circle class="m-2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
@if(count($savedQueries ?? []))
|
||||
<div>
|
||||
<div class="mt-8 my-4 text-xl uppercase tracking-wide font-medium leading-none">Saved Queries</div>
|
||||
<div class="grid md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-2">
|
||||
@foreach($savedQueries as $saved)
|
||||
<div class="flex" wire:key="{{ $saved['id'] }}">
|
||||
<button wire:click="loadRules({{ json_encode($saved['rules']) }})" wire:loading.attr="disabled" class="p-2 flex-grow flex items-center space-x-2 px-3 border border-r-0 border-blue-400 rounded-md rounded-r-none bg-white text-blue-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-blue-200 focus:outline-none">{{ $saved['name'] }}</button>
|
||||
<button wire:click="deleteRules({{ $saved['id'] }})" wire:loading.attr="disabled" class="p-2 flex items-center space-x-2 px-3 border border-red-400 rounded-md rounded-l-none bg-white text-red-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-red-200 focus:outline-none">
|
||||
<x-icons.x-circle wire:loading.remove />
|
||||
<x-icons.cog wire:loading class="h-6 w-6 animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,249 @@
|
||||
<div my-6>
|
||||
@if($beforeTableSlot)
|
||||
<div class="mt-8">
|
||||
@include($beforeTableSlot)
|
||||
</div>
|
||||
@endif
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div class="flex items-center h-10">
|
||||
@if($this->searchableColumns()->count())
|
||||
<div class="flex rounded-lg w-96 shadow-sm">
|
||||
<div class="relative flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<svg class="w-5 h-5 text-theme-muted" viewBox="0 0 20 20" stroke="currentColor" fill="none">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input wire:model.live.debounce.500ms="search" class="block w-full py-3 pl-10 text-sm border-theme-border leading-4 rounded-md shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 focus:outline-none" placeholder="{{__('Search in')}} {{ $this->searchableColumns()->map->label->join(', ') }}" type="text" />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<button wire:click="$set('search', null)" class="text-theme-muted hover:text-red-600 focus:outline-none">
|
||||
<x-icons.x-circle class="w-5 h-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($this->activeFilters)
|
||||
<span class="text-xl text-theme-muted uppercase">{{ __('Fileters Active') }}</span>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap items-center space-x-1">
|
||||
<x-icons.cog wire:loading class="text-theme-muted h-9 w-9 animate-spin" />
|
||||
|
||||
@if($this->activeFilters)
|
||||
<button wire:click="clearAllFilters" class="flex items-center px-3 text-xs font-medium tracking-wider text-red-500 uppercase bg-white border border-red-400 space-x-2 rounded-md leading-4 hover:bg-red-200 focus:outline-none"><span>{{ __('Reset') }}</span>
|
||||
<x-icons.x-circle class="m-2" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if(count($this->massActionsOptions))
|
||||
<div class="flex items-center justify-center space-x-1">
|
||||
<label for="datatables_mass_actions">{{ __('With selected') }}:</label>
|
||||
<select wire:model.live="massActionOption" class="px-3 text-xs font-medium tracking-wider uppercase bg-white border border-green-400 space-x-2 rounded-md leading-4 focus:outline-none" id="datatables_mass_actions">
|
||||
<option value="">{{ __('Choose...') }}</option>
|
||||
@foreach($this->massActionsOptions as $group => $items)
|
||||
@if(!$group)
|
||||
@foreach($items as $item)
|
||||
<option value="{{$item['value']}}">{{$item['label']}}</option>
|
||||
@endforeach
|
||||
@else
|
||||
<optgroup label="{{$group}}">
|
||||
@foreach($items as $item)
|
||||
<option value="{{$item['value']}}">{{$item['label']}}</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<button
|
||||
wire:click="massActionOptionHandler"
|
||||
class="flex items-center px-4 py-2 text-xs font-medium tracking-wider text-green-500 uppercase bg-white border border-green-400 rounded-md leading-4 hover:bg-green-200 focus:outline-none" type="submit" title="Submit"
|
||||
>Go</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($exportable)
|
||||
<div x-data="{ init() {
|
||||
window.livewire.on('startDownload', link => window.open(link, '_blank'))
|
||||
} }" x-init="init">
|
||||
<button wire:click="export" class="flex items-center px-3 text-xs font-medium tracking-wider text-theme-secondary uppercase bg-white border border-theme-secondary space-x-2 rounded-md leading-4 hover:text-theme-muted focus:outline-none"><span>{{ __('Export') }}</span>
|
||||
<x-icons.excel class="m-2" /></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($hideable === 'select')
|
||||
@include('datatables::hide-column-multiselect')
|
||||
@endif
|
||||
|
||||
@foreach ($columnGroups as $name => $group)
|
||||
<button wire:click="toggleGroup('{{ $name }}')"
|
||||
class="px-3 py-2 text-xs font-medium tracking-wider text-green-500 uppercase bg-white border border-green-400 rounded-md leading-4 hover:bg-green-200 focus:outline-none">
|
||||
<span class="flex items-center h-5">{{ isset($this->groupLabels[$name]) ? __($this->groupLabels[$name]) : __('Toggle :group', ['group' => $name]) }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($hideable === 'buttons')
|
||||
<div class="p-2 grid grid-cols-8 gap-2">
|
||||
@foreach($this->columns as $index => $column)
|
||||
@if ($column['hideable'])
|
||||
<button wire:click="toggle('{{ $index }}')" class="px-3 py-2 rounded text-white text-xs focus:outline-none
|
||||
{{ $column['hidden'] ? 'bg-blue-100 hover:bg-blue-300 text-black' : 'bg-blue-500 hover:bg-blue-800' }}">
|
||||
{{ $column['label'] }}
|
||||
</button>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div wire:loading.class="opacity-50" class="rounded-sm @unless($complex || $this->hidePagination) rounded-sm @endunless shadow-sm bg-white max-w-screen overflow-x-scroll @if($this->activeFilters) border-blue-500 @else border-gray-100 border-b-2 rounded-sm @endif @if($complex) @endif">
|
||||
<div>
|
||||
<div class="table min-w-full align-middle">
|
||||
@unless($this->hideHeader)
|
||||
<div class="table-row divide-x divide-gray-200 bg-theme-primary">
|
||||
@foreach($this->columns as $index => $column)
|
||||
@if($hideable === 'inline')
|
||||
@include('datatables::header-inline-hide', ['column' => $column, 'sort' => $sort])
|
||||
@elseif($column['type'] === 'checkbox')
|
||||
@unless($column['hidden'])
|
||||
<div class="flex justify-center table-cell w-32 h-12 px-6 py-4 overflow-hidden text-xs font-medium tracking-wider text-left text-theme-primary uppercase align-top border-gray-100 bg-gray-50 leading-4 focus:outline-none">
|
||||
<div class="px-3 py-1 rounded @if(count($selected)) bg-orange-400 @else bg-theme-primary text-white @endif text-white text-center">
|
||||
{{ count($selected) }}
|
||||
</div>
|
||||
</div>
|
||||
@endunless
|
||||
@else
|
||||
@include('datatables::header-no-hide', ['column' => $column, 'sort' => $sort])
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endunless
|
||||
<div class="table-row bg-blue-100 divide-x divide-blue-200">
|
||||
@foreach($this->columns as $index => $column)
|
||||
@if($column['hidden'])
|
||||
@if($hideable === 'inline')
|
||||
<div class="table-cell w-5 overflow-hidden align-top bg-blue-100"></div>
|
||||
@endif
|
||||
@elseif($column['type'] === 'checkbox')
|
||||
@include('datatables::filters.checkbox')
|
||||
@elseif($column['type'] === 'label')
|
||||
<div class="table-cell overflow-hidden align-top">
|
||||
{{ $column['label'] ?? '' }}
|
||||
</div>
|
||||
@else
|
||||
<div class="table-cell overflow-hidden align-top">
|
||||
@isset($column['filterable'])
|
||||
@if( is_iterable($column['filterable']) )
|
||||
<div wire:key="{{ $index }}">
|
||||
@include('datatables::filters.select', ['index' => $index, 'name' => $column['label'], 'options' => $column['filterable']])
|
||||
</div>
|
||||
@else
|
||||
<div wire:key="{{ $index }}">
|
||||
@include('datatables::filters.' . ($column['filterView'] ?? $column['type']), ['index' => $index, 'name' => $column['label']])
|
||||
</div>
|
||||
@endif
|
||||
@endisset
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@forelse($this->results as $row)
|
||||
<div class="table-row p-1 {{ $this->rowClasses($row, $loop) }}">
|
||||
@foreach($this->columns as $column)
|
||||
@if($column['hidden'])
|
||||
@if($hideable === 'inline')
|
||||
<div class="table-cell w-5 overflow-hidden align-top"></div>
|
||||
@endif
|
||||
@elseif($column['type'] === 'checkbox')
|
||||
@include('datatables::checkbox', ['value' => $row->checkbox_attribute])
|
||||
@elseif($column['type'] === 'label')
|
||||
@include('datatables::label')
|
||||
@else
|
||||
<div class="table-cell px-6 py-2 whitespace-no-wrap @if($column['align'] === 'right') text-right @elseif($column['align'] === 'center') text-center @else text-left @endif {{ $this->cellClasses($row, $column) }}">
|
||||
@if(($column['type'] ?? '') === 'html' || ($column['allow_html'] ?? false))
|
||||
{{-- XSS WARNING: HTML rendering allowed for this column. Ensure data is sanitized! --}}
|
||||
{!! $row->{$column['name']} !!}
|
||||
@else
|
||||
{{-- Default: Escape output for XSS protection --}}
|
||||
{{ $row->{$column['name']} }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@empty
|
||||
<p class="p-3 text-lg text-theme-primary">
|
||||
{{ __("There's Nothing to show at the moment") }}
|
||||
</p>
|
||||
@endforelse
|
||||
|
||||
@if ($this->hasSummaryRow())
|
||||
<div class="table-row p-1">
|
||||
@foreach($this->columns as $column)
|
||||
@unless($column['hidden'])
|
||||
@if ($column['summary'])
|
||||
<div class="table-cell px-6 py-2 whitespace-no-wrap @if($column['align'] === 'right') text-right @elseif($column['align'] === 'center') text-center @else text-left @endif {{ $this->cellClasses($row, $column) }}">
|
||||
{{ $this->summarize($column['name']) }}
|
||||
</div>
|
||||
@else
|
||||
<div class="table-cell"></div>
|
||||
@endif
|
||||
@endunless
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@unless($this->hidePagination)
|
||||
<div class="max-w-screen bg-white @unless($complex) @endunless border-2 @if($this->activeFilters) border-blue-500 @else border-transparent @endif">
|
||||
<div class="items-center justify-between p-2 sm:flex">
|
||||
{{-- check if there is any data --}}
|
||||
@if(count($this->results))
|
||||
<div class="flex items-center my-2 sm:my-0">
|
||||
<select name="perPage" class="block w-full py-2 pl-3 pr-10 mt-1 text-base border-theme-border form-select leading-6 focus:outline-none focus:ring focus:ring-primary-200 focus:ring-opacity-50 focus:border-primary-300 sm:text-sm sm:leading-5" wire:model.live="perPage">
|
||||
@foreach(config('livewire-datatables.per_page_options', [ 10, 25, 50, 100 ]) as $per_page_option)
|
||||
<option value="{{ $per_page_option }}">{{ $per_page_option }}</option>
|
||||
@endforeach
|
||||
<option value="99999999">{{__('All')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="my-4 sm:my-0">
|
||||
<div class="lg:hidden">
|
||||
<span class="space-x-2">{{ $this->results->links('datatables::tailwind-simple-pagination') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="justify-center hidden lg:flex">
|
||||
<span>{{ $this->results->links('datatables::tailwind-pagination') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-theme-secondary">
|
||||
{{__('Results')}} {{ $this->results->firstItem() }} - {{ $this->results->lastItem() }} {{__('of')}}
|
||||
{{ $this->results->total() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($complex)
|
||||
<div class="bg-gray-50 px-4 py-4 rounded-b-lg rounded-t-none shadow-sm border-4 @if($this->activeFilters) border-blue-500 @else border-transparent @endif @if($complex) border-t-0 @endif">
|
||||
<livewire:complex-query :columns="$this->complexColumns" :persistKey="$this->persistKey" :savedQueries="method_exists($this, 'getSavedQueries') ? $this->getSavedQueries() : null" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($afterTableSlot)
|
||||
<div class="mt-8">
|
||||
@include($afterTableSlot)
|
||||
</div>
|
||||
@endif
|
||||
<span class="hidden text-sm text-left text-center text-right text-theme-primary bg-gray-100 bg-yellow-100 leading-5 bg-gray-50"></span>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div x-data="{ open: {{ isset($open) && $open ? 'true' : 'false' }}, working: false }" x-cloak wire:key="delete-{{ $value }}">
|
||||
<span x-on:click="open = true">
|
||||
<button class="p-1 text-red-600 rounded hover:bg-red-600 hover:text-white"><x-icons.trash /></button>
|
||||
</span>
|
||||
|
||||
<div x-show="open"
|
||||
class="fixed z-50 bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
|
||||
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 transition-opacity">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div x-show="open" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="relative bg-gray-100 rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
|
||||
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
|
||||
<button @click="open = false" type="button"
|
||||
class="text-theme-muted hover:text-theme-muted focus:outline-none focus:text-theme-muted transition ease-in-out duration-150">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="mt-3 text-center">
|
||||
<h3 class="text-lg leading-6 font-medium text-theme-primary">
|
||||
{{ __('Delete') }} {{ $value }}
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<div class="mt-10 text-theme-primary">
|
||||
{{ __('Are you sure?')}}
|
||||
</div>
|
||||
<div class="mt-10 flex justify-center">
|
||||
<span class="mr-2">
|
||||
<button x-on:click="open = false" x-bind:disabled="working" class="w-32 shadow-sm inline-flex justify-center items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-theme-secondary hover:bg-theme-primary focus:outline-none focus:border-theme-primary focus:shadow-outline-teal active:bg-theme-primary transition ease-in-out duration-150">
|
||||
{{ __('No')}}
|
||||
</button>
|
||||
</span>
|
||||
<span x-on:click="working = !working">
|
||||
<button wire:click="delete({{ $value }})" class="w-32 shadow-sm inline-flex justify-center items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:border-red-700 focus:shadow-outline-teal active:bg-red-700 transition ease-in-out duration-150">
|
||||
{{ __('Yes')}}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<div x-data="{
|
||||
edit: false,
|
||||
edited: false,
|
||||
init() {
|
||||
window.livewire.on('fieldEdited', (id) => {
|
||||
if (id === '{{ $rowId }}') {
|
||||
this.edited = true
|
||||
setTimeout(() => {
|
||||
this.edited = false
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
}
|
||||
}" x-init="init()" :key="{{ $rowId }}">
|
||||
<button class="min-h-[28px] w-full text-left hover:bg-blue-100 px-2 py-1 -mx-2 -my-1 rounded focus:outline-none" x-bind:class="{ 'text-green-600': edited }" x-show="!edit"
|
||||
x-on:click="edit = true; $nextTick(() => { $refs.input.focus() })">{!! htmlspecialchars($value) !!}</button>
|
||||
<span x-cloak x-show="edit">
|
||||
<input class="border-blue-400 px-2 py-1 -mx-2 -my-1 rounded focus:outline-none focus:border" x-ref="input" value="{!! htmlspecialchars($value) !!}"
|
||||
wire:change="edited($event.target.value, '{{ $key }}', '{{ $column }}', '{{ $rowId }}')"
|
||||
x-on:click.away="edit = false" x-on:blur="edit = false" x-on:keydown.enter="edit = false" />
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<select
|
||||
x-ref="select"
|
||||
name="{{ $name }}"
|
||||
class="m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
wire:input="doBooleanFilter('{{ $index }}', $event.target.value)"
|
||||
x-on:input="$refs.select.value=''"
|
||||
>
|
||||
<option value=""></option>
|
||||
<option value="0">{{ __('No') }}</option>
|
||||
<option value="1">{{ __('Yes') }}</option>
|
||||
</select>
|
||||
|
||||
<div class="flex flex-wrap max-w-48 space-x-1">
|
||||
@isset($this->activeBooleanFilters[$index])
|
||||
@if($this->activeBooleanFilters[$index] == 1)
|
||||
<button wire:click="removeBooleanFilter('{{ $index }}')"
|
||||
class="m-1 pl-1 flex items-center uppercase tracking-wide bg-gray-300 text-white hover:bg-red-600 rounded-full focus:outline-none text-xs space-x-1">
|
||||
<span>{{ __('YES') }}</span>
|
||||
<x-icons.x-circle />
|
||||
</button>
|
||||
@elseif(strlen($this->activeBooleanFilters[$index]) > 0)
|
||||
<button wire:click="removeBooleanFilter('{{ $index }}')"
|
||||
class="m-1 pl-1 flex items-center uppercase tracking-wide bg-gray-300 text-white hover:bg-red-600 rounded-full focus:outline-none text-xs space-x-1">
|
||||
<span>{{ __('No') }}</span>
|
||||
<x-icons.x-circle />
|
||||
</button>
|
||||
@endif
|
||||
@endisset
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div
|
||||
@if (isset($column['tooltip']['text'])) title="{{ $column['tooltip']['text'] }}" @endif
|
||||
class="flex flex-col items-center h-full px-6 py-5 overflow-hidden text-xs font-medium tracking-wider text-left text-theme-muted uppercase align-top bg-blue-100 border-b border-theme-border leading-4 space-y-2 focus:outline-none">
|
||||
<div>{{ __('SELECT ALL') }}</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:click="toggleSelectAll"
|
||||
class="w-4 h-4 mt-1 text-black form-checkbox transition duration-150 ease-in-out"
|
||||
@if(count($selected) === $this->results->total()) checked @endif
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<div class="w-full relative flex">
|
||||
<input x-ref="start" class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" type="date"
|
||||
wire:change="doDateFilterStart('{{ $index }}', $event.target.value)" style="padding-bottom: 5px" />
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.start.value=''" wire:click="doDateFilterStart('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full relative flex items-center">
|
||||
<input x-ref="end" class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" type="date"
|
||||
wire:change="doDateFilterEnd('{{ $index }}', $event.target.value)" style="padding-bottom: 5px" />
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.end.value=''" wire:click="doDateFilterEnd('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<input
|
||||
x-ref="input"
|
||||
type="text"
|
||||
class="pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
wire:change="doTextFilter('{{ $index }}', $event.target.value)"
|
||||
x-on:change="$refs.input.value = ''"
|
||||
/>
|
||||
<div class="flex flex-wrap max-w-48 space-x-1">
|
||||
@foreach($this->activeTextFilters[$index] ?? [] as $key => $value)
|
||||
<button wire:click="removeTextFilter('{{ $index }}', '{{ $key }}')" class="m-1 pl-1 flex items-center uppercase tracking-wide bg-gray-300 text-white hover:bg-red-600 rounded-full focus:outline-none text-xs space-x-1">
|
||||
<span>{{ $this->getDisplayValue($index, $value) }}</span>
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current text-red-500" />
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="flex flex-col">
|
||||
<div x-data class="relative flex">
|
||||
<input
|
||||
x-ref="min"
|
||||
type="number"
|
||||
wire:input.debounce.500ms="doNumberFilterStart('{{ $index }}', $event.target.value)"
|
||||
class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
placeholder="{{ __('MIN') }}"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.min.value=''" wire:click="doNumberFilterStart('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data class="relative flex">
|
||||
<input
|
||||
x-ref="max"
|
||||
type="number"
|
||||
wire:input.debounce.500ms="doNumberFilterEnd('{{ $index }}', $event.target.value)"
|
||||
class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
placeholder="{{ __('MAX') }}"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.max.value=''" wire:click="doNumberFilterEnd('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<div class="flex">
|
||||
<select
|
||||
x-ref="select"
|
||||
name="{{ $name }}"
|
||||
class="w-full m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
wire:input="doSelectFilter('{{ $index }}', $event.target.value)"
|
||||
x-on:input="$refs.select.value=''"
|
||||
>
|
||||
<option value=""></option>
|
||||
@foreach($options as $value => $label)
|
||||
@if(is_object($label))
|
||||
<option value="{{ $label->id }}">{{ $label->name }}</option>
|
||||
@elseif(is_array($label))
|
||||
<option value="{{ $label['id'] }}">{{ $label['name'] }}</option>
|
||||
@elseif(is_numeric($value))
|
||||
<option value="{{ $label }}">{{ $label }}</option>
|
||||
@else
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap max-w-48 space-x-1">
|
||||
@foreach($this->activeSelectFilters[$index] ?? [] as $key => $value)
|
||||
<button wire:click="removeSelectFilter('{{ $index }}', '{{ $key }}')" x-on:click="$refs.select.value=''"
|
||||
class="m-1 pl-1 flex items-center uppercase tracking-wide bg-gray-300 text-white hover:bg-red-600 rounded-full focus:outline-none text-xs space-x-1">
|
||||
<span>{{ $this->getDisplayValue($index, $value) }}</span>
|
||||
<x-icons.x-circle />
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<input
|
||||
x-ref="input"
|
||||
type="text"
|
||||
class="m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
wire:change="doTextFilter('{{ $index }}', $event.target.value)"
|
||||
x-on:change="$refs.input.value = ''"
|
||||
/>
|
||||
<div class="flex flex-wrap max-w-48 space-x-1">
|
||||
@foreach($this->activeTextFilters[$index] ?? [] as $key => $value)
|
||||
<button wire:click="removeTextFilter('{{ $index }}', '{{ $key }}')" class="m-1 pl-1 flex items-center uppercase tracking-wide bg-gray-300 text-white hover:bg-red-600 rounded-full focus:outline-none text-xs space-x-1">
|
||||
<span>{{ $this->getDisplayValue($index, $value) }}</span>
|
||||
<x-icons.x-circle />
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div x-data class="flex flex-col">
|
||||
<div class="w-full relative flex">
|
||||
<input x-ref="start" class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" type="time"
|
||||
wire:change="doTimeFilterStart('{{ $index }}', $event.target.value)" style="padding-bottom: 5px" />
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.start.value=''" wire:click="doTimeFilterStart('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full relative flex">
|
||||
<input x-ref="end" class="w-full pr-8 m-1 text-sm leading-4 block rounded-md border-theme-border shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" type="time"
|
||||
wire:change="doTimeFilterEnd('{{ $index }}', $event.target.value)" style="padding-bottom: 5px" />
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<button x-on:click="$refs.end.value=''" wire:click="doTimeFilterEnd('{{ $index }}', '')" class="-mb-0.5 pr-1 flex text-theme-muted hover:text-red-600 focus:outline-none" tabindex="-1">
|
||||
<x-icons.x-circle class="h-5 w-5 stroke-current" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<div wire:click="toggle('{{ $index }}')"
|
||||
class="@if($column['hidden']) relative table-cell h-12 w-3 bg-blue-100 hover:bg-blue-300 overflow-none align-top group @else hidden @endif"
|
||||
style="min-width:12px; max-width:12px"
|
||||
>
|
||||
<button class="relative h-12 w-3 focus:outline-none">
|
||||
<span
|
||||
class="w-32 hidden group-hover:inline-block absolute z-10 top-0 left-0 ml-3 bg-blue-300 font-medium leading-4 text-xs text-left text-blue-700 tracking-wider transform uppercase focus:outline-none">
|
||||
{{ str_replace('_', ' ', $column['label']) }}
|
||||
</span>
|
||||
</button>
|
||||
<svg class="absolute text-blue-100 fill-current w-full inset-x-0 bottom-0"
|
||||
viewBox="0 0 314.16 207.25">
|
||||
<path stroke-miterlimit="10" d="M313.66 206.75H.5V1.49l157.65 204.9L313.66 1.49v205.26z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="@if($column['hidden']) hidden @else relative h-12 overflow-hidden align-top flex table-cell @endif" @include('datatables::style-width')>
|
||||
|
||||
@if($column['unsortable'])
|
||||
<div class="w-full h-full px-6 py-3 border-b border-theme-border bg-gray-50 text-xs leading-4 font-medium text-theme-muted uppercase tracking-wider flex justify-between items-center focus:outline-none">
|
||||
<span class="inline flex-grow @if($column['align'] === 'right') text-right @elseif($column['align'] === 'center') text-center @endif"">{{ str_replace('_', ' ', $column['label']) }}</span>
|
||||
</div>
|
||||
@else
|
||||
<button wire:click="sort('{{ $index }}')"
|
||||
class="w-full h-full px-6 py-3 border-b border-theme-border bg-gray-50 text-xs leading-4 font-medium text-theme-muted uppercase tracking-wider flex justify-between items-center focus:outline-none">
|
||||
<span class="inline flex-grow @if($column['align'] === 'right') text-right @elseif($column['align'] === 'center') text-center @endif"">{{ str_replace('_', ' ', $column['label']) }}</span>
|
||||
<span class="inline text-xs text-blue-400">
|
||||
@if($sort === $index)
|
||||
@if($direction)
|
||||
<x-icons.chevron-up class="h-6 w-6 text-theme-primary hover:text-theme-secondary stroke-current pl-2" />
|
||||
|
||||
|
||||
@else
|
||||
<x-icons.chevron-down class="h-6 w-6 text-theme-primary hover:text-theme-secondary stroke-current pl-2" />
|
||||
|
||||
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($column['hideable'])
|
||||
<button wire:click="toggle('{{ $index }}')"
|
||||
class="absolute bottom-1 right-1 focus:outline-none">
|
||||
<x-icons.arrow-circle-left class="h-3 w-3 text-theme-muted hover:text-blue-400" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
@unless($column['hidden'])
|
||||
<div
|
||||
@if (isset($column['tooltip']['text'])) title="{{ $column['tooltip']['text'] }}" @endif
|
||||
class="relative table-cell h-12 overflow-hidden align-top" @include('datatables::style-width')>
|
||||
@if($column['unsortable'])
|
||||
<div class="w-full h-full px-6 py-3 border-b border-theme-border bg-gray-50 text-left text-xs leading-4 font-medium text-theme-muted uppercase tracking-wider flex items-center focus:outline-none @if($column['align'] === 'right') justify-end @elseif($column['align'] === 'center') justify-center @endif">
|
||||
<span class="inline ">{{ str_replace('_', ' ', $column['label']) }}</span>
|
||||
</div>
|
||||
@else
|
||||
<button wire:click="sort('{{ $index }}')" class="w-full h-full px-6 py-3 border-b border-theme-border bg-gray-50 text-left text-xs leading-4 font-medium text-theme-muted uppercase tracking-wider flex items-center focus:outline-none @if($column['align'] === 'right') justify-end @elseif($column['align'] === 'center') justify-center @endif">
|
||||
<span class="inline ">{{ str_replace('_', ' ', $column['label']) }}</span>
|
||||
<span class="inline text-xs text-blue-400">
|
||||
@if($sort === $index)
|
||||
@if($direction)
|
||||
<x-icons.chevron-up wire:loading.remove class="w-6 h-6 text-theme-primary hover:text-theme-secondary stroke-current pl-2" />
|
||||
@else
|
||||
<x-icons.chevron-down wire:loading.remove class="w-6 h-6 text-theme-primary hover:text-theme-secondary stroke-current pl-2" />
|
||||
|
||||
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,52 @@
|
||||
<div x-data="{ show: false }" class="flex flex-col items-center">
|
||||
<div class="flex flex-col items-center relative">
|
||||
<button x-on:click="show = !show" class="px-3 py-2 border border-blue-400 rounded-md bg-white text-blue-500 text-xs leading-4 font-medium uppercase tracking-wider hover:bg-blue-200 focus:outline-none">
|
||||
<div class="flex items-center h-5">
|
||||
{{ __('Show / Hide Columns')}}
|
||||
</div>
|
||||
</button>
|
||||
<div x-show="show" x-on:click.away="show = false" class="z-50 absolute mt-16 -mr-4 shadow-2xl top-100 bg-white w-96 right-0 rounded max-h-select overflow-y-auto" x-cloak>
|
||||
<div class="flex flex-col w-full">
|
||||
@foreach($this->columns as $index => $column)
|
||||
<div>
|
||||
<div class="@unless($column['hidden']) hidden @endif cursor-pointer w-full border-theme-secondary border-b bg-theme-primary text-theme-muted hover:bg-blue-600 hover:text-white" wire:click="toggle({{$index}})">
|
||||
<div class="relative flex w-full items-center p-2 group">
|
||||
<div class=" w-full items-center flex">
|
||||
<div class="mx-2 leading-6">{{ $column['label'] }}</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<x-icons.check-circle class="h-3 w-3 stroke-current text-theme-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="@if($column['hidden']) hidden @endif cursor-pointer w-full border-theme-secondary border-b bg-theme-primary text-white hover:bg-red-600" wire:click="toggle({{$index}})">
|
||||
<div class="relative flex w-full items-center p-2 group">
|
||||
<div class=" w-full items-center flex">
|
||||
<div class="mx-2 leading-6">{{ $column['label'] }}</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 pr-2 flex items-center">
|
||||
<x-icons.x-circle class="h-3 w-3 stroke-current text-theme-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.top-100 {
|
||||
top: 100%
|
||||
}
|
||||
|
||||
.bottom-100 {
|
||||
bottom: 100%
|
||||
}
|
||||
|
||||
.max-h-select {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<span class="bg-yellow-100">{{ $slot }}</span>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 15l-3-3m0 0l3-3m-3 3h8M3 12a9 9 0 1118 0 9 9 0 01-18 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 227 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -0,0 +1,4 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 357 B |
@@ -0,0 +1 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 384 512"><path fill="currentColor" d="M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm212-240h-28.8c-4.4 0-8.4 2.4-10.5 6.3-18 33.1-22.2 42.4-28.6 57.7-13.9-29.1-6.9-17.3-28.6-57.7-2.1-3.9-6.2-6.3-10.6-6.3H124c-9.3 0-15 10-10.4 18l46.3 78-46.3 78c-4.7 8 1.1 18 10.4 18h28.9c4.4 0 8.4-2.4 10.5-6.3 21.7-40 23-45 28.6-57.7 14.9 30.2 5.9 15.9 28.6 57.7 2.1 3.9 6.2 6.3 10.6 6.3H260c9.3 0 15-10 10.4-18L224 320c.7-1.1 30.3-50.5 46.3-78 4.7-8-1.1-18-10.3-18z"/></svg>
|
||||
|
After Width: | Height: | Size: 733 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5']) }} fill="none" stroke="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => 'h-5 w-5 stroke-current']) }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1,3 @@
|
||||
<div class="table-cell px-6 py-2 whitespace-no-wrap @if($column['align'] === 'right') text-right @elseif($column['align'] === 'center') text-center @else text-left @endif {{ $this->cellClasses($row, $column) }}">
|
||||
{!! $column['content'] ?? '' !!}
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="{{ $href }}" class="border-2 border-transparent hover:border-blue-500 hover:bg-blue-100 hover:shadow-lg text-black rounded-lg px-3 py-1">{{ $slot }}</a>
|
||||
@@ -0,0 +1,3 @@
|
||||
@if (isset($column['width']))style="width:{{ $column['width'] }};"@endif
|
||||
@if (isset($column['minWidth']))style="min-width:{{ $column['minWidth'] }};"@endif
|
||||
@if (isset($column['maxWidth']))style="max-width:{{ $column['maxWidth'] }};"@endif
|
||||
@@ -0,0 +1,50 @@
|
||||
<div class="flex overflow-hidden border border-theme-border divide-x divide-gray-300 rounded pagination">
|
||||
<!-- Previous Page Link -->
|
||||
@if ($paginator->onFirstPage())
|
||||
<button class="relative inline-flex items-center px-2 py-2 text-sm font-medium leading-5 text-theme-muted bg-white"
|
||||
disabled>
|
||||
<span>«</span>
|
||||
</button>
|
||||
@else
|
||||
<button wire:click="previousPage"
|
||||
id="pagination-desktop-page-previous"
|
||||
class="relative inline-flex items-center px-2 py-2 text-sm font-medium leading-5 text-theme-muted transition duration-150 ease-in-out bg-white hover:text-theme-muted focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-theme-muted">
|
||||
<span>«</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div class="divide-x divide-gray-300">
|
||||
@foreach ($elements as $element)
|
||||
@if (is_string($element))
|
||||
<button class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-theme-primary bg-white" disabled>
|
||||
<span>{{ $element }}</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Array Of Links -->
|
||||
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
<button wire:click="gotoPage({{ $page }})"
|
||||
id="pagination-desktop-page-{{ $page }}"
|
||||
class="-mx-1 relative inline-flex items-center px-4 py-2 text-sm leading-5 font-medium text-theme-primary hover:text-theme-muted focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-theme-primary transition ease-in-out duration-150 {{ $page === $paginator->currentPage() ? 'bg-gray-100' : 'bg-white' }}">
|
||||
{{ $page }}
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Next Page Link -->
|
||||
@if ($paginator->hasMorePages())
|
||||
<button wire:click="nextPage"
|
||||
id="pagination-desktop-page-next"
|
||||
class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium leading-5 text-theme-muted transition duration-150 ease-in-out bg-red hover:text-theme-muted focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-theme-muted">
|
||||
<span>»</span>
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium leading-5 text-theme-muted bg-white "
|
||||
disabled><span>»</span></button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="flex justify-between">
|
||||
<!-- Previous Page Link -->
|
||||
@if ($paginator->onFirstPage())
|
||||
<div class="w-32 flex justify-between items-center relative px-4 py-2 border border-theme-border text-sm leading-5 font-medium rounded-md text-theme-muted bg-gray-50">
|
||||
<x-icons.arrow-left />
|
||||
{{ __('Previous')}}
|
||||
</div>
|
||||
@else
|
||||
<button wire:click="previousPage" id="pagination-mobile-page-previous" class="w-32 flex justify-between items-center relative px-4 py-2 border border-theme-border text-sm leading-5 font-medium rounded-md text-theme-primary bg-white hover:text-theme-muted focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-theme-primary transition ease-in-out duration-150">
|
||||
<x-icons.arrow-left />
|
||||
{{ __('Previous')}}
|
||||
</button>
|
||||
@endif
|
||||
|
||||
|
||||
<!-- Next Page pnk -->
|
||||
@if ($paginator->hasMorePages())
|
||||
<button wire:click="nextPage" id="pagination-mobile-page-next" class="w-32 flex justify-between items-center relative items-center px-4 py-2 border border-theme-border text-sm leading-5 font-medium rounded-md text-theme-primary bg-white hover:text-theme-muted focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-theme-primary transition ease-in-out duration-150">
|
||||
{{ __('Next')}}
|
||||
<x-icons.arrow-right />
|
||||
</button>
|
||||
@else
|
||||
<div class="w-32 flex justify-between items-center relative px-4 py-2 border border-theme-border text-sm leading-5 font-medium rounded-md text-theme-muted bg-gray-50">
|
||||
{{ __('Next')}}
|
||||
<x-icons.arrow-right class="inline" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<span class="relative group cursor-pointer">
|
||||
<span class="flex items-center">{{ Str::limit($slot, $length) }}</span>
|
||||
<span class="hidden group-hover:block absolute z-10 -ml-28 w-96 mt-2 p-2 text-xs whitespace-pre-wrap rounded-lg bg-gray-100 border border-theme-border shadow-xl text-theme-primary text-left">{{ $slot }}</span>
|
||||
</span>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div x-data class="max-w-md mt-4">
|
||||
|
||||
<label for="description" class="mt-2 block text-sm font-medium text-theme-primary"> {{ __('Description') }}</label>
|
||||
<textarea
|
||||
wire:model.live.debounce.800ms="description"
|
||||
x-on:blur="$wire.checkRequired()"
|
||||
placeholder=" {{ __('Payment description') }}"
|
||||
rows="5"
|
||||
class="mt-1 placeholder-gray-300 focus:ring-gray-500 focus:border-theme-muted block w-full shadow-sm sm:text-sm border-theme-border rounded-md @error('description') is-invalid @enderror" name="description" value="{{ old('description') }}">
|
||||
</textarea>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,209 @@
|
||||
<div>
|
||||
@if($posts->isEmpty())
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg">
|
||||
<div class="px-3 sm:px-0">
|
||||
<div class="max-w-4xl mx-auto my-12 p-4">
|
||||
<div class="mt-48 mb-36 flex flex-col items-center justify-center">
|
||||
<h1 class="text-3xl font-bold text-theme-primary dark:text-gray-200">{{ __('Sorry') }}</h1>
|
||||
<p class="text-lg text-theme-secondary dark:text-theme-muted mt-4">{{ __('No page available in your language at the moment') }}</p>
|
||||
|
||||
{{-- Only show button if locales are different AND fallback content actually exists --}}
|
||||
@if(trim(app()->getLocale()) !== trim($fallbackLocale) && $fallbackExists)
|
||||
<x-jetstream.button wire:click="loadFallback" wire:loading.attr="disabled" class="mt-6">
|
||||
<span wire:loading.remove wire:target="loadFallback">
|
||||
{{ __('messages.view_in_language', ['lang' => trim($fallbackLocale)]) }}
|
||||
</span>
|
||||
<span wire:loading wire:target="loadFallback">
|
||||
{{ __('Loading...') }}
|
||||
</span>
|
||||
</x-jetstream.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
@foreach($posts as $post)
|
||||
<div class="overflow-hidden bg-theme-background shadow-xl sm:rounded-lg mb-3">
|
||||
<div class="border-b border-theme-primary bg-theme-background p-6 sm:px-20 lg:px-32 lg:py-18">
|
||||
<livewire:posts.manage-actions :post="$post" />
|
||||
|
||||
{{-- Date & Time Header --}}
|
||||
@if (isset($post->meeting->from))
|
||||
<div class="mb-2 mt-0 md:mt-6 lg:mt-12">
|
||||
<div class="text-xs lg:text-sm text-theme-secondary">
|
||||
{{ Illuminate\Support\Carbon::parse($post->meeting->from)->isoFormat('LL') }}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mt-4 lg:mt-8 text-xl lg:text-4xl font-semibold leading-tight text-theme-primary">
|
||||
{{ ucfirst(Illuminate\Support\Carbon::parse($post->meeting->from)->isoFormat('dddd D MMMM, HH:mm')) }}{{ $post->meeting->till ? ' - ' . Illuminate\Support\Carbon::parse($post->meeting->till)->format('H:i') : '' }} {{ __('h.') }}
|
||||
</h3>
|
||||
@endif
|
||||
|
||||
{{-- Title --}}
|
||||
@if (isset($post->translations[0]->title))
|
||||
<h2 class="mt-4 lg:mt-8 text-3xl lg:text-5xl font-semibold leading-tight text-theme-primary">
|
||||
{{ $post->translations[0]->title }}
|
||||
</h2>
|
||||
@endif
|
||||
|
||||
{{-- Excerpt --}}
|
||||
@if (isset($post->translations[0]->excerpt))
|
||||
<div class="mt-6">
|
||||
<div class="px-0 py-2 text-xl lg:text-2xl leading-normal lg:leading-loose font-bold text-theme-primary">
|
||||
{{ $post->translations[0]->excerpt }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Image --}}
|
||||
<div class="my-4 lg:my-12">
|
||||
@if($post->hasMedia('*'))
|
||||
<img src="{{ $post->getFirstMediaUrl('*') }}" alt="{{ $post->getFirstMedia('*')->getCustomProperty('caption') }}" class="w-full h-auto">
|
||||
@php
|
||||
$locale = $post->translations[0]->locale;
|
||||
$imageCaption = $post->getFirstMedia('*')->getCustomProperty('caption-' . $locale);
|
||||
|
||||
// Fallback to other locales if caption not found
|
||||
if (!$imageCaption) {
|
||||
$fallbackLocales = array_keys(config('laravellocalization.supportedLocales'));
|
||||
foreach ($fallbackLocales as $fallbackLoc) {
|
||||
$imageCaption = $post->getFirstMedia('*')->getCustomProperty('caption-' . $fallbackLoc);
|
||||
if ($imageCaption) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$imageOwner = $post->getFirstMedia('*')->getCustomProperty('owner');
|
||||
$captionParts = array_filter([$imageCaption, $imageOwner]);
|
||||
$captionText = implode(' ', $captionParts);
|
||||
@endphp
|
||||
@if ($captionText)
|
||||
<div class="mt-1 lg:mt-2 text-right text-3xs lg:text-xs text-theme-secondary">
|
||||
{{ $captionText }}
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
@if (isset($post->translations[0]->content) && strlen(trim(strip_tags($post->translations[0]->content))) > 0)
|
||||
<div class="post-content mt-2 lg:mt-6 mb-12 lg:mb-16 lg:mx-32 text-base lg:text-lg leading-relaxed md:leading-loose lg:leading-loose text-theme-primary">
|
||||
{!! \App\Helpers\StringHelper::sanitizeHtml($post->translations[0]->content) !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Meeting Details Table --}}
|
||||
@if (isset($post->meeting))
|
||||
<div class="my-12 bg-theme-surface rounded-lg p-6 md:mx-32 border">
|
||||
<table class="w-full">
|
||||
<tbody>
|
||||
@if(isset($post->meeting->price))
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-semibold text-theme-primary w-1/3">{{ __('Price') }}</td>
|
||||
<td class="py-2 text-theme-primary">
|
||||
@if($post->meeting->transactionType && strtolower($post->meeting->transactionType->name) == 'work')
|
||||
{{ $post->meeting->price === 0 ? __('Free') : tbFormat($post->meeting->price) }}
|
||||
( {{ __('messages.posts.based_on_quantity', ['nr'=> $post->meeting->based_on_quantity]) }} )
|
||||
@elseif($post->meeting->transactionType && strtolower($post->meeting->transactionType->name) == 'gift')
|
||||
{{ __('messages.posts.transaction_types.gift') }}
|
||||
@elseif($post->meeting->transactionType && strtolower($post->meeting->transactionType->name) == 'donation')
|
||||
{{ __('messages.posts.transaction_types.donation') }}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if($post->meeting->venue)
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-semibold text-theme-primary w-1/3">{{ __('Location') }}</td>
|
||||
<td class="py-2 text-theme-primary">{{ $post->meeting->venue }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if($post->meeting->address)
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-semibold text-theme-primary w-1/3">{{ __('Address') }}</td>
|
||||
<td class="py-2 text-theme-primary">
|
||||
<a href="https://www.openstreetmap.org/search?query={{ urlencode($post->meeting->address) }}"
|
||||
target="_blank"
|
||||
class="underline hover:text-theme-secondary">
|
||||
{{ $post->meeting->address }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if($post->meeting->from)
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-semibold text-theme-primary w-1/3">{{ __('Date & Time') }}</td>
|
||||
<td class="py-2 text-theme-primary">{{ ucfirst(Illuminate\Support\Carbon::parse($post->meeting->from)->isoFormat('dddd D MMMM YYYY, H:mm')) }}{{ $post->meeting->till ? ' - ' . Illuminate\Support\Carbon::parse($post->meeting->till)->format('H:i') : '' }} {{ __('h.') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if(isset($post->meeting->meetingable->name))
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-semibold text-theme-primary w-1/3">{{ __('Organizer') }}</td>
|
||||
<td class="py-2 text-theme-primary">
|
||||
<a href="{{ $post->meeting->meetingable->profile_url }}"
|
||||
class="flex items-center space-x-3 hover:opacity-80 cursor-pointer">
|
||||
@if(isset($post->meeting->meetingable->profile_photo_path))
|
||||
<img src="{{ url(Storage::url($post->meeting->meetingable->profile_photo_path)) }}"
|
||||
class="w-10 h-10 rounded-full profile-photo object-cover outline outline-1 outline-offset-1 outline-theme-secondary">
|
||||
@endif
|
||||
<span>{{ $post->meeting->meetingable->name }}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-16 mb-12 flex flex-col md:flex-row justify-between gap-6">
|
||||
{{-- Social Share Buttons --}}
|
||||
<div class="md:self-end">
|
||||
{!! (new \Enflow\SocialShare\SocialShare())
|
||||
->mastodon()
|
||||
->bluesky()
|
||||
->linkedin()
|
||||
->instagram()
|
||||
->facebook()
|
||||
->whatsapp()
|
||||
->text($post->translations[0]->title)
|
||||
->render() !!}
|
||||
<div class="mt-2 text-sm invisible"> </div>
|
||||
</div>
|
||||
|
||||
{{-- Reservation Button --}}
|
||||
<div>
|
||||
<livewire:reserve-button :post="$post" :wire:key="'reserve-'.$post->id" />
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-16 mb-12 flex flex-col md:flex-row justify-between gap-6">
|
||||
{{-- Social Share Buttons --}}
|
||||
<div class="md:self-end">
|
||||
{!! (new \Enflow\SocialShare\SocialShare())
|
||||
->mastodon()
|
||||
->bluesky()
|
||||
->linkedin()
|
||||
->instagram()
|
||||
->facebook()
|
||||
->whatsapp()
|
||||
->text($post->translations[0]->title)
|
||||
->render() !!}
|
||||
<div class="mt-2 text-sm invisible"> </div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<livewire:posts.manage-actions :post="$post" />
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<div>
|
||||
<x-jetstream.dialog-modal wire:model.live="showModal">
|
||||
<x-slot name="title">
|
||||
{{ $title }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<p>{{ $message }}</p>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.button wire:click="confirmLogout" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="confirmLogout">{{ __('OK') }}</span>
|
||||
<span wire:loading wire:target="confirmLogout">{{ __('Loading...') }}</span>
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Listen for custom event from websocket
|
||||
window.addEventListener('show-forced-logout-modal', event => {
|
||||
@this.call('showForcedLogoutModal', event.detail.message, event.detail.title);
|
||||
});
|
||||
|
||||
// Listen for proceed-logout event from Livewire
|
||||
window.addEventListener('proceed-logout', () => {
|
||||
// Create and submit logout form
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ route("logout") }}';
|
||||
|
||||
const csrfToken = document.createElement('input');
|
||||
csrfToken.type = 'hidden';
|
||||
csrfToken.name = '_token';
|
||||
csrfToken.value = '{{ csrf_token() }}';
|
||||
|
||||
form.appendChild(csrfToken);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="mt-4 max-w-md" wire:init="preSelected" x-data="{ open: false, selected: @entangle('selectedAccount') }">
|
||||
|
||||
@isset($label)
|
||||
<x-jetstream.label :value="$label" for="account" />
|
||||
@else
|
||||
<x-jetstream.label for="account" value="{{ __('From account') }}" />
|
||||
@endisset
|
||||
|
||||
<div class="relative mt-1">
|
||||
<button @click="open = !open"
|
||||
class="block w-full rounded-md border border-theme-primary bg-theme-background px-3 py-2 text-left shadow-sm transition duration-150 ease-in-out focus:border-theme-accent focus:ring-theme-accent focus:outline-none sm:text-sm"
|
||||
type="button">
|
||||
<div :class="selected ? 'text-theme-primary' : 'text-theme-light'" class="flex items-center justify-between">
|
||||
@if (!$selectedAccount)
|
||||
<span class="cursor-default text-red-600" x-text="'{{ __('No account available') }}'"></span>
|
||||
@else
|
||||
<span class="flex w-full items-center">
|
||||
<span x-text="selected ? selected.name : '{{ __('Select an account') }}'"></span>
|
||||
<span x-show="selected" class="ml-auto" x-text="selected ? selected.balanceTbFormat : ''"></span>
|
||||
</span>
|
||||
@endif
|
||||
<svg class="text-secondary-400 invalidated:text-negative-400 invalidated:dark:text-negative-600 ml-2 h-5 w-5"
|
||||
fill="none" height="24" stroke="currentColor" viewBox="0 0 24 24" width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.25 15L12 18.75L15.75 15M8.25 9L12 5.25L15.75 9" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="1.5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ul @click.away="open = false"
|
||||
class="absolute z-10 mt-1 w-full rounded-md border border-theme-primary bg-theme-background shadow-lg" x-show="open">
|
||||
@foreach ($profileAccounts as $index => $profileAccount)
|
||||
<li
|
||||
@click="selected = { id: {{ $profileAccount['id'] }}, name: '{{ $profileAccount['name'] }}{{ $profileAccount['inactive'] === true ? ' (' . strtolower(__('Inactive')) . ')' : '' }}', balance: '{{ tbFormat($profileAccount['balance']) }}' }; open = false; $wire.fromAccountSelected({{ $profileAccount['id'] }})"
|
||||
class="flex cursor-pointer justify-between px-3 py-2 hover:bg-theme-surface"
|
||||
wire:key="{{ $index }}">
|
||||
<span>
|
||||
{{ $profileAccount['name'] }}
|
||||
@if($profileAccount['inactive'])
|
||||
({{ strtolower(__('Inactive')) }})
|
||||
@endif
|
||||
</span>
|
||||
<span class="ml-auto">{{ tbFormat($profileAccount['balance']) }}</span>
|
||||
<span class="ml-2 w-5"></span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Nothing in the world is as soft and yielding as water. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<div>
|
||||
<!-- Countries dropdown -->
|
||||
@if ($showCountry)
|
||||
<div class="mb-6">
|
||||
@if (!$hideLabel)
|
||||
<label class="rounder-md block text-sm font-medium text-theme-primary"> {{ __('Country') }}</label>
|
||||
@endif
|
||||
<select class="shadow-outline w-full lg:w-64 rounded border border-theme-muted bg-white p-2 px-4 py-2 pr-8 leading-tight placeholder-gray-300 shadow-md hover:border-theme-muted focus:appearance-none focus:outline-none focus:border-theme-accent focus:ring-1 focus:ring-theme-accent"
|
||||
wire:change="countrySelected" wire:key="country-dropdown" wire:model.live="country">
|
||||
<option selected value="">-- {{ __('Choose a country') }} --</option>
|
||||
@foreach ($countries->sortBy(function ($country) {
|
||||
return $country->translations->first()->name;
|
||||
}) as $country)
|
||||
<option value="{{ $country->id }}">{{ $country->code === 'XX' ? $country->flag . ' ' . __('~ My country is not listed') : $country->flag . ' ' . $country->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Divisions dropdown -->
|
||||
@if ($showDivision && !empty($country) && count($divisions) > 0)
|
||||
<div class="mb-6 mt-6" wire:init="countrySelected">
|
||||
@if (!$hideLabel)
|
||||
<label class="rounder-md block text-sm font-medium text-theme-primary">{{ __('Division') }}</label>
|
||||
@endif
|
||||
<select class="shadow-outline w-full lg:w-64 rounded border border-theme-muted bg-white p-2 px-4 py-2 pr-8 leading-tight placeholder-gray-300 shadow-md hover:border-theme-muted focus:appearance-none focus:outline-none focus:border-theme-accent focus:ring-1 focus:ring-theme-accent"
|
||||
wire:change="divisionSelected" wire:key="division-dropdown" wire:model.live="division">
|
||||
<option selected value="">-- {{ __('Choose a division') }} --</option>
|
||||
@foreach ($divisions->sortBy(function ($division) {
|
||||
return $division->translations->first()->name;
|
||||
}) as $division)
|
||||
<option value="{{ $division->id }}">{{ $division->translations->first()->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Cities dropdown -->
|
||||
@if ($showCity && !empty($country) && count($cities) > 0)
|
||||
<div class="mb-6 mt-6" wire:init="countrySelected">
|
||||
@if (!$hideLabel)
|
||||
<label class="rounder-md block text-sm font-medium text-theme-primary">{{ __('City') }}</label>
|
||||
@endif
|
||||
<select class="shadow-outline w-full lg:w-64 rounded border border-theme-muted bg-white p-2 px-4 py-2 pr-8 leading-tight placeholder-gray-300 shadow-md hover:border-theme-muted focus:appearance-none focus:outline-none focus:border-theme-accent focus:ring-1 focus:ring-theme-accent"
|
||||
wire:key="city-dropdown" wire:model.live="city">
|
||||
<option selected value="">-- {{ __('Choose a city') }} --</option>
|
||||
@foreach ($cities->sortBy(function ($city) {
|
||||
return $city->translations->first()->name;
|
||||
}) as $city)
|
||||
<option value="{{ $city->id }}">{{ $city->translations->first()->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Districts dropdown -->
|
||||
@if ($showDistrict && !empty($city) && count($districts) > 0)
|
||||
<div class="mb-6 mt-6" wire:init="countrySelected">
|
||||
@if (!$hideLabel)
|
||||
<label class="rounder-md block text-sm font-medium text-theme-primary">{{ __('District') }}</label>
|
||||
@endif
|
||||
<select class="shadow-outline w-full lg:w-64 rounded border border-theme-muted bg-white p-2 px-4 py-2 pr-8 leading-tight placeholder-gray-300 shadow-md hover:border-theme-muted focus:appearance-none focus:outline-none focus:border-theme-accent focus:ring-1 focus:ring-theme-accent"
|
||||
wire:key="district-dropdown" wire:model.live="district">
|
||||
<option selected value="">-- {{ __('Choose a district') }} --</option>
|
||||
@foreach ($districts->sortBy(function ($district) {
|
||||
return $district->translations->first()->name;
|
||||
}) as $district)
|
||||
<option value="{{ $district->id }}">{{ $district->translations->first()->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<x-jetstream.form-section submit="updateProfileInformation">
|
||||
<x-slot name="title">
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
<div class="">
|
||||
@livewire('side-post', [
|
||||
'type' => 'SiteContents\User\Edit\Location' ?? null,
|
||||
'sticky' => false, 'random' => true,
|
||||
'fallbackTitle' => __('Location'),
|
||||
'fallbackDescription' => str_replace('@PLATFORM_USERS@', platform_users(), __('Most exchanges take place in your own area. Please indicate where this is so you can easily meet other @PLATFORM_USERS@ who are around.')) ])
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
|
||||
<!--- Location -->
|
||||
<div class="grid col-span-6 sm:col-span-4" wire:init="emitLocationToChildren">
|
||||
<!-- TODO: Explanantion for location dropdowns -->
|
||||
@livewire('locations.locations-dropdown')
|
||||
@error('country')
|
||||
<p class="text-sm text-red-500">{{$message}}</p>
|
||||
@enderror
|
||||
@error('division')
|
||||
<p class="text-sm text-red-500">{{$message}}</p>
|
||||
@enderror
|
||||
@error('city')
|
||||
<p class="text-sm text-red-500">{{$message}}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-jetstream.action-message class="mr-3" on="saved">
|
||||
{{ __('Saved') }}
|
||||
</x-jetstream.action-message>
|
||||
|
||||
<x-jetstream.button wire:loading.attr="disabled" wire:target="updateProfileInformation" wire:click="updateProfileInformation">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.form-section>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<div>
|
||||
<x-notifications.notify />
|
||||
<div class="min-h-screen flex items-center justify-center bg-theme-surface py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full">
|
||||
@if(isMaintenanceMode() || session('maintenance_mode_active'))
|
||||
<div class="mb-6 rounded-md bg-gray-50 border border-gray-300 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-gray-800">
|
||||
{{ __('Site under maintenance') }}
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
<p>
|
||||
{{ __('The site is currently undergoing maintenance, and login is temporarily unavailable. Thank you for your patience.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-on-white.svg"
|
||||
alt="Workflow">
|
||||
<h2 class="mt-6 text-center text-3xl leading-9 font-extrabold text-theme-primary">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm leading-5 text-theme-primary">
|
||||
Or
|
||||
<a href="{{route('register')}}"
|
||||
class="font-medium text-theme-primary hover:text-theme-secondary focus:outline-none focus:underline transition ease-in-out duration-150">
|
||||
Register Here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<form class="mt-8" wire:submit="login">
|
||||
<div class="rounded-md shadow-sm">
|
||||
<div>
|
||||
<input wire:model.blur="email" aria-label="Email address" name="email" type="email" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-theme-primary placeholder-theme-secondary text-theme-primary rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5"
|
||||
placeholder="Email address">
|
||||
@error('email')
|
||||
<p class="text-sm text-red-500 text-center">{{$message}}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="-mt-px">
|
||||
<input wire:model.blur="password" aria-label="Password" name="password" type="password" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-theme-primary placeholder-theme-secondary text-theme-primary rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5"
|
||||
placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-theme-primary hover:bg-theme-surface0 focus:outline-none focus:border-theme-primary focus:shadow-outline-gray active:bg-theme-primary transition duration-150 ease-in-out">
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-theme-secondary group-hover:text-theme-light transition ease-in-out duration-150"
|
||||
fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
<div>
|
||||
@if ($paginator->hasPages() && $paginator->total() > $paginator->perPage())
|
||||
<nav aria-label="Pagination Navigation" class="flex justify-end" role="navigation">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="invisible ml-1 sm:ml-2 md:ml-3">
|
||||
<x-jetstream.light-button disabled>
|
||||
<span class="inline md:hidden"><</span>
|
||||
<span class="hidden md:inline">{{ __('Previous') }}</span>
|
||||
</x-jetstream.light-button>
|
||||
</span>
|
||||
@else
|
||||
<x-jetstream.light-button class="ml-1 sm:ml-2 md:ml-3" x-on:click="$wire.previousPage('{{ $paginator->getPageName() }}'); $dispatch('scroll-to-top');">
|
||||
<span class="inline md:hidden"><</span>
|
||||
<span class="hidden md:inline">{{ __('Previous') }}</span>
|
||||
</x-jetstream.light-button>
|
||||
@endif
|
||||
|
||||
{{-- XS/SM: Only show current and last page --}}
|
||||
<div class="flex md:hidden items-center">
|
||||
{{-- Current Page --}}
|
||||
<span class="relative ml-1 sm:ml-2 inline-flex cursor-default items-center rounded-md border border-theme-brand bg-white px-3 sm:px-4 py-2 text-sm font-medium leading-5 text-theme-brand shadow-sm">
|
||||
{{ $paginator->currentPage() }}
|
||||
</span>
|
||||
{{-- Last Page --}}
|
||||
@if ($paginator->currentPage() != $paginator->lastPage())
|
||||
<x-jetstream.light-button class="ml-1 sm:ml-2" x-on:click="$wire.gotoPage({{ $paginator->lastPage() }}, '{{ $paginator->getPageName() }}'); $dispatch('scroll-to-top');">
|
||||
{{ $paginator->lastPage() }}
|
||||
</x-jetstream.light-button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- SM+: Full paginator --}}
|
||||
<div class="hidden md:flex items-center">
|
||||
@php
|
||||
$currentPage = $paginator->currentPage();
|
||||
$lastPage = $paginator->lastPage();
|
||||
|
||||
if ($paginator->total() === 0) {
|
||||
$start = 0;
|
||||
$end = 0;
|
||||
} else {
|
||||
$start = max($currentPage - 1, 1);
|
||||
$end = min($currentPage + 1, $lastPage);
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($start > 0 && $end > 0)
|
||||
{{-- First Page Link --}}
|
||||
@if ($start > 1)
|
||||
<x-jetstream.light-button class="ml-3" x-on:click="$wire.gotoPage(1, 'page'); $dispatch('scroll-to-top');">
|
||||
1
|
||||
</x-jetstream.light-button>
|
||||
@if ($start > 2)
|
||||
<span class="ml-3">
|
||||
<x-jetstream.light-button disabled>
|
||||
...
|
||||
</x-jetstream.light-button>
|
||||
</span>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Page Links --}}
|
||||
@for ($page = $start; $page <= $end; $page++)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<span class="relative ml-3 inline-flex cursor-default items-center rounded-md border border-theme-brand bg-white px-4 py-2 text-sm font-medium leading-5 text-theme-brand shadow-sm">
|
||||
{{ $page }}
|
||||
</span>
|
||||
@else
|
||||
<x-jetstream.light-button class="ml-3" x-on:click="$wire.gotoPage({{ $page }}, '{{ $paginator->getPageName() }}'); $dispatch('scroll-to-top');">
|
||||
{{ $page }}
|
||||
</x-jetstream.light-button>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
{{-- Last Page Link --}}
|
||||
@if ($end < $lastPage)
|
||||
@if ($end < $lastPage - 1)
|
||||
<span class="ml-3">
|
||||
<x-jetstream.light-button disabled>
|
||||
...
|
||||
</x-jetstream.light-button>
|
||||
</span>
|
||||
@endif
|
||||
<x-jetstream.light-button class="ml-3" x-on:click="$wire.gotoPage({{ $lastPage }}, '{{ $paginator->getPageName() }}'); $dispatch('scroll-to-top');">
|
||||
{{ $lastPage }}
|
||||
</x-jetstream.light-button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<x-jetstream.light-button class="ml-1 sm:ml-2 md:ml-3" x-on:click="$wire.nextPage('{{ $paginator->getPageName() }}'); $dispatch('scroll-to-top');">
|
||||
<span class="inline md:hidden">></span>
|
||||
<span class="hidden md:inline">{{ __('Next') }}</span>
|
||||
</x-jetstream.light-button>
|
||||
@else
|
||||
<span class="invisible ml-1 sm:ml-2 md:ml-3">
|
||||
<x-jetstream.light-button disabled>
|
||||
<span class="inline md:hidden">></span>
|
||||
<span class="hidden md:inline">{{ __('Next') }}</span>
|
||||
</x-jetstream.light-button>
|
||||
</span>
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,127 @@
|
||||
<div class="space-y-4">
|
||||
<!-- Countries -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Countries') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-24 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedCountries(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple countries') }}">
|
||||
@foreach($countries->sortBy(function ($country) { return $country->translations->first()->name; }) as $country)
|
||||
<option value="{{ $country->id }}" @selected(in_array($country->id, $selectedCountries))>
|
||||
{{ $country->flag }} {{ $country->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedCountries) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedCountries)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Divisions -->
|
||||
@if($divisions->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Divisions / States / Provinces') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-20 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedDivisions(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple divisions') }}">
|
||||
@foreach($divisions->sortBy(function ($division) { return $division->translations->first()->name; }) as $division)
|
||||
<option value="{{ $division->id }}" @selected(in_array($division->id, $selectedDivisions))>
|
||||
{{ $division->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedDivisions) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedDivisions)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Cities -->
|
||||
@if($cities->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Cities') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-32 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedCities(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple cities') }}">
|
||||
@foreach($cities->sortBy(function ($city) { return $city->translations->first()->name; }) as $city)
|
||||
<option value="{{ $city->id }}" @selected(in_array($city->id, $selectedCities))>
|
||||
{{ $city->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedCities) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedCities)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Districts -->
|
||||
@if($districts->count() > 0)
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ __('Districts / Neighborhoods') }}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">({{ __('Hold Ctrl/Cmd for multiple') }})</span>
|
||||
</label>
|
||||
<select multiple class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-24 focus:ring-blue-500 focus:border-blue-500"
|
||||
x-on:change="@this.setSelectedDistricts(Array.from($event.target.selectedOptions).map(o => Number(o.value)))"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple districts') }}">
|
||||
@foreach($districts->sortBy(function ($district) { return $district->translations->first()->name; }) as $district)
|
||||
<option value="{{ $district->id }}" @selected(in_array($district->id, $selectedDistricts))>
|
||||
{{ $district->translations->first()->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($selectedDistricts) > 0)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ __(':count selected', ['count' => count($selectedDistricts)]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Clear button -->
|
||||
@if(count($selectedCountries) > 0 || count($selectedDivisions) > 0 || count($selectedCities) > 0 || count($selectedDistricts) > 0)
|
||||
<div class="pt-2">
|
||||
<button type="button" wire:click="resetLocationFilter"
|
||||
class="text-sm text-red-600 hover:text-red-800 flex items-center space-x-1">
|
||||
<x-icon name="x-mark" class="w-4 h-4" />
|
||||
<span>{{ __('Clear all location filters') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Selection summary -->
|
||||
@if(count($selectedCountries) > 0 || count($selectedDivisions) > 0 || count($selectedCities) > 0 || count($selectedDistricts) > 0)
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">{{ __('Active Location Filters:') }}</h4>
|
||||
<div class="space-y-1 text-xs text-gray-600">
|
||||
@if(count($selectedCountries) > 0)
|
||||
<div>{{ __('Countries: :count selected', ['count' => count($selectedCountries)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedDivisions) > 0)
|
||||
<div>{{ __('Divisions: :count selected', ['count' => count($selectedDivisions)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedCities) > 0)
|
||||
<div>{{ __('Cities: :count selected', ['count' => count($selectedCities)]) }}</div>
|
||||
@endif
|
||||
@if(count($selectedDistricts) > 0)
|
||||
<div>{{ __('Districts: :count selected', ['count' => count($selectedDistricts)]) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,651 @@
|
||||
<div class="mt-12">
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
@can('create mailings')
|
||||
<x-jetstream.button wire:click="openCreateModal" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('Create Mailing') }}
|
||||
</x-jetstream.button>
|
||||
@endcan
|
||||
|
||||
<!-- Bulk Delete Button -->
|
||||
@can('delete mailings')
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$bulkDisabled"
|
||||
class=""
|
||||
title="{{ __('Delete') . ' ' . __('selection') }}"
|
||||
wire:click.prevent="openBulkDeleteModal">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="mb-6">
|
||||
<!-- Search box -->
|
||||
<div class="mb-4 flex items-center">
|
||||
<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 mailings') . '...' }}" type="text"
|
||||
wire:model.live="search">
|
||||
|
||||
@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="$set('search', '')">
|
||||
<x-icon mini name="backspace" solid />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<!-- Type Filter -->
|
||||
<div>
|
||||
<x-select :clearable="true" class="!w-60"
|
||||
placeholder="{{ __('Type') }}" wire:model.live="typeFilter">
|
||||
<x-select.option label="{{ __('Local Newsletter') }}" value="local_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter') }}" value="general_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter only subscription') }}" value="general_newsletter_only" />
|
||||
<x-select.option label="{{ __('System Message') }}" value="system_message" />
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<x-select :clearable="true" class="!w-60"
|
||||
placeholder="{{ __('Status') }}" wire:model.live="statusFilter">
|
||||
<x-select.option label="{{ __('Draft') }}" value="draft" />
|
||||
<x-select.option label="{{ __('Scheduled') }}" value="scheduled" />
|
||||
<x-select.option label="{{ __('Sending') }}" value="sending" />
|
||||
<x-select.option label="{{ __('Sent') }}" value="sent" />
|
||||
<x-select.option label="{{ __('Cancelled') }}" value="cancelled" />
|
||||
</x-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mailings 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-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<!-- Select all checkbox could go here -->
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('id')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('ID') }}</span>
|
||||
@if($sortField === 'id')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle w-32"
|
||||
wire:click="sortBy('title')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Title') }}</span>
|
||||
@if($sortField === 'title')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle w-20"
|
||||
wire:click="sortBy('type')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Type') }}</span>
|
||||
@if($sortField === 'type')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('status')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Status') }}</span>
|
||||
@if($sortField === 'status')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
|
||||
{{ __('Recipients') }}
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('scheduled_at')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Scheduled') }}</span>
|
||||
@if($sortField === 'scheduled_at')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
|
||||
wire:click="sortBy('updated_at')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Updated') }}</span>
|
||||
@if($sortField === 'updated_at')
|
||||
<span class="text-gray-400">@if($sortDirection === 'asc') ↑ @else ↓ @endif</span>
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle w-32">
|
||||
{{ __('Actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($mailings as $mailing)
|
||||
<tr>
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<input type="checkbox" value="{{ $mailing->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-400">
|
||||
{{ $mailing->id }}
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ $mailing->title }}</div>
|
||||
<div class="text-sm text-gray-500">{{ Str::limit($mailing->getSubjectForLocale(), 50) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
{{ ucfirst(str_replace('_', ' ', $mailing->type)) }}
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full
|
||||
@if($mailing->status === 'draft') bg-gray-100 text-gray-800
|
||||
@elseif($mailing->status === 'scheduled') bg-yellow-100 text-yellow-800
|
||||
@elseif($mailing->status === 'sending') bg-blue-100 text-blue-800
|
||||
@elseif($mailing->status === 'sent') bg-green-100 text-green-800
|
||||
@elseif($mailing->status === 'aborted') bg-red-100 text-red-800
|
||||
@else bg-theme-danger-light text-theme-danger-dark @endif">
|
||||
{{ ucfirst($mailing->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ number_format($mailing->getEffectiveRecipientsCount()) }}
|
||||
@if($mailing->sent_count > 0)
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ number_format($mailing->sent_count) }} {{ __('sent') }}
|
||||
@if($mailing->failed_count > 0)
|
||||
<br>{{ number_format($mailing->failed_count) }} {{ __('failed') }}
|
||||
@endif
|
||||
@if($mailing->bounced_count > 0)
|
||||
<br>{{ number_format($mailing->bounced_count) }} {{ __('bounced') }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
@if($mailing->scheduled_at)
|
||||
<div class="leading-tight">
|
||||
<div>{{ $mailing->scheduled_at->format('M j') }}</div>
|
||||
<div>{{ $mailing->scheduled_at->format('Y') }}</div>
|
||||
<div class="text-xs">{{ $mailing->scheduled_at->format('H:i') }}</div>
|
||||
</div>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($mailing->updatedByUser)
|
||||
<div class="relative block cursor-pointer flex-shrink-0"
|
||||
onclick="window.location='{{ $mailing->updatedByUser->profile_url }}'"
|
||||
title="{{ $mailing->updatedByUser->name }}">
|
||||
<img class="h-6 w-6 rounded-full profile-photo object-cover outline outline-1 outline-offset-0 outline-gray-600"
|
||||
src="{{ $mailing->updatedByUser->profile_photo_path ? Storage::url($mailing->updatedByUser->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>{{ $mailing->updated_at->format('M j') }}</div>
|
||||
<div>{{ $mailing->updated_at->format('Y') }}</div>
|
||||
<div class="text-xs">{{ $mailing->updated_at->format('H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex items-center justify-start space-x-1">
|
||||
|
||||
<!-- Edit Button -->
|
||||
@if(in_array($mailing->status, ['draft', 'scheduled']))
|
||||
<x-jetstream.secondary-button
|
||||
class=""
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click="openEditModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Preview Button -->
|
||||
@if(in_array($mailing->status, ['draft', 'scheduled', 'sending', 'sent']) && !empty($mailing->content_blocks))
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="sendTestMail({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendTestMail({{ $mailing->id }})"
|
||||
class=""
|
||||
title="{{ __('Preview and Send test mailing emails') }}">
|
||||
<span wire:loading.remove wire:target="sendTestMail({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="document-magnifying-glass" />
|
||||
</span>
|
||||
<span wire:loading wire:target="sendTestMail({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5 animate-spin" name="arrow-path" />
|
||||
</span>
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Send Button -->
|
||||
@if($mailing->canBeSent())
|
||||
<x-jetstream.secondary-button wire:click="openSendConfirmModal({{ $mailing->id }})"
|
||||
:disabled="!Gate::allows('send mailings')"
|
||||
class=""
|
||||
title="{{ Gate::allows('send mailings') ? __('Send Now') : __('No permission to send mailings') }}">
|
||||
<x-icon class="h-5 w-5" name="paper-airplane" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
|
||||
<!-- Stop Button (while sending) -->
|
||||
@if($mailing->status === 'sending')
|
||||
<x-jetstream.danger-button
|
||||
wire:click="openStopConfirmModal({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="openStopConfirmModal({{ $mailing->id }})"
|
||||
:disabled="!Gate::allows('send mailings')"
|
||||
class=""
|
||||
title="{{ Gate::allows('send mailings') ? __('Stop sending') : __('No permission to send mailings') }}">
|
||||
<x-icon class="h-5 w-5" name="stop-circle" solid />
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
|
||||
<!-- Report Button (all statuses) -->
|
||||
<x-jetstream.secondary-button
|
||||
wire:click="openReportModal({{ $mailing->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="openReportModal({{ $mailing->id }})"
|
||||
class=""
|
||||
title="{{ __('View Report') }}">
|
||||
<span wire:loading.remove wire:target="openReportModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5" name="list-bullet" />
|
||||
</span>
|
||||
<span wire:loading wire:target="openReportModal({{ $mailing->id }})">
|
||||
<x-icon class="h-5 w-5 animate-spin" name="arrow-path" />
|
||||
</span>
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
||||
{{ __('No mailings found.') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($mailings->hasPages())
|
||||
<div class="bg-white shadow-sm rounded-lg mt-4">
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $mailings->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showCreateModal" wire:key="showCreateModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Create mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.create-edit-form')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeModals">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="saveMailing" class="ml-3 bg-theme-brand">
|
||||
@if(isset($editingMailing) && $editingMailing)
|
||||
{{ __('Update mailings') }}
|
||||
@else
|
||||
@if($scheduledAt)
|
||||
{{ __('Schedule mailing') }}
|
||||
@else
|
||||
{{ __('Save as draft') }}
|
||||
@endif
|
||||
@endif
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<x-jetstream.dialog-modal wire:model.live="showEditModal" wire:key="showEditModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.create-edit-form')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeModals">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="saveMailing" class="ml-3 bg-theme-brand">
|
||||
@if(isset($editingMailing) && $editingMailing)
|
||||
{{ __('Update mailing') }}
|
||||
@else
|
||||
@if($scheduledAt)
|
||||
{{ __('Schedule mailing') }}
|
||||
@else
|
||||
{{ __('Save as draft') }}
|
||||
@endif
|
||||
@endif
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Post Selector Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showPostSelector" wire:key="showPostSelector" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Select posts for mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@include('livewire.mailings.partials.post-selector')
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showPostSelector', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="addSelectedPosts" class="ml-3 bg-theme-brand" :disabled="count($selectedPostIds) === 0">
|
||||
{{ __('Add selected posts') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Test Mail Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showTestMailModal" wire:key="showTestMailModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Test mail status') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="whitespace-pre-line text-sm text-gray-700">
|
||||
{{ $testMailMessage }}
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.button wire:click="$set('showTestMailModal', false)" class="bg-theme-brand">
|
||||
{{ __('OK') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Test Email Selection Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showTestEmailSelectionModal" wire:key="showTestEmailSelectionModal" maxWidth="4xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Preview your mailing') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
<!-- Email Preview - Top section -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700">{{ __('Email Preview') }}</h3>
|
||||
@if($mailingForTest)
|
||||
@php $previewLocale = $this->resolvePreviewLocale(); @endphp
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600">
|
||||
{{ strtoupper($previewLocale) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex justify-center -mx-[19px] sm:mx-0" wire:ignore>
|
||||
<div class="border border-gray-300 rounded-3xl overflow-hidden bg-white shadow-inner shadow-2xl w-full sm:w-auto" style="max-width: 322px; height: 456px;">
|
||||
<iframe
|
||||
id="mailing-preview-frame"
|
||||
style="width:100%;height:100%;border:0;background:#f4f4f4;"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@media (max-width: 639px) {
|
||||
#mailing-preview-frame {
|
||||
width: 125% !important;
|
||||
height: 125% !important;
|
||||
transform: scale(0.8);
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Selection - Bottom section -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">{{ __('Select Recipients') }}</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Choose which email addresses should receive the test mailing.') }}
|
||||
</p>
|
||||
|
||||
<!-- Available email options -->
|
||||
<div class="space-y-3">
|
||||
@if(isset($availableTestEmails['auth_user']))
|
||||
<label class="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model="sendToAuthUser"
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
|
||||
>
|
||||
<span class="text-sm text-gray-700">
|
||||
{{ $availableTestEmails['auth_user']['label'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
@if(isset($availableTestEmails['active_profile']))
|
||||
<label class="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model="sendToActiveProfile"
|
||||
class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
|
||||
>
|
||||
<span class="text-sm text-gray-700">
|
||||
{{ $availableTestEmails['active_profile']['label'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<!-- Custom email input -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-700 mb-2">
|
||||
{{ __('Custom email address(es), separated by comma:') }}
|
||||
</label>
|
||||
<div>
|
||||
<x-input
|
||||
wire:model="customTestEmail"
|
||||
type="text"
|
||||
placeholder="{{ __('Enter email address(es)') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($mailingForTest)
|
||||
<div class="mt-4 p-3 bg-theme-surface rounded-md border border-theme-border">
|
||||
<p class="text-sm text-theme-primary">
|
||||
<strong>{{ __('Mailing:') }}</strong> {{ $mailingForTest->title }}
|
||||
</p>
|
||||
<p class="text-sm text-theme-secondary mt-1">
|
||||
{{ __('Test emails will be sent in all available languages for this mailing.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error('testRecipients')
|
||||
<p class="mt-3 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@error('customTestEmail')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="cancelTestEmailSelection" class="mr-3">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button
|
||||
wire:click="sendTestMailToSelected"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendTestMailToSelected"
|
||||
class="bg-theme-brand"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sendTestMailToSelected">
|
||||
{{ __('Send test mailing') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="sendTestMailToSelected">
|
||||
{{ __('Sending...') }}
|
||||
</span>
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Bulk Delete Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showBulkDeleteModal" wire:key="showBulkDeleteModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete selected mailings') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Are you sure you want to delete the selected mailings?') }}<br>
|
||||
{{ __('Only draft and scheduled mailings will be deleted. This action cannot be undone.') }}
|
||||
<br><br>
|
||||
<strong>{{ count($bulkSelected) }} {{ __('mailing(s) selected') }}</strong>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showBulkDeleteModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button wire:click="bulkDeleteMailings" class="ml-3">
|
||||
{{ __('Delete selected') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Stop Sending Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showStopConfirmModal" wire:key="showStopConfirmModal" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Stop sending') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if($mailingToStop)
|
||||
{{ __('Are you sure you want to stop this mailing?') }}<br>
|
||||
{{ __('Emails already sent cannot be recalled. This action cannot be undone.') }}
|
||||
<br><br>
|
||||
<strong>{{ $mailingToStop->title }}</strong>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showStopConfirmModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button wire:click="stopMailing" wire:loading.attr="disabled" class="ml-3">
|
||||
{{ __('Stop sending') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Report Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showReportModal" wire:key="showReportModal" maxWidth="3xl" closeButton="true">
|
||||
<x-slot name="title">
|
||||
{{ __('Mailing Report') }}
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@if($mailingForReport)
|
||||
@if($mailingForReport->status === 'sending')
|
||||
<div wire:poll.5s="refreshReport">
|
||||
@include('livewire.mailings.partials.report-modal', ['mailing' => $mailingForReport])
|
||||
</div>
|
||||
@else
|
||||
@include('livewire.mailings.partials.report-modal', ['mailing' => $mailingForReport])
|
||||
@endif
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showReportModal', false)">
|
||||
{{ __('Close') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!-- Send Confirmation Modal -->
|
||||
<x-jetstream.dialog-modal wire:model.live="showSendConfirmModal" wire:key="showSendConfirmModal" maxWidth="2xl">
|
||||
|
||||
<x-slot name="title">
|
||||
{{ __('Send mailing') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@if($mailingToSend)
|
||||
{{ __('Are you sure you want to send this mailing?') }}<br>
|
||||
{{ __('This action cannot be undone and the mailing will be delivered immediately to all recipients.') }}
|
||||
<br><br>
|
||||
<strong>{{ __('Mailing') }}: {{ $mailingToSend->title }}</strong><br>
|
||||
<strong>{{ __('Recipients') }}: {{ number_format($mailingToSend->getEffectiveRecipientsCount()) }}</strong>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button no-spinner wire:click="$set('showSendConfirmModal', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button wire:click="sendMailing" class="ml-3 bg-green-600">
|
||||
{{ __('Send Now') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,585 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Information -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<x-jetstream.label for="title" value="{{ __('Mailing title') }}" />
|
||||
<x-input id="title" type="text" class="mt-1 block w-full" wire:model.defer="title" />
|
||||
<x-jetstream.input-error for="title" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-jetstream.label for="type" value="{{ __('Mailing type') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</div>
|
||||
<x-select id="type" class="mt-1 w-full" wire:model.live="type">
|
||||
<x-select.option label="{{ __('Select mailing type...') }}" value="" />
|
||||
<x-select.option label="{{ __('Local Newsletter') }}" value="local_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter') }}" value="general_newsletter" />
|
||||
<x-select.option label="{{ __('General Newsletter only subscription') }}" value="general_newsletter_only" />
|
||||
<x-select.option label="{{ __('System Message') }}" value="system_message" />
|
||||
</x-select>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
@if($type === 'local_newsletter')
|
||||
{{ __('Sent to users based on their location preferences') }}
|
||||
@elseif($type === 'general_newsletter')
|
||||
{{ __('Sent to all subscribed users and organizations') }}
|
||||
@elseif($type === 'general_newsletter_only')
|
||||
{{ __('Sent to profiles subscribed to the General Newsletter but not to the Local Newsletter') }}
|
||||
@elseif($type === 'system_message')
|
||||
{{ __('System announcements and important notices') }}
|
||||
@else
|
||||
{{ __('Please select a mailing type to continue') }}
|
||||
@endif
|
||||
</p>
|
||||
<x-jetstream.input-error for="type" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Blocks -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<x-jetstream.label value="{{ __('Mailing content') }}" />
|
||||
<x-jetstream.secondary-button wire:click="openPostSelector" class="text-sm">
|
||||
<x-icon name="plus" class="w-4 h-4 mr-1" />
|
||||
{{ __('Add Posts') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
|
||||
@if(empty($selectedPosts))
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
|
||||
<x-icon name="document-text" class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500">{{ __('No posts selected yet') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($selectedPosts as $index => $post)
|
||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 bg-theme-brand text-white rounded-full text-xs flex items-center justify-center">
|
||||
{{ $post['order'] }}
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900">{{ $post['title'] }}</h4>
|
||||
<p class="text-sm text-gray-500">Post ID: {{ $post['post_id'] }}</p>
|
||||
@if (in_array($post['post_id'], $limitedPollPostIds ?? []))
|
||||
<p class="mt-1 text-sm text-amber-700">{{ __('This poll is only for some members. Recipients outside its audience will see it in the mailing but cannot take part.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Move Up -->
|
||||
@if($index > 0)
|
||||
<button wire:click="movePostUp({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<x-icon name="chevron-up" class="w-4 h-4" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Move Down -->
|
||||
@if($index < count($selectedPosts) - 1)
|
||||
<button wire:click="movePostDown({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<x-icon name="chevron-down" class="w-4 h-4" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Remove -->
|
||||
<button wire:click="removePost({{ $index }})"
|
||||
class="p-1 text-red-400 hover:text-red-600">
|
||||
<x-icon name="trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-jetstream.input-error for="selectedPosts" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Subject (Multilingual) — shown only when posts are selected -->
|
||||
@if(!empty($selectedPosts))
|
||||
<div>
|
||||
<x-jetstream.label value="{{ __('Email Subject') }}" />
|
||||
<p class="mt-1 text-sm text-gray-500 mb-3">{{ __('Enter subject lines for each language based on your selected posts') }}</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
@foreach($availableLocales as $locale)
|
||||
@php $isRequired = in_array($locale, $requiredLocales); @endphp
|
||||
<div class="border rounded-lg p-3 {{ $isRequired ? 'border-gray-200' : 'border-dashed border-gray-200' }}">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ strtoupper($locale) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
({{ $locale === 'en' ? 'English' :
|
||||
($locale === 'nl' ? 'Nederlands' :
|
||||
($locale === 'de' ? 'Deutsch' :
|
||||
($locale === 'es' ? 'Español' :
|
||||
($locale === 'fr' ? 'Français' : $locale)))) }})
|
||||
</span>
|
||||
@if($locale === timebank_config('base_language', 'en'))
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-theme-surface text-theme-primary">
|
||||
{{ __('Primary') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($isRequired)
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-700">
|
||||
{{ __('Required') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-500">
|
||||
{{ __('Optional') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<x-input
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="{{ __('Subject line in :language', ['language' => strtoupper($locale)]) }}"
|
||||
wire:model.defer="subjects.{{ $locale }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Scheduling (requires send mailings permission) -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Scheduling') }}" class="{{ !Gate::allows('send mailings') ? 'opacity-50' : '' }}" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="md:w-1/2 {{ !Gate::allows('send mailings') ? 'opacity-50 pointer-events-none' : '' }}"
|
||||
x-data
|
||||
x-on:flatpickr-change.window="$wire.set('scheduledAt', $event.detail.value)"
|
||||
x-on:flatpickr-set-date.window="
|
||||
const fp = document.querySelector('[wire\\:key=\'flatpickr-scheduled-at\'] .flatpickr-input')?._flatpickr;
|
||||
if (fp) { fp.setDate($event.detail.value || null, false); }
|
||||
">
|
||||
<div wire:ignore wire:key="flatpickr-scheduled-at">
|
||||
<x-flatpickr
|
||||
showTime
|
||||
dateFormat="Y-m-d"
|
||||
timeFormat="H:i"
|
||||
altFormat="d-m-Y @ H:i"
|
||||
altInput
|
||||
placeholder="{{ Gate::allows('send mailings') ? __('Select a date and time') : __('No permission to schedule mailings') }}"
|
||||
onClose="(selectedDates, dateStr) => window.dispatchEvent(new CustomEvent('flatpickr-change', { detail: { value: dateStr || null } }))"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<x-jetstream.input-error for="scheduledAt" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Type Filtering -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Profile Type Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Filter by Profile Type Toggle -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByProfileType" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter recipients by profile type') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByProfileType)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Select which profile types should receive this mailing. You can select multiple types.') }}
|
||||
</p>
|
||||
|
||||
<!-- Profile Type Selection -->
|
||||
<div class="space-y-3">
|
||||
<x-jetstream.label value="{{ __('Profile Types') }}" class="font-medium" />
|
||||
|
||||
<select multiple
|
||||
class="block w-full border border-gray-300 rounded-md shadow-sm p-2 h-32 focus:ring-blue-500 focus:border-blue-500"
|
||||
wire:model.live="selectedProfileTypes"
|
||||
title="{{ __('Hold Ctrl/Cmd to select multiple profile types') }}">
|
||||
<option value="User">
|
||||
{{ __('Users') }} ({{ __('Individual profiles') }})
|
||||
</option>
|
||||
<option value="Organization">
|
||||
{{ __('Organizations') }}
|
||||
</option>
|
||||
<option value="Bank">
|
||||
{{ __('Banks') }}
|
||||
</option>
|
||||
<option value="Admin">
|
||||
{{ __('Admins') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
@if(count($selectedProfileTypes) > 0)
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($selectedProfileTypes as $profileType)
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-theme-surface text-theme-primary">
|
||||
@switch($profileType)
|
||||
@case('User')
|
||||
{{ __('Users') }}
|
||||
@break
|
||||
@case('Organization')
|
||||
{{ __('Organizations') }}
|
||||
@break
|
||||
@case('Bank')
|
||||
{{ __('Banks') }}
|
||||
@break
|
||||
@case('Admin')
|
||||
{{ __('Admins') }}
|
||||
@break
|
||||
@endswitch
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
<button type="button"
|
||||
wire:click="clearProfileTypes"
|
||||
class="text-xs text-gray-500 hover:text-gray-700">
|
||||
{{ __('Clear all') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ __('Tip: Hold Ctrl (Windows/Linux) or Cmd (Mac) while clicking to select multiple profile types') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if(count($selectedProfileTypes) > 0)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary">
|
||||
{{ __('Filtering by :count profile type(s)', ['count' => count($selectedProfileTypes)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Filtering -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Contact Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="filterByContacts,selectedContactProfiles" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByContacts" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter by contacts of...') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByContacts)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Only send to profiles that have transactions with the selected profile(s). The selected contacts will also be included.') }}
|
||||
</p>
|
||||
|
||||
<div wire:ignore>
|
||||
<livewire:profile.select-multiple-profiles
|
||||
wire:key="contact-profiles-selector-{{ $editingMailing->id ?? 'new' }}"
|
||||
:label="__('Select profiles')"
|
||||
:typesAvailable="[\App\Models\User::class, \App\Models\Organization::class, \App\Models\Bank::class]"
|
||||
eventName="selectedProfiles"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if(count($selectedContactProfiles) > 0)
|
||||
<div class="mt-3 space-y-2 max-w-md">
|
||||
@foreach($selectedContactProfiles as $index => $contactProfile)
|
||||
<div class="flex items-center justify-between rounded-md border border-theme-border bg-white px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<img src="{{ $contactProfile['profile_photo_path'] }}" class="w-8 rounded-full profile-photo">
|
||||
<div class="ml-2 leading-tight">
|
||||
<div class="text-sm font-semibold text-theme-primary">{{ $contactProfile['name'] }}</div>
|
||||
@php
|
||||
$contactMeta = implode(' · ', array_filter([$contactProfile['description'] ?? '', $contactProfile['location'] ?? '']));
|
||||
@endphp
|
||||
@if($contactMeta)
|
||||
<div class="text-xs text-theme-secondary">{{ $contactMeta }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="ml-3 text-theme-secondary hover:text-red-600 flex-shrink-0"
|
||||
wire:click="removeContactProfile({{ $index }})">
|
||||
<x-icon mini name="x-circle" />
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Filtering -->
|
||||
@if($type === 'local_newsletter')
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Location Filtering') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Filter by Location Toggle -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-checkbox wire:model.live="filterByLocation" />
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ __('Filter recipients by location') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($filterByLocation)
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
{{ __('Select a location to filter recipients. Only profiles with their primary location in the selected area will receive this mailing.') }}
|
||||
</p>
|
||||
|
||||
<!-- Location Filter Component -->
|
||||
<div wire:ignore>
|
||||
<livewire:mailings.location-filter
|
||||
wire:key="mailing-location-filter-{{ $editingMailing->id ?? 'new' }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if(count($selectedCountryIds) > 0 || count($selectedDivisionIds) > 0 || count($selectedCityIds) > 0 || count($selectedDistrictIds) > 0)
|
||||
@php
|
||||
$locationFilteredCount = collect($this->getCurrentRecipientCountsByLocale())->sum('count');
|
||||
@endphp
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg">
|
||||
<p class="text-sm font-medium text-theme-primary">
|
||||
{{ __('Estimated recipients with location filter: :count', ['count' => number_format($locationFilteredCount)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Fallback Locale -->
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 mb-3">
|
||||
<x-jetstream.label value="{{ __('Fallback language for missing translations') }}" />
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</div>
|
||||
<div class="md:w-1/2">
|
||||
@if(empty($fallbackEligibleLocales))
|
||||
<p class="text-sm text-gray-400 italic">{{ __('Select posts first to see available fallback languages.') }}</p>
|
||||
@else
|
||||
<x-select id="fallbackLocale" class="w-full" wire:model.live="fallbackLocale" placeholder="{{ __('Select an available language') }}">
|
||||
@foreach($fallbackEligibleLocales as $locale)
|
||||
@php $localeInfo = config('laravellocalization.supportedLocales.' . $locale); @endphp
|
||||
<x-select.option
|
||||
label="{{ strtoupper($locale) }}{{ $localeInfo ? ' — ' . $localeInfo['native'] : '' }}"
|
||||
value="{{ $locale }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Preview -->
|
||||
<div class="bg-theme-surface border border-theme-border rounded-lg p-4" wire:key="recipient-preview-{{ $type ?: 'no-type' }}">
|
||||
<h4 class="font-medium text-theme-primary mb-2">{{ __('Estimated Recipients') }}</h4>
|
||||
|
||||
@if($type)
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ __('messages.mailings.recipients_info.' . $type) }}
|
||||
@if($filterByLocation && (count($selectedCountryIds) > 0 || count($selectedDivisionIds) > 0 || count($selectedCityIds) > 0 || count($selectedDistrictIds) > 0))
|
||||
<br><strong>{{ __('messages.mailings.recipients_info.location_filtering_active') }}</strong>
|
||||
@endif
|
||||
</p>
|
||||
@else
|
||||
<p class="p-3">
|
||||
<strong>{{ __('messages.mailings.recipients_info.no_type_selected') }}</strong>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<!-- Real-time recipient count -->
|
||||
@if($type)
|
||||
@php
|
||||
$currentRecipientCount = collect($this->getCurrentRecipientCountsByLocale())->where('has_content', true)->sum('count');
|
||||
@endphp
|
||||
@if($currentRecipientCount > 0)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary flex items-center space-x-2">
|
||||
<span>{{ __('Current estimated recipients: :count', ['count' => number_format($currentRecipientCount)]) }}</span>
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-secondary flex items-center space-x-2">
|
||||
<span>{{ __('Current estimated recipients: 0') }}</span>
|
||||
<svg wire:loading.class.remove="hidden" wire:target="type,filterByProfileType,selectedProfileTypes,filterByLocation,fallbackLocale,selectedCountryIds,selectedDivisionIds,selectedCityIds,selectedDistrictIds" class="hidden animate-spin h-3.5 w-3.5 text-theme-brand flex-shrink-0" 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 12h4z"></path></svg>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($type)
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">
|
||||
{{ __('Recipients by Language & Content') }}
|
||||
</p>
|
||||
|
||||
@php
|
||||
$rowsByLocaleLocation = $this->getCurrentRecipientCountsByLocaleAndLocation();
|
||||
@endphp
|
||||
|
||||
@if(count($rowsByLocaleLocation) > 0)
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr class="text-theme-secondary font-medium">
|
||||
<th style="text-align: left; padding: 0 8px 4px 0; font-weight: 500;">{{ __('Language') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500;">{{ __('Posts') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500; white-space: nowrap;">{{ __('Location') }}</th>
|
||||
<th style="text-align: right; padding: 0 0.5rem 4px 8px; font-weight: 500;">{{ __('Recipients') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$prevLocale = null;
|
||||
$fallbackLanguageName = \App\Models\Language::where('lang_code', $fallbackLocale)->value('name') ?? strtoupper($fallbackLocale);
|
||||
@endphp
|
||||
@foreach($rowsByLocaleLocation as $row)
|
||||
@php
|
||||
$isFirstForLocale = $row['locale'] !== $prevLocale;
|
||||
$prevLocale = $row['locale'];
|
||||
$language = $isFirstForLocale ? \App\Models\Language::where('lang_code', $row['locale'])->first() : null;
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
$languageName = $language ? $language->name : strtoupper($row['locale']);
|
||||
$nativePosts = $row['content_blocks'] - ($row['fallback_posts'] ?? 0);
|
||||
$fallbackPosts = $row['fallback_posts'] ?? 0;
|
||||
$missingPosts = $row['missing_posts'] ?? 0;
|
||||
@endphp
|
||||
<tr>
|
||||
{{-- Col 1: Language — only on first row for this locale --}}
|
||||
<td style="padding: 2px 8px 2px 0; white-space: nowrap;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>{{ $flag }}</span>
|
||||
<span class="font-medium text-theme-primary">{{ $languageName }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
{{-- Col 2: Posts — only on first row for this locale --}}
|
||||
<td style="padding: 2px 8px;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 4px; flex-wrap: nowrap;">
|
||||
@if($nativePosts > 0)
|
||||
<span class="font-medium rounded bg-green-100 text-green-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $nativePosts }} {{ __('native') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($fallbackPosts > 0)
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $fallbackPosts }} {{ __('fallback in') }} {{ $fallbackLanguageName }}
|
||||
</span>
|
||||
@endif
|
||||
@if($missingPosts > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $missingPosts }} {{ __('skipped') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($nativePosts === 0 && $fallbackPosts === 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ __('No content') }}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
{{-- Col 3: Location --}}
|
||||
<td class="text-theme-secondary" style="padding: 2px 8px; white-space: nowrap;">
|
||||
{{ $row['location'] ?: __('Unknown') }}
|
||||
</td>
|
||||
{{-- Col 4: Recipients --}}
|
||||
<td class="text-theme-secondary" style="padding: 2px 0.5rem 2px 8px; text-align: right;">
|
||||
{{ number_format($row['count']) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@php
|
||||
$totalRecipients = collect($rowsByLocaleLocation)->sum('count');
|
||||
$skippedRecipients = collect($rowsByLocaleLocation)->where('has_content', false)->sum('count');
|
||||
$incompleteRecipients = collect($rowsByLocaleLocation)->where('has_content', true)->where('all_posts', false)->sum('count');
|
||||
$suppressedCount = $this->getCurrentSuppressedCount();
|
||||
@endphp
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-theme-border">
|
||||
<div class="flex justify-between text-xs" style="padding-right: 0.5rem;">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total recipients:') }}</span>
|
||||
<span class="text-theme-secondary">{{ number_format($totalRecipients) }}</span>
|
||||
</div>
|
||||
@if($suppressedCount > 0)
|
||||
<div class="flex justify-between text-xs mt-1" style="padding-right: 0.5rem;">
|
||||
<span class="text-theme-secondary">{{ __('Skipped (hard bounce):') }}</span>
|
||||
<span class="font-medium text-gray-700">{{ number_format($suppressedCount) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Recipients skipped entirely (zero posts) --}}
|
||||
@if($skippedRecipients > 0)
|
||||
@if($fallbackLocale)
|
||||
<p class="text-xs text-theme-secondary mt-2">
|
||||
{{ __('Some recipients will not receive the mailing because no content exists in their language or the fallback language.') }}
|
||||
</p>
|
||||
@else
|
||||
<div class="mt-2 p-2 bg-red-50 border border-red-200 rounded">
|
||||
<p class="text-xs font-semibold text-red-700">
|
||||
{{ __(':count recipient(s) will be skipped — no content in their language.', ['count' => number_format($skippedRecipients)]) }}
|
||||
</p>
|
||||
<p class="text-xs text-red-600 mt-1">
|
||||
{{ __('Set a fallback language above to include them.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Recipients who get the mailing but miss some posts --}}
|
||||
@if($incompleteRecipients > 0 && !$fallbackLocale)
|
||||
<div class="mt-2 p-2 bg-amber-50 border border-amber-200 rounded">
|
||||
<p class="text-xs font-semibold text-amber-700">
|
||||
{{ __(':count recipient(s) will not receive all posts — some are missing a translation in their language.', ['count' => number_format($incompleteRecipients)]) }}
|
||||
</p>
|
||||
<p class="text-xs text-amber-600 mt-1">
|
||||
{{ __('Set a fallback language above to include the missing posts.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-theme-secondary">{{ __('No recipients found for this mailing type.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Validation errors summary -->
|
||||
<x-errors />
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<div class="space-y-4">
|
||||
<!-- Search -->
|
||||
<div>
|
||||
<x-label for="postSearch" value="{{ __('Search Posts') }}" />
|
||||
<x-input id="postSearch" type="text" class="mt-1 block w-full"
|
||||
wire:model.live="postSearch"
|
||||
placeholder="{{ __('Search by title...') }}" />
|
||||
</div>
|
||||
|
||||
<!-- Available Posts -->
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
@if($availablePosts && $availablePosts->count() > 0)
|
||||
<div class="space-y-2">
|
||||
@foreach($availablePosts as $post)
|
||||
@php
|
||||
$translation = $post->translations->firstWhere('locale', app()->getLocale())
|
||||
?? $post->translations->first();
|
||||
$isSelected = in_array($post->id, $selectedPostIds);
|
||||
@endphp
|
||||
|
||||
@if($translation)
|
||||
<div class="flex items-start justify-between p-4 border border-gray-200 rounded-lg
|
||||
{{ $isSelected ? 'bg-theme-surface border-theme-border' : 'bg-white hover:bg-gray-50' }}">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start space-x-3">
|
||||
<!-- Checkbox -->
|
||||
<input type="checkbox"
|
||||
class="mt-1 rounded border-gray-300 text-theme-brand focus:border-theme-brand focus:ring focus:ring-theme-brand"
|
||||
wire:change="togglePostSelection({{ $post->id }})"
|
||||
{{ $isSelected ? 'checked' : '' }}>
|
||||
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-gray-900">{{ $translation->title }}</h4>
|
||||
|
||||
@if($translation->excerpt)
|
||||
<p class="text-sm text-gray-500 mt-1">{{ Str::limit($translation->excerpt, 100) }}</p>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-400">
|
||||
<span>ID: {{ $post->id }}</span>
|
||||
|
||||
@if($post->category)
|
||||
@php $catTrans = $post->category->translations->firstWhere('locale', app()->getLocale()) ?? $post->category->translations->first(); @endphp
|
||||
<span>{{ $catTrans->name ?? __('Uncategorized') }}</span>
|
||||
@endif
|
||||
|
||||
<span>{{ $post->updated_at->format('M j, Y') }}</span>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$publishedTranslations = $this->getPublishedTranslationsWithFlags($post);
|
||||
@endphp
|
||||
|
||||
<div class="mt-1 text-xs">
|
||||
@if($publishedTranslations->count() > 0)
|
||||
<span class="text-green-600">
|
||||
@foreach($publishedTranslations as $pubTranslation)
|
||||
<span title="{{ $pubTranslation['locale'] }}">{{ $pubTranslation['flag'] }}</span>
|
||||
@endforeach
|
||||
</span>
|
||||
@else
|
||||
<span class="text-yellow-600">Not currently published</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Indicator -->
|
||||
@if($post->category)
|
||||
<div class="ml-3 self-start">
|
||||
@php
|
||||
$categoryId = $post->category->id;
|
||||
$catTrans = $post->category->translations->firstWhere('locale', app()->getLocale()) ?? $post->category->translations->first();
|
||||
$categoryName = $catTrans->name ?? __('Uncategorized');
|
||||
|
||||
// Determine color based on category type
|
||||
$colorClass = 'bg-gray-100 text-gray-800'; // default
|
||||
if(in_array($categoryId, [4, 8])) $colorClass = 'bg-theme-surface text-theme-primary'; // news
|
||||
elseif($categoryId === 5) $colorClass = 'bg-green-100 text-green-800'; // article
|
||||
elseif(in_array($categoryId, [6, 7])) $colorClass = 'bg-theme-danger-light text-theme-danger-dark'; // event
|
||||
@endphp
|
||||
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full {{ $colorClass }}">
|
||||
{{ $categoryName }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8">
|
||||
<x-icon name="document-text" class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500 mb-2">
|
||||
@if($postSearch)
|
||||
{{ __('No posts found matching your search.') }}
|
||||
@else
|
||||
{{ __('No published posts available.') }}
|
||||
@endif
|
||||
</p>
|
||||
@if($postSearch)
|
||||
<p class="text-sm text-gray-400">{{ __('Try a different search term or clear the search to see all posts.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Selected Count -->
|
||||
@if(count($selectedPostIds) > 0)
|
||||
<div class="bg-theme-surface border border-theme-border rounded-lg p-3">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __(':count posts selected', ['count' => count($selectedPostIds)]) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,275 @@
|
||||
{{-- Mailing Report Modal Partial --}}
|
||||
@php
|
||||
$statusColors = [
|
||||
'draft' => 'bg-gray-100 text-gray-800',
|
||||
'scheduled' => 'bg-yellow-100 text-yellow-800',
|
||||
'sending' => 'bg-blue-100 text-blue-800',
|
||||
'sent' => 'bg-green-100 text-green-800',
|
||||
'aborted' => 'bg-red-100 text-red-800',
|
||||
'cancelled' => 'bg-red-100 text-red-800',
|
||||
];
|
||||
$statusColor = $statusColors[$mailing->status] ?? 'bg-gray-100 text-gray-800';
|
||||
@endphp
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-theme-primary">{{ $mailing->title }}</h3>
|
||||
<p class="text-xs text-theme-secondary mt-0.5">{{ $mailing->id }}</p>
|
||||
@if($mailing->sent_at || $mailing->scheduled_at)
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
@if($mailing->sent_at)
|
||||
{{ __('Sent') }}: {{ $mailing->sent_at->format('d M Y H:i') }}
|
||||
@else
|
||||
{{ __('Scheduled') }}: {{ $mailing->scheduled_at->format('d M Y H:i') }}
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
@if($mailing->sending_started_at)
|
||||
@php
|
||||
$endTime = $mailing->sent_at ?? now();
|
||||
$durationSeconds = (int) $mailing->sending_started_at->diffInSeconds($endTime, true);
|
||||
$durationHours = intdiv($durationSeconds, 3600);
|
||||
$durationMinutes = intdiv($durationSeconds % 3600, 60);
|
||||
$durationSecs = $durationSeconds % 60;
|
||||
$durationStr = $durationHours > 0
|
||||
? sprintf('%dh %dm %ds', $durationHours, $durationMinutes, $durationSecs)
|
||||
: ($durationMinutes > 0
|
||||
? sprintf('%dm %ds', $durationMinutes, $durationSecs)
|
||||
: sprintf('%ds', $durationSecs));
|
||||
@endphp
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
{{ __('Started sending') }}: {{ $mailing->sending_started_at->format('d M Y H:i') }}
|
||||
</p>
|
||||
<p class="text-xs text-theme-secondary mt-0.5">
|
||||
{{ $mailing->sent_at ? __('Duration') : __('Running for') }}: {{ $durationStr }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<span class="inline-flex px-2.5 py-0.5 text-xs font-medium rounded-full {{ $statusColor }}">
|
||||
{{ __(ucfirst($mailing->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Block 1: Estimated Recipients --}}
|
||||
<div class="p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Recipients by Language & Content') }}</p>
|
||||
|
||||
@php
|
||||
$rowsByLocaleLocation = $mailing->getRecipientCountsByLocaleAndLocation();
|
||||
$fallbackLocale = $mailing->fallback_locale ?? '';
|
||||
@endphp
|
||||
|
||||
@if(count($rowsByLocaleLocation) > 0)
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr class="text-theme-secondary font-medium">
|
||||
<th style="text-align: left; padding: 0 8px 4px 0; font-weight: 500;">{{ __('Language') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500;">{{ __('Posts') }}</th>
|
||||
<th style="text-align: left; padding: 0 8px 4px 8px; font-weight: 500; white-space: nowrap;">{{ __('Location') }}</th>
|
||||
<th style="text-align: right; padding: 0 0.5rem 4px 8px; font-weight: 500;">{{ __('Recipients') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$prevLocale = null;
|
||||
$fallbackLanguageName = \App\Models\Language::where('lang_code', $fallbackLocale)->value('name') ?? strtoupper($fallbackLocale);
|
||||
@endphp
|
||||
@foreach($rowsByLocaleLocation as $row)
|
||||
@php
|
||||
$isFirstForLocale = $row['locale'] !== $prevLocale;
|
||||
$prevLocale = $row['locale'];
|
||||
$language = $isFirstForLocale ? \App\Models\Language::where('lang_code', $row['locale'])->first() : null;
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
$languageName = $language ? $language->name : strtoupper($row['locale']);
|
||||
$nativePosts = $row['content_blocks'] - ($row['fallback_posts'] ?? 0);
|
||||
$fallbackPosts = $row['fallback_posts'] ?? 0;
|
||||
$missingPosts = $row['missing_posts'] ?? 0;
|
||||
@endphp
|
||||
<tr>
|
||||
<td style="padding: 2px 8px 2px 0; white-space: nowrap;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>{{ $flag }}</span>
|
||||
<span class="font-medium text-theme-primary">{{ $languageName }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="padding: 2px 8px;">
|
||||
@if($isFirstForLocale)
|
||||
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
@if($nativePosts > 0)
|
||||
<span class="font-medium rounded bg-green-100 text-green-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $nativePosts }} {{ __('native') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($fallbackPosts > 0)
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $fallbackPosts }} {{ __('fallback in') }} {{ $fallbackLanguageName }}
|
||||
</span>
|
||||
@endif
|
||||
@if($missingPosts > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ $missingPosts }} {{ __('skipped') }}
|
||||
</span>
|
||||
@endif
|
||||
@if($nativePosts === 0 && $fallbackPosts === 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px; white-space: nowrap;">
|
||||
{{ __('No content') }}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-theme-secondary" style="padding: 2px 8px; white-space: nowrap;">
|
||||
{{ $row['location'] ?: __('Unknown') }}
|
||||
</td>
|
||||
<td class="text-theme-secondary" style="padding: 2px 0.5rem 2px 8px; text-align: right;">
|
||||
{{ number_format($row['count']) }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@php
|
||||
$totalEstimated = collect($rowsByLocaleLocation)->sum('count');
|
||||
$suppressedCount = $mailing->getSuppressedRecipientsCount();
|
||||
@endphp
|
||||
<div class="mt-3 pt-2 border-t border-theme-border space-y-1">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total recipients:') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ number_format($totalEstimated) }}</span>
|
||||
</div>
|
||||
@if($suppressedCount > 0)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-theme-secondary">{{ __('Skipped (hard bounce):') }}</span>
|
||||
<span class="font-medium text-gray-700" style="padding-right: 0.5rem;">{{ number_format($suppressedCount) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-theme-secondary">{{ __('No recipients found for this mailing type.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Block 2: Send Results (only shown once sending has started) --}}
|
||||
@if(in_array($mailing->status, ['sending', 'sent', 'aborted']))
|
||||
@php
|
||||
$bounceStats = $this->getReportBounceStats();
|
||||
$recipientsCount = $mailing->recipients_count ?: 1;
|
||||
$processedCount = $mailing->sent_count + $mailing->failed_count + $mailing->skipped_count + $mailing->aborted_count;
|
||||
$progressPct = min(100, round($processedCount / $recipientsCount * 100));
|
||||
@endphp
|
||||
|
||||
<div class="p-3 bg-theme-surface rounded-lg border border-theme-border">
|
||||
<p class="text-sm font-medium text-theme-primary mb-3">{{ __('Send Results') }}</p>
|
||||
|
||||
{{-- Progress bar (while sending) --}}
|
||||
@if($mailing->status === 'sending')
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-xs text-theme-secondary mb-1">
|
||||
<span>{{ __('Progress') }}</span>
|
||||
<span>{{ number_format($processedCount) }} / {{ number_format($mailing->recipients_count) }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-theme-brand h-2 rounded-full transition-all" style="width: {{ $progressPct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Results table --}}
|
||||
<table class="w-full text-xs" style="border-collapse: collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Successfully sent') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;" class="text-theme-secondary">
|
||||
{{ number_format($mailing->sent_count) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Failed (delivery error)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
@if($mailing->failed_count > 0)
|
||||
<span class="font-medium rounded bg-theme-danger-light text-theme-danger-dark" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->failed_count) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-theme-secondary">0</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if($mailing->skipped_count > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">
|
||||
{{ __('Skipped (hard bounce)') }}
|
||||
</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-gray-100 text-gray-700" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->skipped_count) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($mailing->aborted_count > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Not sent (aborted)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-red-100 text-red-800" style="padding: 2px 6px;">
|
||||
{{ number_format($mailing->aborted_count) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['hard'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Bounced (hard)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-red-100 text-red-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['hard']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['soft'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Bounced (soft)') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-amber-100 text-amber-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['soft']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($bounceStats['complaint'] > 0)
|
||||
<tr>
|
||||
<td style="padding: 3px 8px 3px 0; white-space: nowrap;" class="text-theme-secondary">{{ __('Complaints') }}</td>
|
||||
<td style="padding: 3px 0 3px 8px; text-align: right; padding-right: 0.5rem;">
|
||||
<span class="font-medium rounded bg-orange-100 text-orange-800" style="padding: 2px 6px;">
|
||||
{{ number_format($bounceStats['complaint']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Totals --}}
|
||||
<div class="mt-3 pt-2 border-t border-theme-border space-y-1">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="font-medium text-theme-primary">{{ __('Total sent to:') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ number_format($mailing->recipients_count) }}</span>
|
||||
</div>
|
||||
@if($mailing->sent_count > 0 && $mailing->recipients_count > 0)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-theme-secondary">{{ __('Success rate') }}</span>
|
||||
<span class="text-theme-secondary" style="padding-right: 0.5rem;">{{ round($mailing->sent_count / $mailing->recipients_count * 100, 1) }}%</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
<div x-data="{
|
||||
isAccordionOpen: false,
|
||||
openCategory: null,
|
||||
toggleCategory(categoryId) {
|
||||
// Only allow one category to be open at a time
|
||||
if (this.openCategory === categoryId) {
|
||||
this.openCategory = null;
|
||||
} else {
|
||||
this.openCategory = categoryId;
|
||||
}
|
||||
}
|
||||
}" class="w-full">
|
||||
|
||||
<style>
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Accordion Header - Always Visible -->
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="isAccordionOpen = !isAccordionOpen"
|
||||
class="inline-flex items-center text-theme-muted hover:text-theme-primary px-4 ">
|
||||
<span class="text-sm ">
|
||||
{{ __('Browse categories') }}
|
||||
</span>
|
||||
<svg
|
||||
class="ml-1 w-4 h-4 transition-shadow duration-1000"
|
||||
:class="isAccordionOpen ? 'rotate-180' : ''"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Accordion Content - Hidden by Default -->
|
||||
<div
|
||||
x-show="isAccordionOpen"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 max-h-0"
|
||||
x-transition:enter-end="opacity-100 max-h-[2000px]"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 max-h-[2000px]"
|
||||
x-transition:leave-end="opacity-0 max-h-0"
|
||||
x-cloak
|
||||
class="overflow-hidden">
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
@if($tagCategories && count($tagCategories) > 0)
|
||||
<!-- Top-level categories in horizontal layout -->
|
||||
<div class="mt-4 mb-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($tagCategories as $category)
|
||||
<div class="relative">
|
||||
<div class="flex items-center space-x-1">
|
||||
@if(!empty($category['children']))
|
||||
<button
|
||||
x-on:click="toggleCategory({{ $category['id'] }})"
|
||||
class="bg-{{ $category['color'] }}-400 hover:bg-{{ $category['color'] }}-200 text-black inline-flex items-center rounded-md px-3 py-2 text-sm transition duration-150 ease-in-out cursor-pointer
|
||||
{{ in_array($category['id'], $selectedCategories) ? 'ring-1 ring-black ring-offset-2' : '' }}"
|
||||
wire:click="selectCategory({{ $category['id'] }})">
|
||||
{{ $category['name'] }}
|
||||
<svg class="ml-1 w-4 h-4" :class="openCategory === {{ $category['id'] }} ? 'rotate-180' : ''" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
wire:click="selectCategory({{ $category['id'] }})"
|
||||
class="bg-{{ $category['color'] }}-400 hover:bg-{{ $category['color'] }}-200 text-black inline-flex items-center rounded-md px-3 py-2 text-sm transition duration-150 ease-in-out cursor-pointer
|
||||
{{ in_array($category['id'], $selectedCategories) ? 'ring-1 ring-black ring-offset-2' : '' }}">
|
||||
{{ $category['name'] }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subcategories for the single expanded parent category -->
|
||||
@foreach($tagCategories as $category)
|
||||
@if(!empty($category['children']))
|
||||
<div x-show="openCategory === {{ $category['id'] }}"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@include('livewire.partials.category-tree-horizontal-multi', ['categories' => $category['children'], 'level' => 1])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@else
|
||||
<p class="text-theme-muted text-sm mt-4">{{ __('No categories available') }}</p>
|
||||
@endif
|
||||
|
||||
@if(!empty($selectedCategories))
|
||||
<div class="mt-4 pt-4">
|
||||
<div class="text-left">
|
||||
<span class="block font-medium text-sm text-theme-primary mb-2">{{ __('Selected categories') }}</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($selectedCategories as $selectedId)
|
||||
@php
|
||||
$selectedCategory = \App\Models\Category::find($selectedId);
|
||||
@endphp
|
||||
@if($selectedCategory)
|
||||
<span class="bg-{{ $selectedCategory->relatedColor }}-400 text-black inline-flex items-center rounded-md px-2 py-1 text-sm font-normal">
|
||||
{{ $selectedCategory->translation->name }}
|
||||
<button
|
||||
wire:click="removeCategory({{ $selectedId }})"
|
||||
class="ml-1 text-black hover:text-red-600 focus:outline-none">
|
||||
<svg class="w-2 h-2 ml-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end flex-wrap gap-3">
|
||||
<div class="text-xs text-theme-muted">
|
||||
{{ count($selectedCategories) }} {{ count($selectedCategories) === 1 ? __('category selected') : __('categories selected') }}
|
||||
</div>
|
||||
<x-jetstream.button wire:click="showCategoryResults">
|
||||
{{ __('Show') }}
|
||||
@if($total > 0)
|
||||
{{ $total }}
|
||||
@endif
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
<div wire:key="dashboard-component">
|
||||
<!-- Admin Section - Always visible for Admin profiles -->
|
||||
@if (getActiveProfileType() === 'Admin')
|
||||
@profile('admin')
|
||||
<div class="bg-theme-background p-6 sm:px-20">
|
||||
@if ($lastLoginAt)
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-2xs text-theme-light text-right">
|
||||
{{ __('Previous login') . ': ' . $lastLoginAt }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-xs text-theme-secondary">
|
||||
<div class="py-3"></div>
|
||||
<!-- Show online users for default guard -->
|
||||
<livewire:online-reacted-profiles
|
||||
:guards="['web', 'organization', 'bank', 'admin']"
|
||||
:header="true"
|
||||
:refresh-interval="10"
|
||||
/>
|
||||
<div class="py-3"></div>
|
||||
@php
|
||||
$webUser = \Illuminate\Support\Facades\Auth::guard('web')->user();
|
||||
$activeLogId = session('activeProfileId');
|
||||
$adminLogPermissions = [];
|
||||
if ($webUser && $activeLogId) {
|
||||
$logPrefix = 'Admin\\' . $activeLogId . '\\';
|
||||
$logScopedRoles = $webUser->roles->filter(fn($r) => str_starts_with($r->name, $logPrefix));
|
||||
foreach ($logScopedRoles as $logScopedRole) {
|
||||
$logRoleName = str_replace($logPrefix, '', $logScopedRole->name);
|
||||
$logAdminRole = \App\Models\AdminRole::where('name', $logRoleName)->first();
|
||||
if ($logAdminRole) {
|
||||
$adminLogPermissions = array_merge($adminLogPermissions, $logAdminRole->permissions ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
@if (in_array('view search-metrics', $adminLogPermissions) && timebank_config('search_optimization.search_analytics_enabled'))
|
||||
<livewire:admin.search-metrics />
|
||||
@endif
|
||||
@if (in_array('view application-log', $adminLogPermissions))
|
||||
<div class="py-6"></div>
|
||||
<livewire:admin.log/>
|
||||
@endif
|
||||
@if (in_array('view inactive-profiles-log', $adminLogPermissions))
|
||||
<div class="py-6"></div>
|
||||
<livewire:admin.log-viewer
|
||||
log-filename="inactive-profiles.log"
|
||||
log-title="{{ __('Inactive Profile Processing Log') }}" />
|
||||
@endif
|
||||
@if (in_array('view search-reindex-log', $adminLogPermissions))
|
||||
<div class="py-6"></div>
|
||||
<livewire:admin.log-viewer
|
||||
log-filename="scout-reindex.log"
|
||||
log-title="{{ __('Search Reindex Log') }}" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endprofile
|
||||
@else
|
||||
<!-- Non-Admin Section -->
|
||||
<div class="bg-theme-background p-6 sm:px-20">
|
||||
@if ($lastLoginAt)
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-2xs text-theme-light text-right">
|
||||
{{ __('Previous login') . ': ' . $lastLoginAt }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-theme-secondary">
|
||||
<div>
|
||||
@livewire('main-post', [
|
||||
'type' => $profileType ? 'SiteContents\Welcome\Login\\' . $profileType : null,
|
||||
'latest' => true,
|
||||
'fallbackTitle' => __(''),
|
||||
'fallbackDescription' => __('')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="my-3 md:my-12">
|
||||
@livewire('main-post', [
|
||||
'type' => $profileType ? 'SiteContents\Welcome\Login\\' . $profileType . '\\New' : null,
|
||||
'latest' => true,
|
||||
'fallbackTitle' => __('Welcome new ' . platform_user() . '!'),
|
||||
'fallbackDescription' => __('')
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<!-- Poll: first open poll for this profile that it has not answered yet -->
|
||||
<livewire:main-page.poll-card-full />
|
||||
|
||||
<!-- Call carousel -->
|
||||
<div class="bg-theme-background py-4">
|
||||
<livewire:main-page.call-card-carousel :related="true" :random="false" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Search -->
|
||||
<div class="border-b border-theme-primary bg-theme-background p-3 sm:px-20">
|
||||
<div class="mt-6 mb-3 grid grid-cols-1">
|
||||
<div class="mx-auto pb-6">
|
||||
<x-jetstream.application-mark class="h-12 md:h-20 xl:h-28 w-auto -mt-6 md:mt-0" />
|
||||
</div>
|
||||
<div class="max-w-[20rem] w-full mx-auto">
|
||||
<livewire:main-search-bar />
|
||||
</div>
|
||||
<div class="mx-auto text-center pt-2 pb-3 text-2xs text-theme-secondary">
|
||||
{{ __('Find profiles, skills, events and more...')}}
|
||||
<button
|
||||
wire:click="$dispatch('openSearchInfoModal')"
|
||||
class="ml-1 hover:text-theme-accent underline cursor-pointer">
|
||||
{{ __('info') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Browser -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="mx-auto mb-8">
|
||||
<livewire:main-browse-tag-categories />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Online users -->
|
||||
<div class="bg-theme-background p-3 sm:px-20">
|
||||
@if(timebank_config('online.contact_list.dashboard.enabled'))
|
||||
@if (session('activeProfileType') === 'App\Models\User' || session('activeProfileType') === 'App\Models\Organization' || session('activeProfileType') === 'App\Models\Bank')
|
||||
<livewire:online-reacted-profiles
|
||||
:guards="['web', 'organization', 'bank']"
|
||||
:header="true"
|
||||
:refresh-interval="10"
|
||||
/>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
<div class="border-b border-theme-primary bg-theme-background pb-3 pt-0 sm:px-20"></div>
|
||||
|
||||
|
||||
<!-- Skills --->
|
||||
@if (session('activeProfileType') !== 'App\Models\Admin')
|
||||
<div class="border-b border-theme-primary bg-theme-background p-6 sm:px-20">
|
||||
@include('post-header', ['title' => __('Update your skills')])
|
||||
<div class="my-6">
|
||||
<livewire:main-page.skills-card-full />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
|
||||
{{-- Mark postNr = 1 to view 2nd latest post or the 2nd upcoming event --}}
|
||||
{{-- Mark related as 1 to view also for users / organizations in related locations --}}
|
||||
<livewire:main-page.event-card-full :postNr=0 :related=1>
|
||||
<livewire:main-page.news-card-full :postNr=0 :related=1>
|
||||
<livewire:main-page.event-card-full :postNr=1 :related=0>
|
||||
<livewire:main-page.news-card-full :postNr=1 :related=0>
|
||||
<livewire:main-page.article-card-full :postNr=0 :related=1>
|
||||
<livewire:main-page.call-card-half :related="false" :random="true" :rows="2" />
|
||||
<livewire:main-page.article-card-full :postNr=1 :related=0>
|
||||
|
||||
{{-- <div class="bg-theme-secondary -my-9"></div> --}}
|
||||
|
||||
|
||||
<!-- Search Info Modal -->
|
||||
<livewire:search-info-modal />
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div>
|
||||
@if ($post != null)
|
||||
|
||||
<div class="relative flex flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px] my-9">
|
||||
<a class="relative flex cursor-pointer flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px]"
|
||||
href="{{ route('post.show_by_slug', [$post->slug]) }}">
|
||||
|
||||
<!-- Photo as background -->
|
||||
@if ($media != null)
|
||||
<div class="absolute inset-0 z-0 h-full w-full">
|
||||
{{ $media('4_3', ['class' => 'absolute inset-0 z-0 h-full w-full object-cover blur-[1px] saturate-[1.75]']) }}
|
||||
</div>
|
||||
@else
|
||||
<!-- Default background if no image -->
|
||||
<div class="absolute inset-0 z-0 h-full w-full bg-gradient-to-br from-primary-700 to-theme-brand"></div>
|
||||
@endif
|
||||
|
||||
<!-- Optional overlay for contrast -->
|
||||
<div class="absolute inset-0 z-10 bg-black bg-opacity-65"></div>
|
||||
|
||||
<!-- All card content on top of photo -->
|
||||
<div class="relative z-20 flex h-full flex-col justify-between px-6 md:px-10 lg:px-14 py-14 md:py-14 lg:py-20 text-white">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-4">
|
||||
<h2 class="text text-3xl lg:text-4xl font-semibold leading-tight pr-16 md:pr-20">
|
||||
{{ $post->title }}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="inline-block items-center rounded-sm bg-theme-brand px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal">
|
||||
{{ $post->category }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-6 my-4 flex items-center justify-center">
|
||||
@if (isset($post->excerpt))
|
||||
<p class="w-full text-base md:text-xl font-normal leading-relaxed line-clamp-8">
|
||||
{{ $post->excerpt }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<!-- Bottom section: author info -->
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<h2 class="text-lg md:text-2xl ml-auto pt-3 font-semibold text-white">
|
||||
{{ $post->author }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,156 @@
|
||||
<div>
|
||||
@if (count($calls) > 0)
|
||||
<div x-data="{
|
||||
navigate: true,
|
||||
atStart: true,
|
||||
atEnd: false,
|
||||
init() {
|
||||
this.$nextTick(() => this.updateState());
|
||||
this.$refs.track.addEventListener('scroll', () => this.updateState());
|
||||
},
|
||||
updateState() {
|
||||
const t = this.$refs.track;
|
||||
this.atStart = t.scrollLeft <= 0;
|
||||
this.atEnd = t.scrollLeft + t.offsetWidth >= t.scrollWidth - 1;
|
||||
},
|
||||
scrollLeft() {
|
||||
this.navigate = false;
|
||||
this.$refs.track.scrollBy({ left: -this.$refs.track.offsetWidth, behavior: 'smooth' });
|
||||
setTimeout(() => { this.navigate = true }, 600);
|
||||
},
|
||||
scrollRight() {
|
||||
this.navigate = false;
|
||||
this.$refs.track.scrollBy({ left: this.$refs.track.offsetWidth, behavior: 'smooth' });
|
||||
setTimeout(() => { this.navigate = true }, 600);
|
||||
},
|
||||
}" class="relative group">
|
||||
|
||||
{{-- Scrollable track --}}
|
||||
<div x-ref="track"
|
||||
class="overflow-x-auto"
|
||||
style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||
<div class="flex gap-3" style="width: max-content;">
|
||||
<div class="w-2 flex-shrink-0"></div>
|
||||
@foreach ($calls as $index => $result)
|
||||
<div @click="if (navigate) window.location='{{ route('call.show', ['id' => $result['id']]) }}'"
|
||||
class="relative flex flex-col justify-end overflow-hidden rounded-lg shadow-lg flex-shrink-0 cursor-pointer"
|
||||
style="width: calc((100vw - 3rem) / 3); min-width: 200px; max-width: 320px; height: 170px;">
|
||||
|
||||
{{-- Tag color background + overlay --}}
|
||||
<div class="absolute inset-0 z-0 bg-{{ $result['tag_color'] ?? 'gray' }}-400"></div>
|
||||
<div class="absolute inset-0 z-10 bg-black/50"></div>
|
||||
|
||||
{{-- Card content --}}
|
||||
<div class="relative z-20 flex h-full flex-col justify-between p-4 text-white">
|
||||
<div class="flex-1 min-w-0">
|
||||
{{-- Deepest tag category badge --}}
|
||||
@php $leafCat = !empty($result['tag_categories']) ? end($result['tag_categories']) : null; @endphp
|
||||
@if ($leafCat)
|
||||
<div class="mb-2">
|
||||
<a href="{{ route('search.show') }}?category={{ $leafCat['id'] }}"
|
||||
@click.stop
|
||||
class="bg-{{ $leafCat['color'] ?? 'gray' }}-400 inline-flex items-center rounded px-1.5 py-0.5 text-xs font-normal text-black hover:opacity-80">
|
||||
{{ $leafCat['name'] }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Title --}}
|
||||
<h3 class="text-lg font-semibold leading-tight truncate">
|
||||
{{ $result['title'] }}
|
||||
</h3>
|
||||
|
||||
{{-- Location + expiry badges --}}
|
||||
@if (!empty($result['location']) || !empty($result['expiry_badge_text']))
|
||||
<div class="mt-2 flex items-center gap-1 overflow-hidden">
|
||||
@if (!empty($result['location']))
|
||||
<span class="inline-block flex-shrink-0 rounded-sm bg-black px-1.5 pb-0.5 pt-0.5 text-xs font-normal uppercase text-white truncate max-w-[50%]">
|
||||
{{ $result['location'] }}
|
||||
</span>
|
||||
@endif
|
||||
@if (!empty($result['expiry_badge_text']))
|
||||
<span class="inline-block flex-shrink-0 rounded-sm bg-black px-1.5 pb-0.5 pt-0.5 text-xs font-normal uppercase text-white truncate max-w-[50%]">
|
||||
{{ $result['expiry_badge_text'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Callable avatar + name --}}
|
||||
@if (!empty($result['callable_name']))
|
||||
<div class="flex items-center gap-2 mt-3">
|
||||
@if (!empty($result['photo']))
|
||||
<div class="h-8 w-8 flex-shrink-0 rounded-full outline outline-1 outline-offset-1 outline-white/50 overflow-hidden">
|
||||
<img src="{{ $result['photo'] }}"
|
||||
alt="{{ $result['callable_name'] }}"
|
||||
class="h-full w-full object-cover" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-xs font-medium truncate">{{ $result['callable_name'] }}</span>
|
||||
@if (!empty($result['callable_location']))
|
||||
<span class="text-xs text-white/70 truncate">{{ $result['callable_location'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Prioritisation score --}}
|
||||
@if ($showScore)
|
||||
<div class="absolute bottom-3 right-3 z-30 text-xs text-white/70">
|
||||
{{ round($result['score'], 2) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Reaction button --}}
|
||||
@if ($showReactions ?? true)
|
||||
<div class="absolute top-3 right-3 z-30">
|
||||
@livewire('reaction-button', [
|
||||
'typeName' => 'like',
|
||||
'showCounter' => true,
|
||||
'reactionCounter' => $result['like_count'],
|
||||
'modelClass' => $result['model'],
|
||||
'modelId' => $result['id'],
|
||||
'size' => 'w-5 h-5',
|
||||
'inverseColors' => true,
|
||||
], key('like-carousel-' . $result['id'] . '-' . $index))
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="w-2 flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Left button — hidden at start --}}
|
||||
<button type="button"
|
||||
@mousedown.prevent.stop
|
||||
@click.prevent.stop="scrollLeft()"
|
||||
x-show="!atStart"
|
||||
x-cloak
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 z-20 flex h-9 w-9 items-center justify-center rounded-full bg-black text-white transition-all opacity-0 group-hover:opacity-100 group-hover:outline group-hover:outline-2 group-hover:outline-white group-hover:shadow-lg hover:outline-1"
|
||||
aria-label="{{ __('Scroll left') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{-- Right button — hidden at end --}}
|
||||
<button type="button"
|
||||
@mousedown.prevent.stop
|
||||
@click.prevent.stop="scrollRight()"
|
||||
x-show="!atEnd"
|
||||
x-cloak
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-9 w-9 items-center justify-center rounded-full bg-black text-white transition-all opacity-0 group-hover:opacity-100 group-hover:outline group-hover:outline-2 group-hover:outline-white group-hover:shadow-lg hover:outline-1"
|
||||
aria-label="{{ __('Scroll right') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
<div>
|
||||
@if ($call !== null)
|
||||
|
||||
<div class="relative flex flex-col justify-end overflow-hidden bg-theme-background my-9">
|
||||
<a class="relative flex cursor-pointer flex-col justify-end overflow-hidden bg-{{ $call['tag_color'] ?? 'gray' }}-400"
|
||||
href="{{ route('call.show', ['id' => $call['id']]) }}">
|
||||
|
||||
<!-- Tag color background + overlay -->
|
||||
<div class="absolute inset-0 z-0 bg-{{ $call['tag_color'] ?? 'gray' }}-400"></div>
|
||||
<div class="absolute inset-0 z-10 bg-black/50"></div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="relative z-20 flex h-full flex-col justify-between px-6 md:px-10 lg:px-14 py-14 md:py-14 lg:py-20 text-white">
|
||||
<div class="space-y-3">
|
||||
|
||||
{{-- Deepest tag category badge only --}}
|
||||
@php $deepestCat = !empty($call['tag_categories']) ? last($call['tag_categories']) : null; @endphp
|
||||
@if ($deepestCat)
|
||||
<div class="flex flex-wrap items-center gap-2 pr-16 md:pr-20">
|
||||
<span onclick="event.preventDefault(); event.stopPropagation(); window.location='{{ route('search.show') }}?category={{ $deepestCat['id'] }}'"
|
||||
class="bg-{{ $deepestCat['color'] ?? 'gray' }}-400 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-normal text-black hover:opacity-80">
|
||||
{{ $deepestCat['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Title --}}
|
||||
<h2 class="text text-3xl lg:text-4xl font-semibold leading-tight pr-16 md:pr-20 line-clamp-2">
|
||||
{{ $call['title'] }}
|
||||
</h2>
|
||||
|
||||
{{-- Location + expiry badges --}}
|
||||
@if (!empty($call['location']) || !empty($call['expiry_badge_text']))
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if (!empty($call['location']))
|
||||
<h4 class="inline-block items-center rounded-sm bg-black px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal uppercase text-white">
|
||||
{{ $call['location'] }}
|
||||
</h4>
|
||||
@endif
|
||||
@if (!empty($call['expiry_badge_text']))
|
||||
<h4 class="inline-block items-center rounded-sm bg-black px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal uppercase text-white">
|
||||
{{ $call['expiry_badge_text'] }}
|
||||
</h4>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Excerpt --}}
|
||||
@if (!empty($call['excerpt']))
|
||||
<div class="mx-6 my-4 flex items-center justify-center">
|
||||
<p class="w-full text-base md:text-xl font-normal leading-relaxed line-clamp-2">
|
||||
{{ $call['excerpt'] }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Callable avatar + name + location --}}
|
||||
@if (!empty($call['callable_name']))
|
||||
<div class="flex flex-wrap items-end gap-4 mt-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@if (!empty($call['photo']))
|
||||
<div class="h-16 w-16 flex-shrink-0 rounded-full outline outline-1 outline-offset-1 outline-white/50 overflow-hidden">
|
||||
<img src="{{ $call['photo'] }}"
|
||||
alt="{{ $call['callable_name'] }}"
|
||||
class="h-full w-full object-cover" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="font-medium">{{ $call['callable_name'] }}</span>
|
||||
@if (!empty($call['callable_location']))
|
||||
<span class="text-white/80">{{ $call['callable_location'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Like button positioned absolutely outside anchor tag -->
|
||||
<div class="absolute top-14 right-4 md:right-10 lg:right-14 z-30">
|
||||
@livewire('reaction-button', [
|
||||
'typeName' => 'like',
|
||||
'showCounter' => true,
|
||||
'reactionCounter' => $call['like_count'],
|
||||
'modelClass' => $call['model'],
|
||||
'modelId' => $call['id'],
|
||||
'size' => 'w-10 h-10',
|
||||
'inverseColors' => true,
|
||||
], key('like-call-main-' . $call['id']))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<div>
|
||||
@if (!empty($calls))
|
||||
<div class="flex flex-col gap-6 max-w-7xl mx-auto mt-6 mb-20 px-4 lg:px-8">
|
||||
@foreach (collect($calls)->chunk(2) as $row)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@foreach ($row as $index => $call)
|
||||
<x-call-card
|
||||
:result="$call"
|
||||
:index="$index"
|
||||
:show-score="$showScore"
|
||||
:show-callable="true"
|
||||
:show-reactions="$showReactions ?? true"
|
||||
:photo-blur="$guestPhotoBlur ?? 0"
|
||||
:photo-contrast="$guestPhotoContrast ?? 100"
|
||||
:photo-saturate="$guestPhotoSaturate ?? 100"
|
||||
:photo-brightness="$guestPhotoBrightness ?? 100"
|
||||
height-class="h-[430px] md:h-[550px] lg:h-[430px]"
|
||||
:truncate-excerpt="true"
|
||||
/>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<div>
|
||||
@if ($post != null)
|
||||
|
||||
<div class="relative flex flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px] my-9">
|
||||
<a class="relative flex cursor-pointer flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px]"
|
||||
href="{{ route('post.show_by_slug', [$post->slug]) }}">
|
||||
|
||||
<!-- Photo as background -->
|
||||
@if ($media != null)
|
||||
<div class="absolute inset-0 z-0 h-full w-full">
|
||||
{{ $media('4_3', ['class' => 'absolute inset-0 z-0 h-full w-full object-cover blur-[1px] saturate-[1.75]']) }}
|
||||
</div>
|
||||
@else
|
||||
<!-- Default background if no image -->
|
||||
<div class="absolute inset-0 z-0 h-full w-full bg-gradient-to-br from-primary-700 to-theme-brand"></div>
|
||||
@endif
|
||||
|
||||
<!-- Optional overlay for contrast -->
|
||||
<div class="absolute inset-0 z-10 bg-black bg-opacity-65"></div>
|
||||
|
||||
<!-- All card content on top of photo -->
|
||||
<div class="relative z-20 flex h-full flex-col justify-between px-6 md:px-10 lg:px-14 py-14 md:py-14 lg:py-20 text-white">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-4">
|
||||
<h2 class="text text-3xl lg:text-4xl font-semibold leading-tight pr-16 md:pr-20">
|
||||
{{ $post->title }}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="inline-block items-center rounded-sm bg-theme-brand px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal">
|
||||
{{ $post->category }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-6 my-4 flex items-center justify-center">
|
||||
@if (isset($post->excerpt))
|
||||
<p class="w-full text-base md:text-xl font-normal leading-relaxed line-clamp-8">
|
||||
{{ $post->excerpt }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<!-- Bottom section: author and address -->
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<h2 class="text-lg md:text-2xl ml-auto pt-3 font-semibold text-white">
|
||||
@if (isset($post->venue))
|
||||
{{ $post->venue . ' ' . $post->city}}
|
||||
@endif
|
||||
@if (isset($post->from))
|
||||
<span class="text-center font-semibold">
|
||||
{{ Illuminate\Support\Carbon::parse($post->from)->translatedFormat('j F') }}
|
||||
{{ Illuminate\Support\Carbon::parse($post->from)->format('H:i') }}{{ isset($post->till) && $post->till ? ' - ' . Illuminate\Support\Carbon::parse($post->till)->format('H:i') : '' }} {{ __('messages.hour_abbrevation') }}
|
||||
</span>
|
||||
@endif
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Like button positioned absolutely outside anchor tag -->
|
||||
<div class="absolute top-14 right-4 md:right-10 lg:right-14 z-30">
|
||||
@livewire('reaction-button', [
|
||||
'typeName' => 'like',
|
||||
'showCounter' => true,
|
||||
'reactionCounter' => $post['like_count'],
|
||||
'modelClass' => $post['model'],
|
||||
'modelId' => $post['id'],
|
||||
'size' => 'w-10 h-10',
|
||||
'inverseColors' => true,
|
||||
], key('like-' . $post['model'] . '-' . $post['id'] . '-' . $postNr))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,135 @@
|
||||
<div x-data="{
|
||||
showFullscreen: false,
|
||||
async toggleFullscreen() {
|
||||
if (!this.showFullscreen) {
|
||||
this.showFullscreen = true;
|
||||
await this.$nextTick();
|
||||
const elem = this.$refs.fullscreenModal;
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
this.showFullscreen = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
@fullscreenchange.window="if (!document.fullscreenElement) { showFullscreen = false }"
|
||||
@webkitfullscreenchange.window="if (!document.webkitFullscreenElement) { showFullscreen = false }"
|
||||
@msfullscreenchange.window="if (!document.msFullscreenElement) { showFullscreen = false }">
|
||||
@if ($post != null)
|
||||
|
||||
<div class="relative flex flex-col bg-theme-background my-9">
|
||||
<!-- Caption overlay calculation -->
|
||||
@php
|
||||
$bottomText = '';
|
||||
$isLink = false;
|
||||
$linkUrl = '';
|
||||
$mediaOwner = isset($post->media_owner) && !empty($post->media_owner) ? $post->media_owner : '';
|
||||
|
||||
if (isset($post->author_id) && !empty($post->author_id)) {
|
||||
$bottomText = $post->author;
|
||||
$isLink = true;
|
||||
$linkUrl = $post->author->profile_url;
|
||||
} elseif (isset($post->media_caption) && !empty($post->media_caption)) {
|
||||
$bottomText = $post->media_caption;
|
||||
} elseif (isset($post->content) && !empty($post->content)) {
|
||||
$bottomText = strip_tags($post->content);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="relative flex flex-col justify-center overflow-hidden bg-theme-background {{ !$media ? 'pt-32 pb-32' : '' }}">
|
||||
|
||||
<!-- Photo - full width within container -->
|
||||
@if ($media != null)
|
||||
<div @click="toggleFullscreen()" class="w-full mx-auto max-w-7xl cursor-pointer">
|
||||
{{ $media('hero', ['class' => 'w-full h-auto saturate-[1.25]']) }}
|
||||
</div>
|
||||
|
||||
<!-- Caption overlaid on image - positioned like author in article card -->
|
||||
@if (!empty($bottomText) || !empty($mediaOwner))
|
||||
<div class="absolute bottom-0 w-full">
|
||||
<div class="w-full mx-auto max-w-7xl px-6 md:px-10 lg:px-14 py-4 md:py-6 lg:py-8">
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
@if (!empty($bottomText))
|
||||
<h4 class="inline-flex items-center gap-2 rounded-sm bg-theme-brand px-3 pb-1.5 pt-1 text-base lg:text-xl font-normal text-white">
|
||||
@if ($isLink && isset($post->author_profile_photo_path) && $post->author_profile_photo_path)
|
||||
<a href="{{ $linkUrl }}" @click.stop class="flex-shrink-0">
|
||||
<img src="{{ Storage::url($post->author_profile_photo_path) }}"
|
||||
alt="{{ $bottomText }}"
|
||||
class="w-5 h-5 lg:w-6 lg:h-6 rounded-full profile-photo object-cover">
|
||||
</a>
|
||||
@endif
|
||||
@if ($isLink)
|
||||
<a href="{{ $linkUrl }}" @click.stop class="text-white hover:underline">
|
||||
{{ $bottomText }}
|
||||
</a>
|
||||
@else
|
||||
{{ $bottomText }}
|
||||
@endif
|
||||
</h4>
|
||||
@endif
|
||||
@if (!empty($mediaOwner))
|
||||
<h6 class="inline-block items-center rounded-sm bg-theme-brand mt-1 px-2 pb-0.5 pt-0.5 text-xs lg:text-sm font-normal text-white">
|
||||
{{ $mediaOwner }}
|
||||
</h6>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Excerpt text centered - big like article title (only if no media) -->
|
||||
@elseif (isset($post->excerpt) && !empty($post->excerpt))
|
||||
<div class="flex items-center justify-center px-6 md:px-10 lg:px-14">
|
||||
<h2 class="text-3xl lg:text-5xl font-semibold leading-tight text-center text-theme-text-background bg-theme-brand p-6 m-8">
|
||||
{{ $post->excerpt }}
|
||||
</h2>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full-screen modal -->
|
||||
<div x-show="showFullscreen"
|
||||
x-ref="fullscreenModal"
|
||||
@click="toggleFullscreen()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black"
|
||||
style="display: none;">
|
||||
|
||||
@if ($media != null)
|
||||
<div class="relative w-full h-full flex items-center justify-center p-4">
|
||||
<!-- Close button -->
|
||||
{{-- <button @click="toggleFullscreen()"
|
||||
class="absolute top-4 right-4 z-10 text-white hover:text-gray-300 transition-colors">
|
||||
<svg class="w-8 h-8" 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"></path>
|
||||
</svg>
|
||||
</button> --}}
|
||||
|
||||
<!-- Full-screen image - use first (largest) responsive image -->
|
||||
@php
|
||||
// Get the largest responsive image URL (first in array = largest)
|
||||
$responsiveImages = $media->getResponsiveImageUrls('hero');
|
||||
$largestImageUrl = !empty($responsiveImages) ? reset($responsiveImages) : $media->getUrl('hero');
|
||||
@endphp
|
||||
<img src="{{ $largestImageUrl }}"
|
||||
alt="{{ $post->title ?? '' }}"
|
||||
class="max-w-full max-h-full object-contain"
|
||||
loading="eager">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,127 @@
|
||||
<div x-data="{
|
||||
showFullscreen: false,
|
||||
async toggleFullscreen() {
|
||||
if (!this.showFullscreen) {
|
||||
this.showFullscreen = true;
|
||||
await this.$nextTick();
|
||||
const elem = this.$refs.fullscreenModal;
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
this.showFullscreen = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
@fullscreenchange.window="if (!document.fullscreenElement) { showFullscreen = false }"
|
||||
@webkitfullscreenchange.window="if (!document.webkitFullscreenElement) { showFullscreen = false }"
|
||||
@msfullscreenchange.window="if (!document.msFullscreenElement) { showFullscreen = false }">
|
||||
@if ($post != null)
|
||||
|
||||
<div class="relative flex flex-col bg-theme-background">
|
||||
<!-- Caption overlay calculation -->
|
||||
@php
|
||||
$bottomText = '';
|
||||
$isLink = false;
|
||||
$linkUrl = '';
|
||||
$mediaOwner = isset($post->media_owner) && !empty($post->media_owner) ? $post->media_owner : '';
|
||||
|
||||
if (isset($post->author_id) && !empty($post->author_id)) {
|
||||
$bottomText = $post->author;
|
||||
$isLink = true;
|
||||
$linkUrl = $post->author->profile_url;
|
||||
} elseif (isset($post->media_caption) && !empty($post->media_caption)) {
|
||||
$bottomText = $post->media_caption;
|
||||
} elseif (isset($post->content) && !empty($post->content)) {
|
||||
$bottomText = strip_tags($post->content);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="relative flex flex-col justify-center overflow-hidden bg-theme-background {{ !$media ? 'pt-32 pb-32' : '' }}">
|
||||
|
||||
<!-- Photo - full width within container -->
|
||||
@if ($media != null)
|
||||
<div @click="toggleFullscreen()" class="w-full mx-auto max-w-7xl cursor-pointer">
|
||||
{{ $media('hero', ['class' => 'w-full h-auto saturate-[1.75]']) }}
|
||||
</div>
|
||||
|
||||
<!-- Caption overlaid on image - positioned like author in article card -->
|
||||
@if (!empty($bottomText) || !empty($mediaOwner))
|
||||
<div class="absolute bottom-0 w-full">
|
||||
<div class="w-full mx-auto max-w-7xl px-6 md:px-10 lg:px-14 py-4 md:py-6 lg:py-8">
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
@if (!empty($bottomText))
|
||||
<h4 class="inline-flex items-center gap-2 rounded-sm bg-theme-brand px-3 pb-1.5 pt-1 text-base lg:text-xl font-normal text-white">
|
||||
@if ($isLink && isset($post->author_profile_photo_path) && $post->author_profile_photo_path)
|
||||
<a href="{{ $linkUrl }}" @click.stop class="flex-shrink-0">
|
||||
<img src="{{ Storage::url($post->author_profile_photo_path) }}"
|
||||
alt="{{ $bottomText }}"
|
||||
class="w-5 h-5 lg:w-6 lg:h-6 rounded-full profile-photo object-cover">
|
||||
</a>
|
||||
@endif
|
||||
@if ($isLink)
|
||||
<a href="{{ $linkUrl }}" @click.stop class="text-white hover:underline">
|
||||
{{ $bottomText }}
|
||||
</a>
|
||||
@else
|
||||
{{ $bottomText }}
|
||||
@endif
|
||||
</h4>
|
||||
@endif
|
||||
@if (!empty($mediaOwner))
|
||||
<h6 class="inline-block items-center rounded-sm bg-theme-brand mt-1 px-2 pb-0.5 pt-0.5 text-xs lg:text-sm font-normal text-white">
|
||||
{{ $mediaOwner }}
|
||||
</h6>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Excerpt text centered - big like article title (only if no media) -->
|
||||
@elseif (isset($post->excerpt) && !empty($post->excerpt))
|
||||
<div class="flex items-center justify-center px-6 md:px-10 lg:px-14">
|
||||
<h2 class="text-3xl lg:text-5xl font-semibold leading-tight text-center text-theme-text-background bg-theme-brand p-6 m-8">
|
||||
{{ $post->excerpt }}
|
||||
</h2>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full-screen modal -->
|
||||
<div x-show="showFullscreen"
|
||||
x-ref="fullscreenModal"
|
||||
@click="toggleFullscreen()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black"
|
||||
style="display: none;">
|
||||
|
||||
@if ($media != null)
|
||||
<div class="relative w-full h-full flex items-center justify-center p-4">
|
||||
<!-- Full-screen image - use first (largest) responsive image -->
|
||||
@php
|
||||
// Get the largest responsive image URL (first in array = largest)
|
||||
$responsiveImages = $media->getResponsiveImageUrls('hero');
|
||||
$largestImageUrl = !empty($responsiveImages) ? reset($responsiveImages) : $media->getUrl('hero');
|
||||
@endphp
|
||||
<img src="{{ $largestImageUrl }}"
|
||||
alt="{{ $post->title ?? '' }}"
|
||||
class="max-w-full max-h-full object-contain"
|
||||
loading="eager">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
<div>
|
||||
@if ($post != null)
|
||||
|
||||
<div class="relative flex flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px] my-9">
|
||||
<a class="relative flex cursor-pointer flex-col justify-end overflow-hidden bg-theme-background max-h-[600px] lg:max-h-[800px]"
|
||||
href="{{ route('post.show_by_slug', [$post->slug]) }}">
|
||||
|
||||
<!-- Photo as background -->
|
||||
@if ($media != null)
|
||||
<div class="absolute inset-0 z-0 h-full w-full">
|
||||
{{ $media('4_3', ['class' => 'absolute inset-0 z-0 h-full w-full object-cover blur-[1px] saturate-[1.75]']) }}
|
||||
</div>
|
||||
@else
|
||||
<!-- Default background if no image -->
|
||||
<div class="absolute inset-0 z-0 h-full w-full bg-gradient-to-br from-primary-700 to-theme-brand"></div>
|
||||
@endif
|
||||
|
||||
<!-- Optional overlay for contrast -->
|
||||
<div class="absolute inset-0 z-10 bg-black bg-opacity-65"></div>
|
||||
|
||||
<!-- All card content on top of photo -->
|
||||
<div class="relative z-20 flex h-full flex-col justify-between px-6 md:px-10 lg:px-14 py-14 md:py-14 lg:py-20 text-white">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-4">
|
||||
<h2 class="text text-3xl lg:text-4xl font-semibold leading-tight pr-16 md:pr-20">
|
||||
{{ $post->title }}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="inline-block items-center rounded-sm bg-theme-brand px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal">
|
||||
{{ $post->category }}
|
||||
</h4>
|
||||
<h4 class="inline-block items-center rounded-sm bg-theme-brand ml-3 px-2 pb-1 pt-0.5 text-sm lg:text-base font-normal">
|
||||
@if (isset($post->from))
|
||||
<span class="">
|
||||
{{ Illuminate\Support\Carbon::parse($post->from)->translatedFormat('l, j F') }}
|
||||
</span>
|
||||
@endif
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-6 my-4 flex items-center justify-center">
|
||||
@if (isset($post->excerpt))
|
||||
<p class="w-full text-base md:text-xl font-normal leading-relaxed line-clamp-8">
|
||||
{{ $post->excerpt }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<!-- Bottom section: author info -->
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<h2 class="text-lg md:text-2xl ml-auto pt-3 font-semibold text-white">
|
||||
{{ $post->author }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div>
|
||||
@if ($pollId)
|
||||
<div class="border-b border-theme-primary bg-theme-background p-6 sm:px-20">
|
||||
@include('post-header', ['title' => __('Poll')])
|
||||
<div class="my-6 space-y-4">
|
||||
<div>
|
||||
<a href="{{ route('poll.show', ['slug' => $slug]) }}" class="text-xl font-semibold text-theme-primary hover:text-theme-secondary">
|
||||
{{ $title }}
|
||||
</a>
|
||||
@if ($excerpt)
|
||||
<p class="mt-2 text-base text-theme-primary">{{ $excerpt }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@if ($questionCount > 1)
|
||||
{{-- Several questions: answered on the poll page --}}
|
||||
<div>
|
||||
<x-jetstream.button onclick="window.location.href = '{{ route('poll.show', ['slug' => $slug]) }}'" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('Take part') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@else
|
||||
@if ($hasContent)
|
||||
<a href="{{ route('poll.show', ['slug' => $slug]) }}" class="inline-block text-sm text-theme-primary underline hover:text-theme-secondary">
|
||||
{{ __('Read more') }}...
|
||||
</a>
|
||||
@endif
|
||||
<livewire:polls.poll-form :pollId="$pollId" context="card" :wire:key="'main-poll-'.$pollId" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,236 @@
|
||||
<div class="col-span-6">
|
||||
<div>
|
||||
<!-- Skills -->
|
||||
<x-jetstream.label for="tags" value="{{ $label }}"
|
||||
wire:loading.remove />
|
||||
<x-jetstream.label for="tags" value="{{ __('Loading...') }}" wire:loading />
|
||||
<div wire:ignore>
|
||||
<input class="w-full" data-suggestions='@json($suggestions)' id="tags"
|
||||
placeholder="{{ __('Select a label, or create a new one') }}" type="text"
|
||||
value="{{ $tagsArray }}" x-data="{ input: @entangle('tagsArray').live }"
|
||||
x-ref="input"
|
||||
style="color: white;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 my-3 flex items-center justify-end px-0 pb-3 text-right">
|
||||
|
||||
<div class="grid grid-cols-1 gap-6" wire:loading wire:target="tagsArray">
|
||||
<x-mini-button flat icon="" primary rounded spinner /> <span>
|
||||
</span>
|
||||
</div>
|
||||
@if ($tagsArrayChanged)
|
||||
<div class="text-sm text-theme-primary mr-3">
|
||||
{{ __('You have unsaved changes')}}
|
||||
</div>
|
||||
@endif
|
||||
<x-jetstream.action-message class="mr-3" on="saved">
|
||||
{{ __('Saved') }}
|
||||
</x-jetstream.action-message>
|
||||
<x-jetstream.button
|
||||
wire:click="saveDisabled === true ? $dispatch('updatedTagsArray') : $dispatch('save')"
|
||||
wire:target="save"
|
||||
wire:loading.attr="disabled"
|
||||
wire:key="saveTagsArrayButton">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- New Tag Modal ---->
|
||||
@if ($newTag)
|
||||
<form wire:submit.prevent="createTag">
|
||||
|
||||
<x-jetstream.dialog-modal wire:model.live="modalVisible">
|
||||
<x-slot name="title">
|
||||
{{ str_replace('@PLATFORM_NAME@', platform_name(), __('Add a new activity or skill to @PLATFORM_NAME@')) }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class='my-3 text-xl'>
|
||||
<span
|
||||
class="bg-{{ $categoryColor }}-300 inline-flex items-center rounded-md px-3 py-2 text-sm font-normal">
|
||||
{{ $newTag['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-1 gap-6">
|
||||
<x-input label="{{ __('Activity tag (min. 2 words)') }}"
|
||||
placeholder="{{ __('Accurate and unique name for this activity, avoid vague or general keywords') }}"
|
||||
wire:model.live="newTag.name" />
|
||||
</div>
|
||||
@if (!$sessionLanguageOk)
|
||||
<div class="mt-3 grid grid-cols-1 gap-6">
|
||||
@php $locale = app()->getLocale();
|
||||
$localeName = \Locale::getDisplayName($locale, $locale);
|
||||
@endphp
|
||||
<x-checkbox :disabled="$sessionLanguageOk" id="sessionLang-ignore-checkbox"
|
||||
label="{{ __('This tag is in :locale.', [
|
||||
'locale' => $localeName
|
||||
]) }}"
|
||||
wire:model.live="sessionLanguageIgnored" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($translationPossible && $translationAllowed)
|
||||
<div class="mt-6 grid grid-cols-1 gap-6 ">
|
||||
<x-checkbox id="checkbox"
|
||||
label="{{ __('Attach a translation to this tag (recommended)') }}"
|
||||
wire:model.live="translationVisible" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-6" wire:loading wire:target="translationVisible">
|
||||
<x-mini-button flat icon="" primary rounded spinner /> <span>
|
||||
{{ __('Loading...') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Tag Translation --->
|
||||
|
||||
<div>
|
||||
@if ($translationVisible)
|
||||
|
||||
<div class="my-6 grid grid-cols-1 gap-6 pl-8 md:grid-cols-2"
|
||||
id='select-translation-language'>
|
||||
<x-select :options="$translationLanguages" class="placeholder-theme-light"
|
||||
id="translation-language" label="{{__('Translation language')}}" option-label="name"
|
||||
option-value="lang_code" placeholder="{{ __('Select a translation language') }}"
|
||||
wire:model.live="selectTranslationLanguage"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 pl-8" wire:loading wire:target="selectTranslationLanguage">
|
||||
<x-mini-button flat icon="" primary rounded spinner /> <span>
|
||||
{{ __('Loading...') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if ($selectTranslationLanguage)
|
||||
|
||||
@php
|
||||
if ($selectTranslationLanguage) {
|
||||
$translationLang = App\Models\Language::where('lang_code', $selectTranslationLanguage)->first()->name;
|
||||
} else {
|
||||
$translationLang = "...";
|
||||
}
|
||||
@endphp
|
||||
|
||||
<hr class="border-t border-theme-primary" py-12 />
|
||||
<x-radio id="radio-select"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Select an existing, untranslated activity tag in @LANGUAGE@')) }}"
|
||||
value="select" wire:model.live="translateRadioButton" />
|
||||
<div class="my-6 grid grid-cols-1 gap-6 pl-8 md:grid-cols-2"
|
||||
id='select-translation'>
|
||||
@if (count($translationOptions) > 0)
|
||||
<x-select
|
||||
:disabled="$translateRadioButton === 'input'"
|
||||
:options="$translationOptions"
|
||||
class="placeholder-theme-light"
|
||||
id="translation"
|
||||
label=""
|
||||
option-label="name"
|
||||
option-value="tag_id"
|
||||
placeholder="{{ __('Select a translation') }}"
|
||||
wire:model.live="selectTagTranslation"
|
||||
/>
|
||||
@else
|
||||
<x-select
|
||||
:disabled="count($translationOptions) < 1 || $translateRadioButton === 'input'"
|
||||
:options="count($translationOptions) > 0
|
||||
? $translationOptions
|
||||
: [['tag_id' => '', 'name' => __('No translations available'), 'disabled' => true]]"
|
||||
class="placeholder-theme-light"
|
||||
id="translation"
|
||||
label=""
|
||||
option-label="name"
|
||||
option-value="tag_id"
|
||||
placeholder="{{ __('No existing translation available') }}"
|
||||
wire:model.live="selectTagTranslation"
|
||||
/>
|
||||
@endif
|
||||
<div class="mt-6 grid grid-cols-1 gap-6" wire:loading
|
||||
wire:target="translationOptions">
|
||||
{{ __('Updating...') }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-t border-theme-primary" py-12 />
|
||||
<x-radio id="radio-input"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Or create a new Activity tag in @LANGUAGE@')) }}"
|
||||
value="input" wire:model.live="translateRadioButton" />
|
||||
<div id="input-translation">
|
||||
<div class="mt-6 grid grid-cols-1 gap-6 pl-8">
|
||||
|
||||
<x-input :disabled="$translateRadioButton === 'select'"
|
||||
label="{{ str_replace('@LANGUAGE@', $translationLang, __('Activity tag in @LANGUAGE@ (min. 2 words)')) }}"
|
||||
placeholder="{{ !empty($newTag['name'])
|
||||
? '\'' . $newTag['name'] . '\'' . ' ' . __('in') . ' ' . $translationLang
|
||||
: __('Activity tag name in') . ' ' . $translationLang }}"
|
||||
wire:key="nameInput" wire:model.live="inputTagTranslation.name" />
|
||||
@if (!$transLanguageOk)
|
||||
<div class="grid grid-cols-1 gap-6 transition-opacity duration-1500 ease-in-out opacity-100">
|
||||
@php $localeTranslation = $selectTranslationLanguage ?? '';
|
||||
$localeNameTranslation = \Locale::getDisplayName($localeTranslation, $localeTranslation);
|
||||
@endphp
|
||||
<x-checkbox :disabled="$translateRadioButton === 'select'" id="transLang-ignore-checkbox"
|
||||
label="{{ __('This tag is in :localeTranslation.', [
|
||||
'localeTranslation' => $localeNameTranslation
|
||||
]) }}"
|
||||
wire:model.live="transLanguageIgnored"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<x-select :disabled="$translateRadioButton === 'select'" :options="$categoryOptions"
|
||||
class="placeholder-theme-light" id="category"
|
||||
label="{{ __('Category') }}" option-label="name"
|
||||
option-value="category_id"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
wire:model.live="newTagCategory" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!$translationVisible || !$translationAllowed)
|
||||
<div class="mt-3 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<x-select :options="$categoryOptions" class="placeholder-theme-light" id="category"
|
||||
label="{{ __('Category') }}" option-label="name" option-value="category_id"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
wire:model.live="newTagCategory" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="pt-10 my-3 grid grid-cols-1">
|
||||
<x-skill-tag-warning />
|
||||
</div>
|
||||
{{-- <x-errors /> --}}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="cancelCreateTag" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.secondary-button class="ml-3"
|
||||
wire:click="createTag"
|
||||
wire:key="createTagButton"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Save') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<script src="{{ asset('js/skilltags.js') }}"></script>
|
||||
<script>
|
||||
document.addEventListener('livewire:load', () => {
|
||||
// Listener for page reload event
|
||||
document.addEventListener('reloadPage', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||
@if($image)
|
||||
<div class="md:col-span-2 flex items-center justify-center sm:h-64 md:h-80 overflow-hidden">
|
||||
<img src="{{ $image }}"
|
||||
alt="{{ $posts->getFirstMedia('*')->getCustomProperty('caption') }}"
|
||||
class="w-full h-full object-cover object-center" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="{{ $image ? 'col-span-1 md:col-span-2' : 'col-span-4' }}">
|
||||
<div class="post" id="post-id-{{ $posts->id ?? 'no-id' }}">
|
||||
<h3 class="text-lg font-medium leading-6 text-theme-primary">
|
||||
{{ $posts->translations[0]->title ?? '' }}
|
||||
</h3>
|
||||
</div>
|
||||
@if ($posts)
|
||||
<div class="my-2 text-sm font-bold text-theme-primary">
|
||||
{{ $posts->translations[0]->excerpt ?? '' }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-theme-secondary">
|
||||
{!! \App\Helpers\StringHelper::sanitizeHtml($posts->translations[0]->content ?? '') !!}
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<h3 class="text-lg font-medium leading-6 text-theme-primary">
|
||||
{{ $fallbackTitle ?? '' }}
|
||||
</h3>
|
||||
<div class="text-sm text-theme-secondary">
|
||||
{{ $fallbackDescription ?? '' }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
<div x-data="{ open: false }" class="w-fit lg:w-50 flex-shrink min-w-0 flex items-center max-w-full" x-on:click.away="open = false">
|
||||
<div class="relative text-theme-secondary w-full max-w-full">
|
||||
<div class="flex rounded-md border border-theme-primary bg-theme-background placeholder-theme-light transition duration-150 ease-in-out focus-within:border-theme-accent focus-within:ring-1 focus-within:ring-theme-accent focus:placeholder-gray-600 sm:text-sm w-full">
|
||||
<input
|
||||
@if (timebank_config('main_search_bar.suggestions') === 0)
|
||||
wire:model="search"
|
||||
@else
|
||||
wire:model.live.debounce="search"
|
||||
@endif
|
||||
wire:keydown.enter="showSearchResults"
|
||||
x-on:focus="open = true"
|
||||
x-on:input="open = true"
|
||||
x-on:keydown.enter="open = false"
|
||||
type="search"
|
||||
placeholder="{{ __('Search') . '...' }}"
|
||||
class="w-full border-none focus:border-none focus:outline-none focus:ring-0 mx-2 leading-5 text-sm"
|
||||
placeholder="{{ __('Search name, skill or keyword') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<x-mini-button style="background-color:none" onmouseover="this.style.backgroundColor='transparent'" onmouseout="this.style.backgroundColor='none'" class="!cursor-default focus:ring-offset-0 focus:ring-offset-transparent border-none focus:border-none hover:text-transparent hover:bg-none hover:-bg-transparent focus:outline-hidden focus:ring-transparent focus:outline-none focus:ring-0 hover:border-none hover:outline-none hover:ring-0" flat icon="" spinner />
|
||||
<button wire:click="showSearchResults()"
|
||||
class="px-2 placeholder-theme-light transition duration-150 ease-in-out focus:outline-none sm:text-sm"
|
||||
placeholder="{{ __('Search name, skill or keyword') }}">
|
||||
<svg class="h-5 w-5 text-theme-light" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@if (strlen($search) > 3 && timebank_config('main_search_bar.suggestions') > 0 && !empty($suggestions) && count($suggestions) > 0)
|
||||
<div class="absolute z-[9999] mt-2 w-full">
|
||||
<ul class="cursor-pointer rounded-lg border border-theme-primary bg-theme-background text-sm text-theme-primary shadow-md">
|
||||
@foreach($suggestions as $suggestion)
|
||||
@if ($suggestion)
|
||||
<li class="suggestion-item block px-4 py-2 text-sm leading-5 text-theme-primary hover:bg-theme-surface transition cursor-pointer"
|
||||
onclick="Livewire.find('{{ $_instance->getId() }}').call('selectSuggestion', {{ $suggestion['index'] }})">
|
||||
{{ $suggestion['text'] }}
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($showResults && timebank_config('main_search_bar.suggestions') > 0)
|
||||
<!-- Display search results here -->
|
||||
<label for="mainSearchBar" class="my-2 block text-sm font-medium text-theme-primary">
|
||||
{{ count($results) . ' ' . __('results') }}
|
||||
</label>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Model</th>
|
||||
<th>ID</th>
|
||||
<th>Score</th>
|
||||
<th>Highlight</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($results as $result)
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
@if ($result['model'] === 'App\Models\User')
|
||||
<a
|
||||
href="{{ route('user.show', ['userId' => $result['id']]) }}">{{ $result['model'] }}</a>
|
||||
@elseif($result['model'] === 'App\Models\Organization')
|
||||
<a
|
||||
href="{{ route('org.show', ['orgId' => $result['id']]) }}">{{ $result['model'] }}</a>
|
||||
@elseif($result['model'] === 'App\Models\Post')
|
||||
<a
|
||||
href="{{ route('post.show_by_id', ['postId' => $result['id']]) }}">{{ $result['model'] }}</a>
|
||||
@else
|
||||
{{ $result['model'] }}
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $result['id'] }}</td>
|
||||
<td>{{ $result['score'] }}</td>
|
||||
<td>
|
||||
{{-- XSS SECURITY: Highlights are sanitized in MainSearchBar::sanitizeHighlights() --}}
|
||||
{{-- DO NOT change {!! !!} to {{ }} - highlights contain safe HTML <span> tags for styling --}}
|
||||
{{-- DO NOT bypass sanitization - user content from profiles could contain malicious code --}}
|
||||
{{-- If you modify highlight handling, review MainSearchBar.php lines 1107-1180 for security notes --}}
|
||||
@foreach (collect($result['highlight'])->unique()->toArray() as $highlight)
|
||||
{!! $highlight !!} <br>
|
||||
@endforeach
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
<nav id="navigation-menu"
|
||||
class="shadow-md border-b border-theme-primary bg-theme-background fixed top-0 left-0 right-0 z-[80] w-full text-left"
|
||||
wire:ignore.self
|
||||
style="margin: 0; padding: 0; left: 0; right: 0;">
|
||||
@if(auth()->guest() || request()->routeIs('goodbye-deleted-user'))
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="relative mx-auto max-w-7xl px-4 lg:px-8 w-full">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex shrink-0 items-center">
|
||||
<a href="{{ route('welcome') }}">
|
||||
<x-jetstream.application-mark class="block h-9 w-auto" style="max-width: 80px;" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Language selector and Auth Links -->
|
||||
<div class="flex items-center">
|
||||
@php
|
||||
$languages = Illuminate\Support\Facades\DB::table('languages')->orderBy('lang_code', 'asc')->get();
|
||||
$supportedLocales = LaravelLocalization::getSupportedLocales();
|
||||
uasort($supportedLocales, function ($a, $b) {
|
||||
return strcmp($a['native'], $b['native']);
|
||||
});
|
||||
@endphp
|
||||
|
||||
<!-- Language Dropdown -->
|
||||
<div class="ms-3">
|
||||
<x-jetstream.dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<span class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-2 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-theme-primary hover:text-theme-secondary focus:outline-none focus:bg-theme-surface active:bg-theme-surface transition ease-in-out duration-150">
|
||||
<x-icon name="globe-alt" class="w-5 h-4" outline />
|
||||
</button>
|
||||
</span>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="w-60">
|
||||
@foreach ($supportedLocales as $localeCode => $properties)
|
||||
@php
|
||||
$language = $languages->firstWhere('lang_code', strtolower($localeCode));
|
||||
@endphp
|
||||
@if ($language)
|
||||
<div class="block px-4 py-2">
|
||||
<a class="text-theme-primary transition hover:text-theme-secondary focus:border-theme-primary focus:text-theme-secondary"
|
||||
href="{{ LaravelLocalization::getLocalizedURL($localeCode, null, [], true) }}"
|
||||
hreflang="{{ $localeCode }}" rel="alternate">
|
||||
{{ $language->flag }}
|
||||
<span class="ml-3 text-theme-primary">{{ Lang::get($language->name, [], $localeCode) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Auth Links -->
|
||||
@if (Route::has('login'))
|
||||
<div class="ms-2 flex items-center space-x-3">
|
||||
@if (Route::has('register'))
|
||||
<a href="{{ route('register') }}" class="text-theme-primary hover:text-theme-secondary text-sm font-medium focus:outline-none">
|
||||
{{ __('Register') }}
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('login') }}" class="text-theme-primary hover:text-theme-secondary text-sm font-medium focus:outline-none">
|
||||
{{ __('Log in') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</nav>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="pt-3 -pb-3">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 ">
|
||||
@if ( session('notification.secondary') )
|
||||
<div class="bg-theme-background">
|
||||
<x-alert title="{{ __(session('notification.secondary')) }}" secondary flat>
|
||||
<x-slot name="slot">
|
||||
{{ __(session('notification.secondary.details')) }}
|
||||
</x-slot>
|
||||
</x-alert>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ( session('notification.success') )
|
||||
<div class="bg-theme-background">
|
||||
<x-alert title="{{ __(session('notification.success')) }}" positive flat>
|
||||
<x-slot name="slot">
|
||||
{{ __(session('notification.success.details')) }}
|
||||
</x-slot>
|
||||
</x-alert>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ( session('notification.error') )
|
||||
<div class="bg-theme-background">
|
||||
<x-alert title="{{ __(session('notification.error')) }}" negative flat>
|
||||
<x-slot name="slot">
|
||||
{{ __(session('notification.error.details')) }}
|
||||
</x-slot>
|
||||
</x-alert>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if ( session('notification.alert') )
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg mb-6">
|
||||
<div class="p-6 lg:pt-6 sm:px-20 bg-white text-sm text-theme-secondary">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-theme-primary mr-3 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-medium text-theme-primary">{{ __(session('notification.alert')) }}</div>
|
||||
@if ( session('notification.alert.details') )
|
||||
<div class="mt-2">{{ __(session('notification.alert.details')) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ( session('notification.info') )
|
||||
<div class="bg-theme-background">
|
||||
<x-alert title="{{ __(session('notification.info')) }}" info flat>
|
||||
<x-slot name="slot">
|
||||
{{ __(session('notification.info.details')) }}
|
||||
</x-slot>
|
||||
</x-alert>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-theme-background overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="p-6 lg:pt-6 sm:px-20 bg-theme-background border-b border-theme-border h-30 text-sm text-theme-primary">
|
||||
@if (session('email-profile'))
|
||||
<div class="flex items-center">
|
||||
<x-icon name="check-circle" class="w-5 h-5 mr-2 text-theme-primary" />
|
||||
{{ __('messages.email_of_profile_has_been_verified', ['profile_name' => session('email-profile')]) }}
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center">
|
||||
<x-icon name="check-circle" class="w-5 h-5 mr-2 text-theme-primary" />
|
||||
{{ __('Your email has been verified successfully') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
@php
|
||||
$roleLabel = session('activeProfileRoleLabel')
|
||||
? __(session('activeProfileRoleLabel'))
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
{{-- Centered auto-dismissing overlay — works reliably in WebView without depending on Alpine timing --}}
|
||||
<div id="profile-switch-banner"
|
||||
style="position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.45);">
|
||||
<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:360px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.25);">
|
||||
|
||||
@if(session('activeProfilePhoto'))
|
||||
<img src="{{ Storage::url(session('activeProfilePhoto')) }}"
|
||||
alt="{{ session('activeProfileName') }}"
|
||||
style="width:72px;height:72px;border-radius:50%;object-fit:cover;margin:0 auto 12px;display:block;box-shadow:0 2px 8px rgba(0,0,0,0.15);">
|
||||
@endif
|
||||
|
||||
<div style="color:#6b7280;font-size:13px;margin-bottom:4px;text-align:left;">{{ __('You are now acting as') }}</div>
|
||||
<div style="font-size:18px;font-weight:700;color:#111827;margin-bottom:6px;text-align:left;">{{ session('activeProfileName') }}</div>
|
||||
|
||||
@if($roleLabel)
|
||||
<div style="font-size:12px;color:#6b7280;text-align:left;">{{ $roleLabel }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var banner = document.getElementById('profile-switch-banner');
|
||||
if (!banner) return;
|
||||
// Dismiss on tap/click
|
||||
banner.addEventListener('click', function() {
|
||||
banner.style.display = 'none';
|
||||
});
|
||||
// Auto-dismiss after 3 seconds
|
||||
setTimeout(function() {
|
||||
banner.style.opacity = '0';
|
||||
banner.style.transition = 'opacity 0.4s';
|
||||
setTimeout(function() { banner.style.display = 'none'; }, 400);
|
||||
}, 3000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="p-6 sm:px-20 bg-white h-30">
|
||||
|
||||
<!-- Photo -->
|
||||
<img class="mt-8 mr-8 relative float-right w-24 h-24 rounded-full profile-photo object-cover shadow" src="{{ Storage::url(session('activeProfilePhoto')) }}" alt="{{ __('Your email has been verified successfully') }}" />
|
||||
|
||||
<div class="mt-8 text-lg">
|
||||
{{ session('unauthorizedAction') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<div class="online-reacted-profiles" wire:poll.{{ $refreshInterval }}s="loadOnlineReactedProfiles">
|
||||
|
||||
{{-- Header with count --}}
|
||||
@if ($headerText && $showCount && $totalCount > 0)
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2 text-xs text-theme-muted">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span class="font-sm">
|
||||
{{ $headerText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Count by type if multiple guards --}}
|
||||
@if (count($countByType) > 1)
|
||||
<div class="flex items-center space-x-3 text-xs text-theme-muted">
|
||||
@foreach ($countByType as $type => $count)
|
||||
<span class="ml-2">
|
||||
{{ __($modelLabels[$type]) ?? __(class_basename($type)) }}: {{ $count }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Profiles Display --}}
|
||||
@if ($groupByModel && is_array($onlineReactedProfiles))
|
||||
{{-- Grouped by Model Type --}}
|
||||
@foreach ($onlineReactedProfiles as $modelType => $profiles)
|
||||
<div class="grouped-profiles mb-6">
|
||||
<h3 class="mb-3 text-sm font-medium text-theme-primary border-l-4 border-theme-border pl-3">
|
||||
{{ __($modelLabels[$modelType]) ?? __(class_basename($modelType)) }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
@foreach ($profiles as $profile)
|
||||
<div class="profile-card">
|
||||
<a href="{{ $profile['profile_url'] }}" class="block">
|
||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 transition-colors duration-150">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="relative block cursor-pointer">
|
||||
<img alt="profile"
|
||||
class="h-8 w-8 rounded-full object-cover outline outline-1 outline-offset-1 outline-gray-500"
|
||||
src="{{ Storage::url($profile['avatar']) }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="font-bold text-sm text-theme-primary truncate">
|
||||
{{ $profile['name'] }}
|
||||
</div>
|
||||
<div class="text-xs font-normal text-theme-muted truncate">
|
||||
{{ $profile['location'] }}
|
||||
</div>
|
||||
@if ($lastSeen)
|
||||
<div class="text-xs text-theme-muted truncate">
|
||||
@if ($profile['last_seen'])
|
||||
{{ \Carbon\Carbon::parse($profile['last_seen'])->diffForHumans() }}
|
||||
@else
|
||||
{{ __('online') }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if (getActiveProfileType() === 'Admin' && !$this->isCurrentUserProfile($profile['id'], $profile['model_type']))
|
||||
<button wire:click="openLogoutModal({{ $profile['id'] }}, '{{ addslashes($profile['model_type']) }}')"
|
||||
class="text-xs text-red-600 hover:text-red-800 underline mt-1"
|
||||
onclick="event.preventDefault(); event.stopPropagation();">
|
||||
{{ __('Log out') }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
{{-- Not grouped --}}
|
||||
@if(count($onlineReactedProfiles) > 0)
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
@foreach($onlineReactedProfiles as $profile)
|
||||
<div class="profile-card">
|
||||
<a href="{{ $profile['profile_url'] }}" class="block">
|
||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-50 transition-colors duration-150">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="relative block cursor-pointer">
|
||||
<img alt="profile"
|
||||
class="h-8 w-8 rounded-full object-cover outline outline-1 outline-offset-1 outline-gray-500"
|
||||
src="{{ Storage::url($profile['avatar']) }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="font-bold text-sm text-theme-primary truncate">
|
||||
{{ $profile['name'] }}
|
||||
</div>
|
||||
<div class="text-xs font-normal text-theme-muted truncate">
|
||||
{{ $profile['location'] }}
|
||||
</div>
|
||||
@if ($lastSeen)
|
||||
<div class="text-xs text-theme-muted truncate">
|
||||
@if ($profile['last_seen'])
|
||||
{{ \Carbon\Carbon::parse($profile['last_seen'])->diffForHumans() }}
|
||||
@else
|
||||
{{ __('online') }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if (getActiveProfileType() === 'Admin' && !$this->isCurrentUserProfile($profile['id'], $profile['model_type']))
|
||||
<button wire:click="openLogoutModal({{ $profile['id'] }}, '{{ addslashes($profile['model_type']) }}')"
|
||||
class="text-xs text-red-600 hover:text-red-800 underline mt-1"
|
||||
onclick="event.preventDefault(); event.stopPropagation();">
|
||||
{{ __('Log out') }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2 text-sm text-theme-muted">
|
||||
<span class="font-sm">
|
||||
{{trans_choice('messages.reactions_contacts_online', 0, ['count' => 0]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Logout Confirmation Modal --}}
|
||||
@if (getActiveProfileType() === 'Admin')
|
||||
<x-jetstream.dialog-modal wire:model.live="showLogoutModal">
|
||||
<x-slot name="title">
|
||||
{{ __('Log out user') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="text-base mb-4">{{ __('Are you sure you want to log out this user?') }}</div>
|
||||
@if ($selectedProfileId && $selectedProfileType)
|
||||
@php
|
||||
$selectedProfile = $selectedProfileType::find($selectedProfileId);
|
||||
@endphp
|
||||
@if ($selectedProfile)
|
||||
<div class="flex items-center p-2">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="relative block">
|
||||
<img alt="profile"
|
||||
class="h-8 w-8 rounded-full profile-photo object-cover outline outline-1 outline-offset-1 outline-gray-500"
|
||||
src="{{ Storage::url($selectedProfile->profile_photo_path) }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="font-bold text-sm text-theme-primary truncate">
|
||||
{{ $selectedProfile->name }}
|
||||
</div>
|
||||
<div class="text-xs font-normal text-theme-muted truncate">
|
||||
{{ $selectedProfile->getLocationFirst()['name_short'] ?? '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="closeLogoutModal">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button wire:click="logoutUser" wire:loading.attr="disabled" class="ml-3">
|
||||
<span wire:loading.remove wire:target="logoutUser">{{ __('Log out') }}</span>
|
||||
<span wire:loading wire:target="logoutUser">{{ __('Loading...') }}</span>
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Include JavaScript for real-time updates --}}
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Listen for presence updates from other components
|
||||
window.addEventListener('presence-channel-updated', function(event) {
|
||||
@this.call('loadOnlineReactedProfiles');
|
||||
});
|
||||
|
||||
// Listen for reaction changes
|
||||
window.addEventListener('reaction-toggled', function(event) {
|
||||
@this.call('loadOnlineReactedProfiles');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,47 @@
|
||||
{{-- resources/views/livewire/online-users-list.blade.php --}}
|
||||
<div class="online-users-list" wire:poll.{{ $refreshInterval }}s="loadOnlineUsers">
|
||||
@if($showCount)
|
||||
<div class="flex items-center space-x-2 text-sm text-theme-secondary dark:text-theme-muted mb-3">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span class="font-medium">{{ count($onlineUsers) }} online</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="space-y-2">
|
||||
@forelse($onlineUsers as $user)
|
||||
<div class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-theme-secondary transition-colors">
|
||||
@if($showAvatars)
|
||||
<div class="relative flex-shrink-0">
|
||||
@if(isset($user['avatar']) && $user['avatar'])
|
||||
<img src="{{ $user['avatar'] }}" alt="{{ $user['name'] }}"
|
||||
class="w-8 h-8 rounded-full object-cover">
|
||||
@else
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||
{{ substr($user['name'], 0, 1) }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-theme-background dark:border-theme-secondary rounded-full"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-theme-primary dark:text-white truncate">
|
||||
{{ $user['name'] }}
|
||||
</p>
|
||||
<p class="text-xs text-theme-muted dark:text-theme-muted">
|
||||
{{ isset($user['last_seen']) ? \Carbon\Carbon::parse($user['last_seen'])->diffForHumans() : 'Now' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-4 text-theme-muted dark:text-theme-muted">
|
||||
<div class="w-8 h-8 mx-auto mb-2 text-theme-muted">
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-2.776M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-sm">No users online</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
@foreach($categories as $category)
|
||||
<div class="inline-block">
|
||||
@if(!empty($category['children']))
|
||||
<!-- Category with children - shows dropdown on click -->
|
||||
<div x-data="{ showChildren: false }" class="relative">
|
||||
<button
|
||||
@click="showChildren = !showChildren"
|
||||
wire:click="selectCategory({{ $category['id'] }})"
|
||||
class="bg-{{ $category['color'] }}-400 hover:bg-{{ $category['color'] }}-200 text-black inline-flex items-center rounded-md px-2 py-1 text-xs font-normal transition duration-150 ease-in-out cursor-pointer mr-1 mb-1
|
||||
{{ in_array($category['id'], $selectedCategories) ? 'ring-1 ring-black ring-offset-2' : '' }}">
|
||||
{{ $category['name'] }}
|
||||
<svg class="ml-1 w-3 h-3" :class="showChildren ? 'rotate-180' : ''" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown for children -->
|
||||
<div x-show="showChildren"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
@click.away="showChildren = false"
|
||||
class="absolute top-full left-0 mt-1 bg-white border border-theme-border rounded-md shadow-lg z-10 p-2 min-w-max">
|
||||
<div class="flex flex-wrap gap-1 max-w-xs">
|
||||
@include('livewire.partials.category-tree-horizontal-multi', ['categories' => $category['children'], 'level' => $level + 1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Category without children - simple button -->
|
||||
<button
|
||||
wire:click="selectCategory({{ $category['id'] }})"
|
||||
class="bg-{{ $category['color'] }}-400 hover:bg-{{ $category['color'] }}-200 text-black inline-flex items-center rounded-md px-2 py-1 text-xs font-normal transition duration-150 ease-in-out cursor-pointer mr-1 mb-1
|
||||
{{ in_array($category['id'], $selectedCategories) ? 'ring-1 ring-black ring-offset-2' : '' }}">
|
||||
{{ $category['name'] }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -0,0 +1,312 @@
|
||||
<div>
|
||||
<form wire:submit.prevent="showConfirmPaymentModal">
|
||||
@csrf
|
||||
<div class="bg-theme-background px-4 py-4 shadow sm:rounded-lg sm:p-6">
|
||||
@php
|
||||
$hasNoAccounts = false;
|
||||
$isAdminProfile = session('activeProfileType') === 'App\Models\Admin';
|
||||
|
||||
if ($isAdminProfile) {
|
||||
$hasNoAccounts = true;
|
||||
} elseif (getActiveProfile() && method_exists(getActiveProfile(), 'accounts')) {
|
||||
$hasNoAccounts = getActiveProfile()->accounts()->notRemoved()->count() === 0;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if($hasNoAccounts)
|
||||
<div class="mb-6 rounded-lg border border-theme-border bg-theme-surface p-4 text-theme-primary">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<h3 class="font-semibold">{{ __('No accounts available') }}</h3>
|
||||
<p class="mt-1 text-sm">
|
||||
@if($isAdminProfile)
|
||||
{{ __('Admin profiles do not have accounts and cannot make payments. Please switch to a different profile to make payments.') }}
|
||||
@else
|
||||
{{ __('Your profile does not have any accounts to make payments from. Please contact an administrator to set up an account.') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
|
||||
<!--- Amount --->
|
||||
@livewire('amount', [
|
||||
'maxLengthHoursInput' => timebank_config('maxLengthHoursInput.user'),
|
||||
'hours' => $hours,
|
||||
'minutes' => $minutes,
|
||||
'amount' => $amount,
|
||||
])
|
||||
@error('amount')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<!--- From account --->
|
||||
@livewire('from-account')
|
||||
@error('fromAccountId')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<!--- To Account --->
|
||||
@livewire('to-account', ['toHolderName' => $toHolderName, 'toAccountId' => $toAccountId])
|
||||
@error('toAccountId')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<!--- Description --->
|
||||
<div class="mt-4">
|
||||
<label for="description" class="mt-2 block text-sm font-medium text-theme-primary"> {{ __('Description') }}</label>
|
||||
<textarea
|
||||
wire:model.live.debounce.500ms="description"
|
||||
id="description"
|
||||
placeholder=" {{ __('Payment description') }}"
|
||||
rows="5"
|
||||
class="mt-1 placeholder-theme-light focus:ring-theme-accent focus:border-theme-accent block w-full shadow-sm sm:text-sm border-theme-primary rounded-md @error('description') is-invalid @enderror" name="description">
|
||||
</textarea>
|
||||
</div>
|
||||
@error('description')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<!--- Transaction type --->
|
||||
@livewire('transaction-type-radio', ['type' => $type, 'typeOptions' => $typeOptions])
|
||||
@error('transactionTypeSelected.name')
|
||||
<div class="mb-3 text-sm text-red-700" role="alert">
|
||||
{{ __($message) }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<!--- Remember payment data checkbox ----->
|
||||
<div class="my-4">
|
||||
<x-checkbox id="rememberPaymentData" secondary label="{{ __('Remember payment data for next payment') }}" wire:model="rememberPaymentData"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<x-jetstream.button type="submit" wire:target="showModal" :disabled="!canActiveProfileCreatePayments()">
|
||||
{{ __('Pay') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!----Transfer limit error Modal ---->
|
||||
<style>
|
||||
/* Ensure Pay modals render above the fixed navigation bar (nav is z-[80]) */
|
||||
#payment-limit-error-dialog,
|
||||
#new-recipient-dialog,
|
||||
#confirm-payment-dialog {
|
||||
z-index: 90;
|
||||
}
|
||||
</style>
|
||||
<x-jetstream.dialog-modal id="payment-limit-error-dialog" wire:model.live="isPaymentLimitErrorModalOpen">
|
||||
<x-slot name="title">
|
||||
{{ __('Payment limit') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ $limitError }}
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$toggle('isPaymentLimitErrorModalOpen')" wire:loading.attr="disabled">
|
||||
{{ __('Back') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!----New recipient confirmation Modal ---->
|
||||
<x-jetstream.dialog-modal id="new-recipient-dialog" wire:model.live="isNewRecipientModalOpen">
|
||||
<x-slot name="title">
|
||||
{{ __('New recipient') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('You previously never had any chat conversation or payments with :name (:name) from :location. Are you sure you want to select this account?', [
|
||||
'name' => $pendingToAccountDetails['holderName'] ?? '',
|
||||
'location' => $pendingToAccountDetails['holderLocation'] ?? '',
|
||||
]) }}
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="cancelNewRecipient" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3" wire:click="confirmNewRecipient" wire:loading.attr="disabled">
|
||||
{{ __('Continue') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
<!---- Confirmation Modal ---->
|
||||
@if (!$limitError && !empty($transactionTypeSelected))
|
||||
<x-jetstream.dialog-modal id="confirm-payment-dialog" wire:model.live="isConfirmPaymentModalOpen">
|
||||
<x-slot name="title">
|
||||
{{ __('Confirm your payment') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
|
||||
<div class="pb-3 px-1 sm:px-3">
|
||||
<div class="font-semibold text-sm sm:text-base">
|
||||
{{ __('Description') }}
|
||||
</div>
|
||||
<div class="text-sm sm:text-base">{{ $description }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col py-3 gap-0">
|
||||
<!-- From account row -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex w-16 flex-shrink-0 justify-end">
|
||||
<img alt="{{ session('activeProfileName') }}"
|
||||
class="h-16 w-16 rounded-full profile-photo object-cover outline outline-1 outline-offset-2 outline-theme-primary"
|
||||
src="{{ Storage::url(session('activeProfilePhoto')) }}">
|
||||
</div>
|
||||
<div class="leading-tight">
|
||||
<div class="font-semibold text-sm">{{ session('activeProfileName') }}</div>
|
||||
<div class="text-theme-secondary text-xs">{{ __(ucfirst(strtolower($fromAccountName))) }} {{ __('bank account') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Connector: line, icon, amount, arrow -->
|
||||
<div class="flex items-stretch gap-4">
|
||||
<!-- Left: continuous line with icon centered and arrow at bottom -->
|
||||
<div class="relative flex w-16 flex-shrink-0 justify-center">
|
||||
<!-- Full-height vertical line -->
|
||||
<div class="absolute inset-x-1/2 top-0 bottom-0 w-px bg-theme-primary"></div>
|
||||
<!-- Icon centered vertically -->
|
||||
<div class="relative flex h-full flex-col items-center justify-center">
|
||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-white text-theme-primary outline outline-1 outline-offset-1 outline-theme-primary">
|
||||
<x-icon class="{{ ($transactionTypeSelected['icon'] ?? '') === 'arrow-path' ? '-scale-x-100' : '' }}" mini name="{{ $transactionTypeSelected['icon'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Arrow at the bottom -->
|
||||
<div class="absolute bottom-0 flex justify-center">
|
||||
<svg fill="none" height="10" viewBox="0 0 15 10" width="15" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 0 L7.5 8 M7.5 8 L0 2 M7.5 8 L15 2" stroke-width="1" stroke="#4B5563" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: amount + label -->
|
||||
<div class="flex items-center leading-tight py-2">
|
||||
<div>
|
||||
<div class="font-semibold text-sm">{{ tbFormat($amount) }}</div>
|
||||
<div class="text-theme-secondary text-xs">{{ __($transactionTypeSelected['label']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- To account row -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex w-16 flex-shrink-0 justify-end">
|
||||
<img alt="{{ $toHolderName }}"
|
||||
class="h-16 w-16 rounded-full object-cover outline outline-1 outline-offset-2 outline-theme-primary"
|
||||
src="{{ $toHolderPhoto }}">
|
||||
</div>
|
||||
<div class="leading-tight">
|
||||
<div class="font-semibold text-sm">{{ $toHolderName }}</div>
|
||||
<div class="text-theme-secondary text-xs">{{ __(ucfirst($toAccountName)) }} {{ __('bank account') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger text-red-500">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
@if (session('error'))
|
||||
<x-jetstream.secondary-button wire:click="$toggle('isConfirmPaymentModalOpen')" wire:loading.attr="disabled">
|
||||
{{ __('Back') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@else
|
||||
<x-jetstream.secondary-button class="w-32 justify-center" wire:click="$toggle('isConfirmPaymentModalOpen')"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.button class="ml-3 w-32 justify-center" wire:click="doTransfer()"
|
||||
wire:loading.attr="disabled" :disabled="!canActiveProfileCreatePayments()">
|
||||
{{ __('Pay') }}
|
||||
</x-jetstream.button>
|
||||
@endif
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
</form>
|
||||
|
||||
{{-- Payment success modal --}}
|
||||
<style>
|
||||
/* Constrain success modal to viewport with margin, keep footer always visible */
|
||||
#payment-success-dialog .mb-6 {
|
||||
max-height: calc(100svh - 3rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#payment-success-dialog .mb-6 > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
#payment-success-dialog .mb-6 > div > .m-6 {
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
<x-jetstream.dialog-modal id="payment-success-dialog" wire:model.live="isPaymentSuccessModalOpen" closeButton="true" maxWidth="sm">
|
||||
<x-slot name="title">{{ __('Transaction done!') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="flex flex-col items-center gap-3 py-2">
|
||||
@if ($successHolderPhoto)
|
||||
<img alt="{{ $successHolderName }}"
|
||||
src="{{ $successHolderPhoto }}"
|
||||
class="h-20 w-20 shrink-0 rounded-full object-cover outline outline-1 outline-offset-2 outline-theme-primary">
|
||||
@endif
|
||||
<div class="text-center leading-tight">
|
||||
<div class="text-sm text-theme-secondary">{{ __('Payment sent to') }}</div>
|
||||
<div class="mt-1 font-semibold text-theme-primary">{{ $successHolderName }}</div>
|
||||
@if ($successAccountName)
|
||||
<div class="text-xs text-theme-secondary">{{ $successAccountName }} {{ __('bank account') }}</div>
|
||||
@endif
|
||||
<div class="mt-2 font-bold text-theme-primary">{{ $successAmount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($successQrSvg)
|
||||
{{-- QR hidden via Alpine when rendered size would drop below 200px --}}
|
||||
<div x-data="{ showQr: true }"
|
||||
x-init="$nextTick(() => { showQr = $el.offsetWidth >= 200 })"
|
||||
x-show="showQr"
|
||||
class="mt-4 flex flex-col items-center gap-2">
|
||||
<a href="{{ $successTransactionUrl }}"
|
||||
class="block w-full max-w-[200px] [&>svg]:h-auto [&>svg]:w-full">
|
||||
{!! $successQrSvg !!}
|
||||
</a>
|
||||
<a href="{{ $successTransactionUrl }}"
|
||||
class="text-xs text-theme-secondary underline hover:text-theme-primary">
|
||||
{{ __('View transaction statement') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<div class="w-full flex justify-center">
|
||||
<x-jetstream.button wire:click="$set('isPaymentSuccessModalOpen', false)">
|
||||
{{ __('OK') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Do your work, then step back. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- The whole world belongs to you. --}}
|
||||
</div>
|
||||