Add mobile favorites UX and Android deep link support
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user