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