426 lines
18 KiB
JavaScript
426 lines
18 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- DOM Elements ---
|
|
const emojiGrid = document.getElementById('emoji-grid');
|
|
const searchInput = document.getElementById('search-input');
|
|
const darkModeToggle = document.getElementById('dark-mode-toggle');
|
|
const darkModeToggleDesktop = document.getElementById('dark-mode-toggle-desktop');
|
|
const lightIcon = document.getElementById('theme-toggle-light-icon');
|
|
const darkIcon = document.getElementById('theme-toggle-dark-icon');
|
|
const lightIconDesktop = document.getElementById('theme-toggle-light-icon-desktop');
|
|
const darkIconDesktop = document.getElementById('theme-toggle-dark-icon-desktop');
|
|
|
|
// Check if essential elements exist
|
|
if (!emojiGrid || !searchInput) {
|
|
console.error('Critical DOM elements missing');
|
|
return;
|
|
}
|
|
const modal = document.getElementById('emoji-modal');
|
|
const modalContent = document.getElementById('modal-content');
|
|
const modalCloseBtn = document.getElementById('modal-close-btn');
|
|
const modalEmoji = document.getElementById('modal-emoji');
|
|
const modalName = document.getElementById('modal-name');
|
|
const modalCategory = document.getElementById('modal-category');
|
|
const modalKeywords = document.getElementById('modal-keywords');
|
|
const modalCopyBtn = document.getElementById('modal-copy-btn');
|
|
const loadMoreBtn = document.getElementById('load-more-btn');
|
|
const categoryButtons = document.querySelectorAll('.category-btn');
|
|
const currentCategoryTitle = document.getElementById('current-category-title');
|
|
const currentCategoryCount = document.getElementById('current-category-count');
|
|
const offcanvasToggle = document.getElementById('offcanvas-toggle');
|
|
const offcanvasOverlay = document.getElementById('offcanvas-overlay');
|
|
const offcanvasClose = document.getElementById('offcanvas-close');
|
|
const offcanvasBackdrop = document.getElementById('offcanvas-backdrop');
|
|
const offcanvasSidebar = document.getElementById('offcanvas-sidebar');
|
|
const offcanvasNav = document.getElementById('offcanvas-nav');
|
|
|
|
// --- State ---
|
|
let allEmojis = [];
|
|
let currentEmojiList = [];
|
|
let currentPage = 1;
|
|
const EMOJIS_PER_PAGE = 100;
|
|
let indonesianKeywords = {};
|
|
let currentCategory = 'all';
|
|
let categorizedEmojis = {};
|
|
|
|
// --- Dark Mode Logic ---
|
|
const applyTheme = (isDark) => {
|
|
document.documentElement.classList.toggle('dark', isDark);
|
|
// Mobile icons
|
|
if (lightIcon && darkIcon) {
|
|
lightIcon.classList.toggle('hidden', !isDark);
|
|
darkIcon.classList.toggle('hidden', isDark);
|
|
}
|
|
// Desktop icons
|
|
if (lightIconDesktop && darkIconDesktop) {
|
|
lightIconDesktop.classList.toggle('hidden', !isDark);
|
|
darkIconDesktop.classList.toggle('hidden', isDark);
|
|
}
|
|
};
|
|
|
|
const toggleDarkMode = () => {
|
|
const isDark = !document.documentElement.classList.contains('dark');
|
|
localStorage.setItem('darkMode', isDark);
|
|
applyTheme(isDark);
|
|
};
|
|
|
|
if (darkModeToggle) darkModeToggle.addEventListener('click', toggleDarkMode);
|
|
if (darkModeToggleDesktop) darkModeToggleDesktop.addEventListener('click', toggleDarkMode);
|
|
|
|
// Set initial icon state on load
|
|
const initialIsDark = document.documentElement.classList.contains('dark');
|
|
applyTheme(initialIsDark);
|
|
|
|
// --- Data Fetching & Processing ---
|
|
const jsonFiles = [
|
|
'array.json',
|
|
'list.json',
|
|
'categories.json',
|
|
'indonesian-keywords.json',
|
|
// Add other files if they should be loaded, keeping the initial set small
|
|
].map(file => `src/${file}`);
|
|
|
|
function parseEmojiData(data) {
|
|
let results = [];
|
|
if (Array.isArray(data)) {
|
|
data.forEach(item => results.push(...parseEmojiData(item)));
|
|
} else if (typeof data === 'object' && data !== null) {
|
|
if (data.hasOwnProperty('emoji') && data.hasOwnProperty('name')) {
|
|
results.push(data);
|
|
}
|
|
Object.values(data).forEach(value => results.push(...parseEmojiData(value)));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
Promise.all(jsonFiles.map(file =>
|
|
fetch(file)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
console.error(`Failed to load ${file}: ${response.status}`);
|
|
throw new Error(`Failed to load ${file}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error loading ${file}:`, error);
|
|
return file.includes('indonesian-keywords') ? {keywords: {}} : [];
|
|
})
|
|
))
|
|
.then(allData => {
|
|
// Extract Indonesian keywords from the last file
|
|
const keywordsData = allData[allData.length - 1];
|
|
if (keywordsData && keywordsData.keywords) {
|
|
indonesianKeywords = keywordsData.keywords;
|
|
}
|
|
|
|
// Process emoji data from other files
|
|
const emojiData = allData.slice(0, -1);
|
|
const uniqueEmojis = new Map();
|
|
const parsedEmojis = emojiData.flat().flatMap(parseEmojiData);
|
|
parsedEmojis.forEach(emoji => {
|
|
if (emoji && emoji.name && !uniqueEmojis.has(emoji.name)) {
|
|
// Add Indonesian keywords to emoji object
|
|
if (indonesianKeywords[emoji.emoji]) {
|
|
emoji.indonesianKeywords = indonesianKeywords[emoji.emoji];
|
|
}
|
|
uniqueEmojis.set(emoji.name, emoji);
|
|
}
|
|
});
|
|
allEmojis = Array.from(uniqueEmojis.values());
|
|
|
|
// Organize emojis by category
|
|
categorizedEmojis = {
|
|
'all': allEmojis,
|
|
'Smileys & Emotion': allEmojis.filter(e => e.category === 'Smileys & Emotion'),
|
|
'People & Body': allEmojis.filter(e => e.category === 'People & Body'),
|
|
'Animals & Nature': allEmojis.filter(e => e.category === 'Animals & Nature'),
|
|
'Food & Drink': allEmojis.filter(e => e.category === 'Food & Drink'),
|
|
'Travel & Places': allEmojis.filter(e => e.category === 'Travel & Places'),
|
|
'Activities': allEmojis.filter(e => e.category === 'Activities'),
|
|
'Objects': allEmojis.filter(e => e.category === 'Objects'),
|
|
'Symbols': allEmojis.filter(e => e.category === 'Symbols'),
|
|
'Flags': allEmojis.filter(e => e.category === 'Flags')
|
|
};
|
|
|
|
currentEmojiList = allEmojis;
|
|
initializeCategoryMenu();
|
|
updateCategoryDisplay();
|
|
displayPage(1);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error processing emoji data:', error);
|
|
emojiGrid.innerHTML = '<p class="text-red-500 col-span-full text-center">Failed to load emoji data.</p>';
|
|
});
|
|
|
|
// --- UI Rendering ---
|
|
function updateLoadMoreButton() {
|
|
const end = currentPage * EMOJIS_PER_PAGE;
|
|
if (currentEmojiList.length > end) {
|
|
loadMoreBtn.classList.remove('hidden');
|
|
} else {
|
|
loadMoreBtn.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function displayPage(page) {
|
|
currentPage = page;
|
|
const start = (page - 1) * EMOJIS_PER_PAGE;
|
|
const end = start + EMOJIS_PER_PAGE;
|
|
const emojisToDisplay = currentEmojiList.slice(start, end);
|
|
|
|
if (page === 1) {
|
|
emojiGrid.innerHTML = ''; // Clear the grid only for the first page
|
|
}
|
|
|
|
emojisToDisplay.forEach(emoji => {
|
|
const emojiCard = document.createElement('div');
|
|
emojiCard.className = 'flex flex-col items-center justify-center p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm cursor-pointer transition-transform duration-200 hover:scale-105 hover:shadow-md';
|
|
emojiCard.innerHTML = `
|
|
<div class="text-4xl">${emoji.emoji}</div>
|
|
<div class="mt-2 text-xs text-center text-gray-600 dark:text-gray-300 font-semibold w-full truncate">${emoji.name}</div>
|
|
`;
|
|
emojiCard.addEventListener('click', () => openModal(emoji));
|
|
emojiGrid.appendChild(emojiCard);
|
|
});
|
|
|
|
// Show 'No emojis found' only if the grid is still empty after rendering
|
|
if (emojiGrid.innerHTML === '') {
|
|
emojiGrid.innerHTML = '<p class="text-gray-500 dark:text-gray-400 col-span-full text-center">No emojis found.</p>';
|
|
}
|
|
|
|
updateLoadMoreButton();
|
|
}
|
|
|
|
loadMoreBtn.addEventListener('click', () => {
|
|
displayPage(currentPage + 1);
|
|
});
|
|
|
|
// --- Category Logic ---
|
|
function initializeCategoryMenu() {
|
|
// Desktop category buttons
|
|
categoryButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const category = btn.dataset.category;
|
|
setActiveCategory(category);
|
|
});
|
|
});
|
|
|
|
// Offcanvas category menu
|
|
const categories = [
|
|
{ key: 'all', name: 'All Emojis', icon: '🌟' },
|
|
{ key: 'Smileys & Emotion', name: 'Smileys & Emotion', icon: '😀' },
|
|
{ key: 'People & Body', name: 'People & Body', icon: '👋' },
|
|
{ key: 'Animals & Nature', name: 'Animals & Nature', icon: '🐶' },
|
|
{ key: 'Food & Drink', name: 'Food & Drink', icon: '🍎' },
|
|
{ key: 'Travel & Places', name: 'Travel & Places', icon: '🌍' },
|
|
{ key: 'Activities', name: 'Activities', icon: '⚽' },
|
|
{ key: 'Objects', name: 'Objects', icon: '💡' },
|
|
{ key: 'Symbols', name: 'Symbols', icon: '❤️' },
|
|
{ key: 'Flags', name: 'Flags', icon: '🏳️' }
|
|
];
|
|
|
|
offcanvasNav.innerHTML = categories.map(cat => `
|
|
<button class="offcanvas-category-btn w-full text-left px-3 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" data-category="${cat.key}">
|
|
<span class="flex items-center">
|
|
<span class="mr-2">${cat.icon}</span>
|
|
${cat.name}
|
|
</span>
|
|
</button>
|
|
`).join('');
|
|
|
|
// Offcanvas category button events
|
|
document.querySelectorAll('.offcanvas-category-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const category = btn.dataset.category;
|
|
setActiveCategory(category);
|
|
closeOffcanvas();
|
|
});
|
|
});
|
|
|
|
// Offcanvas menu controls
|
|
offcanvasToggle.addEventListener('click', openOffcanvas);
|
|
offcanvasClose.addEventListener('click', closeOffcanvas);
|
|
offcanvasBackdrop.addEventListener('click', closeOffcanvas);
|
|
}
|
|
|
|
function setActiveCategory(category) {
|
|
currentCategory = category;
|
|
|
|
// Update active state for desktop buttons
|
|
categoryButtons.forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.category === category);
|
|
if (btn.dataset.category === category) {
|
|
btn.classList.add('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300');
|
|
} else {
|
|
btn.classList.remove('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300');
|
|
}
|
|
});
|
|
|
|
// Update active state for offcanvas buttons
|
|
document.querySelectorAll('.offcanvas-category-btn').forEach(btn => {
|
|
if (btn.dataset.category === category) {
|
|
btn.classList.add('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300');
|
|
} else {
|
|
btn.classList.remove('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300');
|
|
}
|
|
});
|
|
|
|
// Filter emojis by category
|
|
const baseEmojis = categorizedEmojis[category] || [];
|
|
|
|
// Apply search filter if there's a search term
|
|
const searchTerm = searchInput.value.toLowerCase().trim();
|
|
if (searchTerm) {
|
|
currentEmojiList = baseEmojis.filter(emoji => {
|
|
const nameMatch = emoji.name.toLowerCase().includes(searchTerm);
|
|
const keywordMatch = emoji.keywords && Array.isArray(emoji.keywords) && emoji.keywords.some(k => k.toLowerCase().includes(searchTerm));
|
|
const categoryMatch = emoji.category && emoji.category.toLowerCase().includes(searchTerm);
|
|
const subcategoryMatch = emoji.subcategory && emoji.subcategory.toLowerCase().includes(searchTerm);
|
|
|
|
// Indonesian keywords search
|
|
const indonesianMatch = emoji.indonesianKeywords && Array.isArray(emoji.indonesianKeywords) &&
|
|
emoji.indonesianKeywords.some(k => k.toLowerCase().includes(searchTerm));
|
|
|
|
return nameMatch || keywordMatch || categoryMatch || subcategoryMatch || indonesianMatch;
|
|
});
|
|
} else {
|
|
currentEmojiList = baseEmojis;
|
|
}
|
|
|
|
updateCategoryDisplay();
|
|
displayPage(1);
|
|
}
|
|
|
|
function updateCategoryDisplay() {
|
|
const categoryName = currentCategory === 'all' ? 'All Emojis' : currentCategory;
|
|
currentCategoryTitle.textContent = categoryName;
|
|
currentCategoryCount.textContent = `${currentEmojiList.length} emojis`;
|
|
}
|
|
|
|
function openOffcanvas() {
|
|
offcanvasOverlay.classList.remove('hidden');
|
|
setTimeout(() => {
|
|
offcanvasSidebar.classList.remove('-translate-x-full');
|
|
}, 10);
|
|
}
|
|
|
|
function closeOffcanvas() {
|
|
offcanvasSidebar.classList.add('-translate-x-full');
|
|
setTimeout(() => {
|
|
offcanvasOverlay.classList.add('hidden');
|
|
}, 300);
|
|
}
|
|
|
|
// --- Search Logic ---
|
|
searchInput.addEventListener('input', (e) => {
|
|
const searchTerm = e.target.value.toLowerCase().trim();
|
|
const baseEmojis = categorizedEmojis[currentCategory] || [];
|
|
|
|
if (searchTerm) {
|
|
currentEmojiList = baseEmojis.filter(emoji => {
|
|
const nameMatch = emoji.name.toLowerCase().includes(searchTerm);
|
|
const keywordMatch = emoji.keywords && Array.isArray(emoji.keywords) && emoji.keywords.some(k => k.toLowerCase().includes(searchTerm));
|
|
const categoryMatch = emoji.category && emoji.category.toLowerCase().includes(searchTerm);
|
|
const subcategoryMatch = emoji.subcategory && emoji.subcategory.toLowerCase().includes(searchTerm);
|
|
|
|
// Indonesian keywords search
|
|
const indonesianMatch = emoji.indonesianKeywords && Array.isArray(emoji.indonesianKeywords) &&
|
|
emoji.indonesianKeywords.some(k => k.toLowerCase().includes(searchTerm));
|
|
|
|
return nameMatch || keywordMatch || categoryMatch || subcategoryMatch || indonesianMatch;
|
|
});
|
|
} else {
|
|
currentEmojiList = baseEmojis;
|
|
}
|
|
|
|
updateCategoryDisplay();
|
|
displayPage(1);
|
|
});
|
|
|
|
// --- Modal Logic ---
|
|
function openModal(emoji) {
|
|
modalEmoji.textContent = emoji.emoji;
|
|
modalName.textContent = emoji.name;
|
|
modalCategory.textContent = [emoji.category, emoji.subcategory].filter(Boolean).join(' / ');
|
|
modalKeywords.innerHTML = '';
|
|
|
|
const tags = new Set();
|
|
if (emoji.keywords && Array.isArray(emoji.keywords)) {
|
|
emoji.keywords.forEach(k => tags.add(k));
|
|
}
|
|
if (emoji.indonesianKeywords && Array.isArray(emoji.indonesianKeywords)) {
|
|
emoji.indonesianKeywords.forEach(k => tags.add(k));
|
|
}
|
|
if (emoji.category) tags.add(emoji.category);
|
|
if (emoji.subcategory) tags.add(emoji.subcategory);
|
|
|
|
const tagArray = Array.from(tags);
|
|
|
|
if (tagArray.length > 0) {
|
|
tagArray.forEach(tagText => {
|
|
const tag = document.createElement('button');
|
|
tag.className = 'px-2 py-1 bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200 rounded-md text-sm hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500';
|
|
tag.textContent = tagText;
|
|
tag.addEventListener('click', () => {
|
|
searchInput.value = tagText;
|
|
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
closeModal();
|
|
});
|
|
modalKeywords.appendChild(tag);
|
|
});
|
|
} else {
|
|
modalKeywords.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No tags available.</p>';
|
|
}
|
|
|
|
modalCopyBtn.onclick = () => copyToClipboard(emoji.emoji);
|
|
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modalContent.classList.remove('scale-95', 'opacity-0');
|
|
}, 10);
|
|
}
|
|
|
|
function closeModal() {
|
|
modalContent.classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}, 300);
|
|
}
|
|
|
|
modalCloseBtn.addEventListener('click', closeModal);
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// --- Clipboard Logic ---
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const originalText = modalCopyBtn.textContent;
|
|
modalCopyBtn.textContent = 'Copied!';
|
|
setTimeout(() => { modalCopyBtn.textContent = originalText; }, 2000);
|
|
}).catch(err => {
|
|
console.error('Failed to copy: ', err);
|
|
const originalText = modalCopyBtn.textContent;
|
|
modalCopyBtn.textContent = 'Failed!';
|
|
setTimeout(() => { modalCopyBtn.textContent = originalText; }, 2000);
|
|
});
|
|
}
|
|
|
|
// Initialize footer with dynamic year
|
|
function initFooter() {
|
|
const yearElement = document.getElementById('current-year');
|
|
if (yearElement) {
|
|
const currentYear = new Date().getFullYear();
|
|
yearElement.textContent = currentYear;
|
|
}
|
|
}
|
|
|
|
// Initialize footer
|
|
initFooter();
|
|
|
|
});
|