Update pricing UX, billing flows, and API rules
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user