Add mobile favorites UX and Android deep link support

This commit is contained in:
Dwindi Ramadhana
2026-02-22 23:32:48 +07:00
parent d5d925d534
commit 32e9282349
10 changed files with 594 additions and 41 deletions

View File

@@ -111,8 +111,12 @@
<div class="glass-card rounded-[32px] aspect-square flex items-center justify-center relative overflow-hidden group">
<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 id="emoji-hero-symbol" 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="copyCurrentEmoji()" 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">
<div class="absolute bottom-6 flex gap-3 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all transform translate-y-0 sm:translate-y-2 sm:group-hover:translate-y-0">
<button id="favorite-toggle-btn" type="button" class="w-12 h-12 bg-black/50 backdrop-blur text-white force-white rounded-full hover:bg-yellow-400/20 transition-colors border border-white/10 inline-flex items-center justify-center shrink-0" title="Add Favorite" aria-label="Add Favorite">
<span id="favorite-toggle-icon" class="text-yellow-300 text-lg leading-none"></span>
<span id="favorite-toggle-label" class="sr-only">Add Favorite</span>
</button>
<button onclick="copyCurrentEmoji()" class="w-12 h-12 bg-black/50 backdrop-blur text-white force-white rounded-full hover:bg-brand-sun hover:text-black transition-colors border border-white/10 inline-flex items-center justify-center shrink-0" title="Copy">
<i data-lucide="copy" class="w-5 h-5 force-white"></i>
</button>
</div>
@@ -150,18 +154,12 @@
<div>
<div class="flex items-center gap-3 mb-2">
<span class="bg-yellow-500/10 text-yellow-400 border border-yellow-500/20 px-3 py-1 rounded-full text-xs font-bold uppercase">{{ $subcategory }}</span>
<span id="favorite-pill" class="hidden bg-yellow-500/10 text-yellow-300 border border-yellow-500/30 px-3 py-1 rounded-full text-xs font-bold uppercase">Favorite</span>
</div>
<h1 class="font-display text-5xl font-bold mb-4">{{ $name }}</h1>
<p class="text-gray-400 text-lg leading-relaxed">{{ $description }}</p>
</div>
<div class="flex gap-4">
<button id="copy-current-emoji-btn" onclick="copyCurrentEmoji()" 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>
@if($supportsTone)
<div class="glass-card rounded-xl p-3">
<div class="text-xs font-mono text-gray-500 mb-2">Skin tone</div>
@@ -287,7 +285,7 @@
</div>
<div id="toast" class="fixed bottom-10 left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50 pointer-events-none">
<div id="toast" class="fixed left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50 pointer-events-none" style="bottom: calc(env(safe-area-inset-bottom, 0px) + 6rem);">
<div class="bg-brand-ocean text-white px-6 py-2 rounded-full font-bold shadow-[0_0_20px_rgba(32,83,255,0.45)] flex items-center gap-2">
<i data-lucide="check" class="w-4 h-4"></i>
<span id="toast-msg">Copied!</span>
@@ -331,8 +329,15 @@
@push('scripts')
<script>
const RECENT_KEY = 'dewemoji_recent';
const FAVORITES_KEY = 'dewemoji_favorites';
const TONE_STORAGE_KEY = 'dewemoji_skin_tone';
const SYMBOL_DEFAULT = @json($symbol);
const DETAIL_SLUG = @json($slug);
const DETAIL_NAME = @json($name);
const DETAIL_CATEGORY = @json($category);
const DETAIL_SUBCATEGORY = @json($subcategory);
const DETAIL_SUPPORTS_TONE = @json($supportsTone);
const DETAIL_VARIANTS_LIST = @json(array_values($toneVariants));
const TONE_VARIANTS = @json($toneVariants);
let currentDisplayEmoji = SYMBOL_DEFAULT;
@@ -363,22 +368,124 @@ function applyTone(tone) {
function loadRecent() {
try {
return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]');
const parsed = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]');
return Array.isArray(parsed) ? parsed.filter(isRecentEmojiToken) : [];
} catch {
return [];
}
}
function isRecentEmojiToken(value) {
const s = String(value || '').trim();
if (!s) return false;
if (s.length > 24) return false;
if (/[:;&<#\\]/.test(s)) return false;
try {
return /\p{Extended_Pictographic}/u.test(s);
} catch {
return /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(s);
}
}
function saveRecent(items) {
localStorage.setItem(RECENT_KEY, JSON.stringify(items.slice(0, 8)));
const clean = items.filter(isRecentEmojiToken);
localStorage.setItem(RECENT_KEY, JSON.stringify(clean.slice(0, 8)));
}
function addRecent(emoji) {
if (!isRecentEmojiToken(emoji)) return;
const curr = loadRecent().filter((e) => e !== emoji);
curr.unshift(emoji);
saveRecent(curr);
}
function loadFavorites() {
try {
const parsed = JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]');
if (!Array.isArray(parsed)) return [];
return parsed
.filter((item) => item && typeof item === 'object')
.map((item) => ({
slug: String(item.slug || '').trim(),
emoji: String(item.emoji || '').trim(),
name: String(item.name || '').trim(),
category: String(item.category || '').trim(),
subcategory: String(item.subcategory || '').trim(),
supports_skin_tone: Boolean(item.supports_skin_tone),
variants: Array.isArray(item.variants) ? item.variants : [],
}))
.filter((item) => item.slug !== '' && isRecentEmojiToken(item.emoji));
} catch {
return [];
}
}
function saveFavorites(items) {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(items.slice(0, 24)));
}
function isCurrentFavorite() {
return loadFavorites().some((item) => item.slug === DETAIL_SLUG);
}
function renderFavoriteButton() {
const btn = document.getElementById('favorite-toggle-btn');
const icon = document.getElementById('favorite-toggle-icon');
const label = document.getElementById('favorite-toggle-label');
const pill = document.getElementById('favorite-pill');
if (!btn || !icon || !label) return;
const active = isCurrentFavorite();
icon.textContent = active ? '★' : '☆';
icon.classList.toggle('text-yellow-300', true);
icon.classList.toggle('text-gray-300', !active);
label.textContent = active ? 'Favorited' : 'Add Favorite';
btn.title = active ? 'Remove Favorite' : 'Add Favorite';
btn.setAttribute('aria-label', active ? 'Remove Favorite' : 'Add Favorite');
btn.classList.toggle('border-yellow-400/30', active);
btn.classList.toggle('bg-yellow-400/10', active);
if (pill) {
pill.classList.toggle('hidden', !active);
}
}
function toggleCurrentFavorite() {
const current = loadFavorites();
const remaining = current.filter((item) => item.slug !== DETAIL_SLUG);
const isRemoving = remaining.length !== current.length;
if (isRemoving) {
saveFavorites(remaining);
renderFavoriteButton();
if (typeof showToast === 'function') showToast('Removed from favorites');
else showDetailToast('Removed from favorites');
return false;
}
remaining.unshift({
slug: DETAIL_SLUG,
emoji: currentDisplayEmoji,
name: DETAIL_NAME,
category: DETAIL_CATEGORY,
subcategory: DETAIL_SUBCATEGORY,
supports_skin_tone: Boolean(DETAIL_SUPPORTS_TONE),
variants: Array.isArray(DETAIL_VARIANTS_LIST) ? DETAIL_VARIANTS_LIST : [],
});
saveFavorites(remaining);
renderFavoriteButton();
if (typeof showToast === 'function') showToast('Added to favorites');
else showDetailToast('Added to favorites');
return true;
}
function showDetailToast(message) {
const toast = document.getElementById('toast');
const msg = document.getElementById('toast-msg');
if (!toast || !msg) return;
msg.innerText = message;
toast.classList.remove('translate-y-24', 'opacity-0');
setTimeout(() => {
toast.classList.add('translate-y-24', 'opacity-0');
}, 1500);
}
function copyCurrentEmoji() {
copyToClipboard(currentDisplayEmoji);
}
@@ -386,13 +493,7 @@ function copyCurrentEmoji() {
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
addRecent(text);
const toast = document.getElementById('toast');
const msg = document.getElementById('toast-msg');
msg.innerText = `Copied ${text}`;
toast.classList.remove('translate-y-24', 'opacity-0');
setTimeout(() => {
toast.classList.add('translate-y-24', 'opacity-0');
}, 1500);
showDetailToast(`Copied ${text}`);
});
}
@@ -414,6 +515,9 @@ document.addEventListener('keydown', (e) => {
});
})();
document.getElementById('favorite-toggle-btn')?.addEventListener('click', toggleCurrentFavorite);
renderFavoriteButton();
// Treat opening the single-emoji page as a "recently viewed emoji" event.
addRecent(currentDisplayEmoji);