Update pricing UX, billing flows, and API rules

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

View File

@@ -0,0 +1,117 @@
@extends('dashboard.app')
@section('title', 'API Keys')
@section('page_title', 'API Keys')
@section('page_subtitle', 'Create and manage access tokens for integrations.')
@section('dashboard_content')
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Your keys</div>
<div class="mt-2 text-xl font-semibold text-white">Manage API access</div>
</div>
@if ($canCreate)
<form method="POST" action="{{ route('dashboard.api-keys.create') }}" class="flex items-center gap-2">
@csrf
<input type="text" name="name" placeholder="Key name (optional)" class="rounded-full bg-white/5 border border-white/10 px-4 py-2 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean">
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm">Create key</button>
</form>
@endif
</div>
@if (!$canCreate)
<div class="mt-4 rounded-2xl border border-amber-400/30 bg-amber-400/10 p-4 text-sm text-amber-200">
API keys are available on the Personal plan. Upgrade to unlock API access.
</div>
@endif
@if ($newKey)
<div class="mt-6 rounded-2xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-200">
New key created. Copy it now; you wont be able to see it again.
<div class="mt-3 flex items-center gap-2">
<code class="rounded-lg bg-black/40 px-3 py-2 text-xs text-emerald-200">{{ $newKey }}</code>
<button type="button" data-copy-key="{{ $newKey }}" class="rounded-full border border-emerald-400/40 px-3 py-1 text-xs text-emerald-200 hover:bg-emerald-400/10">Copy</button>
</div>
</div>
@endif
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Prefix</th>
<th class="px-4 py-3 text-left">Name</th>
<th class="px-4 py-3 text-left">Created</th>
<th class="px-4 py-3 text-left">Last used</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
@forelse ($keys as $key)
<tr class="border-t border-white/5">
<td class="px-4 py-3 font-mono text-xs text-gray-400">{{ $key->key_prefix }}</td>
<td class="px-4 py-3 text-white">{{ $key->name ?? '—' }}</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $key->created_at?->toDateString() }}</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $key->last_used_at?->diffForHumans() ?? 'Never' }}</td>
<td class="px-4 py-3 text-right">
@if ($key->revoked_at)
<span class="text-xs text-gray-500">Revoked</span>
@else
<form method="POST" action="{{ route('dashboard.api-keys.revoke', $key->id) }}" class="inline" data-revoke-form>
@csrf
<button type="submit" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10">Revoke</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">No API keys yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="rounded-3xl glass-card p-6 space-y-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Tips</div>
<div class="mt-2 text-xl font-semibold text-white">Keep keys safe</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Use separate keys for apps and automate revocation if you suspect compromise.
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
API keys are required to access private keyword search results.
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
document.querySelectorAll('[data-copy-key]').forEach((btn) => {
btn.addEventListener('click', () => {
navigator.clipboard.writeText(btn.dataset.copyKey || '');
btn.textContent = 'Copied';
setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
});
});
document.querySelectorAll('[data-revoke-form]').forEach((form) => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const ok = await window.dewemojiConfirm('Revoke this API key? This cannot be undone.', {
title: 'Revoke API key',
okText: 'Revoke',
});
if (!ok) return;
form.submit();
});
});
})();
</script>
@endpush

View File

