From ce4b6d577ec7e1f6446285cffd2a72daf7b2b722 Mon Sep 17 00:00:00 2001 From: dwindown Date: Fri, 29 Aug 2025 23:47:11 +0700 Subject: [PATCH] first from cpanel commit --- .htaccess | 0 __MACOSX/._dewe-api-2 | 0 __MACOSX/dewe-api-2/._.htaccess | 0 __MACOSX/dewe-api-2/._api | 0 __MACOSX/dewe-api-2/._api-docs.html | 0 __MACOSX/dewe-api-2/._assets | 0 __MACOSX/dewe-api-2/._data | 0 __MACOSX/dewe-api-2/._ext | 0 __MACOSX/dewe-api-2/._helpers | 0 __MACOSX/dewe-api-2/._index.html | 0 __MACOSX/dewe-api-2/._package.json | 0 __MACOSX/dewe-api-2/._public | 0 __MACOSX/dewe-api-2/._tools | 0 __MACOSX/dewe-api-2/api/._emoji.php | 0 __MACOSX/dewe-api-2/api/._emojis.php | 0 __MACOSX/dewe-api-2/api/._index.php | 0 __MACOSX/dewe-api-2/assets/._script.js | 0 __MACOSX/dewe-api-2/data/._array.json | 0 __MACOSX/dewe-api-2/data/._categories.json | 0 __MACOSX/dewe-api-2/data/._emojis.json | 0 __MACOSX/dewe-api-2/data/._keywords-id.json | 0 __MACOSX/dewe-api-2/data/._list.json | 0 __MACOSX/dewe-api-2/data/._overrides.json | 0 __MACOSX/dewe-api-2/ext/._updates.xml | 0 __MACOSX/dewe-api-2/helpers/._auth.php | 0 __MACOSX/dewe-api-2/helpers/._filters.php | 0 __MACOSX/dewe-api-2/public/._js | 0 __MACOSX/dewe-api-2/public/js/._api-client.js | 0 __MACOSX/dewe-api-2/tools/._build-emojis.js | 0 api-docs.html | 0 api/emoji.php | 82 ++ api/emojis.php | 231 +++++ api/error_log | 0 api/index.php | 0 assets/script.js | 848 ++++++++++++++++++ backup-old-site.zip | 0 data/array.json | 0 data/categories.json | 0 data/emojis.json | 0 data/keywords-id.json | 0 data/list.json | 0 data/overrides.json | 0 db.php | 48 + dewe-api-2.zip | 0 emoji.php | 0 ext/updates.xml | 0 helpers/auth.php | 0 helpers/filters.php | 0 index.html | 285 ++++++ public/js/api-client.js | 0 robots.txt | 0 sitemap.xml.php | 0 52 files changed, 1494 insertions(+) create mode 100644 .htaccess create mode 100644 __MACOSX/._dewe-api-2 create mode 100644 __MACOSX/dewe-api-2/._.htaccess create mode 100644 __MACOSX/dewe-api-2/._api create mode 100644 __MACOSX/dewe-api-2/._api-docs.html create mode 100644 __MACOSX/dewe-api-2/._assets create mode 100644 __MACOSX/dewe-api-2/._data create mode 100644 __MACOSX/dewe-api-2/._ext create mode 100644 __MACOSX/dewe-api-2/._helpers create mode 100644 __MACOSX/dewe-api-2/._index.html create mode 100644 __MACOSX/dewe-api-2/._package.json create mode 100644 __MACOSX/dewe-api-2/._public create mode 100644 __MACOSX/dewe-api-2/._tools create mode 100644 __MACOSX/dewe-api-2/api/._emoji.php create mode 100644 __MACOSX/dewe-api-2/api/._emojis.php create mode 100644 __MACOSX/dewe-api-2/api/._index.php create mode 100644 __MACOSX/dewe-api-2/assets/._script.js create mode 100644 __MACOSX/dewe-api-2/data/._array.json create mode 100644 __MACOSX/dewe-api-2/data/._categories.json create mode 100644 __MACOSX/dewe-api-2/data/._emojis.json create mode 100644 __MACOSX/dewe-api-2/data/._keywords-id.json create mode 100644 __MACOSX/dewe-api-2/data/._list.json create mode 100644 __MACOSX/dewe-api-2/data/._overrides.json create mode 100644 __MACOSX/dewe-api-2/ext/._updates.xml create mode 100644 __MACOSX/dewe-api-2/helpers/._auth.php create mode 100644 __MACOSX/dewe-api-2/helpers/._filters.php create mode 100644 __MACOSX/dewe-api-2/public/._js create mode 100644 __MACOSX/dewe-api-2/public/js/._api-client.js create mode 100644 __MACOSX/dewe-api-2/tools/._build-emojis.js create mode 100644 api-docs.html create mode 100644 api/emoji.php create mode 100644 api/emojis.php create mode 100644 api/error_log create mode 100644 api/index.php create mode 100644 assets/script.js create mode 100644 backup-old-site.zip create mode 100644 data/array.json create mode 100644 data/categories.json create mode 100644 data/emojis.json create mode 100644 data/keywords-id.json create mode 100644 data/list.json create mode 100644 data/overrides.json create mode 100644 db.php create mode 100644 dewe-api-2.zip create mode 100644 emoji.php create mode 100644 ext/updates.xml create mode 100644 helpers/auth.php create mode 100644 helpers/filters.php create mode 100644 index.html create mode 100644 public/js/api-client.js create mode 100644 robots.txt create mode 100644 sitemap.xml.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/._dewe-api-2 b/__MACOSX/._dewe-api-2 new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._.htaccess b/__MACOSX/dewe-api-2/._.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._api b/__MACOSX/dewe-api-2/._api new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._api-docs.html b/__MACOSX/dewe-api-2/._api-docs.html new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._assets b/__MACOSX/dewe-api-2/._assets new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._data b/__MACOSX/dewe-api-2/._data new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._ext b/__MACOSX/dewe-api-2/._ext new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._helpers b/__MACOSX/dewe-api-2/._helpers new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._index.html b/__MACOSX/dewe-api-2/._index.html new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._package.json b/__MACOSX/dewe-api-2/._package.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._public b/__MACOSX/dewe-api-2/._public new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/._tools b/__MACOSX/dewe-api-2/._tools new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/api/._emoji.php b/__MACOSX/dewe-api-2/api/._emoji.php new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/api/._emojis.php b/__MACOSX/dewe-api-2/api/._emojis.php new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/api/._index.php b/__MACOSX/dewe-api-2/api/._index.php new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/assets/._script.js b/__MACOSX/dewe-api-2/assets/._script.js new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._array.json b/__MACOSX/dewe-api-2/data/._array.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._categories.json b/__MACOSX/dewe-api-2/data/._categories.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._emojis.json b/__MACOSX/dewe-api-2/data/._emojis.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._keywords-id.json b/__MACOSX/dewe-api-2/data/._keywords-id.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._list.json b/__MACOSX/dewe-api-2/data/._list.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/data/._overrides.json b/__MACOSX/dewe-api-2/data/._overrides.json new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/ext/._updates.xml b/__MACOSX/dewe-api-2/ext/._updates.xml new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/helpers/._auth.php b/__MACOSX/dewe-api-2/helpers/._auth.php new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/helpers/._filters.php b/__MACOSX/dewe-api-2/helpers/._filters.php new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/public/._js b/__MACOSX/dewe-api-2/public/._js new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/public/js/._api-client.js b/__MACOSX/dewe-api-2/public/js/._api-client.js new file mode 100644 index 0000000..e69de29 diff --git a/__MACOSX/dewe-api-2/tools/._build-emojis.js b/__MACOSX/dewe-api-2/tools/._build-emojis.js new file mode 100644 index 0000000..e69de29 diff --git a/api-docs.html b/api-docs.html new file mode 100644 index 0000000..e69de29 diff --git a/api/emoji.php b/api/emoji.php new file mode 100644 index 0000000..a720269 --- /dev/null +++ b/api/emoji.php @@ -0,0 +1,82 @@ + $emojiBase.$m, $mods); + } +} + +header('Content-Type: application/json; charset=utf-8'); +$tier = detectTier(); +$slug = trim($_GET['slug'] ?? ''); + +$data = json_decode(file_get_contents(__DIR__.'/../data/emojis.json'), true); +$found = null; +foreach ($data['emojis'] as $e) { + if (($e['slug'] ?? '') === $slug) { $found = $e; break; } +} +if (!$found) { http_response_code(404); echo json_encode(['error'=>'not_found']); exit; } + +$out = filterEmoji($found, $tier, false); +// Add tone support & modifiers for single emoji response +$out['supports_skin_tone'] = dw_supports_skin_tone($found) || dw_supports_skin_tone($out); +if (!isset($out['skin_tone_modifiers'])) { + $out['skin_tone_modifiers'] = dw_skin_tone_modifiers(); +} +if (!empty($out['supports_skin_tone']) && !empty($out['emoji'])) { + $base = dw_strip_tone($out['emoji']); + if ($base !== '') { + $out['emoji_base'] = $base; + $out['variants'] = dw_build_tone_variants($base); + } +} + +$etag = '"'.sha1(json_encode([$slug,$tier,$out])).'"'; +header('ETag: '.$etag); +header('Cache-Control: public, max-age=300'); +if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { http_response_code(304); exit; } + +echo json_encode($out, JSON_UNESCAPED_UNICODE); \ No newline at end of file diff --git a/api/emojis.php b/api/emojis.php new file mode 100644 index 0000000..2c4f2cf --- /dev/null +++ b/api/emojis.php @@ -0,0 +1,231 @@ + $emojiBase.$m, $mods); + } +} + +header('Content-Type: application/json; charset=utf-8'); +$tier = detectTier(); +// Allow client hint via header (extension may send X-Dewemoji-Plan: pro|free) +$hint = strtolower(trim($_SERVER['HTTP_X_DEWEMOJI_PLAN'] ?? '')); +// Optional license verify (server-side spot check) +$acct = $_SERVER['HTTP_X_ACCOUNT_ID'] ?? ''; +$key = $_SERVER['HTTP_X_LICENSE_KEY'] ?? ''; +if ($acct && $key) { + // probabilistic verify, e.g., 1 in 50 requests + if (mt_rand(1,50) === 1) { + $verify = @file_get_contents('https://emojilicense.dewe.pw/v1/license/verify', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => json_encode(['key'=>$key,'account_id'=>$acct]), + 'timeout' => 3, + ] + ])); + if ($verify) { + $v = json_decode($verify,true); + if (!empty($v['ok']) && !empty($v['plan'])) { + $tier = strtolower($v['plan']); + $isPro = ($tier==='pro'); + } + } + } +} +if ($hint === 'pro' || $hint === 'free') { + $tier = $hint; +} + +$isPro = ($tier === 'pro'); + +$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; +$host = $_SERVER['HTTP_HOST'] ?? ''; + +$whitelist = ['emoji.dewe.pw']; + +$isWhitelisted = in_array($host, $whitelist, true) + || in_array(parse_url($origin, PHP_URL_HOST) ?: '', $whitelist, true); + +// --- Free vs Pro gating --- +$maxLimit = $isPro ? 50 : 20; +$maxPages = $isPro ? 20 : 5; + +if ($isWhitelisted) { + $maxLimit = 100; + $maxPages = 1000; +} + +// --- Helpers for slug <-> label normalization --- +$CATEGORY_MAP = [ + 'all' => 'all', + 'smileys' => 'Smileys & Emotion', + 'people' => 'People & Body', + 'animals' => 'Animals & Nature', + 'food' => 'Food & Drink', + 'travel' => 'Travel & Places', + 'activities'=> 'Activities', + 'objects' => 'Objects', + 'symbols' => 'Symbols', + 'flags' => 'Flags', +]; +function dw_slugify($s){ + $s = strtolower(trim((string)$s)); + $s = str_replace('&', 'and', $s); + $s = preg_replace('/[^a-z0-9]+/','-',$s); + return trim($s,'-'); +} + +// Parse incoming filters +$qParam = trim((string)($_GET['q'] ?? '')); +$catParam = trim((string)($_GET['category'] ?? '')); +$subParam = trim((string)($_GET['subcategory'] ?? '')); + +// Normalize category: accept either slug ("people") or label ("People & Body") +$catLabel = ''; +if ($catParam !== '' && strtolower($catParam) !== 'all') { + $catLabel = $CATEGORY_MAP[strtolower($catParam)] ?? $catParam; // fallback to raw label +} + +// Normalize subcategory to a slug for comparison +$subSlug = $subParam !== '' ? dw_slugify($subParam) : ''; + +// Expose normalized vars for filter closure +$q = $qParam; +$cat = $catLabel; // label form (or '') +$sub = $subSlug; // slug form (or '') + +// Parse query params with caps +$limit = min(max((int)($_GET['limit'] ?? $maxLimit), 1), $maxLimit); +$page = max((int)($_GET['page'] ?? 1), 1); +if ($page > $maxPages) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'items' => [], + 'page' => $page, + 'limit' => $limit, + 'total' => 0, + 'plan' => $isPro ? 'pro' : 'free', + 'message' => 'Page limit reached for your plan.' + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// Load data +$data = json_decode(file_get_contents(__DIR__.'/../data/emojis.json'), true); +$items = $data['emojis'] ?? []; + +// Server-side filtering (case-insensitive) +$items = array_values(array_filter($items, function($e) use ($q,$cat,$sub) { + // Category match: compare by slug to be robust + if ($cat !== '') { + $itemCatSlug = dw_slugify($e['category'] ?? ''); + $wantCatSlug = dw_slugify($cat); + if ($itemCatSlug !== $wantCatSlug) return false; + } + // Subcategory match: normalize item subcategory to slug and compare + if ($sub !== '') { + $itemSubSlug = dw_slugify($e['subcategory'] ?? ''); + if ($itemSubSlug !== $sub) return false; + } + // Text search across common fields + if ($q !== '') { + $hay = strtolower(join(' ', [ + $e['emoji'] ?? '', + $e['name'] ?? '', + $e['slug'] ?? '', + $e['category'] ?? '', + $e['subcategory'] ?? '', + join(' ', $e['keywords_en'] ?? []), + join(' ', $e['keywords_id'] ?? []), + join(' ', $e['aliases'] ?? []), + join(' ', $e['shortcodes'] ?? []), + ])); + foreach (preg_split('/\s+/', strtolower($q)) as $tok) { + if ($tok === '') continue; + if (strpos($hay, $tok) === false) return false; + } + } + return true; +})); + +$total = count($items); +$offset = ($page - 1) * $limit; +$items = array_slice($items, $offset, $limit); + +// Field filtering + augmentation +$items = array_map(function($raw) use ($tier, $isPro) { + $e = filterEmoji($raw, $tier, true); + // add tone support & modifiers for clients + $e['supports_skin_tone'] = dw_supports_skin_tone($raw) || dw_supports_skin_tone($e); + if (!isset($e['skin_tone_modifiers'])) { + $e['skin_tone_modifiers'] = dw_skin_tone_modifiers(); + } + if ($e['supports_skin_tone'] && !empty($e['emoji'])) { + $base = dw_strip_tone($e['emoji']); + if ($base !== '') { + $e['emoji_base'] = $base; + if ($isPro) { + $e['variants'] = dw_build_tone_variants($base); + } + } + } + return $e; +}, $items); + +// ETag for caching +$etag = '"'.sha1(json_encode([$page,$limit,$q,$cat,$sub,$tier,$total,$items])).'"'; +header('ETag: '.$etag); +header('Cache-Control: public, max-age=300'); +if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { http_response_code(304); exit; } + +echo json_encode([ + 'items' => $items, + 'page' => $page, + 'limit' => $limit, + 'total' => $total, + 'plan' => $isPro ? 'pro' : 'free', +], JSON_UNESCAPED_UNICODE); \ No newline at end of file diff --git a/api/error_log b/api/error_log new file mode 100644 index 0000000..e69de29 diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..e69de29 diff --git a/assets/script.js b/assets/script.js new file mode 100644 index 0000000..534b560 --- /dev/null +++ b/assets/script.js @@ -0,0 +1,848 @@ +document.addEventListener('DOMContentLoaded', () => { + // --- DOM Elements --- + const emojiGrid = document.getElementById('emoji-grid'); + // Support multiple search inputs (mobile + desktop); class-based selector (no dot in getElementsByClassName) + const searchInputs = Array.from(document.querySelectorAll('.search-input')); + // Dark mode toggles & icons (support multiple instances) + const themeToggles = Array.from(document.querySelectorAll('.theme-toggle')); + const lightIcons = Array.from(document.querySelectorAll('.theme-icon-light')); + const darkIcons = Array.from(document.querySelectorAll('.theme-icon-dark')); + + // Check if essential elements exist + if (!emojiGrid || searchInputs.length === 0) { + 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'); + + // --- Loading state (spinner over grid) --- + const gridWrap = emojiGrid.parentElement; + let spinnerEl = null; + function showSpinner() { + if (spinnerEl) return; + // Hide Load More while loading to avoid redundancy + if (loadMoreBtn) loadMoreBtn.classList.add('hidden'); + spinnerEl = document.createElement('div'); + spinnerEl.id = 'grid-spinner'; + spinnerEl.className = 'w-full flex justify-center py-6'; + spinnerEl.innerHTML = ` +
+ `; + gridWrap.appendChild(spinnerEl); + } + function hideSpinner() { + if (spinnerEl) { spinnerEl.remove(); spinnerEl = null; } + // Recompute Load More visibility after loading completes + if (typeof updateLoadMoreButton === 'function') { + try { updateLoadMoreButton(); } catch(_) {} + } else if (loadMoreBtn) { + // Fallback: show button; displayPage() will hide it if needed + loadMoreBtn.classList.remove('hidden'); + } + } + + // --- Skin tone support (Fitzpatrick modifiers) --- + const SKIN_TONES = [ + { key: '1f3fb', ch: '\u{1F3FB}', label: 'Light' }, + { key: '1f3fc', ch: '\u{1F3FC}', label: 'Medium-Light' }, + { key: '1f3fd', ch: '\u{1F3FD}', label: 'Medium' }, + { key: '1f3fe', ch: '\u{1F3FE}', label: 'Medium-Dark' }, + { key: '1f3ff', ch: '\u{1F3FF}', label: 'Dark' }, + ]; + // Preferred tone helpers + const TONE_SLUGS = ['light','medium-light','medium','medium-dark','dark']; + function getPreferredToneIndex(){ + const slug = localStorage.getItem('preferredSkinTone'); + const i = TONE_SLUGS.indexOf(slug); + return i >= 0 ? i : -1; + } + function setPreferredToneIndex(i){ + if (i>=0 && i { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'w-6 h-6 rounded-md flex items-center justify-center hover:ring-2 hover:ring-blue-500'; + b.textContent = t.ch; + b.title = t.label; + b.addEventListener('click', (e) => { + e.stopPropagation(); + onPick(t); + }); + wrap.appendChild(b); + }); + return wrap; + } + + // ---- Pretty URL helpers (category & subcategory slugs) + const CATEGORY_TO_SLUG = { + 'all': 'all', + 'Smileys & Emotion': 'smileys', + 'People & Body': 'people', + 'Animals & Nature': 'animals', + 'Food & Drink': 'food', + 'Travel & Places': 'travel', + 'Activities': 'activities', + 'Objects': 'objects', + 'Symbols': 'symbols', + 'Flags': 'flags' + }; + const SLUG_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_SLUG).map(([k,v]) => [v,k])); + + // Subcategory is best-effort slug (lowercase, hyphen). Most of your subs already match this. + const subcatToSlug = (s='') => s.toLowerCase() + .replace(/&/g, 'and') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + // For API use, keep hyphenated slug as canonical + const slugToSubcat = (s='') => s; // keep hyphenated for API + + // --- State --- + let allEmojis = []; + let currentEmojiList = []; + let currentPage = 1; + const EMOJIS_PER_PAGE = 50; + let indonesianKeywords = {}; + let currentCategory = 'all'; + let categorizedEmojis = {}; + + // --- Search helpers for multi-input support (mobile + desktop) --- + function getSearchValue() { + const el = searchInputs.find(n => n && typeof n.value === 'string'); + return (el?.value || '').toLowerCase().trim(); + } + function setSearchValue(val) { + searchInputs.forEach(n => { if (n && typeof n.value === 'string') n.value = val; }); + } + function focusFirstSearch() { + if (searchInputs[0]) searchInputs[0].focus(); + updateURLFromFilters(); + } + function wireSearchListeners() { + searchInputs.forEach(el => { + el.addEventListener('input', (e) => { + const v = (e.target.value || '').toLowerCase().trim(); + // keep other inputs in sync visually + searchInputs.forEach(other => { if (other !== e.target) other.value = e.target.value; }); + currentFilters.q = v; + updateURLFromFilters(); + resetAndLoadFirstPage(); + }); + }); + } + + // --- Dark Mode Logic --- + const applyTheme = (isDark) => { + document.documentElement.classList.toggle('dark', isDark); + lightIcons.forEach(el => el.classList.toggle('hidden', !isDark)); + darkIcons.forEach(el => el.classList.toggle('hidden', isDark)); + }; + + const toggleDarkMode = () => { + const isDark = !document.documentElement.classList.contains('dark'); + localStorage.setItem('darkMode', isDark); + applyTheme(isDark); + }; + + themeToggles.forEach(btn => btn.addEventListener('click', toggleDarkMode)); + + // Set initial icon state on load + const initialIsDark = document.documentElement.classList.contains('dark'); + applyTheme(initialIsDark); + wireSearchListeners(); + + // --- Data Fetching & Processing --- + // Switch from loading local JSON files to calling the server API. + // We keep UI logic intact (pagination buttons etc.), only the data source changes. + + // Server pagination total for current filter + let totalAvailable = 0; + + // Current filters used when requesting data from API + const currentFilters = { + q: '', + category: 'all', + subcategory: '' + }; + + // Helper: build query string and fetch a page from the API + async function fetchEmojisFromAPI({ q = '', category = 'all', subcategory = '', page = 1, limit = EMOJIS_PER_PAGE } = {}) { + const params = new URLSearchParams(); + if (q) params.set('q', q); + + // normalize category to slug + const catSlug = CATEGORY_TO_SLUG[category] || category; + if (catSlug && catSlug !== 'all') params.set('category', catSlug); + + if (subcategory) params.set('subcategory', subcategory); + params.set('page', String(page)); + params.set('limit', String(limit)); + + console.debug('[API] /api/emojis?' + params.toString()); + const res = await fetch('/api/emojis?' + params.toString(), { cache: 'no-store' }); + if (!res.ok) throw new Error('Failed to load emojis from API'); + return res.json(); + } + + // Helper: load first page for current filters (reset list) + async function resetAndLoadFirstPage() { + try { + // Clear grid immediately to reflect new scope + emojiGrid.innerHTML = ''; + showSpinner(); + const { total, items, plan } = await fetchEmojisFromAPI({ + q: currentFilters.q, + category: currentFilters.category, + subcategory: currentFilters.subcategory, + page: 1, + limit: EMOJIS_PER_PAGE + }); + console.debug('[API] first page loaded:', { total, count: items?.length, plan }); + + let usedScope = { ...currentFilters }; + let data = { total, items }; + + if (data.total === 0 && (usedScope.category !== 'all' || usedScope.subcategory)) { + const retry = await fetchEmojisFromAPI({ + q: usedScope.q, + category: 'all', + subcategory: '', + page: 1, + limit: EMOJIS_PER_PAGE + }); + if (retry.total > 0) { + showHint(`No matches in "${usedScope.category}${usedScope.subcategory ? ' › ' + usedScope.subcategory : ''}". Showing results across All categories.`); + data = retry; + usedScope.category = 'all'; + usedScope.subcategory = ''; + } else { + clearHint(); + } + } else { + clearHint(); + } + + totalAvailable = data.total || 0; + allEmojis = Array.isArray(data.items) ? data.items.slice() : []; + currentEmojiList = allEmojis.slice(); + currentPage = 1; + + updateCategoryDisplay(); + renderActiveFilters(); + displayPage(1); + } catch (error) { + console.error('Error loading first page from API:', error); + emojiGrid.innerHTML = '

Failed to load emoji data.

'; + } finally { + hideSpinner(); + } + reflectScopeInPlaceholders(); + } + + // Helper: append next page (used by Load More) + async function appendNextPage() { + const nextPage = currentPage + 1; + try { + showSpinner(); + const { items } = await fetchEmojisFromAPI({ + q: currentFilters.q, + category: currentFilters.category, + subcategory: currentFilters.subcategory, + page: nextPage, + limit: EMOJIS_PER_PAGE + }); + // Accumulate and mirror into currentEmojiList for UI rendering + allEmojis = allEmojis.concat(items); + currentEmojiList = allEmojis.slice(); + displayPage(nextPage); + console.debug('[API] appended page', nextPage, 'count:', items?.length); + } catch (error) { + console.error('Error loading next page from API:', error); + } finally { + hideSpinner(); + } + } + + function ensureHint() { + let el = document.getElementById('search-hint'); + if (!el) { + el = document.createElement('div'); + el.id = 'search-hint'; + el.className = 'mb-3 text-sm px-3 py-2 rounded-md bg-amber-100 text-amber-900 dark:bg-amber-900 dark:text-amber-100'; + emojiGrid.parentElement.insertBefore(el, emojiGrid); + } + return el; + } + function showHint(msg) { const el = ensureHint(); el.textContent = msg; el.classList.remove('hidden'); } + function clearHint() { const el = document.getElementById('search-hint'); if (el) el.classList.add('hidden'); + } + + // Build a pretty path for a given category/subcategory + function buildPrettyPathFor(category = 'all', subcategory = '') { + const catSlug = CATEGORY_TO_SLUG[category] || 'all'; + const segs = []; + if (catSlug !== 'all') segs.push(catSlug); + if (subcategory) segs.push(subcatToSlug(subcategory)); + return '/' + segs.join('/'); + } + + // --- UI Rendering --- + function updateLoadMoreButton() { + if (currentEmojiList.length < totalAvailable) { + 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 = 'relative 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 = ` +
${emoji.emoji}
+
${emoji.name}
+ `; + // Open modal on card click + emojiCard.addEventListener('click', () => openModal(emoji)); + // If this emoji supports skin tone, add a small trigger to pick a tone + if (emoji.supports_skin_tone) { + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'absolute top-1 right-1 rounded-md px-1 text-xs bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'; + trigger.setAttribute('aria-label', 'Choose skin tone'); + trigger.textContent = '⋯'; + emojiCard.appendChild(trigger); + let pickerEl = null; + function closePicker() { + if (pickerEl) { + pickerEl.remove(); + pickerEl = null; + document.removeEventListener('click', outsideClose, true); + } + } + function outsideClose(e) { + if (pickerEl && !pickerEl.contains(e.target) && e.target !== trigger) closePicker(); + } + const openPicker = () => { + if (pickerEl) return; + pickerEl = buildSkinTonePicker((tone) => { + // Always use base for variant + const base = emoji.emoji_base || stripSkinTone(emoji.emoji); + const variant = withSkinTone(base, tone.ch); + // persist preferred tone + const idx = SKIN_TONES.findIndex(x => x.key === tone.key); + if (idx >= 0) setPreferredToneIndex(idx); + openModal({ ...emoji, emoji: variant, emoji_base: base }); + closePicker(); + }); + trigger.after(pickerEl); + setTimeout(() => document.addEventListener('click', outsideClose, true), 0); + }; + trigger.addEventListener('click', (e) => { e.stopPropagation(); pickerEl ? closePicker() : openPicker(); }); + // Optional: long-press support for touch + let lp; + trigger.addEventListener('touchstart', () => { lp = setTimeout(openPicker, 350); }, {passive: true}); + trigger.addEventListener('touchend', () => { clearTimeout(lp); }, {passive: true}); + } + emojiGrid.appendChild(emojiCard); + }); + + // Show 'No emojis found' only if the grid is still empty after rendering + if (emojiGrid.innerHTML === '') { + emojiGrid.innerHTML = '

No emojis found.

'; + } + + updateLoadMoreButton(); + } + + loadMoreBtn.addEventListener('click', () => { + appendNextPage(); + }); + + // --- Category Logic --- + function initializeCategoryMenu() { + // Transform existing desktop sidebar buttons into anchors for proper link behavior + const desktopButtons = Array.from(document.querySelectorAll('.category-btn')); + desktopButtons.forEach((btn) => { + const category = btn.getAttribute('data-category') || 'all'; + const a = document.createElement('a'); + // Preserve inner content (icon + label) + a.innerHTML = btn.innerHTML; + // Build pretty URL and assign SPA-recognized classes/attributes + a.href = (typeof buildPrettyPathFor === 'function') ? buildPrettyPathFor(category, '') : '/'; + a.className = 'category-link scope-link block no-underline 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 ' + + 'focus:outline-none focus:ring-2 focus:ring-blue-500'; + a.setAttribute('data-category', category); + a.setAttribute('data-cat', category); + a.setAttribute('data-sub', ''); + // Replace button with anchor + btn.replaceWith(a); + }); + + // 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 => { + const href = (typeof buildPrettyPathFor === 'function') + ? buildPrettyPathFor(cat.name, '') + : '/'; // fallback safeguard + return ` + + + ${cat.icon} + ${cat.name} + + + `; + }).join(''); + + // Offcanvas menu controls + offcanvasToggle.addEventListener('click', openOffcanvas); + offcanvasClose.addEventListener('click', closeOffcanvas); + offcanvasBackdrop.addEventListener('click', closeOffcanvas); + } + + // Initialize category menu (sidebar + desktop listeners) + initializeCategoryMenu(); + + // Intercept plain left-clicks on category links for SPA behavior. + // Modified clicks (Cmd/Ctrl/middle) and no-JS still work as normal links. + document.addEventListener('click', (e) => { + const a = e.target.closest('a.category-link'); + if (!a) return; + // Only intercept unmodified left clicks + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + + e.preventDefault(); + const category = a.dataset.category || 'all'; + setActiveCategory(category); + // Top-level category navigation clears subcategory + currentFilters.subcategory = ''; + updateURLFromFilters(); + // If off-canvas is open, close it + try { closeOffcanvas(); } catch(_) {} + }); + + document.addEventListener('click', (e) => { + const a = e.target.closest('a.scope-link'); + if (!a) return; + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + + e.preventDefault(); + currentFilters.category = a.getAttribute('data-cat') || 'all'; + currentFilters.subcategory = subcatToSlug(a.getAttribute('data-sub') || ''); + updateURLFromFilters(); + resetAndLoadFirstPage(); + try { closeOffcanvas(); } catch(_) {} + }); + + function setActiveCategory(category) { + currentCategory = category; + currentFilters.category = category; + updateURLFromFilters(); + + // Update active state for all category links (desktop + offcanvas) + document.querySelectorAll('.category-link').forEach(link => { + const isActive = (link.dataset.category === category); + link.classList.toggle('active', isActive); + if (isActive) { + link.classList.add('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300'); + } else { + link.classList.remove('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-300'); + } + }); + + // Reset to first page and fetch from API for this category (search term preserved) + currentFilters.q = getSearchValue(); + resetAndLoadFirstPage(); + reflectScopeInPlaceholders(); + } + + // Clear search buttons (support multiple: desktop/mobile) + const searchClearButtons = Array.from(document.querySelectorAll('#search-clear, .search-clear')); + searchClearButtons.forEach(btn => { + btn.addEventListener('click', () => { + const curr = getSearchValue(); + if (!curr) { focusFirstSearch(); return; } + setSearchValue(''); + currentFilters.q = ''; + updateURLFromFilters(); + resetAndLoadFirstPage(); + focusFirstSearch(); + }); + }); + + function updateCategoryDisplay() { + const categoryName = currentCategory === 'all' ? 'All Emojis' : currentCategory; + currentCategoryTitle.textContent = categoryName; + currentCategoryCount.textContent = `${totalAvailable} emojis`; + } + + function ensureFilterUI() { + let bar = document.getElementById('active-filter-bar'); + if (!bar) { + bar = document.createElement('div'); + bar.id = 'active-filter-bar'; + bar.className = 'mb-3 flex flex-wrap items-center gap-2'; + // insert above the grid + emojiGrid.parentElement.insertBefore(bar, emojiGrid); + } + return bar; + } + + function reflectScopeInPlaceholders() { + const prettySub = currentFilters.subcategory ? currentFilters.subcategory.replace(/-/g, ' ') : ''; + const scope = (currentFilters.category !== 'all' ? currentFilters.category : '') + + (prettySub ? ` › ${prettySub}` : ''); + const hint = scope ? `Searching in ${scope.toLowerCase()}…` : 'Search for an emoji…'; + searchInputs.forEach(el => el.placeholder = hint); + } + + function renderActiveFilters() { + const bar = ensureFilterUI(); + bar.innerHTML = ''; // reset + + const mkPill = (label, onClick, color='bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200') => { + const b = document.createElement('button'); + b.className = `px-2 py-1 rounded-md text-sm ${color} hover:opacity-90 transition`; + b.textContent = label; + b.addEventListener('click', onClick); + return b; + }; + + const hasCat = currentFilters.category && currentFilters.category !== 'all'; + const hasSub = !!currentFilters.subcategory; + + if (hasCat) { + bar.appendChild(mkPill( + `Category: ${currentFilters.category} ✕`, + () => { currentFilters.category = 'all'; updateURLFromFilters(); resetAndLoadFirstPage(); } + )); + } + if (hasSub) { + const prettySub = currentFilters.subcategory.replace(/-/g, ' '); + bar.appendChild(mkPill( + `Subcategory: ${prettySub} ✕`, + () => { currentFilters.subcategory = ''; updateURLFromFilters(); resetAndLoadFirstPage(); } + )); + } + if (hasCat || hasSub) { + bar.appendChild(mkPill( + 'Clear all filters', + () => { currentFilters.category = 'all'; currentFilters.subcategory = ''; updateURLFromFilters(); resetAndLoadFirstPage(); }, + 'bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200' + )); + } + } + + 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 --- + // (handled by wireSearchListeners) + + // --- Modal Logic --- + function openModal(emoji) { + const baseForModal = emoji.emoji_base || stripSkinTone(emoji.emoji); + const prefIdx = getPreferredToneIndex(); + const initial = (emoji.supports_skin_tone && prefIdx>=0) + ? withSkinTone(baseForModal, SKIN_TONES[prefIdx].ch) + : emoji.emoji; + modalEmoji.textContent = initial; + // Ensure Copy button always copies the currently shown emoji + let currentEmojiForCopy = initial; + if (modalCopyBtn) { + modalCopyBtn.onclick = () => copyToClipboard(currentEmojiForCopy); + } + // Tone row placed directly under the big emoji + (function renderModalToneRow() { + // Prepare base and current + const base = emoji.emoji_base || stripSkinTone(emoji.emoji); + let current = initial; // what is shown now + // Insert tone row after the emoji display + let toneRow = document.getElementById('modal-tone-row'); + if (!toneRow) { + toneRow = document.createElement('div'); + toneRow.id = 'modal-tone-row'; + toneRow.className = 'mt-2 flex gap-1 flex-wrap justify-center'; + modalEmoji.insertAdjacentElement('afterend', toneRow); + } + toneRow.innerHTML = ''; + if (!emoji.supports_skin_tone) return; + SKIN_TONES.forEach((t, i) => { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'w-7 h-7 rounded-md flex items-center justify-center bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'; + b.textContent = t.ch; + b.title = t.label; + if (typeof prefIdx === 'number' && prefIdx === i) { + b.classList.add('ring-2','ring-blue-500'); + } + b.addEventListener('click', () => { + const variant = withSkinTone(base, t.ch); + current = variant; + modalEmoji.textContent = variant; + currentEmojiForCopy = variant; // update shared copy target + setPreferredToneIndex(i); + // update highlight + Array.from(toneRow.children).forEach(child => child.classList.remove('ring-2','ring-blue-500')); + b.classList.add('ring-2','ring-blue-500'); + }); + toneRow.appendChild(b); + }); + // (removed rebinding of modalCopyBtn.onclick here) + })(); + modalName.textContent = emoji.name; + modalCategory.textContent = [emoji.category, emoji.subcategory].filter(Boolean).join(' / '); + modalKeywords.innerHTML = ''; + + const tags = new Set(); + if (emoji.category) tags.add({ type: 'category', label: emoji.category }); + if (emoji.subcategory) tags.add({ type: 'subcategory', label: emoji.subcategory }); + + if (tags.size > 0) { + [...tags].forEach(({ type, label }) => { + // Create an anchor element for pill, so Cmd/Ctrl/middle-click works + const link = document.createElement('a'); + link.href = buildPrettyPathFor( + type === 'category' ? label : (emoji.category || 'all'), + type === 'subcategory' ? label : '' + ); + link.textContent = label; + link.className = + 'scope-link 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'; + link.setAttribute('data-cat', type === 'category' ? label : (emoji.category || 'all')); + link.setAttribute('data-sub', type === 'subcategory' ? subcatToSlug(label) : ''); + modalKeywords.appendChild(link); + }); + } else { + modalKeywords.innerHTML = + '

No tags available.

'; + } + + // Copy button (handled in tone row logic above) + // modalCopyBtn.onclick = () => copyToClipboard(emoji.emoji); + + // Ensure & wire "Open page" button + const openBtn = ensureModalButtons(); + if (openBtn) { + // Prefer server-provided slug if present; otherwise build a safe fallback from name + const fallbackSlug = (emoji.slug || (emoji.name || 'emoji')) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + openBtn.onclick = () => { + window.location.href = `/emoji/${fallbackSlug}`; + }; + } + + modal.classList.remove('hidden'); + modal.classList.add('flex'); + setTimeout(() => { + modalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); + } + + function ensureModalButtons() { + // We assume modalCopyBtn exists already. + // Append a sibling "Open page" button if not present. + let openBtn = document.getElementById('modal-open-btn'); + if (!openBtn && modalCopyBtn && modalCopyBtn.parentElement) { + openBtn = document.createElement('button'); + openBtn.id = 'modal-open-btn'; + openBtn.type = 'button'; + openBtn.className = + 'ml-2 inline-flex items-center px-3 py-1.5 rounded-md ' + + 'bg-blue-600 text-white hover:bg-blue-700 focus:outline-none ' + + 'focus:ring-2 focus:ring-blue-500'; + openBtn.textContent = 'Open page'; + modalCopyBtn.parentElement.appendChild(openBtn); + } + return openBtn; + } + + 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); + }); + } + + // ---- Deep link: initialize filters from URL ---- + (function initFiltersFromURL() { + // 1) Parse pretty path /Category[/Subcategory] → querystring + (function parsePrettyPathIntoQuery(fv) { + const path = location.pathname; + // ignore known non-app paths + if (path === '/' || path.startsWith('/emoji') || path.startsWith('/api') || path.startsWith('/assets') || path.startsWith('/api-docs')) return; + + const parts = path.replace(/^\/+|\/+$/g, '').split('/'); + if (!parts[0]) return; + + const qs = new URLSearchParams(location.search); + + // slug → display + const catSlug = parts[0].toLowerCase(); + const category = SLUG_TO_CATEGORY[catSlug] || 'all'; + qs.set('category', category); + + if (parts[1]) { + // Keep canonical hyphenated slug for API queries + const sub = (parts[1] + '').toLowerCase(); + qs.set('subcategory', sub); + } + + history.replaceState(null, '', `/?${qs.toString()}`); + })(); + + // 2) Read params + const params = new URLSearchParams(location.search); + const qParam = params.get('q') || ''; + const catParam = params.get('category') || 'all'; + const subParam = params.get('subcategory') || ''; + + // 3) Put q in both search inputs + searchInputs.forEach(el => (el.value = qParam)); + + // 4) Apply to state, activate buttons, and fetch + currentFilters.q = qParam.toLowerCase().trim(); + currentFilters.category = catParam; + currentFilters.subcategory = subcatToSlug(subParam); + + // setActiveCategory also fetches, but we need subcategory respected on first load + setActiveCategory(catParam); + currentFilters.subcategory = subcatToSlug(subParam); + if (subParam) resetAndLoadFirstPage(); + })(); + + function updateURLFromFilters() { + const hasQ = !!(currentFilters.q && currentFilters.q.trim()); + const hasCat = currentFilters.category && currentFilters.category !== 'all'; + const hasSub = !!currentFilters.subcategory; + + // Prefer pretty URLs when no q and we have category/subcategory + if (!hasQ && (hasCat || hasSub)) { + const segs = []; + // map display → slug + const catSlug = CATEGORY_TO_SLUG[currentFilters.category] || 'all'; + if (hasCat && catSlug !== 'all') segs.push(catSlug); + if (hasSub) segs.push(subcatToSlug(currentFilters.subcategory)); + const pretty = '/' + segs.join('/'); + history.replaceState(null, '', pretty || '/'); + return; + } + + // Otherwise keep query form (use display name for category to keep UI-friendly querystring) + const qs = new URLSearchParams(); + if (hasQ) qs.set('q', currentFilters.q.trim()); + if (hasCat) qs.set('category', currentFilters.category); + if (hasSub) qs.set('subcategory', currentFilters.subcategory); + const url = qs.toString() ? `/?${qs.toString()}` : '/'; + history.replaceState(null, '', url); + } + + // 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(); + +}); diff --git a/backup-old-site.zip b/backup-old-site.zip new file mode 100644 index 0000000..e69de29 diff --git a/data/array.json b/data/array.json new file mode 100644 index 0000000..e69de29 diff --git a/data/categories.json b/data/categories.json new file mode 100644 index 0000000..e69de29 diff --git a/data/emojis.json b/data/emojis.json new file mode 100644 index 0000000..e69de29 diff --git a/data/keywords-id.json b/data/keywords-id.json new file mode 100644 index 0000000..e69de29 diff --git a/data/list.json b/data/list.json new file mode 100644 index 0000000..e69de29 diff --git a/data/overrides.json b/data/overrides.json new file mode 100644 index 0000000..e69de29 diff --git a/db.php b/db.php new file mode 100644 index 0000000..bd1d00e --- /dev/null +++ b/db.php @@ -0,0 +1,48 @@ +PDO::ERRMODE_EXCEPTION]); + +function j($ok, $extra=[]) { echo json_encode(array_merge(['ok'=>$ok], $extra)); exit; } +function now() { return date('Y-m-d H:i:s'); } +function row($pdo,$sql,$p){ $st=$pdo->prepare($sql); $st->execute($p); return $st->fetch(PDO::FETCH_ASSOC); } + +// activate.php +require 'db.php'; +$in = json_decode(file_get_contents('php://input'), true) ?: []; +$key = trim($in['key'] ?? ''); $dev = trim($in['device_id'] ?? ''); if (!$key || !$dev) j(false,['message'=>'bad request']); + +$lic = row($pdo,'SELECT * FROM licenses WHERE license_key=?',[$key]); +if (!$lic || $lic['revoked']) j(false,['message'=>'invalid']); +if ($lic['expires_at'] && strtotime($lic['expires_at']) < time()) j(false,['message'=>'expired']); + +$cnt = row($pdo,'SELECT COUNT(*) c FROM license_activations WHERE license_key=?',[$key])['c']; +$has = row($pdo,'SELECT 1 FROM license_activations WHERE license_key=? AND device_id=?',[$key,$dev]); + +if (!$has && $cnt >= (int)$lic['max_devices']) j(false,['message'=>'device limit reached']); + +// upsert activation +$pdo->prepare('INSERT INTO license_activations(license_key,device_id,activated_at,last_seen_at) + VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE last_seen_at=VALUES(last_seen_at)') + ->execute([$key,$dev,now(),now()]); +j(true,['plan'=>$lic['plan'],'expires_at'=>$lic['expires_at']]); + +// deactivate.php +require 'db.php'; +$in = json_decode(file_get_contents('php://input'), true) ?: []; +$key = trim($in['key'] ?? ''); $dev = trim($in['device_id'] ?? ''); if (!$key || !$dev) j(false,['message'=>'bad request']); +$pdo->prepare('DELETE FROM license_activations WHERE license_key=? AND device_id=?')->execute([$key,$dev]); +j(true); + +// verify.php +require 'db.php'; +$in = json_decode(file_get_contents('php://input'), true) ?: []; +$key = trim($in['key'] ?? ''); $dev = trim($in['device_id'] ?? ''); if (!$key || !$dev) j(false,['ok'=>false]); +$lic = row($pdo,'SELECT * FROM licenses WHERE license_key=?',[$key]); +if (!$lic || $lic['revoked']) j(false); +if ($lic['expires_at'] && strtotime($lic['expires_at']) < time()) j(false); + +$has = row($pdo,'SELECT 1 FROM license_activations WHERE license_key=? AND device_id=?',[$key,$dev]); +if (!$has) j(false); +$pdo->prepare('UPDATE license_activations SET last_seen_at=? WHERE license_key=? AND device_id=?')->execute([now(),$key,$dev]); +j(true,['plan'=>$lic['plan'],'expires_at'=>$lic['expires_at']]); \ No newline at end of file diff --git a/dewe-api-2.zip b/dewe-api-2.zip new file mode 100644 index 0000000..e69de29 diff --git a/emoji.php b/emoji.php new file mode 100644 index 0000000..e69de29 diff --git a/ext/updates.xml b/ext/updates.xml new file mode 100644 index 0000000..e69de29 diff --git a/helpers/auth.php b/helpers/auth.php new file mode 100644 index 0000000..e69de29 diff --git a/helpers/filters.php b/helpers/filters.php new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..9d0dc27 --- /dev/null +++ b/index.html @@ -0,0 +1,285 @@ + + + + + + Dewemoji + + + + + + + + + + +
+
+ +
+ +

Dewemoji

+ +
+ +
+
+ + +
+
+
+
+ + + + +
+ + + + + + + +
+
+
+

All Emojis

+

+
+ +
+ +
+ +
+ +
+
+
+
+ +
+

+ © 2025 Dewe Toolsites - Dewemoji. +

+ + + + + + diff --git a/public/js/api-client.js b/public/js/api-client.js new file mode 100644 index 0000000..e69de29 diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/sitemap.xml.php b/sitemap.xml.php new file mode 100644 index 0000000..e69de29