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

@@ -5,7 +5,7 @@
@push('head')
<style>
.doc-table tbody tr { border-bottom: 1px solid rgba(148,163,184,0.2); }
.doc-table tbody tr { border-bottom: 1px solid var(--muted-border); }
.doc-table tbody tr:hover { background: rgba(32,83,255,0.08); }
.codewrap { position: relative; }
.copy-btn {
@@ -56,15 +56,28 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0">
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Documentation</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">API Documentation</h1>
<header class="glass-header px-6 py-5 shrink-0 flex items-center justify-between">
<div>
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Documentation</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">API Documentation</h1>
</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>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">
@@ -85,7 +98,7 @@
<section id="overview" class="glass-card rounded-2xl p-6 scroll-mt-28">
<div class="flex items-center gap-2 mb-2">
<h2 class="font-display text-2xl font-bold">Emoji API Docs</h2>
<span id="pro-badge" class="hidden text-[10px] font-semibold px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-400/40">Pro mode</span>
<span id="api-badge" class="hidden text-[10px] font-semibold px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-400/40">API key active</span>
</div>
<p class="text-sm text-gray-300">Read-only API with search, categories, and emoji detail. Responses are cacheable and include ETag.</p>
<div class="codewrap mt-4">
@@ -96,20 +109,20 @@
<section id="auth" class="glass-card rounded-2xl p-6 scroll-mt-28">
<h3 class="text-lg font-semibold">Authentication</h3>
<p class="text-sm text-gray-300 mt-2">Free tier is public with daily caps. Pro unlocks higher limits with license key.</p>
<p class="text-sm text-gray-300 mt-2">Public endpoints are open. Personal plan holders can create API keys for private keyword access.</p>
<div class="mt-3 space-y-3">
<div class="codewrap">
<button class="copy-btn" data-copy-target="auth-bearer">Copy</button>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-bearer">Authorization: Bearer YOUR_LICENSE_KEY</code></pre>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-bearer">Authorization: Bearer YOUR_API_KEY</code></pre>
</div>
<div class="codewrap">
<button class="copy-btn" data-copy-target="auth-query">Copy</button>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-query">?key=YOUR_LICENSE_KEY</code></pre>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-query">X-Api-Key: YOUR_API_KEY</code></pre>
</div>
</div>
<ul class="mt-3 list-disc pl-5 text-sm text-gray-300 space-y-1">
<li><code>Authorization</code> (recommended)</li>
<li><code>X-License-Key</code> (also supported)</li>
<li><code>Authorization</code> or <code>X-Api-Key</code> for Personal API keys</li>
<li><code>X-License-Key</code> or <code>?key=</code> is only used for <code>/license/*</code> endpoints</li>
<li><code>X-Account-Id</code> (optional usage association)</li>
</ul>
</section>
@@ -122,22 +135,25 @@
<tr class="text-left text-gray-400">
<th class="py-2 pr-6">Tier</th>
<th class="py-2 pr-6 text-right">Page size cap</th>
<th class="py-2 pr-6 text-right">Daily cap</th>
<th class="py-2 pr-6">Auth</th>
<th class="py-2 pr-6">Rate limits</th>
<th class="py-2">Notes</th>
</tr>
</thead>
<tbody class="align-top text-gray-200">
<tr>
<td class="py-2 pr-6"><strong>Free</strong></td>
<td class="py-2 pr-6"><strong>Free (public)</strong></td>
<td class="py-2 pr-6 text-right">20</td>
<td class="py-2 pr-6 text-right">~30 (page-1, distinct)</td>
<td class="py-2">Cached responses do not count.</td>
<td class="py-2 pr-6">None</td>
<td class="py-2 pr-6">Hourly public limit (if enabled)</td>
<td class="py-2">Public dataset (EN + ID) only.</td>
</tr>
<tr>
<td class="py-2 pr-6"><strong>Pro</strong></td>
<td class="py-2 pr-6"><strong>Personal</strong></td>
<td class="py-2 pr-6 text-right">50</td>
<td class="py-2 pr-6 text-right">5,000 / day / license</td>
<td class="py-2">Up to 3 Chrome profiles.</td>
<td class="py-2 pr-6">API key</td>
<td class="py-2 pr-6">Unlimited public + private</td>
<td class="py-2">Unlocks private keyword search + sync.</td>
</tr>
</tbody>
</table>
@@ -204,11 +220,11 @@
<section id="try-it" class="glass-card rounded-2xl p-6 scroll-mt-28">
<h3 class="text-lg font-semibold">Try it</h3>
<p class="text-sm text-gray-300 mt-2">Demo request is limited to <strong>page=1</strong> and <strong>limit=10</strong>.</p>
<p id="ti-pro-note" class="hidden text-xs text-emerald-300 mt-2">Using Pro key via Authorization header.</p>
<p id="ti-api-note" class="hidden text-xs text-emerald-300 mt-2">Using API key via Authorization header.</p>
<div class="grid gap-3 md:grid-cols-3 mt-4">
<input id="ti-q" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="keyword (love)">
<input id="ti-category" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="category (Smileys & Emotion)">
<input id="ti-subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="subcategory (face-smiling)">
<input id="ti-q" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="keyword (love)">
<input id="ti-category" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="category (Smileys & Emotion)">
<input id="ti-subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="subcategory (face-smiling)">
</div>
<div class="mt-4 flex gap-3">
<button id="ti-run" type="button" class="px-4 py-2 rounded-lg bg-brand-ocean hover:bg-brand-oceanSoft text-white text-sm">Run /emojis</button>
@@ -236,12 +252,12 @@
(() => {
const BASE_API = @json(url('/v1'));
const urlParams = new URLSearchParams(location.search);
const proKey = urlParams.get('key') || '';
const proBadge = document.getElementById('pro-badge');
const proNote = document.getElementById('ti-pro-note');
if (proKey) {
proBadge?.classList.remove('hidden');
proNote?.classList.remove('hidden');
const apiKey = urlParams.get('key') || '';
const apiBadge = document.getElementById('api-badge');
const apiNote = document.getElementById('ti-api-note');
if (apiKey) {
apiBadge?.classList.remove('hidden');
apiNote?.classList.remove('hidden');
}
const baseUrlCode = document.getElementById('base-url-code');
@@ -253,14 +269,14 @@
const lim = 10;
const exCurlEmojis = document.getElementById('ex-curl-emojis');
if (exCurlEmojis) {
exCurlEmojis.textContent = proKey
? `curl -H "Authorization: Bearer ${proKey}" "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`
exCurlEmojis.textContent = apiKey
? `curl -H "Authorization: Bearer ${apiKey}" "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`
: `curl "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`;
}
const exCurlCats = document.getElementById('ex-curl-cats');
if (exCurlCats) {
exCurlCats.textContent = proKey
? `curl -H "Authorization: Bearer ${proKey}" "${BASE_API}/categories"`
exCurlCats.textContent = apiKey
? `curl -H "Authorization: Bearer ${apiKey}" "${BASE_API}/categories"`
: `curl "${BASE_API}/categories"`;
}
const exCurlEmoji = document.getElementById('ex-curl-emoji');
@@ -290,15 +306,15 @@
const resultDrawer = document.createElement('aside');
const backdrop = document.createElement('div');
resultDrawer.id = 'ti-drawer';
resultDrawer.className = 'fixed top-0 right-0 h-full w-full max-w-xl bg-[#0b0b0f] border-l border-white/10 translate-x-full transition-transform z-50';
backdrop.className = 'fixed inset-0 bg-black/40 opacity-0 pointer-events-none transition-opacity z-40';
resultDrawer.className = 'fixed top-0 right-0 h-full w-full max-w-xl border-l translate-x-full transition-transform z-50 offcanvas-panel';
backdrop.className = 'fixed inset-0 opacity-0 pointer-events-none transition-opacity z-40 offcanvas-backdrop';
resultDrawer.innerHTML = `
<div class="flex items-center justify-between p-4 border-b border-white/10">
<h3 class="text-lg font-semibold">Try it Result</h3>
<button id="ti-close" class="p-2 rounded-full text-gray-400 hover:bg-white/5"></button>
</div>
<div class="p-4 h-[calc(100%-64px)] overflow-auto">
<pre class="p-3 rounded bg-black/30 overflow-auto text-xs"><code id="ti-result">{ }</code></pre>
<pre class="p-3 rounded overflow-auto text-xs offcanvas-code"><code id="ti-result">{ }</code></pre>
</div>`;
document.body.appendChild(backdrop);
document.body.appendChild(resultDrawer);
@@ -340,7 +356,7 @@
params.set('page', '1');
const headers = {};
if (proKey) headers['Authorization'] = 'Bearer ' + proKey;
if (apiKey) headers['Authorization'] = 'Bearer ' + apiKey;
try {
const res = await fetch(`${BASE_API}/emojis?` + params.toString(), { headers });
if (!res.ok) throw new Error('Request failed: ' + res.status);
@@ -355,7 +371,7 @@
document.getElementById('ti-cats')?.addEventListener('click', async () => {
const headers = {};
if (proKey) headers['Authorization'] = 'Bearer ' + proKey;
if (apiKey) headers['Authorization'] = 'Bearer ' + apiKey;
try {
const res = await fetch(`${BASE_API}/categories`, { headers });
if (!res.ok) throw new Error('Request failed: ' + res.status);

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

View File

@@ -5,16 +5,17 @@
@push('head')
<style>
.glass-header {
background: rgba(5, 5, 5, 0.85);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
#grid {
--card-min: 104px;
--emoji-size: 2rem;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
}
@media (max-width: 640px) {
#grid {
--card-min: 0px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>
@endpush
@@ -48,6 +49,18 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="log-in" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Login</span>
</a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="shield-check" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Privacy</span>
@@ -65,11 +78,11 @@
<div class="flex flex-col md:flex-row gap-4 md:items-center justify-between">
<div class="relative group grow max-w-3xl">
<div class="absolute -inset-0.5 bg-gradient-to-r from-brand-ocean to-brand-sun rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-500"></div>
<div class="relative flex items-center bg-[#151518] border border-white/10 rounded-xl shadow-2xl h-11">
<div class="relative flex items-center bg-[#151518] border border-white/10 rounded-xl shadow-2xl h-11 theme-surface">
<div class="pl-4 pr-3 text-gray-400">
<i data-lucide="search" class="w-5 h-5"></i>
</div>
<input id="q" type="text" placeholder="Search emojis by keyword, mood, meaning..." class="w-full bg-transparent text-white placeholder-gray-500 focus:outline-none font-medium h-full">
<input id="q" type="text" placeholder="Search emojis by keyword, mood, meaning..." class="w-full bg-transparent text-white placeholder-gray-500 focus:outline-none font-medium h-full text-sm sm:text-base">
<div class="hidden md:flex items-center pr-3">
<kbd class="hidden sm:inline-block px-2 py-0.5 text-[10px] font-mono text-gray-500 bg-white/5 border border-white/10 rounded-md">⌘K</kbd>
</div>
@@ -77,14 +90,18 @@
</div>
<div class="flex items-center gap-3 shrink-0">
<select id="category" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none">
<select id="category" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none theme-surface">
<option value="">All Categories</option>
</select>
<select id="subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none" disabled>
<select id="subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none theme-surface" disabled>
<option value="">All Subcategories</option>
</select>
<button id="theme-toggle" class="w-10 h-10 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 class="w-px h-8 bg-white/10 mx-1 hidden lg:block"></div>
<div class="hidden lg:flex w-10 h-10 rounded-full bg-gradient-to-r from-gray-700 to-gray-600 border border-white/10"></div>
</div>
</div>
@@ -159,41 +176,48 @@
</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-8 left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50">
<div class="bg-brand-ocean text-white px-4 py-2 rounded-full font-bold shadow-[0_0_20px_rgba(32,83,255,0.45)] flex items-center gap-2 text-sm">
<i data-lucide="check" class="w-4 h-4"></i>
<span id="toast-msg">Copied!</span>
</div>
</div>
<div id="keyword-edit-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">Edit keyword</h3>
<button id="keyword-edit-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="keyword-edit-form" class="mt-4 grid gap-4">
<input type="hidden" id="keyword-edit-id">
<input type="hidden" id="keyword-edit-slug">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" id="keyword-edit-text" 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" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" id="keyword-edit-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">
</div>
<div class="flex items-center justify-end gap-2">
<button type="button" id="keyword-edit-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</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const state = { page: 1, limit: 32, total: 0, items: [], categories: {} };
const userTier = @json($userTier ?? null);
const isPersonal = userTier === 'personal';
const initialQuery = @json($initialQuery ?? '');
const initialCategory = @json($initialCategory ?? '');
const initialSubcategory = @json($initialSubcategory ?? '');
@@ -216,6 +240,15 @@
const gridSmallerEl = document.getElementById('grid-smaller');
const gridBiggerEl = document.getElementById('grid-bigger');
const densityStorageKey = 'dewemoji_grid_density';
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const keywordEditModal = document.getElementById('keyword-edit-modal');
const keywordEditClose = document.getElementById('keyword-edit-close');
const keywordEditCancel = document.getElementById('keyword-edit-cancel');
const keywordEditForm = document.getElementById('keyword-edit-form');
const keywordEditId = document.getElementById('keyword-edit-id');
const keywordEditSlug = document.getElementById('keyword-edit-slug');
const keywordEditText = document.getElementById('keyword-edit-text');
const keywordEditLang = document.getElementById('keyword-edit-lang');
if (initialQuery) qEl.value = initialQuery;
@@ -423,7 +456,8 @@
if (catEl.value) params.set('category', catEl.value);
if (subEl.value) params.set('subcategory', subEl.value);
const res = await fetch('/v1/emojis?' + params.toString());
const endpoint = isPersonal ? '/dashboard/keywords/search' : '/v1/emojis';
const res = await fetch(endpoint + '?' + params.toString());
const data = await res.json();
if (!res.ok) {
const msg = data.message || data.error || `API error (${res.status})`;
@@ -449,18 +483,24 @@
}
items.forEach((item) => {
const isPrivate = item.source === 'private';
const card = document.createElement('div');
card.className = 'relative aspect-square rounded-lg bg-white/5 hover:bg-white/10 transition-transform hover:scale-[1.02] border border-transparent hover:border-white/20 overflow-hidden group';
card.className = 'emoji-card relative aspect-square rounded-lg bg-white/5 hover:bg-white/10 transition-transform hover:scale-[1.02] border border-transparent hover:border-white/20 overflow-hidden group';
card.innerHTML = `
<a href="/emoji/${encodeURIComponent(item.slug)}" class="absolute inset-0 flex items-center justify-center pb-10">
<span class="leading-none" style="font-size:var(--emoji-size)">${esc(item.emoji)}</span>
</a>
<div class="absolute bottom-0 left-0 right-0 border-t border-white/10 bg-black/20 px-2 py-1.5 flex items-end gap-1">
<div class="emoji-card-bar absolute bottom-0 left-0 right-0 border-t border-white/10 bg-black/20 px-2 py-1.5 flex items-start gap-1">
<span class="emoji-name-clamp text-[10px] text-gray-300 text-left flex-1">${esc(item.name)}</span>
${isPrivate ? `<span class="px-1.5 py-0.5 rounded bg-brand-ocean/20 text-[9px] text-brand-oceanSoft" title="${esc(item.matched_keyword || '')}">Your: ${esc(item.matched_keyword || '')}</span>` : ''}
${isPrivate ? `<button type="button" class="edit-btn shrink-0 rounded bg-white/10 px-1.5 text-[9px] text-gray-200 hover:bg-brand-ocean/30">Edit</button>` : ''}
${isPrivate ? `<button type="button" class="delete-btn shrink-0 rounded bg-white/10 px-1.5 text-[9px] text-gray-200 hover:bg-red-500/30">Del</button>` : ''}
<button type="button" class="copy-btn shrink-0 w-6 h-6 rounded bg-white/10 hover:bg-brand-ocean/30 text-[11px] text-gray-200 hover:text-white transition-colors" title="Copy emoji"></button>
</div>
`;
const copyBtn = card.querySelector('.copy-btn');
const editBtn = card.querySelector('.edit-btn');
const deleteBtn = card.querySelector('.delete-btn');
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
@@ -471,6 +511,36 @@
});
});
}
if (editBtn && isPrivate) {
editBtn.addEventListener('click', (e) => {
e.preventDefault();
openKeywordEdit(item);
});
}
if (deleteBtn && isPrivate) {
deleteBtn.addEventListener('click', async (e) => {
e.preventDefault();
if (!item.matched_keyword_id) return;
const ok = await window.dewemojiConfirm('Delete this keyword?', {
title: 'Delete keyword',
okText: 'Delete',
});
if (!ok) return;
const res = await fetch(`/dashboard/keywords/${item.matched_keyword_id}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
showToast('Could not delete keyword');
return;
}
fetchEmojis(true);
});
}
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
navigator.clipboard.writeText(item.emoji).then(() => {
@@ -514,6 +584,55 @@
});
});
function openKeywordEdit(item) {
if (!isPersonal || !item.matched_keyword_id) return;
keywordEditId.value = item.matched_keyword_id;
keywordEditSlug.value = item.slug;
keywordEditText.value = item.matched_keyword || '';
keywordEditLang.value = item.matched_lang || '';
keywordEditModal.classList.remove('hidden');
keywordEditModal.classList.add('flex');
keywordEditText.focus();
}
function closeKeywordEdit() {
keywordEditModal.classList.add('hidden');
keywordEditModal.classList.remove('flex');
}
keywordEditClose?.addEventListener('click', closeKeywordEdit);
keywordEditCancel?.addEventListener('click', closeKeywordEdit);
keywordEditModal?.addEventListener('click', (e) => {
if (e.target === keywordEditModal) closeKeywordEdit();
});
keywordEditForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const id = keywordEditId.value;
const payload = {
emoji_slug: keywordEditSlug.value,
keyword: keywordEditText.value.trim(),
lang: keywordEditLang.value.trim() || 'und',
};
if (!id || !payload.keyword) return;
const res = await fetch(`/dashboard/keywords/${id}`, {
method: 'PUT',
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 update keyword');
return;
}
closeKeywordEdit();
fetchEmojis(true);
});
if (gridSizeEl && gridSmallerEl && gridBiggerEl) {
const initialDensity = localStorage.getItem(densityStorageKey) ?? '1';
applyGridDensity(Number(initialDensity));

View File

@@ -15,6 +15,7 @@
@endphp
<title>@yield('title', 'Dewemoji')</title>
<meta name="description" content="{{ $metaDescription }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="canonical" href="{{ $canonicalUrl }}">
<meta property="og:title" content="{{ $metaTitle }}">
<meta property="og:description" content="{{ $metaDescription }}">
@@ -25,6 +26,10 @@
<meta name="twitter:title" content="{{ $metaTitle }}">
<meta name="twitter:description" content="{{ $metaDescription }}">
<meta name="theme-color" content="#2053ff">
<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/logo/logo-mark-128.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/logo/logo-mark-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/logo/logo-mark-512.png">
<script type="application/ld+json">
{
"@@context": "https://schema.org",
@@ -48,19 +53,33 @@
]
}
</script>
<script>
(() => {
const stored = localStorage.getItem('dewemoji_theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const mode = stored || (prefersDark ? 'dark' : 'light');
const root = document.documentElement;
if (mode === 'dark') {
root.classList.add('dark');
root.classList.remove('theme-light');
} else {
root.classList.remove('dark');
root.classList.add('theme-light');
}
})();
</script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<link rel="preload" href="/assets/fonts/PlusJakartaSans-Regular.ttf" as="font" type="font/ttf" crossorigin>
<script src="https://unpkg.com/lucide@latest"></script>
@vite(['resources/js/app.js'])
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
sans: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
display: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
@@ -85,28 +104,164 @@
};
</script>
<style>
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-LightItalic.ttf") format("truetype");
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-MediumItalic.ttf") format("truetype");
font-weight: 500;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBoldItalic.ttf") format("truetype");
font-weight: 600;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-BoldItalic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
font-display: swap;
}
:root {
--font-family: "Plus Jakarta Sans", system-ui, sans-serif;
--fs-xl: clamp(2.5rem, 5vw, 3rem);
--fs-l: clamp(1.875rem, 4vw, 2rem);
--fs-m: clamp(1.5rem, 3vw, 1.5rem);
--fs-s: 1.125rem;
--fs-xs: 0.875rem;
--fw-light: 300;
--fw-normal: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--app-bg: #050505;
--app-fg: #f8fafc;
--muted-text: #9ca3af;
--muted-strong: #e5e7eb;
--muted-border: rgba(148,163,184,0.2);
--panel-bg: rgba(20, 20, 23, 0.6);
--panel-border: rgba(255, 255, 255, 0.08);
--card-bg: linear-gradient(145deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
--card-border: rgba(255, 255, 255, 0.05);
--card-hover-bg: linear-gradient(145deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0.02) 100%);
--card-hover-border: rgba(53, 108, 240, 0.35);
--header-bg: rgba(5, 5, 5, 0.85);
--header-border: rgba(255, 255, 255, 0.05);
--surface-bg: #151518;
--surface-border: rgba(255, 255, 255, 0.1);
--nav-bg: rgba(11, 11, 15, 0.95);
--code-bg: rgba(0,0,0,0.3);
--scrollbar-thumb: rgba(255,255,255,.1);
--scrollbar-thumb-hover: rgba(255,255,255,.2);
}
html.theme-light {
--app-bg: #f6f7fb;
--app-fg: #0f172a;
--muted-text: #64748b;
--muted-strong: #1f2937;
--muted-border: rgba(15,23,42,0.12);
--panel-bg: rgba(255, 255, 255, 0.78);
--panel-border: rgba(15, 23, 42, 0.08);
--card-bg: linear-gradient(145deg, rgba(15,23,42,0.04) 0%, rgba(15,23,42,0.02) 100%);
--card-border: rgba(15, 23, 42, 0.08);
--card-hover-bg: linear-gradient(145deg, rgba(15,23,42,0.06) 0%, rgba(15,23,42,0.02) 100%);
--card-hover-border: rgba(32, 83, 255, 0.2);
--header-bg: rgba(255, 255, 255, 0.85);
--header-border: rgba(15, 23, 42, 0.08);
--surface-bg: #ffffff;
--surface-border: rgba(15, 23, 42, 0.12);
--nav-bg: rgba(255, 255, 255, 0.9);
--code-bg: rgba(15,23,42,0.06);
--scrollbar-thumb: rgba(15,23,42,.18);
--scrollbar-thumb-hover: rgba(15,23,42,.28);
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.2); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
.glass-panel {
background: rgba(20, 20, 23, 0.6);
background: var(--panel-bg);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--panel-border);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
.glass-card {
background: linear-gradient(145deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.05);
background: var(--card-bg);
border: 1px solid var(--card-border);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.glass-card:hover {
background: linear-gradient(145deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0.02) 100%);
border-color: rgba(53, 108, 240, 0.35);
background: var(--card-hover-bg);
border-color: var(--card-hover-border);
transform: translateY(-2px);
box-shadow: 0 10px 40px -10px rgba(32, 83, 255, 0.2);
}
.glass-header {
background: var(--header-bg);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--header-border);
}
.theme-surface {
background: var(--surface-bg) !important;
border-color: var(--surface-border) !important;
}
.theme-nav {
background: var(--nav-bg) !important;
border-color: var(--panel-border) !important;
}
.text-gradient {
background: linear-gradient(to right, #fcb735, #2053ff);
-webkit-background-clip: text;
@@ -120,11 +275,79 @@
line-height: 1.1;
max-height: 2.2em;
}
h1 { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: 1.1; }
h2 { font-size: var(--fs-l); font-weight: var(--fw-semibold); line-height: 1.25; }
h3 { font-size: var(--fs-m); font-weight: var(--fw-semibold); line-height: 1.3; }
html.theme-light .text-white:not(.force-white) { color: var(--app-fg) !important; }
html.theme-light .text-gray-200 { color: #334155 !important; }
html.theme-light .text-gray-300 { color: #475569 !important; }
html.theme-light .text-gray-400 { color: var(--muted-text) !important; }
html.theme-light .text-gray-500 { color: #94a3b8 !important; }
html.theme-light .text-gray-100 { color: #1f2937 !important; }
html.theme-light .border-white\/5 { border-color: rgba(15,23,42,0.08) !important; }
html.theme-light .border-white\/10 { border-color: rgba(15,23,42,0.12) !important; }
html.theme-light .border-white\/20 { border-color: rgba(15,23,42,0.18) !important; }
html.theme-light .bg-white\/5 { background: rgba(15,23,42,0.04) !important; }
html.theme-light .bg-white\/10 { background: rgba(15,23,42,0.06) !important; }
html.theme-light .bg-white\/20 { background: rgba(15,23,42,0.1) !important; }
html.theme-light .bg-black\/30 { background: var(--code-bg) !important; }
html.theme-light .bg-black\/40 { background: var(--code-bg) !important; }
html.theme-light .copy-btn {
background: rgba(255,255,255,0.9);
border-color: rgba(15,23,42,0.2);
color: var(--app-fg);
}
html.theme-light .copy-btn:hover { background: rgba(32,83,255,0.18); }
html.theme-light .emoji-card {
background: #ffffff;
border-color: rgba(15,23,42,0.12);
box-shadow: 0 6px 18px rgba(15,23,42,0.08);
}
html.theme-light .emoji-card:hover {
border-color: rgba(32,83,255,0.35);
box-shadow: 0 10px 22px rgba(32,83,255,0.18);
}
html.theme-light .emoji-card-bar {
background: rgba(15,23,42,0.04);
border-top-color: rgba(15,23,42,0.1);
}
.offcanvas-panel {
background: var(--panel-bg);
border-color: var(--panel-border);
color: var(--app-fg);
}
.offcanvas-backdrop {
background: rgba(0,0,0,0.4);
}
html.theme-light .offcanvas-backdrop {
background: rgba(15,23,42,0.3);
}
.offcanvas-code {
background: var(--code-bg);
}
.cookie-banner {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
color: var(--app-fg);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.cookie-btn {
background: rgba(32,83,255,0.14);
border: 1px solid rgba(32,83,255,0.4);
color: var(--app-fg);
}
.cookie-btn:hover { background: rgba(32,83,255,0.22); }
.cookie-btn.secondary {
background: transparent;
border: 1px solid var(--muted-border);
color: var(--muted-text);
}
html.theme-light .cookie-btn.secondary { color: var(--app-fg); }
</style>
@stack('head')
@stack('jsonld')
</head>
<body class="bg-[#050505] text-white min-h-screen selection:bg-brand-ocean selection:text-white">
<body class="bg-[var(--app-bg)] text-[var(--app-fg)] min-h-screen selection:bg-brand-ocean selection:text-white" style="font-family: var(--font-family); font-size: var(--fs-s); font-weight: var(--fw-normal); line-height: 1.6;">
<div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
<div class="absolute top-[-10%] right-[-5%] w-[500px] h-[500px] bg-brand-ocean/20 rounded-full blur-[120px] animate-pulse-slow"></div>
<div class="absolute bottom-[-10%] left-[-10%] w-[600px] h-[600px] bg-blue-900/10 rounded-full blur-[120px]"></div>
@@ -132,7 +355,255 @@
@yield('content')
@hasSection('mobile_nav')
@yield('mobile_nav')
@else
<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 theme-nav">
<div class="grid grid-cols-4 gap-1 text-[11px]">
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('home') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover: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 {{ request()->routeIs('api-docs') ? 'text-brand-sun bg-white/5' : '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 {{ request()->routeIs('pricing') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
</a>
<button id="more-menu-btn" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="more-horizontal" class="w-4 h-4"></i><span>More</span>
</button>
</div>
</nav>
@endif
@hasSection('more_menu')
@yield('more_menu')
@else
<div id="more-menu-backdrop" class="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden"></div>
<div id="more-menu" class="lg:hidden fixed bottom-16 left-4 right-4 glass-panel rounded-2xl p-4 z-50 hidden">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">More</span>
<button id="more-menu-close" class="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="flex flex-col gap-2 text-sm">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i><span>Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="log-in" class="w-4 h-4"></i><span>Login</span>
</a>
@endguest
<a href="{{ route('support') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
</a>
<a href="{{ route('privacy') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
</a>
<a href="{{ route('terms') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="file-text" class="w-4 h-4"></i><span>Terms</span>
</a>
</div>
</div>
@endif
<div id="cookie-banner" class="cookie-banner fixed bottom-6 left-6 right-6 md:right-8 md:left-auto md:max-w-xl rounded-2xl p-4 md:p-5 hidden z-50">
<div class="flex flex-col gap-3">
<div>
<p class="text-sm font-semibold">Cookies & analytics</p>
<p class="text-xs text-gray-400">We use cookies to measure usage and improve Dewemoji. No tracking on staging. You can change this anytime.</p>
</div>
<div class="flex flex-wrap gap-2">
<button id="cookie-accept" class="cookie-btn rounded-xl px-3 py-2 text-xs font-semibold">Accept analytics</button>
<button id="cookie-decline" class="cookie-btn secondary rounded-xl px-3 py-2 text-xs font-semibold">Decline</button>
<a href="/privacy" class="text-xs text-gray-400 underline underline-offset-2">Privacy</a>
</div>
</div>
</div>
<div id="confirm-dialog" class="fixed inset-0 z-[100] hidden items-center justify-center">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="relative w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 text-slate-900 shadow-2xl dark:border-white/10 dark:bg-slate-950 dark:text-white">
<div class="text-xs uppercase tracking-[0.25em] text-slate-400" id="confirm-title">Confirm action</div>
<div class="mt-3 text-lg font-semibold" id="confirm-message">Are you sure?</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button id="confirm-cancel" class="rounded-full border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 dark:border-white/10 dark:text-slate-300 dark:hover:bg-white/5">
Cancel
</button>
<button id="confirm-ok" class="rounded-full bg-rose-500 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-600">
Confirm
</button>
</div>
</div>
</div>
<script>lucide.createIcons();</script>
<script>
(() => {
const root = document.documentElement;
const toggle = document.getElementById('theme-toggle');
const iconDark = document.querySelector('[data-theme-icon="dark"]');
const iconLight = document.querySelector('[data-theme-icon="light"]');
const setTheme = (mode) => {
if (mode === 'dark') {
root.classList.add('dark');
root.classList.remove('theme-light');
} else {
root.classList.remove('dark');
root.classList.add('theme-light');
}
localStorage.setItem('dewemoji_theme', mode);
if (iconDark && iconLight) {
iconDark.classList.toggle('hidden', mode !== 'dark');
iconLight.classList.toggle('hidden', mode === 'dark');
}
};
const stored = localStorage.getItem('dewemoji_theme');
if (stored) setTheme(stored);
else setTheme(root.classList.contains('dark') ? 'dark' : 'light');
if (toggle) {
toggle.addEventListener('click', () => {
const next = root.classList.contains('dark') ? 'light' : 'dark';
setTheme(next);
});
}
})();
</script>
<script>
(() => {
const moreMenuBtn = document.getElementById('more-menu-btn');
const moreMenu = document.getElementById('more-menu');
const moreMenuBackdrop = document.getElementById('more-menu-backdrop');
const moreMenuClose = document.getElementById('more-menu-close');
const openMoreMenu = () => {
if (moreMenu) moreMenu.classList.remove('hidden');
if (moreMenuBackdrop) moreMenuBackdrop.classList.remove('hidden');
};
const closeMoreMenu = () => {
if (moreMenu) moreMenu.classList.add('hidden');
if (moreMenuBackdrop) moreMenuBackdrop.classList.add('hidden');
};
if (moreMenuBtn) moreMenuBtn.addEventListener('click', openMoreMenu);
if (moreMenuBackdrop) moreMenuBackdrop.addEventListener('click', closeMoreMenu);
if (moreMenuClose) moreMenuClose.addEventListener('click', closeMoreMenu);
})();
</script>
<script>
(() => {
const GA_ID = 'G-R7FYYRBVJK';
const allowedHosts = new Set(['dewemoji.com', 'www.dewemoji.com']);
const consentKey = 'dewemoji_cookie_consent';
const banner = document.getElementById('cookie-banner');
const acceptBtn = document.getElementById('cookie-accept');
const declineBtn = document.getElementById('cookie-decline');
const loadGA = () => {
if (!allowedHosts.has(window.location.hostname)) return;
if (window.__dewemojiGaLoaded) return;
window.__dewemojiGaLoaded = true;
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GA_ID, { anonymize_ip: true });
};
const consent = localStorage.getItem(consentKey);
if (!consent && banner) banner.classList.remove('hidden');
if (consent === 'accepted') loadGA();
if (acceptBtn) {
acceptBtn.addEventListener('click', () => {
localStorage.setItem(consentKey, 'accepted');
if (banner) banner.classList.add('hidden');
loadGA();
});
}
if (declineBtn) {
declineBtn.addEventListener('click', () => {
localStorage.setItem(consentKey, 'declined');
if (banner) banner.classList.add('hidden');
});
}
})();
</script>
<script>
(() => {
const dialog = document.getElementById('confirm-dialog');
const titleEl = document.getElementById('confirm-title');
const msgEl = document.getElementById('confirm-message');
const okBtn = document.getElementById('confirm-ok');
const cancelBtn = document.getElementById('confirm-cancel');
let resolver = null;
const close = (result) => {
if (!dialog) return;
dialog.classList.add('hidden');
dialog.classList.remove('flex');
if (resolver) resolver(result);
resolver = null;
};
window.dewemojiConfirm = (message, options = {}) => {
if (!dialog) return Promise.resolve(false);
if (titleEl) titleEl.textContent = options.title || 'Confirm action';
if (msgEl) msgEl.textContent = message || 'Are you sure?';
if (okBtn) okBtn.textContent = options.okText || 'Confirm';
if (cancelBtn) cancelBtn.textContent = options.cancelText || 'Cancel';
if (okBtn) {
okBtn.classList.toggle('bg-rose-500', options.danger !== false);
okBtn.classList.toggle('hover:bg-rose-600', options.danger !== false);
okBtn.classList.toggle('bg-brand-ocean', options.danger === false);
okBtn.classList.toggle('hover:bg-brand-ocean/90', options.danger === false);
}
dialog.classList.remove('hidden');
dialog.classList.add('flex');
return new Promise((resolve) => {
resolver = resolve;
});
};
okBtn?.addEventListener('click', () => close(true));
cancelBtn?.addEventListener('click', () => close(false));
dialog?.addEventListener('click', (event) => {
if (event.target === dialog) close(false);
});
dialog?.querySelector(':scope > div.absolute')?.addEventListener('click', () => close(false));
document.addEventListener('keydown', (event) => {
if (dialog?.classList.contains('hidden')) return;
if (event.key === 'Escape') close(false);
});
document.addEventListener('submit', async (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const message = form.getAttribute('data-confirm');
if (!message || form.dataset.confirmed === 'true') return;
event.preventDefault();
const ok = await window.dewemojiConfirm(message, {
title: form.getAttribute('data-confirm-title') || undefined,
okText: form.getAttribute('data-confirm-ok') || undefined,
cancelText: form.getAttribute('data-confirm-cancel') || undefined,
danger: form.getAttribute('data-confirm-danger') !== 'false',
});
if (ok) {
form.dataset.confirmed = 'true';
form.submit();
}
});
})();
</script>
@stack('scripts')
</body>
</html>

View File

@@ -1,7 +1,7 @@
@extends('site.layout')
@section('title', 'Pricing - Dewemoji')
@section('meta_description', 'Choose Dewemoji pricing for Free, Pro subscription, and Lifetime access for website, extension, and API usage.')
@section('meta_description', 'Choose Dewemoji pricing for Free, Personal subscription, and Lifetime access for website, extension, and API usage.')
@push('jsonld')
<script type="application/ld+json">
@@ -10,20 +10,20 @@
"@@graph": [
{
"@@type": "Product",
"name": "Dewemoji Pro License",
"description": "One Pro license unlocks Dewemoji extension and API access.",
"name": "Dewemoji Personal",
"description": "Personal plan unlocks Dewemoji extension and API access.",
"brand": {"@@type": "Brand", "name": "Dewemoji"},
"offers": [
{"@@type":"Offer","price":"3.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"},
{"@@type":"Offer","price":"27.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"}
{"@@type":"Offer","price":"30000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"},
{"@@type":"Offer","price":"300000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"}
]
},
{
"@@type": "Product",
"name": "Dewemoji Lifetime License",
"name": "Dewemoji Lifetime",
"description": "Lifetime access to Dewemoji extension and API.",
"brand": {"@@type": "Brand", "name": "Dewemoji"},
"offers": {"@@type":"Offer","price":"69.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-lifetime"}
"offers": {"@@type":"Offer","price":"900000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-lifetime"}
}
]
}
@@ -32,6 +32,22 @@
@section('content')
<div class="flex h-screen w-full">
<style>
#qris-code canvas,
#qris-code img {
background: #ffffff;
}
.qris-modal-card.glass-card:hover {
background: #ffffff !important;
border-color: rgba(15, 23, 42, 0.12) !important;
transform: none !important;
box-shadow: none !important;
}
html.dark .qris-modal-card.glass-card:hover {
background: rgba(2, 6, 23, 0.9) !important;
border-color: rgba(255, 255, 255, 0.08) !important;
}
</style>
<aside class="hidden lg:flex w-20 lg:w-64 h-full glass-panel flex-col justify-between p-4 z-50 shrink-0">
<div>
<div class="flex items-center gap-3 px-2 mb-8 mt-2">
@@ -56,6 +72,16 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span>
</a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span>
</a>
@@ -66,9 +92,28 @@
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0 flex justify-end items-center gap-4">
<span class="text-xs text-gray-500 hidden md:block">Billing shown in USD</span>
<header class="glass-header px-6 py-5 shrink-0 flex justify-between items-center gap-4">
<div class="flex items-center gap-3 text-xs text-gray-500">
<span>Prices shown in <strong class="text-gray-300">{{ $currencyPref ?? 'USD' }}</strong></span>
<form method="POST" action="{{ route('pricing.currency') }}" class="flex items-center gap-2">
@csrf
<button type="submit" name="currency" value="IDR"
class="px-3 py-1.5 rounded-full text-[11px] font-semibold border {{ ($currencyPref ?? 'USD') === 'IDR' ? 'bg-white/10 text-white border-white/10' : 'text-gray-400 border-white/5 hover:text-white hover:border-white/10' }}">
IDR
</button>
<button type="submit" name="currency" value="USD"
class="px-3 py-1.5 rounded-full text-[11px] font-semibold border {{ ($currencyPref ?? 'USD') === 'USD' ? 'bg-white/10 text-white border-white/10' : 'text-gray-400 border-white/5 hover:text-white hover:border-white/10' }}">
USD
</button>
</form>
<span class="hidden md:inline">PayPal fixed rate Rp {{ number_format($usdRate ?? 15000) }}/USD</span>
</div>
<button class="text-sm font-semibold text-brand-oceanSoft hover:text-white transition-colors">Restore Purchases</button>
<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>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">
@@ -78,56 +123,558 @@
<p class="text-gray-400">Use Dewemoji for search, extension, and API in one license system.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8">
@php
$pref = $currencyPref ?? 'USD';
$monthlyIdr = $pricing['personal_monthly']['idr'] ?? 30000;
$annualIdr = $pricing['personal_annual']['idr'] ?? 300000;
$lifetimeIdr = $pricing['personal_lifetime']['idr'] ?? 900000;
$monthlyUsd = $pricing['personal_monthly']['usd'] ?? 2;
$annualUsd = $pricing['personal_annual']['usd'] ?? 20;
$lifetimeUsd = $pricing['personal_lifetime']['usd'] ?? 60;
$qrisUrl = $payments['qris_url'] ?? '';
$paypalUrl = $payments['paypal_url'] ?? '';
$paypalJoiner = $paypalUrl && str_contains($paypalUrl, '?') ? '&' : '?';
$paypalLifetimeUrl = $paypalUrl ? $paypalUrl.$paypalJoiner.'plan=personal_lifetime' : '';
$canQris = $pakasirEnabled ?? false;
$paypalEnabled = $paypalEnabled ?? false;
$paypalPlans = $paypalPlans ?? ['personal_monthly' => false, 'personal_annual' => false];
@endphp
<div class="mb-6 flex flex-wrap items-center justify-center gap-3 text-sm text-gray-400">
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center">
<button type="button" class="period-toggle rounded-full px-4 py-2 font-semibold text-gray-200" data-period="monthly">Monthly</button>
<button type="button" class="period-toggle rounded-full px-4 py-2 font-semibold text-gray-400 hover:text-gray-200" data-period="annual">Annual</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8" data-default-currency="{{ $pref === 'IDR' ? 'IDR' : 'USD' }}">
<section class="glass-card rounded-3xl p-6 flex flex-col">
<h2 class="font-display text-xl font-bold">Starter</h2>
<p class="text-sm text-gray-500 mt-1">For casual usage</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$0</span><span class="text-gray-500">/mo</span></div>
<ul class="space-y-2 text-sm text-gray-300 flex-1">
<li>Extension access</li>
<li>Basic API testing</li>
<li>30 daily free queries</li>
</ul>
<div class="flex-1"></div>
<button class="mt-6 w-full py-2.5 rounded-xl bg-white/5 border border-white/10 text-gray-300">Current Plan</button>
</section>
<section class="rounded-3xl p-6 flex flex-col border border-brand-ocean/40 bg-gradient-to-b from-brand-ocean/15 to-transparent shadow-[0_0_28px_rgba(32,83,255,0.18)] md:-translate-y-2">
<div class="inline-flex w-fit mb-3 px-2 py-1 text-[10px] font-bold rounded-full bg-brand-sun text-black">MOST POPULAR</div>
<h2 class="font-display text-xl font-bold text-brand-oceanSoft">Pro</h2>
<p class="text-sm text-gray-400 mt-1">For power users & teams</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$3</span><span class="text-gray-400">/mo</span> <span class="text-sm text-gray-500">or $27/yr</span></div>
<ul class="space-y-2 text-sm text-gray-200 flex-1">
<li>Semantic search engine</li>
<li>API Pro access</li>
<li>3 active Chrome profiles</li>
<li>Priority support</li>
</ul>
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-subscription" target="_blank" rel="noopener noreferrer" class="mt-6 w-full py-2.5 rounded-xl bg-brand-ocean hover:bg-brand-oceanSoft text-white font-semibold text-center">Upgrade to Pro</a>
<section class="relative rounded-3xl p-6 flex flex-col border border-brand-ocean/40 bg-gradient-to-b from-brand-ocean/15 to-transparent shadow-[0_0_28px_rgba(32,83,255,0.18)] md:-translate-y-2">
<div class="absolute -top-3 right-4 rounded-full bg-brand-sun text-black text-[10px] font-bold px-3 py-1 shadow-lg">Most Popular</div>
<div class="flex items-center justify-between gap-3">
<h2 class="font-display text-xl font-bold text-brand-oceanSoft">Personal</h2>
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center text-xs">
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-200" data-currency="USD">USD</button>
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-400 hover:text-gray-200" data-currency="IDR">IDR</button>
</div>
</div>
<p class="text-sm text-gray-400 mt-1">For private keywords + sync</p>
<div class="mt-4 mb-2" id="personal-price"
data-monthly-usd="{{ rtrim(rtrim(number_format($monthlyUsd, 2), '0'), '.') }}"
data-annual-usd="{{ rtrim(rtrim(number_format($annualUsd, 2), '0'), '.') }}"
data-monthly-idr="{{ number_format($monthlyIdr) }}"
data-annual-idr="{{ number_format($annualIdr) }}">
<span class="text-4xl font-bold">$0</span><span class="text-gray-400">/mo</span>
</div>
<div class="mb-6 text-xs text-gray-500" id="personal-secondary"
data-monthly-usd-note="≈ Rp {{ number_format($monthlyIdr) }} (QRIS)"
data-annual-usd-note="≈ Rp {{ number_format($annualIdr) }} (QRIS)"
data-monthly-idr-note="${{ rtrim(rtrim(number_format($monthlyUsd, 2), '0'), '.') }} (PayPal fixed rate)"
data-annual-idr-note="${{ rtrim(rtrim(number_format($annualUsd, 2), '0'), '.') }} (PayPal fixed rate)">
</div>
<div class="flex-1"></div>
<div class="mt-6 space-y-2">
<div class="hidden text-xs text-gray-500" id="personal-pay-note"></div>
<button type="button"
id="personal-pay-btn"
data-paypal-enabled="{{ $paypalEnabled && $paypalPlans['personal_monthly'] ? 'true' : 'false' }}"
data-paypal-annual-enabled="{{ $paypalEnabled && $paypalPlans['personal_annual'] ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
class="!text-white w-full py-2.5 rounded-xl bg-brand-ocean hover:bg-brand-oceanSoft text-white font-semibold text-center block">
Pay now
</button>
</div>
<div class="hidden">
<button type="button" data-paypal-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-paypal-plan="personal_annual" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_annual" data-original="Start Personal"></button>
</div>
</section>
<section class="glass-card rounded-3xl p-6 flex flex-col">
<h2 class="font-display text-xl font-bold">Lifetime</h2>
<div class="flex items-center justify-between gap-3">
<h2 class="font-display text-xl font-bold">Lifetime</h2>
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center text-xs">
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-200" data-currency="USD">USD</button>
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-400 hover:text-gray-200" data-currency="IDR">IDR</button>
</div>
</div>
<p class="text-sm text-gray-500 mt-1">Pay once, use forever</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$69</span></div>
<ul class="space-y-2 text-sm text-gray-300 flex-1">
<li>Lifetime extension + API</li>
<li>Future updates included</li>
<li>IDR/Mayar available on request</li>
</ul>
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-lifetime" target="_blank" rel="noopener noreferrer" class="mt-6 w-full py-2.5 rounded-xl bg-brand-sun hover:bg-brand-sunSoft text-black font-semibold text-center">Buy Lifetime</a>
<div class="mt-5 mb-2" id="lifetime-price"
data-lifetime-usd="{{ rtrim(rtrim(number_format($lifetimeUsd, 2), '0'), '.') }}"
data-lifetime-idr="{{ number_format($lifetimeIdr) }}">
<span class="text-4xl font-bold">$0</span>
</div>
<div class="mb-4 text-xs text-gray-500" id="lifetime-secondary"
data-lifetime-usd-note="≈ Rp {{ number_format($lifetimeIdr) }} (QRIS)"
data-lifetime-idr-note="${{ rtrim(rtrim(number_format($lifetimeUsd, 2), '0'), '.') }} (PayPal fixed rate)">
</div>
<div class="flex-1"></div>
<div class="mt-6 space-y-2">
<div class="hidden text-xs text-gray-500" id="lifetime-pay-note"></div>
<button type="button"
id="lifetime-pay-btn"
data-paypal-enabled="{{ $paypalLifetimeUrl ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
class="force-white w-full py-2.5 rounded-xl border border-brand-ocean/60 text-brand-ocean font-semibold text-center block hover:bg-brand-ocean/10">
Pay now
</button>
</div>
<div class="hidden">
<a href="{{ $paypalLifetimeUrl ?: '#' }}"
target="_blank" rel="noopener noreferrer"
data-paypal-lifetime="true"
class="{{ $paypalLifetimeUrl ? '' : 'pointer-events-none' }}">
PayPal Lifetime
</a>
<button type="button"
data-qris-plan="personal_lifetime" data-original="Get Lifetime Access">
QRIS Lifetime
</button>
</div>
</section>
</div>
<section class="glass-card rounded-2xl p-6 text-sm text-gray-300">
<h3 class="font-semibold text-white mb-2">Licensing basics</h3>
<ul class="list-disc pl-5 space-y-1">
<li>One key unlocks extension and API.</li>
<li>Use <code>Authorization: Bearer YOUR_LICENSE_KEY</code> for API requests.</li>
<li>Maximum 3 active Chrome profiles per license.</li>
</ul>
<h3 class="font-semibold text-white mb-4">Plan comparison</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="py-3 pr-4">Feature</th>
<th class="py-3 pr-4">Starter</th>
<th class="py-3 pr-4">Personal</th>
<th class="py-3 pr-4">Lifetime</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
<tr>
<td class="py-3 pr-4 border-t border-white/10">Emoji search + discovery</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Private keywords</td>
<td class="py-3 pr-4 border-t border-white/10">Up to 20</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Keyword sync</td>
<td class="py-3 pr-4 border-t border-white/10">Account-only</td>
<td class="py-3 pr-4 border-t border-white/10">All channels</td>
<td class="py-3 pr-4 border-t border-white/10">All channels</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">API access</td>
<td class="py-3 pr-4 border-t border-white/10">Not included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Public search usage</td>
<td class="py-3 pr-4 border-t border-white/10">Hourly limits apply</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Support</td>
<td class="py-3 pr-4 border-t border-white/10">Standard</td>
<td class="py-3 pr-4 border-t border-white/10">Priority</td>
<td class="py-3 pr-4 border-t border-white/10">Priority</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Updates</td>
<td class="py-3 pr-4 border-t border-white/10">Regular</td>
<td class="py-3 pr-4 border-t border-white/10">All updates</td>
<td class="py-3 pr-4 border-t border-white/10">All updates</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</main>
</div>
<div id="qris-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 qris-modal-card p-6 bg-white/95 text-slate-900 dark:bg-slate-950/90 dark:text-white">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">QRIS Payment</div>
<div class="mt-2 text-xl font-semibold text-white">Scan to pay</div>
</div>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2 items-center">
<div class="rounded-2xl bg-white/10 border border-white/10 p-4 flex items-center justify-center">
<div id="qris-code" class="rounded-xl bg-white p-3 shadow-lg"></div>
</div>
<div class="space-y-3 text-sm text-gray-300">
<div class="rounded-xl bg-white/5 border border-white/10 p-3">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Amount</div>
<div id="qris-amount" class="mt-1 text-lg font-semibold text-white">Rp 0</div>
</div>
<div class="rounded-xl bg-white/5 border border-white/10 p-3">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Expires</div>
<div id="qris-expiry" class="mt-1 text-sm text-gray-300">Complete within 30 minutes</div>
</div>
<div id="qris-text" class="hidden"></div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button id="qris-cancel" class="rounded-full bg-rose-500 text-white font-semibold px-4 py-2 text-sm hover:bg-rose-600">Cancel payment</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
<script>
(() => {
const buttons = document.querySelectorAll('[data-paypal-plan]');
if (!buttons.length) return;
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const isAuthed = @json(auth()->check());
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
const plan = btn.dataset.paypalPlan;
if (!plan) return;
if (!isAuthed) {
window.location.href = "{{ route('login') }}";
return;
}
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Redirecting...';
try {
const res = await fetch("{{ route('billing.paypal.create') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ plan_code: plan }),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.approve_url) {
const reason = data?.error ? ` (${data.error})` : '';
alert('Could not start PayPal checkout. Please try again.' + reason);
btn.disabled = false;
btn.textContent = original;
return;
}
window.location.href = data.approve_url;
} catch (e) {
alert('Checkout failed. Please try again.');
btn.disabled = false;
btn.textContent = original;
}
});
});
})();
</script>
<script>
(() => {
const periodButtons = document.querySelectorAll('.period-toggle');
const currencyButtons = document.querySelectorAll('.currency-toggle');
const priceWrap = document.getElementById('personal-price');
const secondary = document.getElementById('personal-secondary');
const payBtn = document.getElementById('personal-pay-btn');
const payNote = document.getElementById('personal-pay-note');
const lifetimePrice = document.getElementById('lifetime-price');
const lifetimeSecondary = document.getElementById('lifetime-secondary');
const lifetimePay = document.getElementById('lifetime-pay-btn');
const lifetimeNote = document.getElementById('lifetime-pay-note');
if (!priceWrap || !secondary || !payBtn || !lifetimePrice || !lifetimeSecondary || !lifetimePay) return;
let period = 'monthly';
let currency = document.querySelector('[data-default-currency]')?.dataset.defaultCurrency || 'USD';
const setActive = (nodes, value) => {
nodes.forEach((btn) => {
const active = btn.dataset[btn.classList.contains('period-toggle') ? 'period' : 'currency'] === value;
btn.classList.toggle('text-gray-200', active);
btn.classList.toggle('text-gray-400', !active);
});
};
const paypalIcon = `<svg role="img" viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4"><path fill="currentColor" d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"/></svg>`;
const qrisIcon = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h6v6H3z"/><path d="M15 3h6v6h-6z"/><path d="M3 15h6v6H3z"/><path d="M15 15h2v2h-2z"/><path d="M17 19h2v2h-2z"/><path d="M19 15h2v2h-2z"/><path d="M15 19h2v2h-2z"/></svg>`;
const setButtonLabel = (btn, label, icon) => {
if (!btn) return;
btn.classList.add('inline-flex', 'items-center', 'justify-center', 'gap-2');
btn.innerHTML = `${icon}<span>${label}</span>`;
};
const updatePrice = () => {
const amount = currency === 'USD'
? priceWrap.dataset[period === 'monthly' ? 'monthlyUsd' : 'annualUsd']
: priceWrap.dataset[period === 'monthly' ? 'monthlyIdr' : 'annualIdr'];
const suffix = period === 'monthly' ? '/mo' : '/yr';
if (currency === 'USD') {
priceWrap.innerHTML = `<span class="text-4xl font-bold">$${amount}</span><span class="text-gray-400">${suffix}</span>`;
secondary.textContent = secondary.dataset[`${period}UsdNote`] || '—';
} else {
priceWrap.innerHTML = `<span class="text-4xl font-bold">Rp ${amount}</span><span class="text-gray-400">${suffix}</span>`;
secondary.textContent = secondary.dataset[`${period}IdrNote`] || '—';
}
const canPaypal = (period === 'monthly' ? payBtn.dataset.paypalEnabled === 'true' : payBtn.dataset.paypalAnnualEnabled === 'true');
const canQris = payBtn.dataset.qrisEnabled === 'true';
let disabled = false;
let label = 'Start Personal';
let note = '';
if (currency === 'USD') {
disabled = !canPaypal;
label = 'Start Personal';
note = canPaypal ? '' : 'PayPal is not configured for this plan.';
payBtn.classList.remove('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
payBtn.classList.add('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
payBtn.classList.remove('text-white');
} else {
disabled = !canQris;
label = 'Start Personal';
note = canQris ? '' : 'QRIS is not available right now.';
payBtn.classList.remove('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
payBtn.classList.add('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
}
setButtonLabel(payBtn, label, currency === 'USD' ? paypalIcon : qrisIcon);
payBtn.disabled = disabled;
payBtn.classList.toggle('opacity-60', disabled);
payBtn.classList.toggle('pointer-events-none', disabled);
if (payNote) {
payNote.textContent = note;
payNote.classList.toggle('hidden', note === '');
}
const lifetimeAmount = currency === 'USD'
? lifetimePrice.dataset.lifetimeUsd
: lifetimePrice.dataset.lifetimeIdr;
if (currency === 'USD') {
lifetimePrice.innerHTML = `<span class="text-4xl font-bold">$${lifetimeAmount}</span>`;
lifetimeSecondary.textContent = lifetimeSecondary.dataset.lifetimeUsdNote || '—';
} else {
lifetimePrice.innerHTML = `<span class="text-4xl font-bold">Rp ${lifetimeAmount}</span>`;
lifetimeSecondary.textContent = lifetimeSecondary.dataset.lifetimeIdrNote || '—';
}
const canLifetimePaypal = lifetimePay.dataset.paypalEnabled === 'true';
const canLifetimeQris = lifetimePay.dataset.qrisEnabled === 'true';
let lifetimeDisabled = false;
let lifetimeLabel = 'Get Lifetime Access';
let lifetimeHint = '';
if (currency === 'USD') {
lifetimeDisabled = !canLifetimePaypal;
lifetimeLabel = 'Get Lifetime Access';
lifetimeHint = canLifetimePaypal ? '' : 'PayPal is not configured.';
lifetimePay.classList.remove('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
lifetimePay.classList.add('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
} else {
lifetimeDisabled = !canLifetimeQris;
lifetimeLabel = 'Get Lifetime Access';
lifetimeHint = canLifetimeQris ? '' : 'QRIS is not available right now.';
lifetimePay.classList.remove('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
lifetimePay.classList.add('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
}
setButtonLabel(lifetimePay, lifetimeLabel, currency === 'USD' ? paypalIcon : qrisIcon);
lifetimePay.disabled = lifetimeDisabled;
lifetimePay.classList.toggle('opacity-60', lifetimeDisabled);
lifetimePay.classList.toggle('pointer-events-none', lifetimeDisabled);
if (lifetimeNote) {
lifetimeNote.textContent = lifetimeHint;
lifetimeNote.classList.toggle('hidden', lifetimeHint === '');
}
};
periodButtons.forEach((btn) => {
btn.addEventListener('click', () => {
period = btn.dataset.period;
setActive(periodButtons, period);
updatePrice();
});
});
currencyButtons.forEach((btn) => {
btn.addEventListener('click', () => {
currency = btn.dataset.currency;
setActive(currencyButtons, currency);
updatePrice();
});
});
payBtn.addEventListener('click', async () => {
if (payBtn.disabled) return;
const plan = period === 'monthly' ? 'personal_monthly' : 'personal_annual';
if (currency === 'USD') {
const button = document.querySelector(`[data-paypal-plan="${plan}"]`);
button?.click();
return;
}
const button = document.querySelector(`[data-qris-plan="${plan}"]`);
button?.click();
});
lifetimePay.addEventListener('click', async () => {
if (lifetimePay.disabled) return;
if (currency === 'USD') {
const paypal = document.querySelector('[data-paypal-lifetime="true"]');
paypal?.click();
return;
}
const button = document.querySelector('[data-qris-plan="personal_lifetime"]');
button?.click();
});
setActive(periodButtons, period);
setActive(currencyButtons, currency);
updatePrice();
})();
</script>
<script>
(() => {
const buttons = document.querySelectorAll('[data-qris-plan]');
if (!buttons.length) return;
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const isAuthed = @json(auth()->check());
const modal = document.getElementById('qris-modal');
const qrTarget = document.getElementById('qris-code');
const qrText = document.getElementById('qris-text');
const qrAmount = document.getElementById('qris-amount');
const qrExpiry = document.getElementById('qris-expiry');
const cancelBtn = document.getElementById('qris-cancel');
let currentOrderId = null;
let modalOpen = false;
const openModal = () => {
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
modalOpen = true;
};
const closeModal = () => {
if (!modal) return;
modal.classList.add('hidden');
modal.classList.remove('flex');
modalOpen = false;
};
cancelBtn?.addEventListener('click', async () => {
const ok = await window.dewemojiConfirm('Cancel this QRIS payment? You will need to start a new checkout.', {
title: 'Cancel payment',
okText: 'Cancel payment',
});
if (!ok) return;
try {
await fetch("{{ route('billing.pakasir.cancel') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ order_id: currentOrderId }),
});
} catch (e) {
// best-effort cancel
} finally {
currentOrderId = null;
closeModal();
}
});
document.addEventListener('keydown', (event) => {
if (!modalOpen) return;
if (event.key === 'Escape') {
event.preventDefault();
}
});
const formatExpiry = (value) => {
if (!value) return null;
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return null;
return new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(parsed);
};
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
const plan = btn.dataset.qrisPlan;
if (!plan) return;
if (!isAuthed) {
window.location.href = "{{ route('login') }}";
return;
}
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Generating QR...';
try {
const res = await fetch("{{ route('billing.pakasir.create') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ plan_code: plan }),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.payment_number) {
alert('Could not generate QRIS. Please try again.');
btn.disabled = false;
btn.textContent = original;
return;
}
currentOrderId = data.order_id || null;
if (qrTarget) qrTarget.innerHTML = '';
if (qrTarget && window.QRCode) {
new QRCode(qrTarget, {
text: data.payment_number,
width: 220,
height: 220,
colorDark: '#0b0b0f',
colorLight: '#ffffff',
});
}
if (qrText) qrText.textContent = data.payment_number;
if (qrAmount) qrAmount.textContent = `Rp ${Number(data.total_payment || data.amount || 0).toLocaleString('id-ID')}`;
if (qrExpiry) {
const formatted = formatExpiry(data.expired_at);
qrExpiry.textContent = formatted ? `Expires ${formatted}` : 'Complete within 30 minutes';
}
openModal();
btn.disabled = false;
btn.textContent = original;
} catch (e) {
alert('QRIS request failed. Please try again.');
btn.disabled = false;
btn.textContent = original;
}
});
});
})();
</script>
@endpush

View File

@@ -5,10 +5,10 @@
@push('head')
<style>
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: #fff; font-weight: 700; }
.legal-h3 { font-family: 'Space Grotesk', sans-serif; margin-top: 1.4rem; margin-bottom: .5rem; font-size: 1.05rem; color: #e5e7eb; font-weight: 600; }
.legal-p { color: #9ca3af; line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: #9ca3af; margin-bottom: .9rem; }
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: var(--app-fg); font-weight: 700; }
.legal-h3 { font-family: 'Space Grotesk', sans-serif; margin-top: 1.4rem; margin-bottom: .5rem; font-size: 1.05rem; color: var(--muted-strong); font-weight: 600; }
.legal-p { color: var(--muted-text); line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: var(--muted-text); margin-bottom: .9rem; }
</style>
@endpush
@@ -30,6 +30,12 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl bg-white/10 text-brand-sun border border-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
@@ -41,9 +47,16 @@
<div class="text-[11px] uppercase tracking-wider text-gray-500">Legal / Privacy Policy</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Privacy Policy</h1>
</div>
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
</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>
</header>

View File

@@ -35,15 +35,28 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0">
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Support</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Support Center</h1>
<header class="glass-header px-6 py-5 shrink-0 flex items-center justify-between">
<div>
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Support</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Support Center</h1>
</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>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">

View File

@@ -5,9 +5,9 @@
@push('head')
<style>
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: #fff; font-weight: 700; }
.legal-p { color: #9ca3af; line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: #9ca3af; margin-bottom: .9rem; }
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: var(--app-fg); font-weight: 700; }
.legal-p { color: var(--muted-text); line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: var(--muted-text); margin-bottom: .9rem; }
</style>
@endpush
@@ -29,6 +29,12 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl bg-white/10 text-brand-sun border border-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
@@ -40,9 +46,16 @@
<div class="text-[11px] uppercase tracking-wider text-gray-500">Legal / Terms</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Terms of Service</h1>
</div>
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
</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>
</header>