Polish UI and billing flows: route fallback, searchable selectors, light-mode fixes
This commit is contained in:
@@ -293,20 +293,27 @@ class SiteController extends Controller
|
||||
}
|
||||
|
||||
$user = request()->user();
|
||||
$canManageKeywords = (bool) $user;
|
||||
$isPersonal = $user && (string) $user->tier === 'personal';
|
||||
$freeLimit = (int) config('dewemoji.pagination.free_max_limit', 20);
|
||||
$keywordLimit = $isPersonal ? null : $freeLimit;
|
||||
$userKeywords = [];
|
||||
if ($isPersonal) {
|
||||
if ($canManageKeywords) {
|
||||
$userKeywords = UserKeyword::where('user_id', $user->id)
|
||||
->where('emoji_slug', $slug)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
$limitReached = $keywordLimit !== null && $userKeywords->count() >= $keywordLimit;
|
||||
|
||||
return view('site.emoji-detail', [
|
||||
'emoji' => $match,
|
||||
'relatedDetails' => $relatedDetails,
|
||||
'canonicalPath' => '/emoji/'.$slug,
|
||||
'userKeywords' => $userKeywords,
|
||||
'canManageKeywords' => $canManageKeywords,
|
||||
'keywordLimit' => $keywordLimit,
|
||||
'limitReached' => $limitReached,
|
||||
'userTier' => $user?->tier,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
|
||||
<main class="flex-1 flex flex-col h-full min-w-0 relative">
|
||||
<header class="glass-header px-6 py-6 shrink-0 sticky top-0 z-40">
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
<div class="flex flex-col md:flex-row gap-4 md:items-center justify-between">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@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' : '' }}>
|
||||
<button id="add-keyword-btn" class="rounded-full bg-brand-ocean text-white force-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' : '' }}>
|
||||
@@ -69,7 +69,7 @@
|
||||
</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>
|
||||
<button type="submit" class="rounded-full bg-brand-ocean text-white force-white font-semibold px-5 py-2 text-sm" {{ $limitReached ? 'disabled' : '' }}>Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="keyword-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
|
||||
<div id="keyword-modal" class="hidden fixed inset-0 z-[90] 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 theme-surface p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -144,8 +144,12 @@
|
||||
@csrf
|
||||
<input type="hidden" name="_method" id="keyword-form-method" value="POST">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Emoji Slug</label>
|
||||
<input type="text" name="emoji_slug" id="keyword-emoji" class="mt-2 w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="sparkles" required>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Emoji</label>
|
||||
<input type="hidden" name="emoji_slug" id="keyword-emoji" required>
|
||||
<div class="relative mt-2">
|
||||
<input type="text" id="keyword-emoji-combo" autocomplete="off" class="w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="Search emoji (e.g. sparkles)" required>
|
||||
<div id="keyword-emoji-menu" class="hidden absolute left-0 right-0 mt-2 max-h-56 overflow-y-auto rounded-xl border border-slate-200 bg-white shadow-xl z-[100] dark:border-white/10 dark:bg-[#0b0b0f]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Keyword</label>
|
||||
@@ -154,8 +158,13 @@
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Language</label>
|
||||
<input type="hidden" name="lang" id="keyword-lang" value="und">
|
||||
<input type="text" id="keyword-lang-display" list="keyword-language-options" class="mt-2 w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="Search language (e.g. English)">
|
||||
@include('partials.language-datalist', ['id' => 'keyword-language-options'])
|
||||
<div class="relative mt-2">
|
||||
<input type="text" id="keyword-lang-combo" autocomplete="off" class="w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="Search language (e.g. Indonesian)">
|
||||
<div id="keyword-lang-menu" class="hidden absolute left-0 right-0 mt-2 max-h-56 overflow-y-auto rounded-xl border border-slate-200 bg-white shadow-xl z-[100] dark:border-white/10 dark:bg-[#0b0b0f]"></div>
|
||||
</div>
|
||||
<select id="keyword-lang-source" class="hidden">
|
||||
@include('partials.language-datalist')
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button type="button" id="keyword-cancel" class="rounded-full border border-slate-300 px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-white/10 dark:text-gray-200 dark:hover:bg-white/5">Cancel</button>
|
||||
@@ -179,44 +188,206 @@
|
||||
const form = document.getElementById('keyword-form');
|
||||
const methodEl = document.getElementById('keyword-form-method');
|
||||
const emojiEl = document.getElementById('keyword-emoji');
|
||||
const emojiComboEl = document.getElementById('keyword-emoji-combo');
|
||||
const emojiMenuEl = document.getElementById('keyword-emoji-menu');
|
||||
const textEl = document.getElementById('keyword-text');
|
||||
const langEl = document.getElementById('keyword-lang');
|
||||
const langDisplayEl = document.getElementById('keyword-lang-display');
|
||||
const langDataList = document.getElementById('keyword-language-options');
|
||||
const langComboEl = document.getElementById('keyword-lang-combo');
|
||||
const langMenuEl = document.getElementById('keyword-lang-menu');
|
||||
const langSourceEl = document.getElementById('keyword-lang-source');
|
||||
const searchEl = document.getElementById('keyword-search');
|
||||
const importBtn = document.getElementById('import-btn');
|
||||
const importPanel = document.getElementById('import-panel');
|
||||
const importCancel = document.getElementById('import-cancel');
|
||||
|
||||
window.dewemojiPopulateLanguageOptions?.(langDataList);
|
||||
window.dewemojiPopulateLanguageSelect?.(langSourceEl);
|
||||
const canonicalizeLanguageCode = (raw) => window.dewemojiCanonicalLanguageCode?.(raw) || String(raw || '').trim().toLowerCase();
|
||||
const emojiLookup = @json($emojiLookup);
|
||||
const emojiSearchUrl = @json(route('dashboard.keywords.search'));
|
||||
const publicEmojiSearchUrl = @json(url('/v1/emojis'));
|
||||
|
||||
const langDisplayToCode = new Map();
|
||||
const langCodeToDisplay = new Map();
|
||||
langDataList?.querySelectorAll('option').forEach((option) => {
|
||||
const code = (option.dataset.code || '').trim();
|
||||
const display = (option.value || '').trim();
|
||||
if (!code || !display) return;
|
||||
langDisplayToCode.set(display.toLowerCase(), code);
|
||||
langCodeToDisplay.set(code.toLowerCase(), display);
|
||||
});
|
||||
const DEFAULT_LANGUAGE_CODE = 'en';
|
||||
const UNDETERMINED_LANGUAGE_CODE = 'und';
|
||||
const emojiItems = Object.entries(emojiLookup || {}).map(([slug, item]) => {
|
||||
const name = String(item?.name || slug);
|
||||
const glyph = String(item?.emoji || '');
|
||||
const label = `${glyph ? `${glyph} ` : ''}${name}`;
|
||||
return {
|
||||
slug,
|
||||
name,
|
||||
glyph,
|
||||
label,
|
||||
search: `${slug} ${name} ${glyph}`.toLowerCase(),
|
||||
};
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const emojiBySlug = new Map(emojiItems.map((item) => [item.slug.toLowerCase(), item]));
|
||||
let remoteEmojiItems = [];
|
||||
let remoteEmojiSearched = false;
|
||||
let emojiQueryToken = 0;
|
||||
let emojiSearchTimer = null;
|
||||
let emojiSearchAbort = null;
|
||||
|
||||
const normalizeLang = (value) => {
|
||||
const v = (value || '').trim();
|
||||
if (!v) return 'und';
|
||||
const byDisplay = langDisplayToCode.get(v.toLowerCase());
|
||||
if (byDisplay) return byDisplay;
|
||||
const languageItems = Array.from(langSourceEl?.options || []).map((option) => ({
|
||||
code: option.value,
|
||||
label: option.text,
|
||||
}));
|
||||
const labelByCode = new Map(languageItems.map((item) => [item.code.toLowerCase(), item.label]));
|
||||
const itemByCode = new Map(languageItems.map((item) => [item.code.toLowerCase(), item]));
|
||||
|
||||
const bracket = v.match(/\(([A-Za-z]{2,3}(?:-[A-Za-z]{2})?)\)\s*$/);
|
||||
if (bracket?.[1]) return bracket[1];
|
||||
|
||||
if (/^[A-Za-z]{2,3}(?:-[A-Za-z]{2})?$/.test(v)) return v;
|
||||
return 'und';
|
||||
const renderLanguageMenu = (query = '') => {
|
||||
if (!langMenuEl) return;
|
||||
const q = query.trim().toLowerCase();
|
||||
const filtered = languageItems.filter((item) => item.label.toLowerCase().includes(q));
|
||||
langMenuEl.innerHTML = filtered.map((item) => {
|
||||
const selected = langEl.value === item.code;
|
||||
return `<button type="button" data-code="${item.code}" class="w-full text-left px-3 py-2 text-sm ${selected ? 'bg-brand-ocean/10 text-brand-ocean dark:text-brand-oceanSoft' : 'text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10'}">${item.label}</button>`;
|
||||
}).join('') || '<div class="px-3 py-2 text-sm text-slate-500 dark:text-gray-400">No matches</div>';
|
||||
};
|
||||
|
||||
const displayLang = (code) => {
|
||||
const c = (code || '').trim().toLowerCase();
|
||||
if (!c) return '';
|
||||
return langCodeToDisplay.get(c) || code;
|
||||
const openLanguageMenu = () => {
|
||||
renderLanguageMenu(langComboEl.value);
|
||||
langMenuEl.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const closeLanguageMenu = () => {
|
||||
langMenuEl.classList.add('hidden');
|
||||
};
|
||||
|
||||
const setLanguageByCode = (code, { silentInput = false } = {}) => {
|
||||
const value = canonicalizeLanguageCode(code || UNDETERMINED_LANGUAGE_CODE) || UNDETERMINED_LANGUAGE_CODE;
|
||||
langEl.value = value;
|
||||
if (silentInput || value.toLowerCase() === UNDETERMINED_LANGUAGE_CODE) {
|
||||
langComboEl.value = '';
|
||||
return;
|
||||
}
|
||||
langComboEl.value = labelByCode.get(value.toLowerCase()) || value;
|
||||
};
|
||||
|
||||
const resolveLanguageCode = (raw) => {
|
||||
const value = (raw || '').trim();
|
||||
if (!value) return UNDETERMINED_LANGUAGE_CODE;
|
||||
const byCode = itemByCode.get(canonicalizeLanguageCode(value));
|
||||
if (byCode) return byCode.code;
|
||||
const byLabel = languageItems.find((item) => item.label.toLowerCase() === value.toLowerCase());
|
||||
if (byLabel) return canonicalizeLanguageCode(byLabel.code);
|
||||
return UNDETERMINED_LANGUAGE_CODE;
|
||||
};
|
||||
|
||||
const renderEmojiMenu = (query = '') => {
|
||||
if (!emojiMenuEl) return;
|
||||
const q = query.trim().toLowerCase();
|
||||
const useRemote = q.length >= 2 && remoteEmojiSearched;
|
||||
const filtered = useRemote
|
||||
? remoteEmojiItems.slice(0, 80)
|
||||
: emojiItems.filter((item) => !q || item.search.includes(q)).slice(0, 80);
|
||||
emojiMenuEl.innerHTML = filtered.map((item) => {
|
||||
const selected = emojiEl.value === item.slug;
|
||||
const glyph = item.glyph || ' ';
|
||||
return `<button type="button" data-slug="${item.slug}" class="w-full text-left px-3 py-2 text-sm ${selected ? 'bg-brand-ocean/10 text-brand-ocean dark:text-brand-oceanSoft' : 'text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10'}"><span class="inline-flex items-center gap-2"><span class="inline-flex w-6 shrink-0 justify-center">${glyph}</span><span class="truncate">${item.name}</span></span></button>`;
|
||||
}).join('') || '<div class="px-3 py-2 text-sm text-slate-500 dark:text-gray-400">No matches</div>';
|
||||
};
|
||||
|
||||
const mapApiItemsToEmojiItems = (items) => (items || []).map((item) => {
|
||||
const slug = String(item?.slug || '').trim();
|
||||
if (!slug) return null;
|
||||
const existing = emojiBySlug.get(slug.toLowerCase());
|
||||
const name = String(item?.name || existing?.name || slug);
|
||||
const glyph = String(item?.emoji || existing?.glyph || '');
|
||||
return {
|
||||
slug,
|
||||
name,
|
||||
glyph,
|
||||
label: `${glyph ? `${glyph} ` : ''}${name}`,
|
||||
search: `${slug} ${name} ${glyph}`.toLowerCase(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
const fetchEmojiMenuItems = async (query) => {
|
||||
const q = String(query || '').trim();
|
||||
if (q.length < 2) {
|
||||
remoteEmojiItems = [];
|
||||
remoteEmojiSearched = false;
|
||||
renderEmojiMenu(q);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = ++emojiQueryToken;
|
||||
if (emojiSearchAbort) {
|
||||
emojiSearchAbort.abort();
|
||||
}
|
||||
emojiSearchAbort = new AbortController();
|
||||
|
||||
const base = isPersonal ? emojiSearchUrl : publicEmojiSearchUrl;
|
||||
const url = `${base}?${new URLSearchParams({ q, limit: '30', page: '1' }).toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: emojiSearchAbort.signal,
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (token !== emojiQueryToken) return;
|
||||
const items = Array.isArray(data?.items) ? data.items : [];
|
||||
remoteEmojiItems = mapApiItemsToEmojiItems(items);
|
||||
remoteEmojiSearched = true;
|
||||
renderEmojiMenu(q);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') return;
|
||||
if (token !== emojiQueryToken) return;
|
||||
remoteEmojiItems = [];
|
||||
remoteEmojiSearched = true;
|
||||
renderEmojiMenu(q);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleEmojiFetch = (query) => {
|
||||
if (emojiSearchTimer) {
|
||||
clearTimeout(emojiSearchTimer);
|
||||
}
|
||||
emojiSearchTimer = setTimeout(() => {
|
||||
fetchEmojiMenuItems(query);
|
||||
}, 180);
|
||||
};
|
||||
|
||||
const openEmojiMenu = () => {
|
||||
renderEmojiMenu(emojiComboEl.value);
|
||||
emojiMenuEl?.classList.remove('hidden');
|
||||
scheduleEmojiFetch(emojiComboEl.value);
|
||||
};
|
||||
|
||||
const closeEmojiMenu = () => {
|
||||
emojiMenuEl?.classList.add('hidden');
|
||||
};
|
||||
|
||||
const setEmojiBySlug = (slug, { keepRaw = false } = {}) => {
|
||||
const normalized = String(slug || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
emojiEl.value = '';
|
||||
emojiComboEl.value = '';
|
||||
return;
|
||||
}
|
||||
const item = emojiBySlug.get(normalized);
|
||||
if (!item) {
|
||||
emojiEl.value = normalized;
|
||||
emojiComboEl.value = keepRaw ? String(slug || '') : normalized;
|
||||
return;
|
||||
}
|
||||
emojiEl.value = item.slug;
|
||||
emojiComboEl.value = item.label;
|
||||
};
|
||||
|
||||
const resolveEmojiSlug = (raw) => {
|
||||
const value = String(raw || '').trim().toLowerCase();
|
||||
if (!value) return '';
|
||||
const exact = emojiBySlug.get(value);
|
||||
if (exact) return exact.slug;
|
||||
const exactLabel = emojiItems.find((item) => item.label.toLowerCase() === value);
|
||||
if (exactLabel) return exactLabel.slug;
|
||||
const exactName = emojiItems.find((item) => item.name.toLowerCase() === value);
|
||||
if (exactName) return exactName.slug;
|
||||
const starts = emojiItems.filter((item) => item.slug.toLowerCase().startsWith(value) || item.name.toLowerCase().startsWith(value));
|
||||
if (starts.length === 1) return starts[0].slug;
|
||||
return '';
|
||||
};
|
||||
|
||||
const openModal = (mode, data = {}) => {
|
||||
@@ -226,11 +397,14 @@
|
||||
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 || '';
|
||||
setEmojiBySlug(data.emoji || '');
|
||||
textEl.value = data.keyword || '';
|
||||
langEl.value = (data.lang || 'und').trim() || 'und';
|
||||
langDisplayEl.value = displayLang(data.lang || 'und');
|
||||
emojiEl.focus();
|
||||
const initialLanguage = mode === 'edit'
|
||||
? (data.lang || UNDETERMINED_LANGUAGE_CODE)
|
||||
: DEFAULT_LANGUAGE_CODE;
|
||||
setLanguageByCode(initialLanguage, { silentInput: initialLanguage.toLowerCase() === UNDETERMINED_LANGUAGE_CODE });
|
||||
closeLanguageMenu();
|
||||
emojiComboEl.focus();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -245,12 +419,37 @@
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
langDisplayEl?.addEventListener('input', () => {
|
||||
langEl.value = normalizeLang(langDisplayEl.value);
|
||||
emojiComboEl?.addEventListener('focus', openEmojiMenu);
|
||||
emojiComboEl?.addEventListener('input', () => {
|
||||
openEmojiMenu();
|
||||
scheduleEmojiFetch(emojiComboEl.value);
|
||||
});
|
||||
emojiComboEl?.addEventListener('blur', () => {
|
||||
const nextSlug = resolveEmojiSlug(emojiComboEl.value);
|
||||
setEmojiBySlug(nextSlug);
|
||||
setTimeout(closeEmojiMenu, 120);
|
||||
});
|
||||
emojiMenuEl?.addEventListener('mousedown', (event) => {
|
||||
const button = event.target.closest('button[data-slug]');
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
setEmojiBySlug(button.dataset.slug);
|
||||
closeEmojiMenu();
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', () => {
|
||||
langEl.value = normalizeLang(langDisplayEl?.value || '');
|
||||
langComboEl?.addEventListener('focus', openLanguageMenu);
|
||||
langComboEl?.addEventListener('input', openLanguageMenu);
|
||||
langComboEl?.addEventListener('blur', () => {
|
||||
const nextCode = resolveLanguageCode(langComboEl.value);
|
||||
setLanguageByCode(nextCode, { silentInput: nextCode.toLowerCase() === UNDETERMINED_LANGUAGE_CODE });
|
||||
setTimeout(closeLanguageMenu, 120);
|
||||
});
|
||||
langMenuEl?.addEventListener('mousedown', (event) => {
|
||||
const button = event.target.closest('button[data-code]');
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
setLanguageByCode(button.dataset.code);
|
||||
closeLanguageMenu();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.edit-btn').forEach((btn) => {
|
||||
@@ -264,6 +463,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', (event) => {
|
||||
const slug = resolveEmojiSlug(emojiComboEl?.value || emojiEl?.value || '');
|
||||
if (!slug) {
|
||||
event.preventDefault();
|
||||
alert('Please select a valid emoji from the dropdown.');
|
||||
emojiComboEl?.focus();
|
||||
openEmojiMenu();
|
||||
return;
|
||||
}
|
||||
setEmojiBySlug(slug);
|
||||
});
|
||||
|
||||
if (searchEl) {
|
||||
searchEl.addEventListener('input', () => {
|
||||
const value = searchEl.value.trim().toLowerCase();
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
@php
|
||||
$listId = $id ?? 'language-options';
|
||||
// Fallbacks only. Full list is generated in JS via Intl APIs.
|
||||
// Fallback seed list. Full set is generated in JS for capable browsers.
|
||||
$languages = [
|
||||
['code' => 'und', 'name' => 'Undetermined'],
|
||||
['code' => 'en', 'name' => 'English'],
|
||||
['code' => 'id', 'name' => 'Indonesian'],
|
||||
['code' => 'es', 'name' => 'Spanish'],
|
||||
['code' => 'fr', 'name' => 'French'],
|
||||
['code' => 'de', 'name' => 'German'],
|
||||
];
|
||||
@endphp
|
||||
<datalist id="{{ $listId }}" data-language-options="1">
|
||||
@foreach ($languages as $language)
|
||||
<option value="{{ $language['name'] }} ({{ $language['code'] }})" data-code="{{ $language['code'] }}"></option>
|
||||
@endforeach
|
||||
</datalist>
|
||||
@foreach ($languages as $language)
|
||||
<option value="{{ $language['code'] }}">{{ $language['name'] }} ({{ $language['code'] }})</option>
|
||||
@endforeach
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
<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>
|
||||
<button id="ti-run" type="button" class="px-4 py-2 rounded-lg bg-brand-ocean hover:bg-brand-oceanSoft text-white force-white text-sm">Run /emojis</button>
|
||||
<button id="ti-cats" type="button" class="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-sm">Run /categories</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -94,8 +94,8 @@
|
||||
<div class="absolute w-64 h-64 bg-yellow-500/20 rounded-full blur-3xl group-hover:bg-yellow-500/30 transition-colors duration-500"></div>
|
||||
<div class="text-[140px] md:text-[180px] leading-none select-none relative z-10 animate-float drop-shadow-2xl">{{ $symbol }}</div>
|
||||
<div class="absolute bottom-6 flex gap-3 opacity-0 group-hover:opacity-100 transition-all transform translate-y-2 group-hover:translate-y-0">
|
||||
<button onclick="copyToClipboard('{{ $symbol }}')" class="bg-black/50 backdrop-blur text-white p-3 rounded-full hover:bg-brand-sun hover:text-black transition-colors border border-white/10" title="Copy">
|
||||
<i data-lucide="copy" class="w-5 h-5"></i>
|
||||
<button onclick="copyToClipboard('{{ $symbol }}')" class="bg-black/50 backdrop-blur text-white force-white p-3 rounded-full hover:bg-brand-sun hover:text-black transition-colors border border-white/10" title="Copy">
|
||||
<i data-lucide="copy" class="w-5 h-5 force-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick="copyToClipboard('{{ $item['emoji'] }}'); event.preventDefault(); event.stopPropagation();"
|
||||
class="absolute -bottom-1 -right-1 w-5 h-5 rounded bg-black/70 border border-white/10 text-[10px] text-gray-200 hover:bg-brand-ocean/40"
|
||||
class="absolute -bottom-1 -right-1 w-5 h-5 rounded bg-black/70 border border-white/10 text-[10px] text-white force-white hover:bg-brand-ocean/40"
|
||||
title="Copy {{ $item['name'] }}"
|
||||
>⧉</button>
|
||||
</div>
|
||||
@@ -138,8 +138,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button onclick="copyToClipboard('{{ $symbol }}')" class="flex-1 bg-brand-ocean hover:bg-brand-oceanSoft text-white font-bold h-14 rounded-xl flex items-center justify-center gap-3 text-lg transition-all shadow-[0_0_20px_rgba(32,83,255,0.35)] hover:shadow-[0_0_30px_rgba(32,83,255,0.55)]">
|
||||
<i data-lucide="copy" class="w-5 h-5"></i>
|
||||
<button onclick="copyToClipboard('{{ $symbol }}')" class="flex-1 bg-brand-ocean hover:bg-brand-oceanSoft text-white force-white font-bold h-14 rounded-xl flex items-center justify-center gap-3 text-lg transition-all shadow-[0_0_20px_rgba(32,83,255,0.35)] hover:shadow-[0_0_30px_rgba(32,83,255,0.55)]">
|
||||
<i data-lucide="copy" class="w-5 h-5 force-white"></i>
|
||||
Copy Emoji
|
||||
</button>
|
||||
</div>
|
||||
@@ -206,12 +206,17 @@
|
||||
<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>
|
||||
@if ($canManageKeywords)
|
||||
<button id="user-keyword-add" class="rounded-full bg-brand-ocean text-white force-white text-xs font-semibold px-3 py-1.5 {{ $limitReached ? 'opacity-60 cursor-not-allowed' : '' }}" {{ $limitReached ? 'disabled' : '' }}>Add keyword</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($isPersonal)
|
||||
@if ($canManageKeywords)
|
||||
@if (!is_null($keywordLimit))
|
||||
<div class="mt-3 text-xs text-gray-400">
|
||||
Free plan limit: {{ $userKeywords->count() }} / {{ $keywordLimit }} keywords.
|
||||
</div>
|
||||
@endif
|
||||
<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">
|
||||
@@ -222,17 +227,6 @@
|
||||
<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.
|
||||
@@ -256,7 +250,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="user-keyword-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
|
||||
<div id="user-keyword-modal" class="hidden fixed inset-0 z-[90] 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 theme-surface p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -272,8 +266,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Language</label>
|
||||
<input type="text" id="user-keyword-lang" list="user-keyword-language-options" class="mt-2 w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="Search language (e.g. English)">
|
||||
@include('partials.language-datalist', ['id' => 'user-keyword-language-options'])
|
||||
<input type="hidden" id="user-keyword-lang" value="und">
|
||||
<div class="relative mt-2">
|
||||
<input type="text" id="user-keyword-lang-combo" autocomplete="off" class="w-full rounded-xl bg-white border border-slate-300 px-4 py-3 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:border-brand-ocean dark:bg-black/40 dark:border-white/10 dark:text-gray-200 dark:placeholder-gray-500" placeholder="Search language (e.g. Indonesian)">
|
||||
<div id="user-keyword-lang-menu" class="hidden absolute left-0 right-0 mt-2 max-h-56 overflow-y-auto rounded-xl border border-slate-200 bg-white shadow-xl z-[100] dark:border-white/10 dark:bg-[#0b0b0f]"></div>
|
||||
</div>
|
||||
<select id="user-keyword-lang-source" class="hidden">
|
||||
@include('partials.language-datalist')
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button type="button" id="user-keyword-cancel" class="rounded-full border border-slate-300 px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-white/10 dark:text-gray-200 dark:hover:bg-white/5">Cancel</button>
|
||||
@@ -329,8 +329,9 @@ document.addEventListener('keydown', (e) => {
|
||||
addRecent(@json($symbol));
|
||||
|
||||
(() => {
|
||||
const isPersonal = @json($isPersonal);
|
||||
if (!isPersonal) return;
|
||||
const canManageKeywords = @json($canManageKeywords);
|
||||
const limitReached = @json($limitReached);
|
||||
if (!canManageKeywords) return;
|
||||
const modal = document.getElementById('user-keyword-modal');
|
||||
const openBtn = document.getElementById('user-keyword-add');
|
||||
const closeBtn = document.getElementById('user-keyword-close');
|
||||
@@ -338,38 +339,74 @@ addRecent(@json($symbol));
|
||||
const form = document.getElementById('user-keyword-form');
|
||||
const keywordInput = document.getElementById('user-keyword-input');
|
||||
const langInput = document.getElementById('user-keyword-lang');
|
||||
const langDataList = document.getElementById('user-keyword-language-options');
|
||||
const langComboInput = document.getElementById('user-keyword-lang-combo');
|
||||
const langMenuEl = document.getElementById('user-keyword-lang-menu');
|
||||
const langSourceEl = document.getElementById('user-keyword-lang-source');
|
||||
const list = document.getElementById('user-keyword-list');
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
window.dewemojiPopulateLanguageOptions?.(langDataList);
|
||||
window.dewemojiPopulateLanguageSelect?.(langSourceEl);
|
||||
const canonicalizeLanguageCode = (raw) => window.dewemojiCanonicalLanguageCode?.(raw) || String(raw || '').trim().toLowerCase();
|
||||
|
||||
const langDisplayToCode = new Map();
|
||||
langDataList?.querySelectorAll('option').forEach((option) => {
|
||||
const code = (option.dataset.code || '').trim();
|
||||
const display = (option.value || '').trim();
|
||||
if (!code || !display) return;
|
||||
langDisplayToCode.set(display.toLowerCase(), code);
|
||||
});
|
||||
const DEFAULT_LANGUAGE_CODE = 'en';
|
||||
const UNDETERMINED_LANGUAGE_CODE = 'und';
|
||||
|
||||
const normalizeLang = (value) => {
|
||||
const v = (value || '').trim();
|
||||
if (!v) return 'und';
|
||||
const byDisplay = langDisplayToCode.get(v.toLowerCase());
|
||||
if (byDisplay) return byDisplay;
|
||||
const languageItems = Array.from(langSourceEl?.options || []).map((option) => ({
|
||||
code: option.value,
|
||||
label: option.text,
|
||||
}));
|
||||
const labelByCode = new Map(languageItems.map((item) => [item.code.toLowerCase(), item.label]));
|
||||
const itemByCode = new Map(languageItems.map((item) => [item.code.toLowerCase(), item]));
|
||||
|
||||
const bracket = v.match(/\(([A-Za-z]{2,3}(?:-[A-Za-z]{2})?)\)\s*$/);
|
||||
if (bracket?.[1]) return bracket[1];
|
||||
const renderLanguageMenu = (query = '') => {
|
||||
if (!langMenuEl) return;
|
||||
const q = query.trim().toLowerCase();
|
||||
const filtered = languageItems.filter((item) => item.label.toLowerCase().includes(q));
|
||||
langMenuEl.innerHTML = filtered.map((item) => {
|
||||
const selected = langInput.value === item.code;
|
||||
return `<button type="button" data-code="${item.code}" class="w-full text-left px-3 py-2 text-sm ${selected ? 'bg-brand-ocean/10 text-brand-ocean dark:text-brand-oceanSoft' : 'text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10'}">${item.label}</button>`;
|
||||
}).join('') || '<div class="px-3 py-2 text-sm text-slate-500 dark:text-gray-400">No matches</div>';
|
||||
};
|
||||
|
||||
if (/^[A-Za-z]{2,3}(?:-[A-Za-z]{2})?$/.test(v)) return v;
|
||||
return 'und';
|
||||
const openLanguageMenu = () => {
|
||||
renderLanguageMenu(langComboInput.value);
|
||||
langMenuEl.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const closeLanguageMenu = () => {
|
||||
langMenuEl.classList.add('hidden');
|
||||
};
|
||||
|
||||
const setLanguageByCode = (code, { silentInput = false } = {}) => {
|
||||
const value = canonicalizeLanguageCode(code || UNDETERMINED_LANGUAGE_CODE) || UNDETERMINED_LANGUAGE_CODE;
|
||||
langInput.value = value;
|
||||
if (silentInput || value.toLowerCase() === UNDETERMINED_LANGUAGE_CODE) {
|
||||
langComboInput.value = '';
|
||||
return;
|
||||
}
|
||||
langComboInput.value = labelByCode.get(value.toLowerCase()) || value;
|
||||
};
|
||||
|
||||
const resolveLanguageCode = (raw) => {
|
||||
const value = (raw || '').trim();
|
||||
if (!value) return UNDETERMINED_LANGUAGE_CODE;
|
||||
const byCode = itemByCode.get(canonicalizeLanguageCode(value));
|
||||
if (byCode) return byCode.code;
|
||||
const byLabel = languageItems.find((item) => item.label.toLowerCase() === value.toLowerCase());
|
||||
if (byLabel) return canonicalizeLanguageCode(byLabel.code);
|
||||
return UNDETERMINED_LANGUAGE_CODE;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
if (limitReached) {
|
||||
showToast('Free plan keyword limit reached');
|
||||
return;
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
keywordInput.value = '';
|
||||
langInput.value = '';
|
||||
setLanguageByCode(DEFAULT_LANGUAGE_CODE);
|
||||
closeLanguageMenu();
|
||||
keywordInput.focus();
|
||||
};
|
||||
|
||||
@@ -384,13 +421,27 @@ addRecent(@json($symbol));
|
||||
modal?.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
langComboInput?.addEventListener('focus', openLanguageMenu);
|
||||
langComboInput?.addEventListener('input', openLanguageMenu);
|
||||
langComboInput?.addEventListener('blur', () => {
|
||||
const nextCode = resolveLanguageCode(langComboInput.value);
|
||||
setLanguageByCode(nextCode, { silentInput: nextCode.toLowerCase() === UNDETERMINED_LANGUAGE_CODE });
|
||||
setTimeout(closeLanguageMenu, 120);
|
||||
});
|
||||
langMenuEl?.addEventListener('mousedown', (event) => {
|
||||
const button = event.target.closest('button[data-code]');
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
setLanguageByCode(button.dataset.code);
|
||||
closeLanguageMenu();
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
emoji_slug: @json($slug),
|
||||
keyword: keywordInput.value.trim(),
|
||||
lang: normalizeLang(langInput.value),
|
||||
lang: langInput.value || 'und',
|
||||
};
|
||||
if (!payload.keyword) return;
|
||||
const res = await fetch('{{ route('dashboard.keywords.store') }}', {
|
||||
|
||||
@@ -279,6 +279,14 @@
|
||||
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 button.text-white,
|
||||
html.theme-light a.text-white,
|
||||
html.theme-light [role="button"].text-white,
|
||||
html.theme-light button .text-white,
|
||||
html.theme-light a .text-white,
|
||||
html.theme-light [role="button"] .text-white {
|
||||
color: #ffffff !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; }
|
||||
@@ -606,67 +614,74 @@
|
||||
</script>
|
||||
<script>
|
||||
(() => {
|
||||
const normalizeCode = (raw) => {
|
||||
const input = String(raw || '').trim().replace('_', '-');
|
||||
if (!input) return null;
|
||||
const parts = input.split('-');
|
||||
if (parts.length === 1) {
|
||||
const code = parts[0].toLowerCase();
|
||||
return /^[a-z]{2,3}$/.test(code) ? code : null;
|
||||
}
|
||||
const base = parts[0].toLowerCase();
|
||||
const region = parts[1].toUpperCase();
|
||||
const code = `${base}-${region}`;
|
||||
return /^[a-z]{2,3}-[A-Z]{2}$/.test(code) ? code : null;
|
||||
const alpha = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const deprecatedAliases = {
|
||||
in: 'id',
|
||||
iw: 'he',
|
||||
ji: 'yi',
|
||||
jw: 'jv',
|
||||
};
|
||||
|
||||
window.dewemojiPopulateLanguageOptions = (target) => {
|
||||
const list = typeof target === 'string' ? document.getElementById(target) : target;
|
||||
if (!list || list.dataset.generated === '1') return;
|
||||
const makeIso6391Codes = () => {
|
||||
const codes = [];
|
||||
for (let i = 0; i < alpha.length; i += 1) {
|
||||
for (let j = 0; j < alpha.length; j += 1) {
|
||||
codes.push(alpha[i] + alpha[j]);
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
};
|
||||
|
||||
const codeToName = new Map();
|
||||
list.querySelectorAll('option[data-code]').forEach((option) => {
|
||||
const code = normalizeCode(option.dataset.code);
|
||||
if (!code) return;
|
||||
const fallbackName = String(option.value || '').replace(/\s+\([^)]+\)\s*$/, '').trim() || code;
|
||||
codeToName.set(code, fallbackName);
|
||||
});
|
||||
const canonicalLanguageCode = (raw) => {
|
||||
const code = String(raw || '').trim().toLowerCase();
|
||||
if (!code) return '';
|
||||
return deprecatedAliases[code] || code;
|
||||
};
|
||||
|
||||
if (!codeToName.has('und')) codeToName.set('und', 'Undetermined');
|
||||
window.dewemojiCanonicalLanguageCode = canonicalLanguageCode;
|
||||
|
||||
if (typeof Intl !== 'undefined' && typeof Intl.supportedValuesOf === 'function') {
|
||||
const display = typeof Intl.DisplayNames === 'function'
|
||||
? new Intl.DisplayNames(['en'], { type: 'language' })
|
||||
: null;
|
||||
window.dewemojiPopulateLanguageSelect = (target) => {
|
||||
const select = typeof target === 'string' ? document.getElementById(target) : target;
|
||||
if (!(select instanceof HTMLSelectElement) || select.dataset.generated === '1') return;
|
||||
|
||||
Intl.supportedValuesOf('language').forEach((rawCode) => {
|
||||
const code = normalizeCode(rawCode);
|
||||
if (!code) return;
|
||||
const name = display?.of(code) || display?.of(code.split('-')[0]) || code;
|
||||
codeToName.set(code, name);
|
||||
const existing = Array.from(select.options).map((opt) => ({
|
||||
code: canonicalLanguageCode(opt.value),
|
||||
label: (opt.textContent || '').trim(),
|
||||
})).filter((row) => row.code !== '');
|
||||
|
||||
const byCode = new Map(existing.map((row) => [row.code.toLowerCase(), row]));
|
||||
|
||||
if (typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function') {
|
||||
const dn = new Intl.DisplayNames(['en'], { type: 'language' });
|
||||
makeIso6391Codes().forEach((code) => {
|
||||
const name = dn.of(code);
|
||||
if (!name) return;
|
||||
// Unknown/unsupported codes are echoed back by many engines.
|
||||
if (name.toLowerCase() === code) return;
|
||||
const canonical = canonicalLanguageCode(code);
|
||||
if (!canonical) return;
|
||||
byCode.set(canonical, { code: canonical, label: `${name} (${canonical})` });
|
||||
});
|
||||
}
|
||||
|
||||
const entries = Array.from(codeToName.entries()).sort((a, b) => {
|
||||
if (a[0] === 'und') return -1;
|
||||
if (b[0] === 'und') return 1;
|
||||
return a[1].localeCompare(b[1]);
|
||||
const rows = Array.from(byCode.values()).sort((a, b) => {
|
||||
if (a.code === 'und') return -1;
|
||||
if (b.code === 'und') return 1;
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
list.innerHTML = '';
|
||||
entries.forEach(([code, name]) => {
|
||||
const previous = (select.value || '').trim().toLowerCase();
|
||||
select.innerHTML = '';
|
||||
rows.forEach((row) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${name} (${code})`;
|
||||
option.dataset.code = code;
|
||||
list.appendChild(option);
|
||||
option.value = row.code;
|
||||
option.textContent = row.label;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
list.dataset.generated = '1';
|
||||
select.value = previous || select.value;
|
||||
select.dataset.generated = '1';
|
||||
};
|
||||
|
||||
document.querySelectorAll('datalist[data-language-options="1"]').forEach((list) => {
|
||||
window.dewemojiPopulateLanguageOptions(list);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@stack('scripts')
|
||||
|
||||
@@ -549,6 +549,10 @@
|
||||
if (!buttons.length) return;
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const isAuthed = @json(auth()->check());
|
||||
const pakasirStatusUrl = @json(\Illuminate\Support\Facades\Route::has('billing.pakasir.status') ? route('billing.pakasir.status') : url('/billing/pakasir/status'));
|
||||
const pakasirCancelUrl = @json(\Illuminate\Support\Facades\Route::has('billing.pakasir.cancel') ? route('billing.pakasir.cancel') : url('/billing/pakasir/cancel'));
|
||||
const pakasirCreateUrl = @json(\Illuminate\Support\Facades\Route::has('billing.pakasir.create') ? route('billing.pakasir.create') : url('/billing/pakasir/create'));
|
||||
const billingSuccessUrl = @json(\Illuminate\Support\Facades\Route::has('dashboard.billing') ? route('dashboard.billing', ['status' => 'success']) : url('/dashboard/billing?status=success'));
|
||||
const modal = document.getElementById('qris-modal');
|
||||
const qrTarget = document.getElementById('qris-code');
|
||||
const qrText = document.getElementById('qris-text');
|
||||
@@ -584,7 +588,7 @@
|
||||
pollTimer = setInterval(async () => {
|
||||
if (!modalOpen || !currentOrderId) return;
|
||||
try {
|
||||
const res = await fetch("{{ route('billing.pakasir.status') }}", {
|
||||
const res = await fetch(pakasirStatusUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -596,7 +600,7 @@
|
||||
const data = await res.json().catch(() => null);
|
||||
if (res.ok && data?.paid) {
|
||||
closeModal();
|
||||
window.location.href = "{{ route('dashboard.billing', ['status' => 'success']) }}";
|
||||
window.location.href = billingSuccessUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
// keep polling silently
|
||||
@@ -611,7 +615,7 @@
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await fetch("{{ route('billing.pakasir.cancel') }}", {
|
||||
await fetch(pakasirCancelUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -657,7 +661,7 @@
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generating QR...';
|
||||
try {
|
||||
const res = await fetch("{{ route('billing.pakasir.create') }}", {
|
||||
const res = await fetch(pakasirCreateUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user