@@ -0,0 +1,127 @@
@extends('dashboard.app')
@section('title', 'Billing')
@section('page_title', 'Billing')
@section('page_subtitle', 'Subscription status and plan details.')
@section('dashboard_content')
@php
$user = $user ?? auth()->user();
$subscription = $subscription ?? null;
$hasSub = $subscription !== null;
$orders = $orders ?? collect();
$payments = $payments ?? collect();
@endphp
<div class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Plan</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $hasSub ? ucfirst($subscription->plan ?? 'Personal') : 'Free' }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $hasSub ? ucfirst($subscription->status ?? 'active') : 'No subscription' }}</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Renewal</div>
<div class="mt-3 text-2xl font-semibold text-white">
{{ $subscription?->next_renewal_at?->toDateString() ?? '—' }}
</div>
<div class="mt-2 text-sm text-gray-400">Next charge date</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Status</div>
<div class="mt-3 text-2xl font-semibold text-white">
{{ $subscription?->expires_at?->toDateString() ?? 'Active' }}
</div>
<div class="mt-2 text-sm text-gray-400">Access ends</div>
</div>
</div>
<div class="mt-8 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Billing summary</div>
<div class="mt-2 text-xl font-semibold text-white">Subscription details</div>
</div>
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-300 theme-surface">Current cycle</span>
</div>
<div class="mt-4 text-sm text-gray-300">
@if ($hasSub)
Provider: {{ $subscription->provider ?? 'direct' }} · Started {{ $subscription->started_at?->toDateString() }}
@else
You are on the free plan. Upgrade to unlock private keywords and sync.
@endif
</div>
<div class="mt-3 text-xs text-gray-400">
Downgrading to Free revokes any active API keys immediately.
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-sm font-semibold text-gray-200">Payment method</div>
<div class="mt-2 text-2xl font-semibold text-white">Card</div>
<div class="mt-1 text-xs text-gray-400">Coming soon</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-sm font-semibold text-gray-200">Invoices</div>
<div class="mt-2 text-2xl font-semibold text-white">0</div>
<div class="mt-1 text-xs text-gray-400">Billing history</div>
</div>
</div>
@if ($payments->count() > 0)
<div class="mt-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Recent payments</div>
<div class="mt-3 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Provider</th>
<th class="px-4 py-3 text-left">Plan</th>
<th class="px-4 py-3 text-left">Amount</th>
<th class="px-4 py-3 text-left">Status</th>
<th class="px-4 py-3 text-left">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
@foreach ($payments as $payment)
@php
$status = $payment->status ?? 'pending';
$pill = $status === 'paid'
? ['bg' => 'bg-emerald-100 dark:bg-emerald-500/20', 'text' => 'text-emerald-800 dark:text-emerald-200']
: ($status === 'failed'
? ['bg' => 'bg-rose-100 dark:bg-rose-500/20', 'text' => 'text-rose-800 dark:text-rose-200']
: ['bg' => 'bg-amber-100 dark:bg-amber-500/20', 'text' => 'text-amber-800 dark:text-amber-200']);
@endphp
<tr>
<td class="px-4 py-3">{{ $payment->provider ?? '—' }}</td>
<td class="px-4 py-3">{{ $payment->plan_code ?? '—' }}</td>
<td class="px-4 py-3">{{ $payment->currency ?? 'USD' }} {{ number_format((float) ($payment->amount ?? 0), 2) }}</td>
<td class="px-4 py-3">
<span class="rounded-full {{ $pill['bg'] }} px-3 py-1 text-xs font-semibold {{ $pill['text'] }}">{{ $status }}</span>
</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $payment->created_at?->toDateString() ?? '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($payments->contains(fn ($payment) => in_array($payment->status, ['pending', 'failed'], true)))
<div class="mt-4 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-200">
Pending or failed payments need a new checkout. Start a fresh transaction from the pricing page.
</div>
<a href="{{ route('pricing') }}" class="mt-3 inline-flex items-center justify-center rounded-full border border-amber-300 px-4 py-2 text-xs font-semibold text-amber-800 hover:bg-amber-100 dark:border-amber-300/40 dark:text-amber-200 dark:hover:bg-amber-400/10">
Start new checkout
</a>
@endif
</div>
@endif
@if (!$hasSub || (string) $user?->tier !== 'personal')
<div class="mt-6 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-brand-sun/30 dark:bg-brand-sun/10 dark:text-brand-sun">
Upgrade to Personal for private keywords and synced personalization.
</div>
<a href="{{ route('pricing') }}" class="mt-4 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>
@endif
</div>
@endsection

View File

