Update pricing UX, billing flows, and API rules

This commit is contained in:
Dwindi Ramadhana
2026-02-12 00:52:40 +07:00
parent cf065fab1e
commit a905256353
202 changed files with 22348 additions and 301 deletions

View File

@@ -12,6 +12,10 @@
$description = $emoji['description'] ?? '';
$unified = $emoji['unified'] ?? '';
$shortcode = $emoji['shortcodes'][0] ?? '';
$user = auth()->user();
$userTier = $userTier ?? $user?->tier;
$isPersonal = $userTier === 'personal';
$userKeywords = $userKeywords ?? collect();
$htmlHex = '';
$cssCode = '';
if (!empty($emoji['codepoints'][0])) {
@@ -66,13 +70,22 @@
</div>
</aside>
<main class="flex-1 h-full overflow-y-auto relative p-4 sm:p-6 lg:p-10 pb-24 lg:pb-10 flex flex-col">
<div class="flex items-center gap-2 text-sm text-gray-500 mb-8 font-mono">
<a href="{{ route('home') }}" class="hover:text-white transition-colors">Home</a>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="hover:text-white">{{ $category }}</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="text-brand-sun">{{ $name }}</span>
<main class="flex-1 h-full overflow-y-auto relative p-4 pt-0 sm:p-6 lg:p-10 pb-24 lg:pb-10 flex flex-col">
<div class="sticky top-0 z-40 -mx-4 px-4 py-3 mb-6 bg-[var(--app-bg)]/90 backdrop-blur border-b border-white/10 sm:static sm:mx-0 sm:px-0 sm:py-0 sm:mb-8 sm:border-0">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs sm:text-sm text-gray-500 font-mono">
<a href="{{ route('home') }}" class="hover:text-white transition-colors">Home</a>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="hidden sm:inline-flex hover:text-white">{{ $category }}</span>
<i data-lucide="chevron-right" class="w-3 h-3 hidden sm:inline-flex"></i>
<span class="text-brand-sun">{{ $name }}</span>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 max-w-6xl mx-auto w-full">
@@ -186,6 +199,48 @@
</div>
</div>
@endif
<div class="mt-6 glass-card rounded-2xl p-5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i data-lucide="sparkles" class="w-4 h-4 text-brand-ocean"></i>
<h3 class="text-sm font-bold text-gray-200 uppercase tracking-wide">Your Keywords</h3>
</div>
@if ($isPersonal)
<button id="user-keyword-add" class="rounded-full bg-brand-ocean text-white text-xs font-semibold px-3 py-1.5">Add keyword</button>
@endif
</div>
@if ($isPersonal)
<div id="user-keyword-list" class="mt-4 flex flex-wrap gap-2">
@forelse ($userKeywords as $keyword)
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-sm text-gray-200">
<span>{{ $keyword->keyword }}</span>
<span class="text-[10px] uppercase tracking-[0.2em] text-gray-500">{{ $keyword->lang ?? 'und' }}</span>
</span>
@empty
<span class="text-sm text-gray-400">No private keywords yet. Add one to personalize search.</span>
@endforelse
</div>
@elseif ($user)
<div class="mt-4 rounded-xl border border-brand-sun/30 bg-brand-sun/10 p-3 text-sm text-brand-sun">
Upgrade to Personal to add private keywords for this emoji.
</div>
<div class="mt-4 flex flex-wrap gap-2 opacity-70">
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-1</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-2</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-3</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-400">Unlock with Personal</span>
</div>
<a href="{{ route('pricing') }}" class="mt-3 inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">Upgrade to Personal</a>
@else
<div class="mt-4 rounded-xl border border-white/10 bg-white/5 p-3 text-sm text-gray-300">
Sign up to personalize keywords and sync across devices.
</div>
<a href="{{ route('register') }}" class="mt-3 inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">Sign up free</a>
@endif
</div>
</div>
</div>
@@ -193,28 +248,6 @@
</main>
</div>
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
<div class="grid grid-cols-6 gap-1 text-[11px]">
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
</a>
<a href="{{ route('api-docs') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="book-open" class="w-4 h-4"></i><span>Docs</span>
</a>
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
</a>
<a href="{{ route('support') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
</a>
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
</a>
<a href="{{ route('terms') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="file-text" class="w-4 h-4"></i><span>Terms</span>
</a>
</div>
</nav>
<div id="toast" class="fixed bottom-10 left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50 pointer-events-none">
<div class="bg-brand-ocean text-white px-6 py-2 rounded-full font-bold shadow-[0_0_20px_rgba(32,83,255,0.45)] flex items-center gap-2">
@@ -222,6 +255,32 @@
<span id="toast-msg">Copied!</span>
</div>
</div>
<div id="user-keyword-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div class="relative z-10 w-full max-w-lg rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-white">Add keyword</h3>
<button id="user-keyword-close" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-gray-200">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form id="user-keyword-form" class="mt-4 grid gap-4">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" name="keyword" id="user-keyword-input" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="magic" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" name="lang" id="user-keyword-lang" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="en">
</div>
<div class="flex items-center justify-end gap-2">
<button type="button" id="user-keyword-cancel" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">Cancel</button>
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-5 py-2 text-sm">Save keyword</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
@@ -267,5 +326,74 @@ document.addEventListener('keydown', (e) => {
// Treat opening the single-emoji page as a "recently viewed emoji" event.
addRecent(@json($symbol));
(() => {
const isPersonal = @json($isPersonal);
if (!isPersonal) return;
const modal = document.getElementById('user-keyword-modal');
const openBtn = document.getElementById('user-keyword-add');
const closeBtn = document.getElementById('user-keyword-close');
const cancelBtn = document.getElementById('user-keyword-cancel');
const form = document.getElementById('user-keyword-form');
const keywordInput = document.getElementById('user-keyword-input');
const langInput = document.getElementById('user-keyword-lang');
const list = document.getElementById('user-keyword-list');
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const openModal = () => {
modal.classList.remove('hidden');
modal.classList.add('flex');
keywordInput.value = '';
langInput.value = '';
keywordInput.focus();
};
const closeModal = () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
};
openBtn?.addEventListener('click', openModal);
closeBtn?.addEventListener('click', closeModal);
cancelBtn?.addEventListener('click', closeModal);
modal?.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const payload = {
emoji_slug: @json($slug),
keyword: keywordInput.value.trim(),
lang: langInput.value.trim() || 'und',
};
if (!payload.keyword) return;
const res = await fetch('{{ route('dashboard.keywords.store') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
showToast('Could not save keyword');
return;
}
const badge = document.createElement('span');
badge.className = 'inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-sm text-gray-200';
badge.innerHTML = `<span>${payload.keyword}</span><span class="text-[10px] uppercase tracking-[0.2em] text-gray-500">${payload.lang}</span>`;
if (list) {
const empty = list.querySelector('span.text-sm');
if (empty) empty.remove();
list.prepend(badge);
}
closeModal();
showToast('Keyword added');
});
})();
</script>
@endpush