252 lines
13 KiB
PHP
252 lines
13 KiB
PHP
@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
|