[FEAT] 實作通行碼編輯功能與 UI 組件優化
1. 實作通行碼 (Pass Code) 編輯功能,包含後端 Controller 邏輯與前端 Alpine.js 編輯彈窗。 2. 優化 searchable-select 組件的空值處理與樣式。 3. 更新繁體中文語系檔,統一通行碼相關操作之回傳訊息翻譯。 4. 修正通行碼更新邏輯,支援 nullable 狀態與到期日空值處理。 5. 在桌機版與手機版介面加入編輯操作入口。
This commit is contained in:
parent
b42695c4b8
commit
84bf7de750
@ -211,10 +211,17 @@ class SalesController extends Controller
|
||||
$validated = $request->validate([
|
||||
'name' => 'nullable|string|max:50',
|
||||
'expires_at' => 'nullable|date',
|
||||
'status' => 'required|in:active,disabled',
|
||||
'status' => 'nullable|in:active,disabled',
|
||||
]);
|
||||
|
||||
$passCode->update($validated);
|
||||
// 確保 expires_at 為空字串時轉為 null
|
||||
if (isset($validated['expires_at']) && empty($validated['expires_at'])) {
|
||||
$validated['expires_at'] = null;
|
||||
}
|
||||
|
||||
$passCode->update(array_filter($validated, function ($value, $key) use ($request) {
|
||||
return $request->has($key);
|
||||
}, ARRAY_FILTER_USE_BOTH));
|
||||
|
||||
return back()->with('success', __('Pass code updated.'));
|
||||
}
|
||||
@ -225,7 +232,7 @@ class SalesController extends Controller
|
||||
public function destroyPassCode(PassCode $passCode)
|
||||
{
|
||||
$passCode->update(['status' => 'disabled']);
|
||||
return back()->with('success', __('Pass code disabled.'));
|
||||
return back()->with('success', __('Pass code cancelled.'));
|
||||
}
|
||||
|
||||
// 來店禮設定
|
||||
|
||||
@ -931,7 +931,8 @@
|
||||
"Pass Codes": "通行碼",
|
||||
"Pass code created: :code": "通行碼已建立::code",
|
||||
"Pass code deleted.": "通行碼已刪除",
|
||||
"Pass code disabled.": "通行碼已停用。",
|
||||
"Pass code cancelled.": "通行碼已取消。",
|
||||
"Pass code disabled.": "通行碼已取消。",
|
||||
"Pass code updated.": "通行碼已更新",
|
||||
"Password": "密碼",
|
||||
"Password updated successfully.": "密碼已成功變更。",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
@section('content')
|
||||
<div class="space-y-4 pb-20" x-data="{
|
||||
showCreateModal: false,
|
||||
showEditModal: false,
|
||||
showDeleteModal: false,
|
||||
deleteTargetForm: '',
|
||||
selectedMachine: '',
|
||||
@ -14,6 +15,21 @@
|
||||
showQrModal: false,
|
||||
activeQrCode: '',
|
||||
activeTicketUrl: '',
|
||||
editFormAction: '',
|
||||
editPassCodeName: '',
|
||||
editExpiresAt: '',
|
||||
|
||||
openEditModal(item) {
|
||||
this.editFormAction = '{{ route('admin.sales.pass-codes.update', ':id') }}'.replace(':id', item.id);
|
||||
this.editPassCodeName = item.name;
|
||||
this.editExpiresAt = item.expires_at;
|
||||
this.showEditModal = true;
|
||||
this.$nextTick(() => {
|
||||
window.dispatchEvent(new CustomEvent('set-date', {
|
||||
detail: { name: 'expires_at', value: this.editExpiresAt }
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
generateRandomCode() {
|
||||
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
|
||||
@ -174,8 +190,7 @@
|
||||
|
||||
<div class="w-full md:w-48">
|
||||
<x-searchable-select name="status" id="filter-status" :placeholder="__('All Status')"
|
||||
x-model="status"
|
||||
@change="$el.closest('form').dispatchEvent(new Event('submit'))">
|
||||
x-model="status" @change="$el.closest('form').dispatchEvent(new Event('submit'))">
|
||||
<option value="active" {{ request('status')==='active' ? 'selected' : '' }}
|
||||
data-title="{{ __('Active') }}">{{ __('Active') }}</option>
|
||||
<option value="expired" {{ request('status')==='expired' ? 'selected' : '' }}
|
||||
@ -275,20 +290,38 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
@php
|
||||
$displayStatus = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired' : $code->status;
|
||||
$displayStatus = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired'
|
||||
: ($code->status === 'disabled' ? 'cancelled' : $code->status);
|
||||
@endphp
|
||||
<x-status-badge :status="$displayStatus" />
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
|
||||
@if($code->status === 'active' && (!$code->expires_at ||
|
||||
$code->expires_at->isFuture()))
|
||||
<button type="button" @click="openEditModal({
|
||||
id: {{ $code->id }},
|
||||
name: '{{ addslashes($code->name) }}',
|
||||
expires_at: '{{ $code->expires_at ? $code->expires_at->format('Y/m/d H:i') : '' }}'
|
||||
})"
|
||||
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all duration-300 border border-slate-100 dark:border-slate-800"
|
||||
title="{{ __('Edit') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
|
||||
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all duration-300 border border-slate-100 dark:border-slate-800"
|
||||
title="{{ __('View QR') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
@ -302,7 +335,8 @@
|
||||
@click="confirmDelete('delete-form-desktop-{{ $code->id }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20"
|
||||
title="{{ __('Cancel Code') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-3.38a2.25 2.25 0 00-2.25-2.25h-3.51a2.25 2.25 0 00-2.25 2.25v3.38" />
|
||||
</svg>
|
||||
@ -353,11 +387,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
@php
|
||||
$displayStatusMobile = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired' : $code->status;
|
||||
@endphp
|
||||
<x-status-badge :status="$displayStatusMobile" size="sm" />
|
||||
</div>
|
||||
@php
|
||||
$displayStatusMobile = $code->status === 'active' && $code->expires_at?->isPast() ?
|
||||
'expired' : ($code->status === 'disabled' ? 'cancelled' : $code->status);
|
||||
@endphp
|
||||
<x-status-badge :status="$displayStatusMobile" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Info Grid --}}
|
||||
@ -380,14 +415,28 @@
|
||||
</div>
|
||||
|
||||
{{-- Action Buttons --}}
|
||||
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex gap-3">
|
||||
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex flex-wrap gap-3">
|
||||
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
|
||||
<button type="button" @click="openEditModal({
|
||||
id: {{ $code->id }},
|
||||
name: '{{ addslashes($code->name) }}',
|
||||
expires_at: '{{ $code->expires_at ? $code->expires_at->format('Y/m/d H:i') : '' }}'
|
||||
})"
|
||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest border border-slate-100 dark:border-slate-800 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all duration-300">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
|
||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest border border-slate-100 dark:border-slate-800 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all duration-300">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
</svg>
|
||||
{{ __('QR Code') }}
|
||||
</button>
|
||||
@ -395,8 +444,7 @@
|
||||
|
||||
@if($code->status === 'active')
|
||||
<form id="delete-form-mobile-{{ $code->id }}"
|
||||
action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST"
|
||||
class="flex-1">
|
||||
action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST" class="flex-1">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button" @click="confirmDelete('delete-form-mobile-{{ $code->id }}')"
|
||||
@ -452,7 +500,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST" class="space-y-6" @submit.prevent="validateAndSubmit">
|
||||
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST" class="space-y-6"
|
||||
@submit.prevent="validateAndSubmit">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
@ -498,13 +547,16 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity Period (Days)') }}</label>
|
||||
|
||||
<div class="bg-slate-50 dark:bg-slate-800/40 rounded-[2rem] p-6 border border-slate-100 dark:border-slate-800/50 shadow-sm">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity
|
||||
Period (Days)') }}</label>
|
||||
|
||||
<div
|
||||
class="bg-slate-50 dark:bg-slate-800/40 rounded-[2rem] p-6 border border-slate-100 dark:border-slate-800/50 shadow-sm">
|
||||
<div class="flex items-center gap-6 mb-6">
|
||||
<button type="button" @click="expiresDays = Math.max(0, parseInt(expiresDays || 0) - 1)"
|
||||
class="shrink-0 w-12 h-12 rounded-xl bg-white dark:bg-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:shadow-lg active:scale-90 transition-all border border-slate-100 dark:border-slate-800">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -512,11 +564,14 @@
|
||||
<input type="number" name="expires_days" x-model="expiresDays"
|
||||
class="w-full bg-transparent border-none text-center font-black text-3xl text-slate-800 dark:text-white focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
placeholder="{{ __('Permanent') }}">
|
||||
<div class="text-center mt-1 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Days') }}</div>
|
||||
<div
|
||||
class="text-center mt-1 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
||||
{{ __('Days') }}</div>
|
||||
</div>
|
||||
<button type="button" @click="expiresDays = parseInt(expiresDays || 0) + 1"
|
||||
class="shrink-0 w-12 h-12 rounded-xl bg-white dark:bg-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:shadow-lg active:scale-90 transition-all border border-slate-100 dark:border-slate-800">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -524,18 +579,28 @@
|
||||
|
||||
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/50">
|
||||
<div class="flex items-center justify-between mb-3 px-1">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Estimated Expiry') }}</span>
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{
|
||||
__('Estimated Expiry') }}</span>
|
||||
<template x-if="!expiresDays || expiresDays <= 0">
|
||||
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-widest px-2 py-0.5 bg-emerald-500/10 rounded-lg border border-emerald-500/20">{{ __('Permanent Code') }}</span>
|
||||
<span
|
||||
class="text-[10px] font-black text-emerald-500 uppercase tracking-widest px-2 py-0.5 bg-emerald-500/10 rounded-lg border border-emerald-500/20">{{
|
||||
__('Permanent Code') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-900/50 rounded-2xl p-4 border border-slate-100 dark:border-slate-700/50 flex items-center justify-center gap-3">
|
||||
<svg class="w-5 h-5 text-cyan-500/50 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/50 rounded-2xl p-4 border border-slate-100 dark:border-slate-700/50 flex items-center justify-center gap-3">
|
||||
<svg class="w-5 h-5 text-cyan-500/50 shrink-0" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-xl font-mono font-black text-slate-700 dark:text-slate-200 tracking-wider" x-text="calculateExpiry()"></span>
|
||||
<span
|
||||
class="text-xl font-mono font-black text-slate-700 dark:text-slate-200 tracking-wider"
|
||||
x-text="calculateExpiry()"></span>
|
||||
</div>
|
||||
<p class="text-center mt-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Leave empty or 0 for permanent code') }}</p>
|
||||
<p
|
||||
class="text-center mt-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
{{ __('Leave empty or 0 for permanent code') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -554,8 +619,8 @@
|
||||
|
||||
{{-- Confirm Cancel Modal --}}
|
||||
<x-confirm-modal alpineVar="showDeleteModal" :title="__('Cancel Pass Code')"
|
||||
:message="__('Are you sure you want to cancel this pass code? This action cannot be undone.')" :confirmText="__('Yes, Cancel')"
|
||||
confirmAction="submitDelete()" confirmColor="rose" iconType="danger" />
|
||||
:message="__('Are you sure you want to cancel this pass code? This action cannot be undone.')"
|
||||
:confirmText="__('Yes, Cancel')" confirmAction="submitDelete()" confirmColor="rose" iconType="danger" />
|
||||
|
||||
{{-- QR Code Modal --}}
|
||||
<div x-show="showQrModal" class="fixed inset-0 z-[60] overflow-y-auto"
|
||||
@ -568,15 +633,17 @@
|
||||
|
||||
<div x-show="showQrModal" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 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 sm:scale-100"
|
||||
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 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
|
||||
class="inline-block w-full max-w-sm transition-all transform luxury-card rounded-[2.5rem] bg-white dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl overflow-hidden">
|
||||
|
||||
|
||||
<div class="p-8 pb-0 flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pass Code QR') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1" x-text="activeQrCode"></p>
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
|
||||
__('Pass Code QR') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1"
|
||||
x-text="activeQrCode"></p>
|
||||
</div>
|
||||
<button @click="showQrModal = false"
|
||||
class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
@ -603,7 +670,8 @@
|
||||
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-slate-100 dark:border-slate-700 hover:border-cyan-500/20 transition-all flex items-center justify-center gap-2 group"
|
||||
title="{{ __('Copy Link') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
<span class="text-xs font-black uppercase tracking-widest">{{ __('Link') }}</span>
|
||||
</button>
|
||||
@ -613,7 +681,8 @@
|
||||
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-slate-100 dark:border-slate-700 hover:border-cyan-500/20 transition-all flex items-center justify-center gap-2 group"
|
||||
title="{{ __('Copy Code') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="text-xs font-black uppercase tracking-widest">{{ __('Copy') }}</span>
|
||||
</button>
|
||||
@ -632,12 +701,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-center border-t border-slate-100 dark:border-slate-800">
|
||||
<button @click="showQrModal = false" class="btn-luxury-primary w-full py-4 rounded-2xl">{{ __('Close') }}</button>
|
||||
<div
|
||||
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-center border-t border-slate-100 dark:border-slate-800">
|
||||
<button @click="showQrModal = false" class="btn-luxury-primary w-full py-4 rounded-2xl">{{
|
||||
__('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Edit Modal --}}
|
||||
<div x-show="showEditModal" class="fixed inset-0 z-50 overflow-y-auto"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||
@click="showEditModal = false"></div>
|
||||
|
||||
<div x-show="showEditModal" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 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 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
|
||||
class="inline-block px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-xl sm:w-full overflow-visible">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
|
||||
__('Edit Pass Code') }}</h3>
|
||||
<button @click="showEditModal = false"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form :action="editFormAction" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
||||
{{ __('Description / Name') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" x-model="editPassCodeName" class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g. Test Code for Maintenance') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Expires
|
||||
At') }}</label>
|
||||
<x-luxury-datetime-input name="expires_at" :placeholder="__('Permanent')"
|
||||
x-model="editExpiresAt" />
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest pl-1 mt-1">{{ __('Leave
|
||||
empty for permanent code') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8">
|
||||
<button type="button" @click="showEditModal = false" class="btn-luxury-ghost px-8">{{
|
||||
__('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">
|
||||
{{ __('Update') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@ -6,8 +6,10 @@ $slotSelectConfig = [
|
||||
"hasSearch" => true,
|
||||
"searchPlaceholder" => __("Search Slot..."),
|
||||
"isHidePlaceholder" => false,
|
||||
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg
|
||||
focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200
|
||||
placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
|
||||
class="text-slate-800 dark:text-slate-200" data-title></span>
|
||||
@ -17,8 +19,11 @@ $slotSelectConfig = [
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg></div>
|
||||
</button>',
|
||||
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
|
||||
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
|
||||
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10
|
||||
rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
|
||||
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer
|
||||
hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between
|
||||
transition-all duration-300",
|
||||
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
|
||||
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
||||
@ -127,7 +132,7 @@ $slotSelectConfig = [
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Expires At') }}
|
||||
{{ __('Expiry') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
@ -174,11 +179,12 @@ $slotSelectConfig = [
|
||||
}}: {{ $code->slot_no }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div
|
||||
class="text-sm font-black text-slate-600 dark:text-slate-300 font-mono tracking-widest">
|
||||
{{ $code->expires_at->format('Y-m-d H:i') }}</div>
|
||||
<div class="text-xs font-bold text-slate-400 mt-0.5">{{
|
||||
$code->expires_at->diffForHumans() }}</div>
|
||||
<div class="text-sm font-black text-slate-600 dark:text-slate-300">
|
||||
{{ $code->expires_at->format('Y-m-d H:i') }}
|
||||
</div>
|
||||
<div class="text-[10px] font-bold text-slate-400 mt-0.5">
|
||||
{{ $code->expires_at->diffForHumans() }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
@php
|
||||
@ -290,9 +296,12 @@ $slotSelectConfig = [
|
||||
<div>
|
||||
<p
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||
{{ __('Expires At') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono tracking-tighter">
|
||||
{{ $code->expires_at->format('Y-m-d H:i') }}</p>
|
||||
{{ __('Expiry') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
|
||||
{{ $code->expires_at->format('m-d H:i') }}
|
||||
<span class="text-[10px] text-slate-400 ml-1">{{ $code->expires_at->diffForHumans()
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -372,7 +381,8 @@ $slotSelectConfig = [
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.sales.pickup-codes.store') }}" method="POST" class="space-y-6" @submit.prevent="validateAndSubmit">
|
||||
<form action="{{ route('admin.sales.pickup-codes.store') }}" method="POST" class="space-y-6"
|
||||
@submit.prevent="validateAndSubmit">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
@ -422,57 +432,46 @@ $slotSelectConfig = [
|
||||
maxlength="12" required>
|
||||
</div>
|
||||
|
||||
{{-- Validity Period Section --}}
|
||||
<div class="max-w-2xl mx-auto w-full">
|
||||
{{-- Validity Period Section (Compact) --}}
|
||||
<div class="space-y-4">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity
|
||||
Period') }}</label>
|
||||
|
||||
<div
|
||||
class="bg-slate-50 dark:bg-slate-800/40 rounded-[2.5rem] p-8 border border-slate-100 dark:border-slate-800/50 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-8 px-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-cyan-500/50"></div>
|
||||
<label
|
||||
class="text-[13px] font-black text-slate-500 dark:text-slate-400 tracking-wider">{{
|
||||
__('Validity Period') }}</label>
|
||||
class="bg-slate-50 dark:bg-slate-800/40 rounded-[2rem] p-6 border border-slate-100 dark:border-slate-800/50 shadow-sm">
|
||||
<div class="flex items-center gap-6 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<input type="range" name="expires_hours" x-model="expiresHours" min="1"
|
||||
:max="maxHours"
|
||||
class="w-full h-1.5 bg-slate-200 dark:bg-slate-700/50 rounded-lg appearance-none cursor-pointer accent-cyan-500 transition-all hover:accent-cyan-400">
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 w-20 py-3 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm text-center">
|
||||
<span class="text-xl font-black text-slate-800 dark:text-white leading-none"
|
||||
x-text="expiresHours"></span>
|
||||
<span
|
||||
class="block text-[9px] font-black text-slate-400 uppercase mt-0.5 tracking-widest">{{
|
||||
__('Hrs') }}</span>
|
||||
</div>
|
||||
<span class="text-[11px] font-bold text-slate-400/70 tracking-wide">{{ __('Max 24 Hours') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-10">
|
||||
{{-- Slider and Value --}}
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex-1 relative">
|
||||
<input type="range" name="expires_hours" x-model="expiresHours" min="1"
|
||||
:max="maxHours"
|
||||
class="w-full h-1 bg-slate-200 dark:bg-slate-700/50 rounded-lg appearance-none cursor-pointer accent-cyan-500 transition-all hover:accent-cyan-400">
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 w-24 py-4 bg-white dark:bg-slate-900 rounded-[1.5rem] border border-slate-200 dark:border-slate-700 shadow-sm text-center ring-4 ring-slate-50 dark:ring-slate-800/50">
|
||||
<span class="text-2xl font-black text-slate-800 dark:text-white leading-none"
|
||||
x-text="expiresHours"></span>
|
||||
<span
|
||||
class="block text-[10px] font-black text-slate-400 uppercase mt-1 tracking-widest">{{
|
||||
__('Hrs') }}</span>
|
||||
</div>
|
||||
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/50">
|
||||
<div class="flex items-center justify-between mb-3 px-1">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{
|
||||
__('Expected Expiry') }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400/70 tracking-wide">{{ __('Max 24
|
||||
Hours') }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Result Display --}}
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-8 px-8 bg-white dark:bg-slate-900/50 rounded-[2rem] border border-slate-200/60 dark:border-slate-700/50 shadow-inner">
|
||||
<span
|
||||
class="text-base font-bold text-slate-600 dark:text-slate-400 mb-5 tracking-wide">{{
|
||||
__('Expected Expiry Date & Time') }}</span>
|
||||
<div
|
||||
class="flex items-center gap-3 py-3 px-6 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-slate-700/50 shadow-sm">
|
||||
<svg class="w-5 h-5 text-cyan-500/50" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span
|
||||
class="text-xl sm:text-2xl font-black text-slate-700 dark:text-cyan-100 font-mono tracking-tight"
|
||||
x-text="calculateExpiry()"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/50 rounded-2xl p-4 border border-slate-100 dark:border-slate-700/50 flex items-center justify-center gap-3">
|
||||
<svg class="w-5 h-5 text-cyan-500/50 shrink-0" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span
|
||||
class="text-xl font-mono font-black text-slate-700 dark:text-slate-200 tracking-wider"
|
||||
x-text="calculateExpiry()"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -546,7 +545,8 @@ $slotSelectConfig = [
|
||||
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-slate-100 dark:border-slate-700 hover:border-cyan-500/20 transition-all flex items-center justify-center gap-2 group"
|
||||
title="{{ __('Copy Code') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="text-xs font-black uppercase tracking-widest">{{ __('Copy') }}</span>
|
||||
</button>
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
<div x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
// All focusable element types...
|
||||
@ -30,49 +29,32 @@ $maxWidth = [
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
}" x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
document.body.classList.remove('overflow-y-hidden');
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||
style="display: {{ $show ? 'block' : 'none' }};"
|
||||
>
|
||||
<div
|
||||
x-show="show"
|
||||
class="fixed inset-0 transform transition-all"
|
||||
x-on:click="show = false"
|
||||
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"
|
||||
>
|
||||
})" x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null" x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false" x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()" x-show="show"
|
||||
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" style="display: {{ $show ? 'block' : 'none' }};">
|
||||
<div x-show="show" class="fixed inset-0 transform transition-all" x-on:click="show = false"
|
||||
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">
|
||||
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="show"
|
||||
<div x-show="show"
|
||||
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
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: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"
|
||||
>
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,52 +1,70 @@
|
||||
@props([
|
||||
'name' => null,
|
||||
'options' => [],
|
||||
'selected' => null,
|
||||
'placeholder' => null,
|
||||
'id' => null,
|
||||
'hasSearch' => true,
|
||||
'name' => null,
|
||||
'options' => [],
|
||||
'selected' => null,
|
||||
'placeholder' => null,
|
||||
'id' => null,
|
||||
'hasSearch' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
$id = $id ?? $name ?? 'select-' . uniqid();
|
||||
$options = is_iterable($options) ? $options : [];
|
||||
|
||||
// Skill Standard: Use " " for empty/all options to bypass Preline hiding while staying 'blank'
|
||||
$isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' ');
|
||||
|
||||
$config = [
|
||||
"placeholder" => $placeholder ?: __('Select...'),
|
||||
"hasSearch" => (bool)$hasSearch,
|
||||
"searchPlaceholder" => $placeholder ?: __('Search...'),
|
||||
"isHidePlaceholder" => false,
|
||||
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg></div></button>',
|
||||
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
|
||||
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
|
||||
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||
];
|
||||
$id = $id ?? $name ?? 'select-' . uniqid();
|
||||
$options = is_iterable($options) ? $options : [];
|
||||
|
||||
// Skill Standard: Use " " for empty/all options to bypass Preline hiding while staying 'blank'
|
||||
$isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' ');
|
||||
|
||||
$config = [
|
||||
"placeholder" => $placeholder ?: __('Select...'),
|
||||
"hasSearch" => (bool)$hasSearch,
|
||||
"searchPlaceholder" => $placeholder ?: __('Search...'),
|
||||
"isHidePlaceholder" => false,
|
||||
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg
|
||||
focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200
|
||||
placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
|
||||
class="text-slate-800 dark:text-slate-200" data-title></span>
|
||||
<div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300"
|
||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg></div>
|
||||
</button>',
|
||||
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10
|
||||
rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
|
||||
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer
|
||||
hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between
|
||||
transition-all duration-300",
|
||||
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
|
||||
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg></span></div>'
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'relative w-full'])->only('class') }}>
|
||||
<select name="{{ $name }}" id="{{ $id }}" data-hs-select='{!! json_encode($config) !!}' class="hidden" {{ $attributes->except(['options', 'selected', 'placeholder', 'id', 'name', 'class', 'hasSearch']) }}>
|
||||
<select name="{{ $name }}" id="{{ $id }}" data-hs-select='{!! json_encode($config) !!}' class="hidden" {{
|
||||
$attributes->except(['options', 'selected', 'placeholder', 'id', 'name', 'class', 'hasSearch']) }}>
|
||||
@if($placeholder)
|
||||
<option value=" " {{ $isEmptySelected ? 'selected' : '' }} data-title="{{ $placeholder }}">
|
||||
{{ $placeholder }}
|
||||
</option>
|
||||
<option value=" " {{ $isEmptySelected ? 'selected' : '' }} data-title="{{ $placeholder }}">
|
||||
{{ $placeholder }}
|
||||
</option>
|
||||
@endif
|
||||
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@foreach($options as $v => $l)
|
||||
@php
|
||||
$val = is_object($l) ? ($l->id ?? $l->value) : $v;
|
||||
$text = is_object($l) ? ($l->name ?? $l->label ?? $l->title) : $l;
|
||||
@endphp
|
||||
<option value="{{ $val }}" {{ (string)$selected === (string)$val ? 'selected' : '' }} data-title="{{ $text }}">
|
||||
{{ $text }}
|
||||
</option>
|
||||
@php
|
||||
$val = is_object($l) ? ($l->id ?? $l->value) : $v;
|
||||
$text = is_object($l) ? ($l->name ?? $l->label ?? $l->title) : $l;
|
||||
@endphp
|
||||
<option value="{{ $val }}" {{ (string)$selected===(string)$val ? 'selected' : '' }} data-title="{{ $text }}">
|
||||
{{ $text }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user