Implement catalog CRUD overhaul, snapshot fallback activation, and billing/UX hardening

This commit is contained in:
Dwindi Ramadhana
2026-02-17 00:03:35 +07:00
parent e6aef31dd1
commit 2726b6c312
37 changed files with 2936 additions and 204 deletions

View File

@@ -9,7 +9,8 @@
$user = $user ?? auth()->user();
$isPersonal = $user && (string) $user->tier === 'personal';
$freeLimit = $freeLimit ?? null;
$limitReached = $freeLimit !== null && $items->count() >= $freeLimit;
$activeCount = (int) ($activeCount ?? $items->where('is_active', true)->count());
$limitReached = $freeLimit !== null && $activeCount >= $freeLimit;
$emojiLookup = $emojiLookup ?? [];
@endphp
@@ -31,7 +32,7 @@
<div class="mt-2 text-xl font-semibold text-white">{{ $isPersonal ? 'Ready to personalize' : 'Free plan keywords' }}</div>
<p class="mt-2 text-sm text-gray-400">Add keywords to emojis to improve your personal search results.</p>
@if (!$isPersonal && $freeLimit)
<p class="mt-1 text-xs text-gray-500">Free plan limit: {{ $items->count() }} / {{ $freeLimit }} keywords.</p>
<p class="mt-1 text-xs text-gray-500">Free active limit: {{ $activeCount }} / {{ $freeLimit }} keywords. Inactive keywords are stored but not used in search.</p>
@endif
</div>
<div class="flex flex-wrap items-center gap-2">
@@ -52,9 +53,12 @@
@if (!$isPersonal && $freeLimit)
<div class="mt-6 rounded-2xl border border-brand-sun/30 bg-brand-sun/10 p-4 text-sm text-brand-sun">
Free plan includes up to {{ $freeLimit }} keywords total. Upgrade for unlimited keywords.
Free plan allows up to {{ $freeLimit }} active keywords. You can keep extras as inactive and reactivate after upgrading.
</div>
@endif
<div class="mt-4 rounded-2xl border border-white/10 bg-white/5 p-4 text-xs text-gray-400">
Search behavior: only <strong class="text-gray-200">Active</strong> private keywords are used in emoji matching. Inactive keywords stay saved in your account but are ignored by search and API results.
</div>
<div id="import-panel" class="mt-6 hidden rounded-2xl border border-white/10 bg-white/5 p-5">
<form method="POST" action="{{ route('dashboard.keywords.import') }}" enctype="multipart/form-data" class="grid gap-3 md:grid-cols-2">
@@ -81,6 +85,7 @@
<th class="px-4 py-3 text-left">Emoji</th>
<th class="px-4 py-3 text-left">Keyword</th>
<th class="px-4 py-3 text-left">Language</th>
<th class="px-4 py-3 text-left">Status</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
@@ -101,6 +106,15 @@
</td>
<td class="px-4 py-3 font-semibold text-white">{{ $item->keyword }}</td>
<td class="px-4 py-3 text-xs uppercase tracking-[0.15em] text-gray-400">{{ $item->lang ?? 'und' }}</td>
<td class="px-4 py-3">
@php
$isActive = (bool) ($item->is_active ?? true);
$canActivate = $isActive || $isPersonal || !$limitReached;
@endphp
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $isActive ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-200' : 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-200' }}">
{{ $isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="px-4 py-3 text-right">
<button
type="button"
@@ -109,9 +123,21 @@
data-emoji="{{ $item->emoji_slug }}"
data-keyword="{{ $item->keyword }}"
data-lang="{{ $item->lang }}"
>
>
Edit
</button>
<form method="POST" action="{{ route('dashboard.keywords.toggle_active', $item->id) }}" class="inline">
@csrf
@method('PUT')
<input type="hidden" name="is_active" value="{{ $isActive ? '0' : '1' }}">
<button
type="submit"
class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10 {{ (!$canActivate && !$isActive) ? 'opacity-50 cursor-not-allowed' : '' }}"
{{ (!$canActivate && !$isActive) ? 'disabled' : '' }}
>
{{ $isActive ? 'Deactivate' : 'Activate' }}
</button>
</form>
<form method="POST" action="{{ route('dashboard.keywords.delete', $item->id) }}" class="inline">
@csrf
@method('DELETE')
@@ -123,7 +149,7 @@
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">No keywords yet.</td>
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">No keywords yet.</td>
</tr>
@endforelse
</tbody>
@@ -317,7 +343,7 @@
}
emojiSearchAbort = new AbortController();
const base = isPersonal ? emojiSearchUrl : publicEmojiSearchUrl;
const base = emojiSearchUrl;
const url = `${base}?${new URLSearchParams({ q, limit: '30', page: '1' }).toString()}`;
try {