first from cpanel commit

This commit is contained in:
dwindown
2025-08-29 23:47:11 +07:00
commit ce4b6d577e
52 changed files with 1494 additions and 0 deletions

0
.htaccess Normal file
View File

0
__MACOSX/._dewe-api-2 Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

0
api-docs.html Normal file
View File

82
api/emoji.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
// api/emoji.php (route: /api/emoji?slug=... or rewrite to /api/emoji/{slug})
require_once __DIR__.'/../helpers/auth.php';
require_once __DIR__.'/../helpers/filters.php';
// ---- Skin tone helpers (Fitzpatrick modifiers) ----
if (!function_exists('dw_supports_skin_tone')) {
function dw_supports_skin_tone(array $emoji): bool {
// Respect explicit flag if present in source data
if (isset($emoji['supports_skin_tone'])) {
return (bool)$emoji['supports_skin_tone'];
}
$cat = strtolower(trim($emoji['category'] ?? ''));
$sub = strtolower(trim($emoji['subcategory'] ?? ''));
$name = strtolower(trim($emoji['name'] ?? ''));
// 1) category must be People & Body
if ($cat !== 'people & body') return false;
// 2) Exclude faces: many face emojis are not tone-modifiable
if (preg_match('~\bface\b~', $name) || preg_match('~\bface\b~', $sub)) return false;
// 3) Allowlist of tone-capable subcategories (based on Unicode charts)
$allowSubs = [
'hand-fingers-open','hand-fingers-partial','hand-fingers-closed','hand-single-finger',
'hands','hand-prop','handshake','body-parts','person','person-gesture','person-activity','person-resting'
];
if (in_array($sub, $allowSubs, true) || str_starts_with($sub, 'hand')) return true;
// Fallback by keywords mentioning body parts / gestures
$keys = [];
foreach (['keywords','keywords_en','keywords_id'] as $k) {
if (!empty($emoji[$k]) && is_array($emoji[$k])) $keys = array_merge($keys, $emoji[$k]);
}
$hay = strtolower(' '.implode(' ', $keys).' ');
return (bool)preg_match('~\b(hand|hands|wave|clap|thumb|finger|palm|point|ear|nose|leg|foot|person|man|woman|boy|girl|salute)\b~', $hay);
}
}
if (!function_exists('dw_skin_tone_modifiers')) {
function dw_skin_tone_modifiers(): array {
return ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"]; // light .. dark
}
}
if (!function_exists('dw_strip_tone')) {
function dw_strip_tone(string $emoji): string {
return preg_replace('/\x{1F3FB}|\x{1F3FC}|\x{1F3FD}|\x{1F3FE}|\x{1F3FF}/u', '', $emoji);
}
}
if (!function_exists('dw_build_tone_variants')) {
function dw_build_tone_variants(string $emojiBase): array {
$mods = ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"];
return array_map(fn($m) => $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);

231
api/emojis.php Normal file
View File

@@ -0,0 +1,231 @@
<?php
// api/emojis.php (route: /api/emojis?q=...&category=...&subcategory=...&page=1&limit=50)
require_once __DIR__.'/../helpers/auth.php';
require_once __DIR__.'/../helpers/filters.php';
// ---- Skin tone helpers (Fitzpatrick modifiers) ----
if (!function_exists('dw_supports_skin_tone')) {
function dw_supports_skin_tone(array $emoji): bool {
// Respect explicit flag if present in source data
if (isset($emoji['supports_skin_tone'])) {
return (bool)$emoji['supports_skin_tone'];
}
$cat = strtolower(trim($emoji['category'] ?? ''));
$sub = strtolower(trim($emoji['subcategory'] ?? ''));
$name = strtolower(trim($emoji['name'] ?? ''));
// 1) category must be People & Body
if ($cat !== 'people & body') return false;
// 2) Exclude faces: many face emojis are not tone-modifiable
if (preg_match('~\bface\b~', $name) || preg_match('~\bface\b~', $sub)) return false;
// 3) Allowlist of tone-capable subcategories (based on Unicode charts)
$allowSubs = [
'hand-fingers-open','hand-fingers-partial','hand-fingers-closed','hand-single-finger',
'hands','hand-prop','handshake','body-parts','person','person-gesture','person-activity','person-resting'
];
if (in_array($sub, $allowSubs, true) || (function_exists('str_starts_with') ? str_starts_with($sub, 'hand') : strpos($sub, 'hand') === 0)) return true;
// Fallback by keywords mentioning body parts / gestures
$keys = [];
foreach (['keywords','keywords_en','keywords_id'] as $k) {
if (!empty($emoji[$k]) && is_array($emoji[$k])) $keys = array_merge($keys, $emoji[$k]);
}
$hay = strtolower(' '.implode(' ', $keys).' ');
return (bool)preg_match('~\b(hand|hands|wave|clap|thumb|finger|palm|point|ear|nose|leg|foot|person|man|woman|boy|girl|salute)\b~', $hay);
}
}
if (!function_exists('dw_skin_tone_modifiers')) {
function dw_skin_tone_modifiers(): array {
return ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"]; // light .. dark
}
}
if (!function_exists('dw_strip_tone')) {
function dw_strip_tone(string $emoji): string {
return preg_replace('/\x{1F3FB}|\x{1F3FC}|\x{1F3FD}|\x{1F3FE}|\x{1F3FF}/u', '', $emoji);
}
}
if (!function_exists('dw_build_tone_variants')) {
function dw_build_tone_variants(string $emojiBase): array {
$mods = ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"];
return array_map(fn($m) => $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);

0
api/error_log Normal file
View File

0
api/index.php Normal file
View File

848
assets/script.js Normal file
View File

@@ -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 = `
<div class="animate-spin inline-block w-8 h-8 border-4 border-current border-t-transparent text-blue-600 rounded-full" role="status" aria-label="loading"></div>
`;
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<SKIN_TONES.length) localStorage.setItem('preferredSkinTone', TONE_SLUGS[i]);
}
// Append a skin tone modifier to an emoji (browsers ignore invalid combos gracefully)
function withSkinTone(emojiChar, modifierChar) {
if (!emojiChar) return emojiChar;
return `${emojiChar}${modifierChar}`;
}
// Helper: strip Fitzpatrick skin tone modifiers from emoji string
function stripSkinTone(emojiChar) {
if (!emojiChar) return emojiChar;
// Remove Fitzpatrick modifiers (\u{1F3FB}-\u{1F3FF})
return emojiChar.replace(/[\u{1F3FB}-\u{1F3FF}]/gu, '');
}
function buildSkinTonePicker(onPick) {
const wrap = document.createElement('div');
wrap.className = 'absolute z-50 mt-2 p-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg flex gap-1';
SKIN_TONES.forEach(t => {
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 = '<p class="text-red-500 col-span-full text-center">Failed to load emoji data.</p>';
} 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 = `
<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>
`;
// 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 = '<p class="text-gray-500 dark:text-gray-400 col-span-full text-center">No emojis found.</p>';
}
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 `
<a href="${href}"
class="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"
data-category="${cat.key}"
data-cat="${cat.name}"
data-sub="">
<span class="flex items-center">
<span class="mr-2">${cat.icon}</span>
${cat.name}
</span>
</a>
`;
}).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 =
'<p class="text-sm text-gray-500 dark:text-gray-400">No tags available.</p>';
}
// 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();
});

