first upload
This commit is contained in:
90
index.html
Normal file
90
index.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Emoji Glossary</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
}
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
if (localStorage.getItem('darkMode') === 'true' || (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="flex flex-col h-full bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
|
||||
<header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="relative">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Emoji Glossary</h1>
|
||||
<button id="dark-mode-toggle" class="absolute top-1/2 right-0 -translate-y-1/2 p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 5.05A1 1 0 003.636 6.464l.707.707a1 1 0 001.414-1.414l-.707-.707zM3 11a1 1 0 100-2H2a1 1 0 100 2h1zM13.536 14.95a1 1 0 011.414 0l.707.707a1 1 0 01-1.414 1.414l-.707-.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 max-w-md mx-auto">
|
||||
<input type="text" id="search-input" placeholder="Search for an emoji..." class="w-full px-4 py-2 border border-gray-300 rounded-full shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-grow w-full max-w-6xl mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<div id="emoji-grid" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-4">
|
||||
<!-- Emojis will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div id="load-more-container" class="text-center mt-8">
|
||||
<button id="load-more-btn" class="hidden px-6 py-2 bg-blue-500 text-white font-semibold rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition">
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-6">
|
||||
<p class="text-center text-sm text-gray-500">Created with ❤️ by Cascade</p>
|
||||
</footer>
|
||||
|
||||
<div id="emoji-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black bg-opacity-50 transition-opacity duration-300">
|
||||
<div id="modal-content" class="relative w-full max-w-md p-6 mx-4 bg-white rounded-lg shadow-xl transform transition-all duration-300 scale-95 opacity-0 dark:bg-gray-800">
|
||||
<button id="modal-close-btn" class="absolute top-3 right-3 p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
|
||||
<div class="text-center">
|
||||
<div id="modal-emoji" class="text-8xl mb-4"></div>
|
||||
<h2 id="modal-name" class="text-2xl font-bold text-gray-900 dark:text-gray-100"></h2>
|
||||
<p id="modal-category" class="text-sm text-gray-500 dark:text-gray-400 mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Tags</h3>
|
||||
<div id="modal-keywords" class="flex flex-wrap gap-2 mt-2"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button id="modal-copy-btn" class="w-full px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800">
|
||||
Copy Emoji
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
214
script.js
Normal file
214
script.js
Normal file
@@ -0,0 +1,214 @@
|
||||
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 lightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const darkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
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');
|
||||
|
||||
// --- State ---
|
||||
let allEmojis = [];
|
||||
let currentEmojiList = [];
|
||||
let currentPage = 1;
|
||||
const EMOJIS_PER_PAGE = 100;
|
||||
|
||||
// --- Dark Mode Logic ---
|
||||
const applyTheme = (isDark) => {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
lightIcon.classList.toggle('hidden', !isDark);
|
||||
darkIcon.classList.toggle('hidden', isDark);
|
||||
};
|
||||
|
||||
darkModeToggle.addEventListener('click', () => {
|
||||
const isDark = !document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem('darkMode', isDark);
|
||||
applyTheme(isDark);
|
||||
});
|
||||
|
||||
// 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',
|
||||
// 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) throw new Error(`Failed to load ${file}`);
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => { console.error(error); return []; })
|
||||
))
|
||||
.then(allData => {
|
||||
const uniqueEmojis = new Map();
|
||||
const parsedEmojis = allData.flat().flatMap(parseEmojiData);
|
||||
parsedEmojis.forEach(emoji => {
|
||||
if (emoji && emoji.name && !uniqueEmojis.has(emoji.name)) {
|
||||
uniqueEmojis.set(emoji.name, emoji);
|
||||
}
|
||||
});
|
||||
allEmojis = Array.from(uniqueEmojis.values());
|
||||
currentEmojiList = allEmojis;
|
||||
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);
|
||||
});
|
||||
|
||||
// --- Search Logic ---
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase().trim();
|
||||
currentEmojiList = allEmojis.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);
|
||||
return nameMatch || keywordMatch || categoryMatch || subcategoryMatch;
|
||||
});
|
||||
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.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);
|
||||
});
|
||||
}
|
||||
});
|
||||
1919
src/array.json
Normal file
1919
src/array.json
Normal file
File diff suppressed because it is too large
Load Diff
14674
src/categories.json
Normal file
14674
src/categories.json
Normal file
File diff suppressed because it is too large
Load Diff
18276
src/list.json
Normal file
18276
src/list.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user