PHP Code Editor
<?php /** * HYBRID ENGINE V2.2 - MOBILE UI & CUSTOM SCROLLBAR * Khusus PHP 7.3 + Apache2 Debian 10 */ $db_folder = __DIR__ . '/db/'; $file_path = $db_folder . 'long-poll-chat-messages.json'; $endpoint = $_SERVER['PHP_SELF']; if (!file_exists($db_folder)) mkdir($db_folder, 0755, true); // --- LOGIKA DATABASE (Fast JSON) --- function handle_db($action, $data_param = null) { global $file_path; if (!file_exists($file_path)) file_put_contents($file_path, json_encode(array())); $fp = fopen($file_path, "c+"); if (!$fp) return array(); flock($fp, LOCK_EX); $size = filesize($file_path); $data = ($size > 0) ? json_decode(fread($fp, $size), true) : array(); if (!is_array($data)) $data = array(); if ($action === 'write') { $data[] = $data_param; if (count($data) > 30) $data = array_slice($data, -30); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode(array_values($data))); } elseif ($action === 'delete') { $data = array_values(array_filter($data, function($m) use ($data_param) { return (string)$m['id'] !== (string)$data_param; })); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($data)); } flock($fp, LOCK_UN); fclose($fp); return $data; } // --- API HANDLING --- if ($_SERVER['REQUEST_METHOD'] === 'POST' || isset($_GET['poll'])) { if (session_status() == PHP_SESSION_ACTIVE) session_write_close(); // ANTI-DELAY if (isset($_POST['action']) && $_POST['action'] === 'delete') { header('Content-Type: application/json'); handle_db('delete', $_POST['id']); echo json_encode(array('status' => 'deleted')); exit; } if (isset($_POST['msg'])) { header('Content-Type: application/json'); $entry = array( 'id' => (string)microtime(true), 'user' => htmlspecialchars($_POST['user']), 'msg' => htmlspecialchars($_POST['msg']), 'time' => date('H:i') ); handle_db('write', $entry); echo json_encode(array('status' => 'ok')); exit; } if (isset($_GET['poll'])) { header('Content-Type: application/json'); header('X-Accel-Buffering: no'); $last_id = (float)$_GET['last_id']; set_time_limit(45); for ($i = 0; $i < 60; $i++) { clearstatcache(); $all = handle_db('read'); $new = array_values(array_filter($all, function($m) use ($last_id) { return (float)$m['id'] > $last_id; })); if (!empty($new)) { echo json_encode($new); flush(); exit; } usleep(300000); } echo json_encode(array()); exit; } } ?> <!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hybrid Mobile Chat</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com"> <style> .chat-bg { background-color: #e5ddd5; background-image: url('https://user-images.githubusercontent.com'); opacity: 0.98; } /* CUSTOM SCROLLBAR TIPIS (MOBILE STYLE) */ .mobile-scroll::-webkit-scrollbar { width: 4px; } .mobile-scroll::-webkit-scrollbar-track { background: transparent; } .mobile-scroll::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 10px; } .mobile-scroll:hover::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.3); } .bubble:hover .del-btn { visibility: visible; opacity: 1; } </style> </head> <body class="bg-slate-900 h-screen flex flex-col md:p-6 overflow-hidden"> <div class="w-full max-w-2xl mx-auto h-full bg-white md:rounded-3xl shadow-2xl flex flex-col overflow-hidden"> <!-- Header --> <div class="bg-[#075e54] p-4 flex items-center justify-between text-white shadow-md z-20"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center border border-white/30"><i class="fa-solid fa-bolt text-yellow-300"></i></div> <div> <h1 class="font-bold text-sm">Hybrid Chat Engine</h1> <p id="status" class="text-[10px] text-green-300">● Online</p> </div> </div> <button onclick="clearLocalData()" class="text-[9px] bg-red-600/20 hover:bg-red-600 px-3 py-1.5 rounded-full border border-red-500/50 transition-all">Clear DB</button> </div> <!-- Chat Area dengan Custom Scrollbar --> <div id="chat" class="flex-1 chat-bg p-4 overflow-y-auto flex flex-col gap-2 scroll-smooth mobile-scroll"></div> <!-- Input Box --> <div class="bg-gray-50 p-4 border-t flex flex-col gap-2"> <div class="flex items-center gap-2"> <input type="text" id="user" class="w-20 p-2 text-xs rounded-xl border border-gray-300 font-bold bg-white text-emerald-800 outline-none focus:ring-1 focus:ring-emerald-500" placeholder="Nama"> <div class="flex-1 bg-white rounded-2xl px-4 py-2.5 flex shadow-sm border border-gray-200"> <input type="text" id="msg" placeholder="Ketik pesan..." class="flex-1 outline-none text-sm bg-transparent" autocomplete="off" onkeyup="if(event.key==='Enter') send()"> </div> <button onclick="send()" class="bg-[#00a884] w-12 h-12 rounded-full text-white shadow-xl flex items-center justify-center hover:scale-110 active:scale-95 transition-all"> <i class="fa-solid fa-paper-plane"></i> </button> </div> </div> </div> <script> const endpoint = window.location.pathname; const chatEl = document.getElementById('chat'); const msgInput = document.getElementById('msg'); const userInput = document.getElementById('user'); let lastId = localStorage.getItem('last_chat_id') || "0"; userInput.value = localStorage.getItem('chat_username') || `User-${Math.floor(Math.random()*999)}`; let db; const dbReq = indexedDB.open("HyperDB_v22", 1); dbReq.onupgradeneeded = e => e.target.result.createObjectStore("messages", { keyPath: "id" }); dbReq.onsuccess = e => { db = e.target.result; loadHistory(); poll(); }; async function loadHistory() { const tx = db.transaction("messages", "readonly"); const req = tx.objectStore("messages").getAll(); req.onsuccess = () => { req.result.sort((a,b) => parseFloat(a.id) - parseFloat(b.id)).forEach(m => appendBubble(m)); chatEl.scrollTop = chatEl.scrollHeight; }; } async function send() { const val = msgInput.value.trim(); if(!val) return; const fd = new FormData(); fd.append('user', userInput.value); fd.append('msg', val); localStorage.setItem('chat_username', userInput.value); msgInput.value = ''; fetch(endpoint, { method: 'POST', body: fd }); } async function deleteMsg(id) { const fd = new FormData(); fd.append('action', 'delete'); fd.append('id', id); await fetch(endpoint, { method: 'POST', body: fd }); const tx = db.transaction("messages", "readwrite"); tx.objectStore("messages").delete(id); document.getElementById(`msg-${id}`)?.remove(); } function appendBubble(m) { if (document.getElementById(`msg-${m.id}`)) return; const isMe = m.user === userInput.value; const div = document.createElement('div'); div.id = `msg-${m.id}`; div.className = `bubble flex ${isMe ? 'justify-end' : 'justify-start'} w-full mb-1 animate-in fade-in slide-in-from-bottom-1 duration-300`; div.innerHTML = ` <div class="relative ${isMe ? 'bg-[#dcf8c6]' : 'bg-white'} p-2 px-3 rounded-2xl shadow-sm max-w-[85%] border border-black/5"> <button onclick="deleteMsg('${m.id}')" class="del-btn invisible opacity-0 absolute -top-1 -right-1 bg-red-600 text-white w-5 h-5 rounded-full text-[10px] flex items-center justify-center border border-white shadow-md z-30 transition-all hover:scale-110"> <i class="fa-solid fa-xmark"></i> </button> <span class="block text-[10px] font-bold ${isMe ? 'text-emerald-700' : 'text-blue-600'} mb-1 truncate">${m.user}</span> <p class="text-[13px] text-gray-800 pr-4 leading-relaxed">${m.msg}</p> <span class="block text-[8px] text-gray-400 text-right mt-1 font-mono uppercase">${m.time} ${isMe ? '<i class="fa-solid fa-check-double text-blue-400 ml-1"></i>' : ''}</span> </div>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } async function poll() { try { const res = await fetch(`${endpoint}?poll=1&last_id=${lastId}`); const messages = await res.json(); if (Array.isArray(messages) && messages.length > 0) { const tx = db.transaction("messages", "readwrite"); const store = tx.objectStore("messages"); messages.forEach(m => { store.put(m); appendBubble(m); if (parseFloat(m.id) > parseFloat(lastId)) lastId = m.id; }); localStorage.setItem('last_chat_id', lastId); } document.getElementById('status').innerText = "● Connected"; setTimeout(poll, 100); } catch (e) { document.getElementById('status').innerText = "○ Reconnecting..."; setTimeout(poll, 2000); } } function clearLocalData() { if(confirm("Hapus history permanen?")) { indexedDB.deleteDatabase("HyperDB_v22"); localStorage.clear(); location.reload(); } } </script> </body> </html>
Run Code
<?php /** * HYBRID ENGINE V2.2 - MOBILE UI & CUSTOM SCROLLBAR * Khusus PHP 7.3 + Apache2 Debian 10 */ $db_folder = __DIR__ . '/db/'; $file_path = $db_folder . 'long-poll-chat-messages.json'; $endpoint = $_SERVER['PHP_SELF']; if (!file_exists($db_folder)) mkdir($db_folder, 0755, true); // --- LOGIKA DATABASE (Fast JSON) --- function handle_db($action, $data_param = null) { global $file_path; if (!file_exists($file_path)) file_put_contents($file_path, json_encode(array())); $fp = fopen($file_path, "c+"); if (!$fp) return array(); flock($fp, LOCK_EX); $size = filesize($file_path); $data = ($size > 0) ? json_decode(fread($fp, $size), true) : array(); if (!is_array($data)) $data = array(); if ($action === 'write') { $data[] = $data_param; if (count($data) > 30) $data = array_slice($data, -30); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode(array_values($data))); } elseif ($action === 'delete') { $data = array_values(array_filter($data, function($m) use ($data_param) { return (string)$m['id'] !== (string)$data_param; })); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($data)); } flock($fp, LOCK_UN); fclose($fp); return $data; } // --- API HANDLING --- if ($_SERVER['REQUEST_METHOD'] === 'POST' || isset($_GET['poll'])) { if (session_status() == PHP_SESSION_ACTIVE) session_write_close(); // ANTI-DELAY if (isset($_POST['action']) && $_POST['action'] === 'delete') { header('Content-Type: application/json'); handle_db('delete', $_POST['id']); echo json_encode(array('status' => 'deleted')); exit; } if (isset($_POST['msg'])) { header('Content-Type: application/json'); $entry = array( 'id' => (string)microtime(true), 'user' => htmlspecialchars($_POST['user']), 'msg' => htmlspecialchars($_POST['msg']), 'time' => date('H:i') ); handle_db('write', $entry); echo json_encode(array('status' => 'ok')); exit; } if (isset($_GET['poll'])) { header('Content-Type: application/json'); header('X-Accel-Buffering: no'); $last_id = (float)$_GET['last_id']; set_time_limit(45); for ($i = 0; $i < 60; $i++) { clearstatcache(); $all = handle_db('read'); $new = array_values(array_filter($all, function($m) use ($last_id) { return (float)$m['id'] > $last_id; })); if (!empty($new)) { echo json_encode($new); flush(); exit; } usleep(300000); } echo json_encode(array()); exit; } } ?> <!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hybrid Mobile Chat</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com"> <style> .chat-bg { background-color: #e5ddd5; background-image: url('https://user-images.githubusercontent.com'); opacity: 0.98; } /* CUSTOM SCROLLBAR TIPIS (MOBILE STYLE) */ .mobile-scroll::-webkit-scrollbar { width: 4px; } .mobile-scroll::-webkit-scrollbar-track { background: transparent; } .mobile-scroll::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 10px; } .mobile-scroll:hover::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.3); } .bubble:hover .del-btn { visibility: visible; opacity: 1; } </style> </head> <body class="bg-slate-900 h-screen flex flex-col md:p-6 overflow-hidden"> <div class="w-full max-w-2xl mx-auto h-full bg-white md:rounded-3xl shadow-2xl flex flex-col overflow-hidden"> <!-- Header --> <div class="bg-[#075e54] p-4 flex items-center justify-between text-white shadow-md z-20"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center border border-white/30"><i class="fa-solid fa-bolt text-yellow-300"></i></div> <div> <h1 class="font-bold text-sm">Hybrid Chat Engine</h1> <p id="status" class="text-[10px] text-green-300">● Online</p> </div> </div> <button onclick="clearLocalData()" class="text-[9px] bg-red-600/20 hover:bg-red-600 px-3 py-1.5 rounded-full border border-red-500/50 transition-all">Clear DB</button> </div> <!-- Chat Area dengan Custom Scrollbar --> <div id="chat" class="flex-1 chat-bg p-4 overflow-y-auto flex flex-col gap-2 scroll-smooth mobile-scroll"></div> <!-- Input Box --> <div class="bg-gray-50 p-4 border-t flex flex-col gap-2"> <div class="flex items-center gap-2"> <input type="text" id="user" class="w-20 p-2 text-xs rounded-xl border border-gray-300 font-bold bg-white text-emerald-800 outline-none focus:ring-1 focus:ring-emerald-500" placeholder="Nama"> <div class="flex-1 bg-white rounded-2xl px-4 py-2.5 flex shadow-sm border border-gray-200"> <input type="text" id="msg" placeholder="Ketik pesan..." class="flex-1 outline-none text-sm bg-transparent" autocomplete="off" onkeyup="if(event.key==='Enter') send()"> </div> <button onclick="send()" class="bg-[#00a884] w-12 h-12 rounded-full text-white shadow-xl flex items-center justify-center hover:scale-110 active:scale-95 transition-all"> <i class="fa-solid fa-paper-plane"></i> </button> </div> </div> </div> <script> const endpoint = window.location.pathname; const chatEl = document.getElementById('chat'); const msgInput = document.getElementById('msg'); const userInput = document.getElementById('user'); let lastId = localStorage.getItem('last_chat_id') || "0"; userInput.value = localStorage.getItem('chat_username') || `User-${Math.floor(Math.random()*999)}`; let db; const dbReq = indexedDB.open("HyperDB_v22", 1); dbReq.onupgradeneeded = e => e.target.result.createObjectStore("messages", { keyPath: "id" }); dbReq.onsuccess = e => { db = e.target.result; loadHistory(); poll(); }; async function loadHistory() { const tx = db.transaction("messages", "readonly"); const req = tx.objectStore("messages").getAll(); req.onsuccess = () => { req.result.sort((a,b) => parseFloat(a.id) - parseFloat(b.id)).forEach(m => appendBubble(m)); chatEl.scrollTop = chatEl.scrollHeight; }; } async function send() { const val = msgInput.value.trim(); if(!val) return; const fd = new FormData(); fd.append('user', userInput.value); fd.append('msg', val); localStorage.setItem('chat_username', userInput.value); msgInput.value = ''; fetch(endpoint, { method: 'POST', body: fd }); } async function deleteMsg(id) { const fd = new FormData(); fd.append('action', 'delete'); fd.append('id', id); await fetch(endpoint, { method: 'POST', body: fd }); const tx = db.transaction("messages", "readwrite"); tx.objectStore("messages").delete(id); document.getElementById(`msg-${id}`)?.remove(); } function appendBubble(m) { if (document.getElementById(`msg-${m.id}`)) return; const isMe = m.user === userInput.value; const div = document.createElement('div'); div.id = `msg-${m.id}`; div.className = `bubble flex ${isMe ? 'justify-end' : 'justify-start'} w-full mb-1 animate-in fade-in slide-in-from-bottom-1 duration-300`; div.innerHTML = ` <div class="relative ${isMe ? 'bg-[#dcf8c6]' : 'bg-white'} p-2 px-3 rounded-2xl shadow-sm max-w-[85%] border border-black/5"> <button onclick="deleteMsg('${m.id}')" class="del-btn invisible opacity-0 absolute -top-1 -right-1 bg-red-600 text-white w-5 h-5 rounded-full text-[10px] flex items-center justify-center border border-white shadow-md z-30 transition-all hover:scale-110"> <i class="fa-solid fa-xmark"></i> </button> <span class="block text-[10px] font-bold ${isMe ? 'text-emerald-700' : 'text-blue-600'} mb-1 truncate">${m.user}</span> <p class="text-[13px] text-gray-800 pr-4 leading-relaxed">${m.msg}</p> <span class="block text-[8px] text-gray-400 text-right mt-1 font-mono uppercase">${m.time} ${isMe ? '<i class="fa-solid fa-check-double text-blue-400 ml-1"></i>' : ''}</span> </div>`; chatEl.appendChild(div); chatEl.scrollTop = chatEl.scrollHeight; } async function poll() { try { const res = await fetch(`${endpoint}?poll=1&last_id=${lastId}`); const messages = await res.json(); if (Array.isArray(messages) && messages.length > 0) { const tx = db.transaction("messages", "readwrite"); const store = tx.objectStore("messages"); messages.forEach(m => { store.put(m); appendBubble(m); if (parseFloat(m.id) > parseFloat(lastId)) lastId = m.id; }); localStorage.setItem('last_chat_id', lastId); } document.getElementById('status').innerText = "● Connected"; setTimeout(poll, 100); } catch (e) { document.getElementById('status').innerText = "○ Reconnecting..."; setTimeout(poll, 2000); } } function clearLocalData() { if(confirm("Hapus history permanen?")) { indexedDB.deleteDatabase("HyperDB_v22"); localStorage.clear(); location.reload(); } } </script> </body> </html>
Run Code New Tab
Result