0
backup-old-site.zip Normal file
View File

0
data/array.json Normal file
View File

0
data/categories.json Normal file
View File

0
data/emojis.json Normal file
View File

0
data/keywords-id.json Normal file
View File

0
data/list.json Normal file
View File

0
data/overrides.json Normal file
View File

48
db.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
// db.php
$pdo = new PDO('mysql:host=localhost;dbname=dewepw_emoji;charset=utf8mb4','dewepw_emoji_bro','3vgxEHxf2oBirx*D',
[PDO::ATTR_ERRMODE=>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']]);

0
dewe-api-2.zip Normal file
View File

0
emoji.php Normal file
View File

0
ext/updates.xml Normal file
View File

0
helpers/auth.php Normal file
View File

0
helpers/filters.php Normal file
View File

285
index.html Normal file
View File

@@ -0,0 +1,285 @@
<!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>Dewemoji</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;
}
.sidebar-sticky { position: sticky; top: 95px!important; }
</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="sticky top-0 z-40 md:hidden bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="mx-auto py-8 px-4 sm:px-6 lg:px-8 text-center">
<!-- Mobile Header Layout -->
<div class="flex items-center justify-between mb-4 lg:hidden">
<button id="offcanvas-toggle" class="p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 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="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<h1 class="text-4xl font-bold tracking-tight flex-1 text-center">Dewemoji</h1>
<button class="theme-toggle p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none">
<svg class="theme-icon-dark block dark: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 class="theme-icon-light hidden dark:block 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">
<div class="relative">
<input
type="text"
placeholder="Search for an emoji..."
class="search-input w-full pl-4 pr-10 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"
>
<button
type="button"
aria-label="Clear search"
class="search-clear absolute inset-y-0 right-0 px-3 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
></button>
</div>
</div>
</div>
</header>
<!-- DESKTOP header (new) -->
<header class="hidden md:block sticky top-0 z-40 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="mx-auto py-4 px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between gap-4">
<!-- Title -->
<h1 class="text-2xl font-bold tracking-tight">Dewemoji</h1>
<!-- Right: search + dark toggle -->
<div class="flex items-center gap-3 min-w-[420px]">
<!-- Search with clear -->
<div class="relative w-full">
<input
type="text"
placeholder="Search for an emoji…"
class="search-input w-full pl-4 pr-10 py-2 rounded-lg bg-gray-100 dark:bg-gray-900 border border-transparent focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-900 outline-none transition"
/>
<button
id="search-clear"
type="button"
aria-label="Clear search"
class="absolute inset-y-0 right-0 px-3 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
</button>
</div>
<!-- Dark mode toggle (reuse your existing button/ids if you have them) -->
<button class="theme-toggle p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none">
<svg class="theme-icon-dark block dark: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 class="theme-icon-light hidden dark:block 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>
</div>
</header>
<main class="flex-grow flex">
<!-- Category Sidebar -->
<aside class="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 p-4 hidden lg:block">
<div class="md:sticky md:top-20 sidebar-sticky">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Menu</h2>
<nav id="menu-nav" class="space-y-2 mb-4">
<a href="/" class="w-full block 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 active">
<span class="flex items-center">
<span class="mr-2">🏡</span>
Emoji
</span>
</a>
<a href="/api-docs" class="w-full block 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">
<span class="flex items-center">
<span class="mr-2">🔥</span>
API Docs
</span>
</a>
</nav>
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Categories</h2>
<nav id="category-nav" class="space-y-2">
<button class="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 active" data-category="all">
<span class="flex items-center">
<span class="mr-2">🌟</span>
All Emojis
</span>
</button>
<button class="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="Smileys & Emotion">
<span class="flex items-center">
<span class="mr-2">😀</span>
Smileys & Emotion
</span>
</button>
<button class="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="People & Body">
<span class="flex items-center">
<span class="mr-2">👋</span>
People & Body
</span>
</button>
<button class="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="Animals & Nature">
<span class="flex items-center">
<span class="mr-2">🐶</span>
Animals & Nature
</span>
</button>
<button class="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="Food & Drink">
<span class="flex items-center">
<span class="mr-2">🍎</span>
Food & Drink
</span>
</button>
<button class="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="Travel & Places">
<span class="flex items-center">
<span class="mr-2">🌍</span>
Travel & Places
</span>
</button>
<button class="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="Activities">
<span class="flex items-center">
<span class="mr-2"></span>
Activities
</span>
</button>
<button class="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="Objects">
<span class="flex items-center">
<span class="mr-2">💡</span>
Objects
</span>
</button>
<button class="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="Symbols">
<span class="flex items-center">
<span class="mr-2">❤️</span>
Symbols
</span>
</button>
<button class="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="Flags">
<span class="flex items-center">
<span class="mr-2">🏳️</span>
Flags
</span>
</button>
</nav>
</div>
</aside>
<!-- Mobile Offcanvas Overlay -->
<div id="offcanvas-overlay" class="lg:hidden fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black bg-opacity-50" id="offcanvas-backdrop"></div>
<!-- Offcanvas Sidebar -->
<div id="offcanvas-sidebar" class="absolute left-0 top-0 h-full w-64 bg-white dark:bg-gray-800 transform -translate-x-full transition-transform duration-300 ease-in-out">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Menu</h2>
<button id="offcanvas-close" class="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">
<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>
</div>
<div class="p-4 overflow-y-auto h-full pb-20">
<!-- Menu (same as desktop sidebar) -->
<nav id="offcanvas-menu" class="space-y-2 mb-6">
<a href="/" class="w-full block 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">
<span class="flex items-center">
<span class="mr-2">🏡</span>
Emoji
</span>
</a>
<a href="/api-docs" class="w-full block 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">
<span class="flex items-center">
<span class="mr-2">🔥</span>
API Docs
</span>
</a>
</nav>
<!-- Categories (JS will populate #offcanvas-nav) -->
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Categories</h3>
<nav id="offcanvas-nav" class="space-y-2">
<!-- Offcanvas category buttons will be populated by JavaScript -->
</nav>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1 p-4 sm:p-6 lg:p-8">
<div class="max-w-6xl mx-auto">
<div class="mb-6">
<h2 id="current-category-title" class="text-2xl font-bold text-gray-900 dark:text-gray-100">All Emojis</h2>
<p id="current-category-count" class="text-sm text-gray-500 dark:text-gray-400 mt-1"></p>
</div>
<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>
</div>
</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">
&copy; <span id="current-year">2025</span> Dewe Toolsites - Dewemoji.
</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 flex gap-2">
<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 flex gap-2 justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.829 12.861c.171-.413.171-.938.171-1.986s0-1.573-.171-1.986a2.25 2.25 0 0 0-1.218-1.218c-.413-.171-.938-.171-1.986-.171H11.1c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C7.5 9.209 7.5 9.839 7.5 11.1v6.525c0 1.048 0 1.573.171 1.986c.229.551.667.99 1.218 1.218c.413.171.938.171 1.986.171s1.573 0 1.986-.171m7.968-7.968a2.25 2.25 0 0 1-1.218 1.218c-.413.171-.938.171-1.986.171s-1.573 0-1.986.171a2.25 2.25 0 0 0-1.218 1.218c-.171.413-.171.938-.171 1.986s0 1.573-.171 1.986a2.25 2.25 0 0 1-1.218 1.218m7.968-7.968a11.68 11.68 0 0 1-7.75 7.9l-.218.068M16.5 7.5v-.9c0-1.26 0-1.89-.245-2.371a2.25 2.25 0 0 0-.983-.984C14.79 3 14.16 3 12.9 3H6.6c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C3 4.709 3 5.339 3 6.6v6.3c0 1.26 0 1.89.245 2.371c.216.424.56.768.984.984c.48.245 1.111.245 2.372.245H7.5"/></svg>
Copy Emoji
</button>
<button id="modal-open-btn" type="button" class="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">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9.367 2.25H9.4a.75.75 0 0 1 0 1.5c-1.132 0-1.937 0-2.566.052c-.62.05-1.005.147-1.31.302a3.25 3.25 0 0 0-1.42 1.42c-.155.305-.251.69-.302 1.31c-.051.63-.052 1.434-.052 2.566v5.2c0 1.133 0 1.937.052 2.566c.05.62.147 1.005.302 1.31a3.25 3.25 0 0 0 1.42 1.42c.305.155.69.251 1.31.302c.63.051 1.434.052 2.566.052h5.2c1.133 0 1.937 0 2.566-.052c.62-.05 1.005-.147 1.31-.302a3.25 3.25 0 0 0 1.42-1.42c.155-.305.251-.69.302-1.31c.051-.63.052-1.434.052-2.566v-1.1a.75.75 0 0 1 1.5 0v1.133c0 1.092 0 1.958-.057 2.655c-.058.714-.18 1.317-.46 1.869a4.75 4.75 0 0 1-2.076 2.075c-.552.281-1.155.403-1.869.461c-.697.057-1.563.057-2.655.057H9.367c-1.092 0-1.958 0-2.655-.057c-.714-.058-1.317-.18-1.868-.46a4.75 4.75 0 0 1-2.076-2.076c-.281-.552-.403-1.155-.461-1.869c-.057-.697-.057-1.563-.057-2.655V9.367c0-1.092 0-1.958.057-2.655c.058-.714.18-1.317.46-1.868a4.75 4.75 0 0 1 2.077-2.076c.55-.281 1.154-.403 1.868-.461c.697-.057 1.563-.057 2.655-.057M13.5 3a.75.75 0 0 1 .75-.75H21a.75.75 0 0 1 .75.75v6.75a.75.75 0 0 1-1.5 0V4.81l-6.97 6.97a.75.75 0 1 1-1.06-1.06l6.97-6.97h-4.94A.75.75 0 0 1 13.5 3"/></svg>
</button>
</div>
</div>
</div>
<script src="/assets/script.js"></script>
</body>
</html>

0
public/js/api-client.js Normal file
View File

0
robots.txt Normal file
View File

0
sitemap.xml.php Normal file
View File