[FEAT] 廣告排程功能與 UI 優化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s
1. 新增廣告排程功能,支援設定發布時間與下架時間。 2. 整合 Flatpickr 時間選擇器,提供與機台日誌一致的極簡奢華風 UI。 3. 優化廣告列表中的數字字體,套用 font-mono 與 tabular-nums,與客戶管理模組風格同步。 4. 修正 Alpine.js 資料同步邏輯,確保編輯模式下排程時間能正確回填。
This commit is contained in:
@@ -21,7 +21,9 @@ class AdvertisementController extends AdminController
|
|||||||
|
|
||||||
// Tab 1: 廣告列表
|
// Tab 1: 廣告列表
|
||||||
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
||||||
$allAds = Advertisement::active()->get();
|
|
||||||
|
// Tab 2: 機台廣告設置 (所需資料) - 隱藏已過期的廣告
|
||||||
|
$allAds = Advertisement::playing()->get();
|
||||||
|
|
||||||
// Tab 2: 機台廣告設置 (所需資料)
|
// Tab 2: 機台廣告設置 (所需資料)
|
||||||
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
|
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
|
||||||
@@ -54,6 +56,8 @@ class AdvertisementController extends AdminController
|
|||||||
$request->type === 'image' ? 'max:10240' : 'max:51200', // Image 10MB, Video 50MB
|
$request->type === 'image' ? 'max:10240' : 'max:51200', // Image 10MB, Video 50MB
|
||||||
],
|
],
|
||||||
'company_id' => 'nullable|exists:companies,id',
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
|
'start_at' => 'nullable|date',
|
||||||
|
'end_at' => 'nullable|date|after_or_equal:start_at',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -71,13 +75,15 @@ class AdvertisementController extends AdminController
|
|||||||
$companyId = $user->company_id;
|
$companyId = $user->company_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Advertisement::create([
|
$advertisement = Advertisement::create([
|
||||||
'company_id' => $companyId,
|
'company_id' => $companyId,
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'type' => $request->type,
|
'type' => $request->type,
|
||||||
'duration' => (int) $request->duration,
|
'duration' => (int) $request->duration,
|
||||||
'url' => Storage::disk('public')->url($path),
|
'url' => Storage::disk('public')->url($path),
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'start_at' => $request->start_at,
|
||||||
|
'end_at' => $request->end_at,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->wantsJson()) {
|
if ($request->wantsJson()) {
|
||||||
@@ -99,6 +105,8 @@ class AdvertisementController extends AdminController
|
|||||||
'duration' => 'required|in:15,30,60',
|
'duration' => 'required|in:15,30,60',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'company_id' => 'nullable|exists:companies,id',
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
|
'start_at' => 'nullable|date',
|
||||||
|
'end_at' => 'nullable|date|after_or_equal:start_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($request->hasFile('file')) {
|
if ($request->hasFile('file')) {
|
||||||
@@ -111,7 +119,7 @@ class AdvertisementController extends AdminController
|
|||||||
|
|
||||||
$request->validate($rules);
|
$request->validate($rules);
|
||||||
|
|
||||||
$data = $request->only(['name', 'type', 'duration']);
|
$data = $request->only(['name', 'type', 'duration', 'start_at', 'end_at']);
|
||||||
$data['is_active'] = $request->has('is_active');
|
$data['is_active'] = $request->has('is_active');
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -150,7 +158,7 @@ class AdvertisementController extends AdminController
|
|||||||
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
|
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Advertisement $advertisement)
|
public function destroy(Request $request, Advertisement $advertisement)
|
||||||
{
|
{
|
||||||
// 檢查是否有機台正投放中
|
// 檢查是否有機台正投放中
|
||||||
if ($advertisement->machineAdvertisements()->exists()) {
|
if ($advertisement->machineAdvertisements()->exists()) {
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ class MachineController extends Controller
|
|||||||
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
|
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
|
||||||
->with([
|
->with([
|
||||||
'advertisement' => function ($query) {
|
'advertisement' => function ($query) {
|
||||||
$query->active();
|
$query->playing();
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
->get()
|
->get()
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ class Advertisement extends Model
|
|||||||
'duration',
|
'duration',
|
||||||
'url',
|
'url',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
'start_at',
|
||||||
|
'end_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'duration' => 'integer',
|
'duration' => 'integer',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
|
'start_at' => 'datetime',
|
||||||
|
'end_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,4 +52,21 @@ class Advertisement extends Model
|
|||||||
{
|
{
|
||||||
return $query->where('is_active', true);
|
return $query->where('is_active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to only include advertisements that should be playing now.
|
||||||
|
*/
|
||||||
|
public function scopePlaying($query)
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
return $query->where('is_active', true)
|
||||||
|
->where(function ($q) use ($now) {
|
||||||
|
$q->whereNull('start_at')
|
||||||
|
->orWhere('start_at', '<=', $now);
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($now) {
|
||||||
|
$q->whereNull('end_at')
|
||||||
|
->orWhere('end_at', '>=', $now);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('advertisements', function (Blueprint $table) {
|
||||||
|
$table->dateTime('start_at')->nullable()->after('url')->comment('發布時間');
|
||||||
|
$table->dateTime('end_at')->nullable()->after('start_at')->comment('下架時間');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('advertisements', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['start_at', 'end_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1145,5 +1145,12 @@
|
|||||||
"Service Terms": "Service Periods",
|
"Service Terms": "Service Periods",
|
||||||
"Contract": "Contract",
|
"Contract": "Contract",
|
||||||
"Warranty": "Warranty",
|
"Warranty": "Warranty",
|
||||||
"Software": "Software"
|
"Software": "Software",
|
||||||
|
"Schedule": "Schedule",
|
||||||
|
"Immediate": "Immediate",
|
||||||
|
"Indefinite": "Indefinite",
|
||||||
|
"Ongoing": "Ongoing",
|
||||||
|
"Waiting": "Waiting",
|
||||||
|
"Publish Time": "Publish Time",
|
||||||
|
"Expired Time": "Expired Time"
|
||||||
}
|
}
|
||||||
@@ -1144,5 +1144,12 @@
|
|||||||
"Service Terms": "サービス期間",
|
"Service Terms": "サービス期間",
|
||||||
"Contract": "契約",
|
"Contract": "契約",
|
||||||
"Warranty": "保証",
|
"Warranty": "保証",
|
||||||
"Software": "ソフトウェア"
|
"Software": "ソフトウェア",
|
||||||
|
"Schedule": "スケジュール設定",
|
||||||
|
"Immediate": "即時",
|
||||||
|
"Indefinite": "無期限",
|
||||||
|
"Ongoing": "進行中",
|
||||||
|
"Waiting": "待機中",
|
||||||
|
"Publish Time": "公開時間",
|
||||||
|
"Expired Time": "終了時間"
|
||||||
}
|
}
|
||||||
@@ -1145,5 +1145,12 @@
|
|||||||
"Service Terms": "服務期程",
|
"Service Terms": "服務期程",
|
||||||
"Contract": "合約",
|
"Contract": "合約",
|
||||||
"Warranty": "保固",
|
"Warranty": "保固",
|
||||||
"Software": "軟體"
|
"Software": "軟體",
|
||||||
|
"Schedule": "排程區間",
|
||||||
|
"Immediate": "立即",
|
||||||
|
"Indefinite": "無限期",
|
||||||
|
"Ongoing": "進行中",
|
||||||
|
"Waiting": "等待中",
|
||||||
|
"Publish Time": "發布時間",
|
||||||
|
"Expired Time": "下架時間"
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
@endif
|
@endif
|
||||||
<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 text-center">{{ __('Type') }}</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 text-center">{{ __('Type') }}</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 text-center">{{ __('Duration') }}</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 text-center">{{ __('Duration') }}</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 text-center">{{ __('Schedule') }}</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 text-center">{{ __('Status') }}</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 text-center">{{ __('Status') }}</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 text-right">{{ __('Actions') }}</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 text-right">{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -104,15 +105,34 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
{{ __($ad->type) }}
|
{{ __($ad->type) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-black text-slate-700 dark:text-slate-200">
|
<td class="px-6 py-4 text-center whitespace-nowrap text-sm font-mono font-bold text-slate-700 dark:text-slate-200">
|
||||||
{{ $ad->duration }}s
|
{{ $ad->duration }}s
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||||
|
<div class="flex flex-col items-center gap-0.5">
|
||||||
|
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('From') }}: {{ $ad->start_at?->format('Y-m-d H:i') ?? __('Immediate') }}</span>
|
||||||
|
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('To') }}: {{ $ad->end_at?->format('Y-m-d H:i') ?? __('Indefinite') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@if($ad->is_active)
|
@php
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Active') }}</span>
|
$now = now();
|
||||||
@else
|
$isStarted = !$ad->start_at || $ad->start_at <= $now;
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
|
$isExpired = $ad->end_at && $ad->end_at < $now;
|
||||||
@endif
|
$isPlaying = $ad->is_active && $isStarted && !$isExpired;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
@if(!$ad->is_active)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
|
||||||
|
@elseif($isExpired)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Expired') }}</span>
|
||||||
|
@elseif(!$isStarted)
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">{{ __('Waiting') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Ongoing') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right">
|
<td class="px-6 py-4 text-right">
|
||||||
<div class="flex justify-end items-center gap-2">
|
<div class="flex justify-end items-center gap-2">
|
||||||
@@ -211,7 +231,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
</button>
|
</button>
|
||||||
<div class="flex-1 min-w-0 flex flex-col justify-center cursor-pointer group-hover:text-cyan-500 transition-colors" @click="openPreview(assign.advertisement)">
|
<div class="flex-1 min-w-0 flex flex-col justify-center cursor-pointer group-hover:text-cyan-500 transition-colors" @click="openPreview(assign.advertisement)">
|
||||||
<p class="text-xs font-black text-slate-700 dark:text-white truncate transition-colors" x-text="assign.advertisement.name"></p>
|
<p class="text-xs font-black text-slate-700 dark:text-white truncate transition-colors" x-text="assign.advertisement.name"></p>
|
||||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-tighter mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
|
<p class="text-[11px] font-mono font-bold text-slate-400 uppercase tracking-tight mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
|
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
|
||||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
@@ -324,7 +344,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
||||||
<span class="text-cyan-400 font-black tracking-widest text-xs uppercase" x-text="(currentSequenceIndex + 1) + ' / ' + sequenceAds.length"></span>
|
<span class="text-cyan-400 font-black tracking-widest text-xs uppercase" x-text="(currentSequenceIndex + 1) + ' / ' + sequenceAds.length"></span>
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-white/20"></span>
|
||||||
<span class="text-white/80 font-bold tracking-widest text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
|
<span class="text-white/80 font-mono font-bold tracking-tight text-xs tabular-nums" x-text="Math.ceil(sequenceRemainingTime) + 's'"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -385,7 +405,9 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
type: 'image',
|
type: 'image',
|
||||||
duration: 15,
|
duration: 15,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
url: ''
|
url: '',
|
||||||
|
start_at: '',
|
||||||
|
end_at: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
// Assign Modal
|
// Assign Modal
|
||||||
@@ -604,6 +626,17 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formatDateForInput(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
|
||||||
async submitAssignment() {
|
async submitAssignment() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.urls.assign, {
|
const response = await fetch(this.urls.assign, {
|
||||||
@@ -650,6 +683,14 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
if (document.querySelector('#ad_company_select')) {
|
if (document.querySelector('#ad_company_select')) {
|
||||||
window.HSSelect.getInstance('#ad_company_select')?.setValue(this.adForm.company_id || '');
|
window.HSSelect.getInstance('#ad_company_select')?.setValue(this.adForm.company_id || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 確保 Flatpickr 實例同步顯示目前的時間值
|
||||||
|
if (this.$refs.startAtPicker?._flatpickr) {
|
||||||
|
this.$refs.startAtPicker._flatpickr.setDate(this.adForm.start_at);
|
||||||
|
}
|
||||||
|
if (this.$refs.endAtPicker?._flatpickr) {
|
||||||
|
this.$refs.endAtPicker._flatpickr.setDate(this.adForm.end_at);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -685,7 +726,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
|
|
||||||
openAddModal() {
|
openAddModal() {
|
||||||
this.adFormMode = 'add';
|
this.adFormMode = 'add';
|
||||||
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '' };
|
this.adForm = { id: null, company_id: '', name: '', type: 'image', duration: 15, is_active: true, url: '', start_at: '', end_at: '' };
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.mediaPreview = null;
|
this.mediaPreview = null;
|
||||||
this.isAdModalOpen = true;
|
this.isAdModalOpen = true;
|
||||||
@@ -693,7 +734,11 @@ $baseRoute = 'admin.data-config.advertisements';
|
|||||||
|
|
||||||
openEditModal(ad) {
|
openEditModal(ad) {
|
||||||
this.adFormMode = 'edit';
|
this.adFormMode = 'edit';
|
||||||
this.adForm = { ...ad };
|
this.adForm = {
|
||||||
|
...ad,
|
||||||
|
start_at: this.formatDateForInput(ad.start_at),
|
||||||
|
end_at: this.formatDateForInput(ad.end_at)
|
||||||
|
};
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.mediaPreview = ad.url; // Use existing URL as preview
|
this.mediaPreview = ad.url; // Use existing URL as preview
|
||||||
this.isAdModalOpen = true;
|
this.isAdModalOpen = true;
|
||||||
|
|||||||
@@ -94,6 +94,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scheduling -->
|
||||||
|
<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">
|
||||||
|
{{ __('Publish Time') }}
|
||||||
|
</label>
|
||||||
|
<div class="relative group/input">
|
||||||
|
<input type="text" name="start_at" x-ref="startAtPicker" x-model="adForm.start_at"
|
||||||
|
x-init="flatpickr($refs.startAtPicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
onClose: (selectedDates, dateStr) => { adForm.start_at = dateStr; }
|
||||||
|
})"
|
||||||
|
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 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="YYYY/MM/DD HH:MM">
|
||||||
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||||
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
||||||
|
{{ __('Expired Time') }}
|
||||||
|
</label>
|
||||||
|
<div class="relative group/input">
|
||||||
|
<input type="text" name="end_at" x-ref="endAtPicker" x-model="adForm.end_at"
|
||||||
|
x-init="flatpickr($refs.endAtPicker, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y/m/d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: window.flatpickrLocale,
|
||||||
|
onClose: (selectedDates, dateStr) => { adForm.end_at = dateStr; }
|
||||||
|
})"
|
||||||
|
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 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="YYYY/MM/DD HH:MM">
|
||||||
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||||
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File Upload (Luxury UI Pattern) -->
|
<!-- File Upload (Luxury UI Pattern) -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest ml-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user