feat(ui): searchable language picker and light-mode keyword modals
This commit is contained in:
@@ -258,25 +258,26 @@
|
||||
|
||||
<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="relative z-10 w-full max-w-lg rounded-3xl glass-card theme-surface 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">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Add keyword</h3>
|
||||
<button id="user-keyword-close" class="w-8 h-8 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-white/10 dark:hover:bg-white/20 flex items-center justify-center text-slate-600 dark: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>
|
||||
<label class="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-gray-400">Keyword</label>
|
||||
<input type="text" name="keyword" id="user-keyword-input" 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="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">
|
||||
<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'])
|
||||
</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>
|
||||
<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>
|
||||
<button type="submit" class="rounded-full bg-brand-ocean text-white force-white font-semibold px-5 py-2 text-sm">Save keyword</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -337,9 +338,33 @@ 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 list = document.getElementById('user-keyword-list');
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
window.dewemojiPopulateLanguageOptions?.(langDataList);
|
||||
|
||||
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 normalizeLang = (value) => {
|
||||
const v = (value || '').trim();
|
||||
if (!v) return 'und';
|
||||
const byDisplay = langDisplayToCode.get(v.toLowerCase());
|
||||
if (byDisplay) return byDisplay;
|
||||
|
||||
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 openModal = () => {
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
@@ -365,7 +390,7 @@ addRecent(@json($symbol));
|
||||
const payload = {
|
||||
emoji_slug: @json($slug),
|
||||
keyword: keywordInput.value.trim(),
|
||||
lang: langInput.value.trim() || 'und',
|
||||
lang: normalizeLang(langInput.value),
|
||||
};
|
||||
if (!payload.keyword) return;
|
||||
const res = await fetch('{{ route('dashboard.keywords.store') }}', {
|
||||
|
||||
@@ -604,6 +604,71 @@
|
||||
});
|
||||
})();
|
||||
</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;
|
||||
};
|
||||
|
||||
window.dewemojiPopulateLanguageOptions = (target) => {
|
||||
const list = typeof target === 'string' ? document.getElementById(target) : target;
|
||||
if (!list || list.dataset.generated === '1') return;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
if (!codeToName.has('und')) codeToName.set('und', 'Undetermined');
|
||||
|
||||
if (typeof Intl !== 'undefined' && typeof Intl.supportedValuesOf === 'function') {
|
||||
const display = typeof Intl.DisplayNames === 'function'
|
||||
? new Intl.DisplayNames(['en'], { type: 'language' })
|
||||
: null;
|
||||
|
||||
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 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]);
|
||||
});
|
||||
|
||||
list.innerHTML = '';
|
||||
entries.forEach(([code, name]) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${name} (${code})`;
|
||||
option.dataset.code = code;
|
||||
list.appendChild(option);
|
||||
});
|
||||
|
||||
list.dataset.generated = '1';
|
||||
};
|
||||
|
||||
document.querySelectorAll('datalist[data-language-options="1"]').forEach((list) => {
|
||||
window.dewemojiPopulateLanguageOptions(list);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user