Spaces:
Running
Running
| ; | |
| SM.injectLayout('nav-data'); | |
| const store = SM.loadData(); | |
| if (!store) { | |
| window.location.replace('upload'); | |
| throw new Error('No data β redirecting to upload'); | |
| } | |
| const { rows, meta } = store; | |
| document.getElementById('topbarMeta').textContent = `${meta.filename} β ${rows.length} tweets`; | |
| // ββ Column definitions ββ | |
| const COLS = [ | |
| { key:'id', label:'No', visible:true, sortable:true }, | |
| { key:'raw', label:'Teks Asli', visible:true, sortable:false }, | |
| { key:'cleaned', label:'Teks Bersih', visible:false, sortable:false }, | |
| { key:'username', label:'Username', visible:true, sortable:true }, | |
| { key:'location', label:'Lokasi', visible:true, sortable:true }, | |
| { key:'date', label:'Waktu', visible:true, sortable:true }, | |
| { key:'sentiment', label:'Sentimen', visible:true, sortable:true }, | |
| { key:'confidence', label:'Kepercayaan', visible:true, sortable:true }, | |
| { key:'fav', label:'Like', visible:true, sortable:true }, | |
| { key:'rt', label:'Retweet', visible:true, sortable:true }, | |
| { key:'rep', label:'Reply', visible:false, sortable:true }, | |
| { key:'qot', label:'Quote', visible:false, sortable:true }, | |
| { key:'engagement', label:'Engagement', visible:true, sortable:true }, | |
| ]; | |
| // Populate location & user dropdowns | |
| const locs = [...new Set(rows.map(r=>(r.location||'').trim()||'β'))].sort(); | |
| const users = [...new Set(rows.map(r=>r.username))].sort(); | |
| const fLoc = document.getElementById('fLocation'); | |
| const fUser = document.getElementById('fUser'); | |
| locs.forEach(l => { const o=document.createElement('option'); o.value=l; o.textContent=l; fLoc.appendChild(o); }); | |
| users.forEach(u => { const o=document.createElement('option'); o.value=u; o.textContent='@'+u; fUser.appendChild(o); }); | |
| // Column toggle buttons | |
| document.getElementById('colToggle').innerHTML = COLS.map((c,i) => | |
| `<div class="col-pill ${c.visible?'on':''}" data-colidx="${i}">${c.label}</div>` | |
| ).join(''); | |
| document.querySelectorAll('.col-pill').forEach(pill => { | |
| pill.addEventListener('click', () => { | |
| const i = +pill.dataset.colidx; | |
| COLS[i].visible = !COLS[i].visible; | |
| pill.classList.toggle('on', COLS[i].visible); | |
| renderTable(); | |
| }); | |
| }); | |
| // ββ State ββ | |
| let filtered = [...rows]; | |
| let currentPage = 1; | |
| let pageSize = 20; | |
| let sortKey = 'id', sortDir = 1; | |
| let expandedRows = new Set(); | |
| // ββ Filters ββ | |
| function applyFilters() { | |
| const s = document.getElementById('fSentiment').value; | |
| const loc = document.getElementById('fLocation').value; | |
| const usr = document.getElementById('fUser').value; | |
| const q = document.getElementById('fSearch').value.toLowerCase().trim(); | |
| const minE= parseInt(document.getElementById('fMinEngage').value)||0; | |
| const minC= (parseFloat(document.getElementById('fMinConf').value)||0)/100; | |
| filtered = rows.filter(r => | |
| (s==='all' || r.sentiment===s) && | |
| (loc==='all' || (r.location||'β')===loc) && | |
| (usr==='all' || r.username===usr) && | |
| (r.engagement >= minE) && | |
| (r.confidence >= minC) && | |
| (!q || r.raw.toLowerCase().includes(q) || r.username.toLowerCase().includes(q) || r.cleaned.toLowerCase().includes(q)) | |
| ); | |
| // Sort | |
| filtered.sort((a,b)=>{ | |
| const va=a[sortKey], vb=b[sortKey]; | |
| if(typeof va==='number') return (va-vb)*sortDir; | |
| return String(va).localeCompare(String(vb))*sortDir; | |
| }); | |
| currentPage = 1; | |
| renderTable(); | |
| } | |
| ['fSentiment','fLocation','fUser'].forEach(id => document.getElementById(id).addEventListener('change', applyFilters)); | |
| document.getElementById('fSearch').addEventListener('input', applyFilters); | |
| document.getElementById('fMinEngage').addEventListener('input', applyFilters); | |
| document.getElementById('fMinConf').addEventListener('input', applyFilters); | |
| document.getElementById('pageSize').addEventListener('change', e => { pageSize=+e.target.value; currentPage=1; renderTable(); }); | |
| document.getElementById('btnReset').addEventListener('click', () => { | |
| document.getElementById('fSentiment').value='all'; | |
| document.getElementById('fLocation').value='all'; | |
| document.getElementById('fUser').value='all'; | |
| document.getElementById('fSearch').value=''; | |
| document.getElementById('fMinEngage').value=''; | |
| document.getElementById('fMinConf').value=''; | |
| // Refresh custom select labels after reset | |
| ['fSentiment','fLocation','fUser','pageSize'].forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.dispatchEvent(new Event('_csdRefresh')); | |
| }); | |
| applyFilters(); | |
| }); | |
| // ββ Table HEAD ββ | |
| function renderHead() { | |
| const visCols = COLS.filter(c=>c.visible); | |
| document.getElementById('tableHead').innerHTML = `<tr> | |
| ${visCols.map(c => { | |
| const isSorted = sortKey === c.key; | |
| const sortIcon = isSorted | |
| ? (sortDir === 1 | |
| ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="width:12px;height:12px;margin-left:4px"><polyline points="18 15 12 9 6 15"/></svg>' | |
| : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="width:12px;height:12px;margin-left:4px"><polyline points="6 9 12 15 18 9"/></svg>') | |
| : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;margin-left:4px;opacity:0.2"><polyline points="6 9 12 15 18 9"/></svg>'; | |
| return `<th ${c.sortable ? `data-sort="${c.key}" class="${isSorted ? 'active-sort' : ''}"` : ''}> | |
| <div style="display:flex; align-items:center; justify-content:space-between"> | |
| ${c.label} | |
| ${c.sortable ? sortIcon : ''} | |
| </div> | |
| </th>`; | |
| }).join('')} | |
| </tr>`; | |
| document.querySelectorAll('th[data-sort]').forEach(th => { | |
| th.addEventListener('click', () => { | |
| if(sortKey===th.dataset.sort) sortDir*=-1; | |
| else { sortKey=th.dataset.sort; sortDir=-1; } | |
| applyFilters(); // Use applyFilters to trigger sorting before re-render | |
| }); | |
| }); | |
| } | |
| // ββ Table BODY ββ | |
| function renderTable() { | |
| renderHead(); | |
| const visCols = COLS.filter(c=>c.visible); | |
| const start = (currentPage-1)*pageSize; | |
| const page = filtered.slice(start, start+pageSize); | |
| document.getElementById('tableBody').innerHTML = page.map(r => { | |
| const cells = visCols.map(c => { | |
| const v = r[c.key]; | |
| if(c.key==='sentiment') return `<td><span class="badge badge-${r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu'}">${r.sentiment}</span></td>`; | |
| if(c.key==='confidence') return `<td> | |
| <div class="conf-row"> | |
| <span class="conf-num">${(v*100).toFixed(1)}%</span> | |
| <div class="conf-track"><div class="conf-fill conf-${r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu'}" style="width:${(v*100).toFixed(0)}%"></div></div> | |
| </div></td>`; | |
| if(c.key==='raw') return `<td class="td-trunc" title="${SM.esc(v)}">${SM.esc(v.slice(0,80))}${v.length>80?'β¦':''}</td>`; | |
| if(c.key==='cleaned') return `<td class="td-trunc" title="${SM.esc(v)}">${SM.esc(v.slice(0,60))}${v.length>60?'β¦':''}</td>`; | |
| if(c.key==='date') { | |
| const d=new Date(v); return `<td class="td-trunc-sm">${isNaN(d)?v:d.toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit'})}</td>`; | |
| } | |
| if(c.key==='username') return `<td style="font-weight:500;color:var(--tx1);white-space:nowrap">@${SM.esc(v)}</td>`; | |
| if(c.key==='id') return `<td class="td-no">${v}</td>`; | |
| return `<td>${SM.esc(String(v))}</td>`; | |
| }).join(''); | |
| return `<tr data-rowid="${r.id}"> | |
| ${cells} | |
| </tr>`; | |
| }).join(''); | |
| renderPagination(); | |
| document.getElementById('tableInfo').textContent = | |
| `Menampilkan ${Math.min(start+1,filtered.length)}β${Math.min(start+pageSize,filtered.length)} dari ${filtered.length} data (total ${rows.length})`; | |
| } | |
| function renderPagination() { | |
| const total = Math.ceil(filtered.length/pageSize); | |
| const pg = document.getElementById('pagination'); | |
| if(total<=1) { pg.innerHTML=''; return; } | |
| const range=[1]; | |
| if(currentPage>3) range.push('β¦'); | |
| for(let i=Math.max(2,currentPage-1);i<=Math.min(total-1,currentPage+1);i++) range.push(i); | |
| if(currentPage<total-2) range.push('β¦'); | |
| if(total>1) range.push(total); | |
| pg.innerHTML = ` | |
| <button class="pg-btn" ${currentPage===1?'disabled':''} onclick="goPage(${currentPage-1})" title="Previous"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><polyline points="15 18 9 12 15 6"/></svg> | |
| </button> | |
| ${range.map(p=>p==='β¦'?`<span class="pg-btn dots">β¦</span>` | |
| :`<button class="pg-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`).join('')} | |
| <button class="pg-btn" ${currentPage===total?'disabled':''} onclick="goPage(${currentPage+1})" title="Next"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><polyline points="9 18 15 12 9 6"/></svg> | |
| </button>`; | |
| } | |
| window.goPage = function(p) { | |
| currentPage=Math.max(1,Math.min(p,Math.ceil(filtered.length/pageSize))); | |
| expandedRows.clear(); renderTable(); | |
| document.getElementById('dataTable').scrollIntoView({behavior:'smooth'}); | |
| }; | |
| // Initial render | |
| applyFilters(); | |
| // ββ Custom Dropdowns ββ | |
| SM.initCustomSelect(document.getElementById('fSentiment'), { | |
| showDots: { | |
| 'all': null, | |
| 'Positif': '#34d399', | |
| 'Negatif': '#f87171', | |
| 'Netral': '#fbbf24', | |
| } | |
| }); | |
| SM.initCustomSelect(document.getElementById('fLocation')); | |
| SM.initCustomSelect(document.getElementById('fUser')); | |
| SM.initCustomSelect(document.getElementById('pageSize'), { compact: true }); | |
| SM.initCustomNumber(document.getElementById('fMinEngage')); | |
| SM.initCustomNumber(document.getElementById('fMinConf')); | |