@@ -0,0 +1,251 @@
@extends('dashboard.app')
@section('title', 'My Keywords')
@section('page_title', 'My Keywords')
@section('page_subtitle', 'Manage your private emoji keywords and language tags.')
@section('dashboard_content')
@php
$user = $user ?? auth()->user();
$isPersonal = $user && (string) $user->tier === 'personal';
$freeLimit = $freeLimit ?? null;
$limitReached = $freeLimit !== null && $items->count() >= $freeLimit;
$emojiLookup = $emojiLookup ?? [];
@endphp
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="mb-6 rounded-2xl border border-amber-300/40 bg-amber-400/10 px-4 py-3 text-sm text-amber-200">
{{ $errors->first() }}
</div>
@endif
<div class="rounded-3xl glass-card p-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Keyword library</div>
<div class="mt-2 text-xl font-semibold text-white">{{ $isPersonal ? 'Ready to personalize' : 'Free plan keywords' }}</div>
<p class="mt-2 text-sm text-gray-400">Add keywords to emojis to improve your personal search results.</p>
@if (!$isPersonal && $freeLimit)
<p class="mt-1 text-xs text-gray-500">Free plan limit: {{ $items->count() }} / {{ $freeLimit }} keywords.</p>
@endif
</div>
<div class="flex flex-wrap items-center gap-2">
<button id="add-keyword-btn" class="rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm {{ $limitReached ? 'opacity-50 cursor-not-allowed' : '' }}" {{ $limitReached ? 'disabled' : '' }}>
+ Add Keyword
</button>
<button id="import-btn" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5 {{ $limitReached ? 'opacity-50 cursor-not-allowed' : '' }}" {{ $limitReached ? 'disabled' : '' }}>
Import JSON
</button>
<a href="{{ route('dashboard.keywords.export') }}" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">
Export JSON
</a>
<div class="relative">
<input id="keyword-search" type="text" placeholder="Search keywords..." class="rounded-full bg-white/5 border border-white/10 px-4 py-2 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean">
</div>
</div>
</div>
@if (!$isPersonal && $freeLimit)
<div class="mt-6 rounded-2xl border border-brand-sun/30 bg-brand-sun/10 p-4 text-sm text-brand-sun">
Free plan includes up to {{ $freeLimit }} keywords total. Upgrade for unlimited keywords.
</div>
@endif
<div id="import-panel" class="mt-6 hidden rounded-2xl border border-white/10 bg-white/5 p-5">
<form method="POST" action="{{ route('dashboard.keywords.import') }}" enctype="multipart/form-data" class="grid gap-3 md:grid-cols-2">
@csrf
<div class="space-y-2">
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">JSON file</label>
<input type="file" name="file" accept="application/json" class="block w-full text-sm text-gray-200 file:mr-4 file:rounded-full file:border-0 file:bg-brand-ocean file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-brand-oceanSoft" {{ $limitReached ? 'disabled' : '' }}>
</div>
<div class="space-y-2">
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Or paste JSON</label>
<textarea name="payload" rows="4" class="w-full rounded-2xl bg-black/30 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder='[{"emoji_slug":"sparkles","keyword":"magic","lang":"en"}]'></textarea>
</div>
<div class="md:col-span-2 flex items-center justify-end gap-2">
<button type="button" id="import-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" {{ $limitReached ? 'disabled' : '' }}>Import</button>
</div>
</form>
</div>
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Emoji</th>
<th class="px-4 py-3 text-left">Keyword</th>
<th class="px-4 py-3 text-left">Language</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody id="keyword-table">
@forelse ($items as $item)
<tr class="border-t border-white/5 keyword-row" data-keyword="{{ strtolower($item->keyword) }}" data-slug="{{ strtolower($item->emoji_slug) }}">
@php
$lookup = $emojiLookup[$item->emoji_slug] ?? null;
@endphp
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<span class="text-xl">{{ $lookup['emoji'] ?? '⬚' }}</span>
<div>
<div class="text-sm text-white">{{ $lookup['name'] ?? $item->emoji_slug }}</div>
<div class="text-xs text-gray-500">{{ $item->emoji_slug }}</div>
</div>
</div>
</td>
<td class="px-4 py-3 font-semibold text-white">{{ $item->keyword }}</td>
<td class="px-4 py-3 text-xs uppercase tracking-[0.15em] text-gray-400">{{ $item->lang ?? 'und' }}</td>
<td class="px-4 py-3 text-right">
<button
type="button"
class="edit-btn rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10"
data-id="{{ $item->id }}"
data-emoji="{{ $item->emoji_slug }}"
data-keyword="{{ $item->keyword }}"
data-lang="{{ $item->lang }}"
>
Edit
</button>
<form method="POST" action="{{ route('dashboard.keywords.delete', $item->id) }}" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10">
Delete
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">No keywords yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div id="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" id="keyword-modal-title">Add keyword</h3>
<button id="keyword-modal-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-form" method="POST" action="{{ route('dashboard.keywords.store') }}" class="mt-4 grid gap-4">
@csrf
<input type="hidden" name="_method" id="keyword-form-method" value="POST">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Emoji Slug</label>
<input type="text" name="emoji_slug" id="keyword-emoji" 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="sparkles" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" name="keyword" id="keyword-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" placeholder="magic" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" name="lang" id="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="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')
<script>
(() => {
const isPersonal = @json($isPersonal);
const limitReached = @json($limitReached);
const modal = document.getElementById('keyword-modal');
const modalTitle = document.getElementById('keyword-modal-title');
const openBtn = document.getElementById('add-keyword-btn');
const closeBtn = document.getElementById('keyword-modal-close');
const cancelBtn = document.getElementById('keyword-cancel');
const form = document.getElementById('keyword-form');
const methodEl = document.getElementById('keyword-form-method');
const emojiEl = document.getElementById('keyword-emoji');
const textEl = document.getElementById('keyword-text');
const langEl = document.getElementById('keyword-lang');
const searchEl = document.getElementById('keyword-search');
const importBtn = document.getElementById('import-btn');
const importPanel = document.getElementById('import-panel');
const importCancel = document.getElementById('import-cancel');
const openModal = (mode, data = {}) => {
if (mode === 'add' && limitReached) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
modalTitle.textContent = mode === 'edit' ? 'Edit keyword' : 'Add keyword';
form.action = mode === 'edit' ? `/dashboard/keywords/${data.id}` : '{{ route('dashboard.keywords.store') }}';
methodEl.value = mode === 'edit' ? 'PUT' : 'POST';
emojiEl.value = data.emoji || '';
textEl.value = data.keyword || '';
langEl.value = data.lang || '';
emojiEl.focus();
};
const closeModal = () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
};
openBtn?.addEventListener('click', () => openModal('add'));
closeBtn?.addEventListener('click', closeModal);
cancelBtn?.addEventListener('click', closeModal);
modal?.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
document.querySelectorAll('.edit-btn').forEach((btn) => {
btn.addEventListener('click', () => {
openModal('edit', {
id: btn.dataset.id,
emoji: btn.dataset.emoji,
keyword: btn.dataset.keyword,
lang: btn.dataset.lang,
});
});
});
if (searchEl) {
searchEl.addEventListener('input', () => {
const value = searchEl.value.trim().toLowerCase();
document.querySelectorAll('.keyword-row').forEach((row) => {
const match = row.dataset.keyword?.includes(value) || row.dataset.slug?.includes(value);
row.classList.toggle('hidden', value && !match);
});
});
}
importBtn?.addEventListener('click', () => {
if (!isPersonal) return;
importPanel.classList.remove('hidden');
});
importCancel?.addEventListener('click', () => {
importPanel.classList.add('hidden');
});
if (window.location.hash === '#add') {
openModal('add');
}
window.addEventListener('hashchange', () => {
if (window.location.hash === '#add') {
openModal('add');
}
});
})();
</script>
@endpush

