1919Live
Mode Gelap
Artikel teks besar

lonceng notifikasi

 Bagus — berikut fitur lonceng notifikasi yang canggih, responsif (mobile + desktop), aksesibel (ARIA), dan dibuat dengan perhatian pada SEO (fallback HTML & JSON-LD). Kode ini siap-pasang: cukup taruh satu blok di <head> (CSS + script) dan satu baris kecil di body jika ingin tempat khusus. Saya juga sertakan contoh pemakaian (API lokal/simple push simulation) — bisa langsung dipakai atau dihubungkan ke backend (push server / Firebase / OneSignal) nanti.


Cara pakai singkat

  1. Salin seluruh blok ke dalam <head> warkasa1919.com (atau ke template Blogger → bagian <head>).

  2. Jika ingin menempelkan bell di header khusus, tambahkan <div id="w1919-notif-mount"></div> di tempat header Anda; kalau tidak ada, script akan otomatis menempelkannya di kanan atas layar.

  3. Gunakan fungsi W1919Notifications.push({title, body, url, image}) dari kode untuk menambahkan notifikasi baru (contoh ada di akhir).


Kode (paste ke <head>) — siap pakai

<!-- START: Warkasa1919 Notification Bell (place this in <head>) -->
<style>
/* Styles responsif & minimal */
#w1919-notif-root { position: fixed; z-index: 9999; top: 12px; right: 12px; display:flex; align-items:center; gap:8px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
@media (max-width:640px) { #w1919-notif-root { top: 8px; right: 8px; } }

.w1919-bell-btn {
  display:inline-grid; place-items:center;
  width:48px; height:48px; border-radius:12px; background:linear-gradient(180deg,#ffffffcc,#f5f5f5cc);
  box-shadow: 0 6px 18px rgba(17,17,17,0.08); cursor:pointer; position:relative; border:1px solid rgba(0,0,0,0.04);
}
.w1919-bell-btn:focus { outline:3px solid rgba(59,130,246,0.22); outline-offset:2px; }
.w1919-bell-icon { font-size:22px; line-height:1; }

.w1919-badge {
  position:absolute; top:6px; right:6px; min-width:18px; height:18px;
  padding:0 6px; border-radius:18px; font-size:12px; font-weight:700;
  display:inline-flex; align-items:center; justify-content:center;
  background:#ef4444; color:#fff; box-shadow:0 2px 6px rgba(0,0,0,0.15);
}

/* Panel */
.w1919-panel {
  width:320px; max-width:calc(100vw - 24px); background:#fff; border-radius:12px;
  box-shadow: 0 20px 50px rgba(11,20,50,0.12); overflow:hidden; border:1px solid rgba(0,0,0,0.04);
  transform-origin: top right; animation:pop .12s ease;
}
@keyframes pop { from { transform: translateY(-6px) scale(.98); opacity:0 } to { transform:none; opacity:1 } }
.w1919-panel .head { display:flex; align-items:center; justify-content:space-between; padding:12px 12px; border-bottom:1px solid #f1f5f9; }
.w1919-panel .head h4 { margin:0; font-size:14px; font-weight:700; }
.w1919-panel .clear-btn { font-size:13px; color:#0ea5a3; background:none; border:0; cursor:pointer; }

.w1919-list { max-height:360px; overflow:auto; display:flex; flex-direction:column; gap:0; }
.w1919-item { display:flex; gap:10px; padding:12px; align-items:flex-start; border-bottom:1px solid #f8fafc; text-decoration:none; color:inherit; }
.w1919-item:hover { background:#f8fafc; }
.w1919-item .meta { display:flex; flex-direction:column; gap:4px; }
.w1919-item .title { font-weight:700; font-size:13px; }
.w1919-item .time { font-size:12px; color:#64748b; }
.w1919-item img { width:56px; height:40px; object-fit:cover; border-radius:6px; }

.w1919-empty { padding:24px; text-align:center; color:#64748b; font-size:13px; }

/* small-screen tweaks */
@media (max-width:420px){
  .w1919-panel{ width:92vw; right:6px; left:6px; border-radius:10px; }
  .w1919-item img{ display:none; }
}

/* simple accessibility helpers */
.sr-only { position:absolute !important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
</style>

<script>
/* Warkasa1919 Notification Bell — lightweight, no lib */
(function(){
  if (window.W1919Notifications) return; // avoid double load

  // Storage keys
  const STORAGE_KEY_ITEMS = 'w1919_notif_items_v1';
  const STORAGE_KEY_UNREAD = 'w1919_notif_unread_v1';

  // utils
  const $ = sel => document.querySelector(sel);
  const create = (tag, attrs={}, children=[]) => {
    const el = document.createElement(tag);
    for(const k in attrs){
      if(k === 'class') el.className = attrs[k];
      else if(k === 'html') el.innerHTML = attrs[k];
      else if(k === 'text') el.textContent = attrs[k];
      else el.setAttribute(k, attrs[k]);
    }
    children.forEach(c => el.appendChild(c));
    return el;
  };

  // load stored items
  function loadItems(){ try { return JSON.parse(localStorage.getItem(STORAGE_KEY_ITEMS) || '[]'); } catch(e){ return []; } }
  function saveItems(items){ localStorage.setItem(STORAGE_KEY_ITEMS, JSON.stringify(items)); }
  function loadUnread(){ return Number(localStorage.getItem(STORAGE_KEY_UNREAD) || 0); }
  function saveUnread(n){ localStorage.setItem(STORAGE_KEY_UNREAD, String(n)); }

  // root container
  let root = document.getElementById('w1919-notif-root');
  if(!root){
    root = document.createElement('div');
    root.id = 'w1919-notif-root';
    document.body.appendChild(root);
  }

  // create bell
  const bellBtn = create('button', { class: 'w1919-bell-btn', 'aria-label':'Lonceng notifikasi', 'aria-haspopup':'dialog', 'aria-expanded':'false', title:'Notifikasi' });
  bellBtn.innerHTML = '<span class="w1919-bell-icon" aria-hidden="true">🔔</span>';
  const badge = create('span', { class:'w1919-badge', 'aria-hidden':'true' });
  bellBtn.appendChild(badge);

  // panel (hidden initially)
  const panelWrap = create('div', { style:'display:none; position:relative;' });
  const panel = create('div', { class:'w1919-panel', role:'dialog', 'aria-label':'Daftar notifikasi', tabindex:'-1' });
  panelWrap.appendChild(panel);

  // panel contents
  const head = create('div', { class:'head' });
  head.appendChild(create('h4', { text:'Notifikasi' }));
  const clearBtn = create('button', { class:'clear-btn', text:'Bersihkan' });
  head.appendChild(clearBtn);
  panel.appendChild(head);

  const list = create('div', { class:'w1919-list', id:'w1919-notif-list' });
  panel.appendChild(list);

  const emptyNote = create('div', { class:'w1919-empty', text:'Belum ada notifikasi.' });
  panel.appendChild(emptyNote);

  root.appendChild(panelWrap);
  root.appendChild(bellBtn);

  // keyboard/close helpers
  function closePanel(){ panelWrap.style.display='none'; bellBtn.setAttribute('aria-expanded','false'); panelBlur(); }
  function openPanel(){ renderList(); panelWrap.style.display='block'; bellBtn.setAttribute('aria-expanded','true'); panel.focus(); markAllRead(); }

  function panelBlur(){ /* nothing for now */ }

  // render
  function renderList(){
    const items = loadItems().slice().reverse(); // newest first
    list.innerHTML = '';
    if(items.length===0){
      list.style.display='none';
      emptyNote.style.display='block';
      return;
    }
    list.style.display='flex';
    emptyNote.style.display='none';
    items.forEach((it, idx) => {
      const a = create('a', { class:'w1919-item', href: it.url || '#', title: it.title || '', role:'link' });
      a.addEventListener('click', (e) => {
        // when clicked, mark as read and optionally navigate
        e.preventDefault();
        // open in new tab
        if(it.url) window.open(it.url,'_blank');
        markItemRead(it.id);
        renderList();
      });
      if(it.image){
        const img = create('img', { src: it.image, alt: '' });
        a.appendChild(img);
      }
      const meta = create('div', { class:'meta' });
      meta.appendChild(create('div', { class:'title', text: it.title || 'Tanpa judul' }));
      const t = new Date(it.time || Date.now());
      meta.appendChild(create('div', { class:'time', text: t.toLocaleString() }));
      a.appendChild(meta);
      list.appendChild(a);
    });
  }

  function updateBadge(){
    const n = loadUnread();
    if(n>0){ badge.style.display='inline-flex'; badge.textContent = n>99? '99+': String(n); }
    else badge.style.display='none';
  }

  function pushNotification(obj){
    // obj: { title, body, url, image }
    const items = loadItems();
    const id = 'w1919_'+Date.now()+'_'+Math.floor(Math.random()*1000);
    items.push({ id, title: obj.title||'Notifikasi', body: obj.body||'', url: obj.url||'', image: obj.image||'', time: Date.now(), read:false });
    saveItems(items);
    const unread = loadUnread() + 1;
    saveUnread(unread);
    updateBadge();
    // visual micro-toast (optional)
    toast(obj.title, obj.body);
  }

  function toast(title, body){
    // small non-intrusive toast for immediate feedback
    const existing = document.getElementById('w1919-toast');
    if(existing) existing.remove();
    const t = create('div', { id:'w1919-toast', class:'w1919-toast', html:`<strong>${escapeHtml(title)}</strong><div style="font-size:13px;margin-top:4px;color:#475569">${escapeHtml(body||'')}</div>` });
    Object.assign(t.style, { position:'fixed', right:'16px', bottom:'18px', zIndex:99999, background:'#0f172a', color:'#fff', padding:'12px 14px', borderRadius:'10px', boxShadow:'0 8px 30px rgba(2,6,23,0.4)', maxWidth:'92vw' });
    document.body.appendChild(t);
    setTimeout(()=> t.style.opacity='0', 3000);
    setTimeout(()=> t.remove(), 3800);
  }

  function escapeHtml(str=''){ return String(str).replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[s])); }

  function markAllRead(){ saveUnread(0); updateBadge(); const items = loadItems().map(i=>({...i, read:true})); saveItems(items); }
  function markItemRead(id){
    const items = loadItems().map(i => i.id===id ? {...i, read:true} : i );
    saveItems(items);
    // recalc unread
    const unread = items.filter(i=>!i.read).length;
    saveUnread(unread);
    updateBadge();
  }

  // clear (remove all)
  clearBtn.addEventListener('click', () => {
    localStorage.removeItem(STORAGE_KEY_ITEMS);
    localStorage.removeItem(STORAGE_KEY_UNREAD);
    renderList(); updateBadge();
  });

  // bell interactions
  bellBtn.addEventListener('click', (e) => {
    const open = panelWrap.style.display === 'block';
    if(open) closePanel(); else openPanel();
  });

  // click outside to close
  document.addEventListener('click', (e) => {
    if(!panelWrap.contains(e.target) && !bellBtn.contains(e.target)) closePanel();
  });

  // keyboard
  document.addEventListener('keydown', (e) => {
    if(e.key === 'Escape') closePanel();
  });

  // initial render
  updateBadge();
  renderList();

  // expose global API
  window.W1919Notifications = {
    push: function(obj){
      pushNotification(obj);
    },
    list: function(){ return loadItems(); },
    unreadCount: function(){ return loadUnread(); },
    markAllRead: markAllRead,
    mountTo: function(selector){
      const el = document.querySelector(selector);
      if(el){
        // move root inside element
        el.appendChild(root);
      }
    },
    // helper for server push (example only: should be adapted to real push)
    simulatePushFromServer: function(){ // demo: add sample
      this.push({ title:'Contoh Berita Baru', body:'Baca artikel terbaru di warkasa1919.com', url:'/', image:'' });
    }
  };

  // SEO + noscript fallback: inject a small hidden link list for crawlers & non-js users
  (function insertSEOFallback(){
    const seo = document.createElement('div');
    seo.setAttribute('aria-hidden','true');
    seo.style.position='absolute'; seo.style.left='-9999px'; seo.style.height='1px'; seo.style.overflow='hidden';
    seo.innerHTML = '<strong>Notifikasi:</strong> <a href="/notifikasi">Lihat notifikasi terbaru</a>';
    document.body.appendChild(seo);
  })();

  // small hydration: if notifications page anchor exists, mount there
  window.addEventListener('DOMContentLoaded', () => {
    const mount = document.getElementById('w1919-notif-mount');
    if(mount) mount.appendChild(root);
  });

})();
</script>

<!-- Optional structured data (SEO) -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "Warkasa1919",
  "url": "https://warkasa1919.com",
  "potentialAction": {
    "@type": "SearchAction",
    "target": "https://warkasa1919.com/?s={search_term_string}",
    "query-input": "required name=search_term_string"
  }
}
</script>
<!-- END: Warkasa1919 Notification Bell -->

Contoh penggunaan (panggil dari mana saja di situs, mis. ketika artikel baru diterbitkan)

Letakkan ini pada script server-side atau panggil via AJAX dari server Anda untuk menambahkan notifikasi ke seluruh pengguna (contoh: setelah artikel publish, panggil endpoint yang memanggil W1919Notifications.push(...) pada client via WebSocket / SSE / push service). Untuk pengujian lokal, buka console browser dan jalankan:

// tambahkan notifikasi manual (bisa dijalankan dari console untuk uji)
W1919Notifications.push({
  title: 'Artikel Baru: Rahasia Kopi Pagi',
  body: 'Baca sekarang: 5 kebiasaan pagi yang mengubah hidup.',
  url: '/artikel/rahasia-kopi-pagi',
  image: 'https://warkasa1919.com/assets/thumbs/kopi.jpg'
});

// lihat daftar
console.log(W1919Notifications.list());
// unread count
console.log(W1919Notifications.unreadCount());

Integrasi lebih lanjut (opsional, rekomendasi)

  • Push nyata (recommended): Hubungkan ke Push API (service worker) atau gunakan provider seperti Firebase Cloud Messaging / OneSignal untuk mengirim push browser nyata. Saya bisa bantu tambahkan Service Worker + step-by-step integrasi jika mau.

  • Server sync: Simpan notifikasi di DB (per user) lalu gunakan WebSocket / SSE untuk mendorong notifikasi ke klien agar badge sinkron antar device.

  • Analytics: Catat klik notifikasi di server untuk metrik CTR.

  • SEO: Saya sudah menyertakan fallback link dan JSON-LD. Untuk hasil terbaik, sediakan halaman /notifikasi yang merender notifikasi server-side sehingga search engine dapat mengindeks daftar berita penting.


Kalau setuju, saya bisa:

  • Tambahkan versi Blogger-ready (HTML siap-paste ke template Blogger) jika Anda mau; atau

  • Tambahkan Service Worker + contoh FCM untuk push nyata agar lonceng benar-benar bisa mendorong notifikasi ke pengguna (desktop & mobile) — saya buatkan langkah lengkap dengan file service-worker.js.

Mau saya lanjut ke salah satu dari dua opsi itu sekarang?

Posting Komentar

]]>