Files
star-cloud/resources/views/admin/ads/partials/ad-modal.blade.php
sky121113 54d62c5378
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m7s
[FEAT] 實作機台廣告管理模組與多語系支援
1. 新增廣告管理列表與機台配置介面,包含多語系 (zh_TW, en, ja) 與完整 CRUD
2. 實作基於 Alpine 的廣告素材預覽輪播功能
3. 優化廣告素材下拉選單,強制綁定所屬公司以達成多租戶資料隔離
4. 重構廣告配置中廣告影片的縮圖渲染邏輯,移除 <video> 標籤以大幅提升頁面載入速度與節省頻寬
5. 放寬個人檔案頭像上傳限制,支援 WebP 格式
2026-03-31 13:30:41 +08:00

195 lines
10 KiB
PHP

<div x-show="isAdModalOpen"
class="fixed inset-0 z-[100] overflow-y-auto"
x-cloak
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">
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative w-full max-w-xl bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl border border-slate-200 dark:border-white/10 overflow-hidden animate-luxury-in"
@click.away="isAdModalOpen = false">
<!-- Modal Header -->
<div class="bg-slate-50/50 dark:bg-slate-800/50 px-8 py-5 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight" x-text="adFormMode === 'add' ? '{{ __("Add Advertisement") }}' : '{{ __("Edit Advertisement") }}'"></h3>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Manage your ad material details') }}</p>
</div>
<button @click="isAdModalOpen = false" class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form :action="adFormMode === 'add' ? urls.store : urls.update.replace(':id', adForm.id)"
method="POST"
enctype="multipart/form-data"
@submit.prevent="submitAdForm"
class="px-8 pt-4 pb-8 space-y-4">
@csrf
<template x-if="adFormMode === 'edit'">
@method('PUT')
</template>
@if(auth()->user()->isSystemAdmin())
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Company Name') }}
</label>
<x-searchable-select
name="company_id"
id="ad_company_select"
:has-search="true"
:selected="null"
:options="['' => __('System Default (All Companies)')] + $companies->pluck('name', 'id')->toArray()"
@change="adForm.company_id = $event.target.value"
/>
</div>
@endif
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Material Name') }}
<span class="text-rose-500 ml-0.5">*</span>
</label>
<input type="text" name="name" x-model="adForm.name" required
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
placeholder="{{ __('Enter ad material name') }}">
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Material Type') }}
<span class="text-rose-500 ml-0.5">*</span>
</label>
<x-searchable-select
name="type"
id="ad_type_select"
:has-search="false"
:selected="null"
:options="['image' => __('image'), 'video' => __('video')]"
@change="adForm.type = $event.target.value; fileName = ''; document.querySelector('input[name=file]').value = ''"
/>
</div>
<!-- Duration -->
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
{{ __('Duration (Seconds)') }}
<span class="text-rose-500 ml-0.5">*</span>
</label>
<x-searchable-select
name="duration"
id="ad_duration_select"
:has-search="false"
:selected="null"
:options="['15' => '15 ' . __('Seconds'), '30' => '30 ' . __('Seconds'), '60' => '60 ' . __('Seconds')]"
@change="adForm.duration = $event.target.value"
/>
</div>
</div>
<!-- File Upload -->
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
<span x-text="adForm.type === 'image' ? '{{ __("Upload Image") }}' : '{{ __("Upload Video") }}'"></span>
<template x-if="adFormMode === 'add'">
<span class="text-rose-500 ml-0.5">*</span>
</template>
</label>
<div class="relative group">
<div class="absolute inset-0 bg-gradient-to-tr from-cyan-500/5 to-indigo-500/5 rounded-2xl border-2 border-dashed border-slate-200 dark:border-white/10 group-hover:border-cyan-500/30 transition-all"></div>
<label class="relative flex flex-col items-center justify-center p-10 cursor-pointer">
<svg class="size-8 text-slate-300 group-hover:text-cyan-500 transition-colors mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
<span class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest group-hover:text-cyan-500 transition-colors">{{ __('Click to upload') }}</span>
<span class="text-[10px] font-bold text-slate-400 mt-1 uppercase" x-text="adForm.type === 'image' ? 'JPG, PNG, WEBP (' + '{{ __('Max 5MB') }}' + ')' : 'MP4, MOV (' + '{{ __('Max 50MB') }}' + ')'"></span>
<input type="file" name="file"
:accept="adForm.type === 'image' ? 'image/jpeg,image/png,image/gif,image/webp' : 'video/mp4,video/quicktime,video/x-msvideo'"
class="hidden" @change="handleFileChange">
</label>
</div>
<!-- Preview Filename -->
<p class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 italic px-2" x-show="fileName" x-text="fileName"></p>
</div>
<!-- Status -->
<div class="flex items-center gap-3 px-2">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="is_active" x-model="adForm.is_active" value="1" class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-500"></div>
</label>
<span class="text-sm font-black text-slate-700 dark:text-slate-200 tracking-tight">{{ __('Active Status') }}</span>
</div>
<!-- Action Footer -->
<div class="flex items-center justify-end gap-3 pt-2">
<button type="button" @click="isAdModalOpen = false" class="px-6 py-3 text-sm font-black text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors uppercase tracking-widest">
{{ __('Cancel') }}
</button>
<button type="submit" class="btn-luxury-primary px-10 py-3">
{{ __('Save Material') }}
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function handleFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const type = this.adForm.type;
const maxSize = type === 'image' ? 5 * 1024 * 1024 : 50 * 1024 * 1024;
// Check size
if (file.size > maxSize) {
window.showToast?.('error', '{{ __("File is too large") }} (Max: ' + (maxSize / 1024 / 1024) + 'MB)');
e.target.value = '';
this.fileName = '';
return;
}
// Check extension/mime
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
if (type === 'image' && !isImage) {
window.showToast?.('error', '{{ __("Please upload an image file") }}');
e.target.value = '';
this.fileName = '';
return;
}
if (type === 'video' && !isVideo) {
window.showToast?.('error', '{{ __("Please upload a video file") }}');
e.target.value = '';
this.fileName = '';
return;
}
this.fileName = file.name;
}
function submitAdForm(e) {
// Final check before submission
if (!this.adForm.name || !this.adForm.type || !this.adForm.duration) {
window.showToast?.('error', '{{ __("Please fill in all required fields") }}');
return;
}
if (this.adFormMode === 'add' && !this.fileName) {
window.showToast?.('error', '{{ __("Please upload a material file") }}');
return;
}
e.target.submit();
}
</script>