View File

@@ -0,0 +1,93 @@
@extends('dashboard.app')
@section('title', 'Dashboard')
@section('page_title', 'Your Overview')
@section('page_subtitle', 'Quick stats and recent keyword activity.')
@section('dashboard_content')
@php
$user = auth()->user();
$isPersonal = $user && (string) $user->tier === 'personal';
$subscription = $subscription ?? null;
@endphp
<div class="grid gap-6 lg:grid-cols-4">
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Total keywords</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($totalKeywords ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">Across your emoji library</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Last 7 days</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($recentWeekCount ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">New keywords added</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">API keys</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($apiKeyCount ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">Active keys</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Plan</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $isPersonal ? 'Personal' : 'Free' }}</div>
<div class="mt-2 text-sm text-gray-400">
{{ $subscription?->status ? ucfirst($subscription->status) : 'No active subscription' }}
</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Synced devices</div>
<div class="mt-3 text-3xl font-semibold text-white">0</div>
<div class="mt-2 text-sm text-gray-400">Coming soon</div>
</div>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Recent keywords</div>
<div class="mt-2 text-xl font-semibold text-white">Latest additions</div>
</div>
<a href="{{ route('dashboard.keywords') }}" class="rounded-full border border-white/10 px-4 py-2 text-xs text-gray-300 hover:bg-white/5">
Manage keywords
</a>
</div>
<div class="mt-6 grid gap-3 md:grid-cols-2">
@forelse ($recentKeywords ?? [] as $keyword)
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-xs uppercase tracking-[0.2em] text-gray-500">{{ $keyword->lang ?? 'und' }}</div>
<div class="mt-2 text-lg font-semibold text-white">{{ $keyword->keyword }}</div>
<div class="mt-1 text-xs text-gray-400">Emoji slug: {{ $keyword->emoji_slug }}</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 p-6 text-sm text-gray-400">
No keywords yet. Add your first keyword from an emoji detail page or the dashboard.
</div>
@endforelse
</div>
</div>
<div class="rounded-3xl glass-card p-6 flex flex-col gap-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Next steps</div>
<div class="mt-2 text-xl font-semibold text-white">Personalize faster</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Add keywords right on emoji detail pages to speed up your searches.
</div>
@if (!$isPersonal)
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-brand-sun/30 dark:bg-brand-sun/10 dark:text-brand-sun">
Upgrade to Personal to unlock private keywords and sync across devices.
</div>
<a href="{{ route('pricing') }}" class="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
<a href="{{ route('dashboard.keywords') }}" class="inline-flex items-center justify-center rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm">
Add keywords
</a>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,122 @@
@extends('dashboard.app')
@section('title', 'Preferences')
@section('page_title', 'Preferences')
@section('page_subtitle', 'Personalize your Dewemoji experience.')
@section('dashboard_content')
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Personal settings</div>
<div class="mt-2 text-xl font-semibold text-white">Preferences</div>
<p class="mt-2 text-sm text-gray-400">Saved locally for now. We will sync these to your account later.</p>
<form id="preferences-form" class="mt-6 grid gap-4 md:grid-cols-2">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Theme</label>
<select id="pref-theme" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<div class="mt-1 text-xs text-gray-400">Matches the global theme toggle.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Locale</label>
<select id="pref-locale" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="en-US">English (US)</option>
<option value="id-ID">Bahasa Indonesia</option>
</select>
<div class="mt-1 text-xs text-gray-400">Controls language defaults.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Tone lock</label>
<select id="pref-tone" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="off">Off</option>
<option value="on">Always on</option>
</select>
<div class="mt-1 text-xs text-gray-400">Prefer a skin tone when available.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Insert mode</label>
<select id="pref-insert" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="copy">Copy</option>
<option value="insert">Insert</option>
</select>
<div class="mt-1 text-xs text-gray-400">Default action for emoji pickers.</div>
</div>
</form>
<div id="pref-status" class="mt-4 text-sm text-gray-400"></div>
</div>
<div class="rounded-3xl glass-card p-6 space-y-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Preferences</div>
<div class="mt-2 text-xl font-semibold text-white">Whats next</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Sync preferences across devices and extensions once account settings are wired.
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Add tone presets and quick language switching.
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const storageKey = 'dewemoji_prefs';
const prefTheme = document.getElementById('pref-theme');
const prefLocale = document.getElementById('pref-locale');
const prefTone = document.getElementById('pref-tone');
const prefInsert = document.getElementById('pref-insert');
const status = document.getElementById('pref-status');
const readPrefs = () => {
try {
return JSON.parse(localStorage.getItem(storageKey) || '{}');
} catch {
return {};
}
};
const writePrefs = (prefs) => {
localStorage.setItem(storageKey, JSON.stringify(prefs));
if (status) {
status.textContent = 'Saved just now.';
setTimeout(() => { status.textContent = ''; }, 1600);
}
};
const applyThemePref = (value) => {
if (value === 'light') localStorage.setItem('dewemoji_theme', 'light');
if (value === 'dark') localStorage.setItem('dewemoji_theme', 'dark');
if (value === 'system') localStorage.removeItem('dewemoji_theme');
window.location.reload();
};
const prefs = readPrefs();
if (prefTheme) prefTheme.value = prefs.theme || 'system';
if (prefLocale) prefLocale.value = prefs.locale || 'en-US';
if (prefTone) prefTone.value = prefs.tone || 'off';
if (prefInsert) prefInsert.value = prefs.insert || 'copy';
[prefTheme, prefLocale, prefTone, prefInsert].forEach((el) => {
if (!el) return;
el.addEventListener('change', () => {
const next = {
theme: prefTheme?.value || 'system',
locale: prefLocale?.value || 'en-US',
tone: prefTone?.value || 'off',
insert: prefInsert?.value || 'copy',
};
writePrefs(next);
if (el === prefTheme) applyThemePref(next.theme);
});
});
})();
</script>
@endpush

View File

@@ -0,0 +1,25 @@
@extends('dashboard.app')
@section('title', 'Profile')
@section('page_title', 'Profile')
@section('page_subtitle', 'Update your account details and security settings.')
@section('dashboard_content')
@if (session('status') === 'profile-updated')
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
Profile updated.
</div>
@endif
<div class="grid gap-6">
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.update-profile-information-form')
</div>
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.update-password-form')
</div>
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.delete-user-form')
</div>
</div>
@endsection