Add NAS file manager integration and all pending platform changes

- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-13 13:24:32 +03:00
1 parent d44490dfe0
commit 0b2e95ea65
161 files changed
+27496 -7250

No files matched your search

@@ -0,0 +1,785 @@
{{--
Video Insights component.
Expects: $video (Video model)
Renders the Insights tab panel + modal + all JS — only for the video owner.
--}}
@php
$isVideoOwner = Auth::check() && Auth::id() === $video->user_id;
@endphp
@if($isVideoOwner)
<style>
/* ── Stat cards ──────────────────────────────────────── */
.ins-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin-bottom:18px; }
@media(max-width:680px){ .ins-grid { grid-template-columns:repeat(2,1fr); } }
.ins-card { background:var(--bg-dark); border:1px solid var(--border-color); border-radius:10px; padding:14px 14px 12px; display:flex; flex-direction:column; gap:4px; cursor:pointer; transition:border-color .15s, transform .12s, box-shadow .15s; user-select:none; }
.ins-card:hover { border-color:rgba(239,68,68,.5); transform:translateY(-2px); box-shadow:0 4px 16px rgba(0,0,0,.3); }
.ins-card:active { transform:translateY(0); }
.ins-card-icon { font-size:18px; margin-bottom:2px; }
.ins-card-val { font-size:26px; font-weight:900; line-height:1; color:var(--text-primary); }
.ins-card-label { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--text-secondary); }
.ins-card-sub { font-size:12px; margin-top:3px; font-weight:600; }
.ins-card-sub.up { color:#4ade80; } .ins-card-sub.down { color:#f87171; } .ins-card-sub.neu { color:var(--text-secondary); }
/* ── Section titles / charts ────────────────────────── */
.ins-section-title { font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-secondary); margin-bottom:12px; display:flex; align-items:center; gap:6px; }
.ins-chart { display:flex; align-items:flex-end; gap:4px; height:80px; margin-bottom:6px; }
.ins-bar-col { flex:1; display:flex; flex-direction:column; align-items:center; gap:4px; min-width:0; }
.ins-bar { width:100%; border-radius:4px 4px 0 0; background:#ef444466; transition:height .6s cubic-bezier(.22,.61,.36,1), background .15s; min-height:2px; }
.ins-bar.clickable { cursor:pointer; }
.ins-bar.clickable:hover { background:#ef4444cc !important; }
.ins-bar-today { background:#ef4444 !important; }
.ins-bar-label { font-size:9px; color:var(--text-secondary); text-align:center; overflow:hidden; white-space:nowrap; }
.ins-peak-badge { display:inline-flex; align-items:center; gap:6px; background:rgba(239,68,68,.12); border:1px solid rgba(239,68,68,.25); border-radius:20px; padding:4px 12px; font-size:12px; font-weight:600; color:#fca5a5; }
.ins-body { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media(max-width:600px){ .ins-body { grid-template-columns:1fr; } }
/* ── Country rows ───────────────────────────────────── */
.ins-country-row { display:flex; align-items:center; gap:10px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.04); cursor:pointer; border-radius:6px; transition:background .12s; }
.ins-country-row:last-child { border-bottom:none; }
.ins-country-row:hover { background:rgba(255,255,255,.04); padding-left:6px; }
.ins-country-flag { font-size:20px; flex-shrink:0; width:28px; text-align:center; line-height:1; }
.ins-country-name { flex:1; font-size:13px; font-weight:500; color:var(--text-primary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ins-country-bar-wrap { width:100px; height:5px; background:rgba(255,255,255,.07); border-radius:4px; overflow:hidden; flex-shrink:0; }
.ins-country-bar { height:100%; background:#ef4444; border-radius:4px; }
.ins-country-pct { font-size:12px; font-weight:700; color:var(--text-secondary); min-width:34px; text-align:right; }
.ins-country-cnt { font-size:11px; color:var(--text-secondary); min-width:40px; text-align:right; }
/* ── Download rows ───────────────────────────────────── */
.ins-dl-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; flex-wrap:wrap; gap:8px; }
.ins-dl-type-pills { display:flex; gap:6px; }
.ins-dl-pill { display:inline-flex; align-items:center; gap:5px; border-radius:20px; padding:3px 11px; font-size:12px; font-weight:600; }
.ins-dl-pill.video { border:1px solid rgba(239,68,68,.3); color:#fca5a5; background:rgba(239,68,68,.08); }
.ins-dl-pill.mp3 { border:1px solid rgba(96,165,250,.3); color:#93c5fd; background:rgba(96,165,250,.08); }
.ins-dl-pill.guest { border:1px solid rgba(255,255,255,.12); color:var(--text-secondary); background:rgba(255,255,255,.05); }
.ins-dl-user-row { display:flex; align-items:center; gap:10px; padding:8px 6px; border-bottom:1px solid rgba(255,255,255,.04); cursor:pointer; border-radius:8px; transition:background .12s; }
.ins-dl-user-row:last-child { border-bottom:none; }
.ins-dl-user-row:hover { background:rgba(255,255,255,.05); }
.ins-dl-avatar { width:34px; height:34px; border-radius:50%; object-fit:cover; flex-shrink:0; background:rgba(255,255,255,.08); }
.ins-dl-user-name { flex:1; font-size:13px; font-weight:600; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ins-dl-user-meta { font-size:11px; color:var(--text-secondary); margin-top:1px; }
.ins-dl-count-badge { flex-shrink:0; background:rgba(239,68,68,.12); border:1px solid rgba(239,68,68,.25); color:#fca5a5; border-radius:20px; font-size:12px; font-weight:700; padding:2px 10px; }
.ins-recent-row { display:flex; align-items:center; gap:10px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.04); font-size:12px; }
.ins-recent-row:last-child { border-bottom:none; }
.ins-recent-avatar { width:28px; height:28px; border-radius:50%; object-fit:cover; flex-shrink:0; background:rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center; font-size:14px; }
.ins-recent-name { flex:1; font-weight:600; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ins-recent-type { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; padding:2px 7px; border-radius:6px; flex-shrink:0; }
.ins-recent-type.video { background:rgba(239,68,68,.12); color:#fca5a5; }
.ins-recent-type.mp3 { background:rgba(96,165,250,.1); color:#93c5fd; }
.ins-recent-time { color:var(--text-secondary); flex-shrink:0; min-width:70px; text-align:right; }
.ins-recent-flag { font-size:16px; flex-shrink:0; }
/* ── Demographics (gender / age) ─────────────────────── */
.ins-demo-row { display:flex; align-items:center; gap:8px; padding:5px 0; }
.ins-demo-sym { font-size:16px; flex-shrink:0; width:18px; text-align:center; }
.ins-demo-label { font-size:12px; font-weight:600; color:var(--text-primary); min-width:52px; }
.ins-age-label { min-width:68px; }
.ins-demo-bar-wrap { flex:1; height:6px; background:rgba(255,255,255,.07); border-radius:4px; overflow:hidden; }
.ins-demo-bar { height:100%; border-radius:4px; transition:width .6s cubic-bezier(.22,.61,.36,1); }
.ins-demo-pct { font-size:12px; font-weight:700; color:var(--text-secondary); min-width:34px; text-align:right; }
.ins-demo-cnt { font-size:11px; color:var(--text-secondary); min-width:30px; text-align:right; }
/* ── Skeleton ─────────────────────────────────────────── */
.ins-skeleton { display:flex; flex-direction:column; gap:12px; padding:4px 0; }
.ins-skel-row { height:14px; border-radius:6px; background:rgba(255,255,255,.06); animation:skelPulse 1.4s ease-in-out infinite; }
@keyframes skelPulse { 0%,100%{opacity:.5} 50%{opacity:1} }
/* ── Insight modal ───────────────────────────────────── */
.ins-modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:9200; display:flex; align-items:center; justify-content:center; padding:16px; animation:insBackIn .15s ease; }
@keyframes insBackIn { from{opacity:0} to{opacity:1} }
.ins-modal { background:var(--bg-secondary); border:1px solid var(--border-color); border-radius:18px; width:100%; max-width:500px; max-height:84vh; display:flex; flex-direction:column; overflow:hidden; animation:insModalIn .22s cubic-bezier(.34,1.4,.64,1); }
@keyframes insModalIn { from{opacity:0;transform:scale(.88) translateY(20px)} to{opacity:1;transform:none} }
.ins-modal-head { display:flex; align-items:center; gap:12px; padding:16px 18px 13px; border-bottom:1px solid var(--border-color); flex-shrink:0; }
.ins-modal-icon { font-size:26px; line-height:1; }
.ins-modal-titles { flex:1; min-width:0; }
.ins-modal-title { font-size:15px; font-weight:700; color:var(--text-primary); }
.ins-modal-subtitle { font-size:12px; color:var(--text-secondary); margin-top:1px; }
.ins-modal-close { background:rgba(255,255,255,.06); border:none; color:var(--text-secondary); border-radius:50%; width:30px; height:30px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:16px; transition:background .15s,color .15s; flex-shrink:0; }
.ins-modal-close:hover { background:rgba(239,68,68,.2); color:#fca5a5; }
.ins-modal-body { padding:18px 18px 22px; overflow-y:auto; flex:1; }
/* Modal content pieces */
.ins-modal-hero { text-align:center; padding:4px 0 20px; }
.ins-modal-hero-num { font-size:52px; font-weight:900; line-height:1; color:var(--text-primary); }
.ins-modal-hero-flag { font-size:56px; line-height:1; margin-bottom:4px; }
.ins-modal-hero-label { font-size:13px; color:var(--text-secondary); margin-top:6px; }
.ins-modal-stat { display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid rgba(255,255,255,.05); font-size:13px; }
.ins-modal-stat:last-child { border-bottom:none; }
.ins-modal-stat-lbl { color:var(--text-secondary); font-weight:500; display:flex; align-items:center; gap:7px; }
.ins-modal-stat-val { font-weight:700; color:var(--text-primary); }
.ins-modal-section { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-secondary); margin:18px 0 10px; display:flex; align-items:center; gap:6px; }
.ins-modal-section:first-child { margin-top:0; }
/* Hourly chart (24 bars) */
.ins-hourly-chart { display:flex; align-items:flex-end; gap:2px; height:60px; margin-bottom:5px; }
.ins-hourly-col { flex:1; display:flex; flex-direction:column; align-items:center; gap:2px; min-width:0; }
.ins-hourly-bar { width:100%; border-radius:3px 3px 0 0; background:#ef444455; min-height:2px; transition:height .5s cubic-bezier(.22,.61,.36,1); }
.ins-hourly-bar.peak { background:#ef4444; }
.ins-hourly-lbl { font-size:7px; color:var(--text-secondary); text-align:center; overflow:hidden; white-space:nowrap; }
/* Person list */
.ins-person-row { display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid rgba(255,255,255,.04); cursor:pointer; border-radius:6px; transition:background .12s; }
.ins-person-row:last-child { border-bottom:none; }
.ins-person-row:hover { background:rgba(255,255,255,.04); padding-left:6px; }
.ins-person-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; background:rgba(255,255,255,.08); }
.ins-person-name { flex:1; font-size:13px; font-weight:600; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ins-person-meta { font-size:11px; color:var(--text-secondary); margin-top:1px; }
.ins-person-badge { flex-shrink:0; background:rgba(239,68,68,.1); border:1px solid rgba(239,68,68,.25); color:#fca5a5; border-radius:20px; font-size:11px; font-weight:700; padding:2px 9px; }
.ins-guest-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid rgba(255,255,255,.04); font-size:12px; }
.ins-guest-row:last-child { border-bottom:none; }
.ins-guest-flag-big { font-size:22px; flex-shrink:0; }
/* Download history entries */
.ins-dl-hist-row { display:flex; align-items:center; gap:10px; padding:9px 12px; border-radius:8px; background:rgba(255,255,255,.03); margin-bottom:6px; font-size:12px; }
.ins-dl-hist-num { font-size:11px; font-weight:700; color:var(--text-secondary); min-width:18px; text-align:center; }
.ins-dl-hist-type { font-size:10px; font-weight:700; text-transform:uppercase; padding:2px 8px; border-radius:6px; flex-shrink:0; }
.ins-dl-hist-type.video { background:rgba(239,68,68,.12); color:#fca5a5; }
.ins-dl-hist-type.mp3 { background:rgba(96,165,250,.1); color:#93c5fd; }
.ins-dl-hist-time { flex:1; color:var(--text-secondary); }
.ins-dl-hist-flag { font-size:15px; flex-shrink:0; }
/* Loading state inside modal */
.ins-modal-loading { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:40px 0; gap:10px; color:var(--text-secondary); font-size:13px; }
.ins-modal-spin { width:28px; height:28px; border:3px solid rgba(255,255,255,.1); border-top-color:#ef4444; border-radius:50%; animation:spin .7s linear infinite; }
@keyframes spin { to{transform:rotate(360deg)} }
</style>
{{-- Panel: placed inside .vdb-wrap right after the About panel --}}
<div class="vdb-panel" id="vdb-insights">
<div id="insightsContent">
<div class="ins-skeleton">
<div class="ins-skel-row" style="width:60%;height:18px;"></div>
<div class="ins-skel-row" style="width:100%;height:80px;"></div>
<div class="ins-skel-row" style="width:80%;"></div>
<div class="ins-skel-row" style="width:90%;"></div>
<div class="ins-skel-row" style="width:70%;"></div>
</div>
</div>
</div>
{{-- Modal (position:fixed, nesting doesn't matter) --}}
<div id="insModalBackdrop" class="ins-modal-backdrop" style="display:none;" onclick="if(event.target===this)closeInsModal()">
<div class="ins-modal" onclick="event.stopPropagation()">
<div class="ins-modal-head">
<span class="ins-modal-icon" id="insModalIcon"></span>
<div class="ins-modal-titles">
<div class="ins-modal-title" id="insModalTitle"></div>
<div class="ins-modal-subtitle" id="insModalSubtitle"></div>
</div>
<button class="ins-modal-close" onclick="closeInsModal()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="ins-modal-body" id="insModalBody"></div>
</div>
</div>
<script>
window._insLoaded = false;
window._insData = null;
// ── Helpers ────────────────────────────────────────────
const _cflag = code => (!code||code.length!==2) ? '🌐' : code.toUpperCase().split('').map(c=>String.fromCodePoint(0x1F1E6+c.charCodeAt(0)-65)).join('');
const _fmt = n => { n=n??0; if(n>=1e6) return (n/1e6).toFixed(1).replace(/\.0$/,'')+'M'; if(n>=1e3) return (n/1e3).toFixed(1).replace(/\.0$/,'')+'K'; return String(n); };
const _fmtH = h => (h===null||h===undefined) ? '—' : (h%12||12)+':00 '+(h>=12?'PM':'AM');
const _ago = s => { const d=Math.floor((Date.now()-new Date(s))/1000); if(d<60) return d+'s ago'; if(d<3600) return Math.floor(d/60)+'m ago'; if(d<86400) return Math.floor(d/3600)+'h ago'; return Math.floor(d/86400)+'d ago'; };
const _fmtDt = s => new Date(s).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
// ── Modal helpers ──────────────────────────────────────
function _openModal(icon, title, subtitle, html) {
document.getElementById('insModalIcon').textContent = icon;
document.getElementById('insModalTitle').textContent = title;
document.getElementById('insModalSubtitle').textContent = subtitle || '';
document.getElementById('insModalBody').innerHTML = html;
document.getElementById('insModalBackdrop').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function _modalLoading(icon, title, subtitle) {
_openModal(icon, title, subtitle,
`<div class="ins-modal-loading"><div class="ins-modal-spin"></div>Loading details…</div>`);
}
function closeInsModal() {
document.getElementById('insModalBackdrop').style.display = 'none';
document.body.style.overflow = '';
}
document.addEventListener('keydown', e => { if(e.key==='Escape') closeInsModal(); });
// ── Person list HTML (shared) ──────────────────────────
function _personListHtml(users, emptyMsg) {
if (!users || !users.length) return `<p style="font-size:13px;color:var(--text-secondary);margin:0;">${emptyMsg}</p>`;
return users.map((u,i) => `
<div class="ins-person-row" onclick="window.location.href='/channel/${u.channel||u.id}'" title="View ${u.name}'s profile">
<div style="font-size:11px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<img src="${u.avatar}" alt="${u.name}" class="ins-person-avatar">
<div style="flex:1;min-width:0;">
<div class="ins-person-name">${u.name}</div>
<div class="ins-person-meta">Last seen ${_ago(u.last_at)}</div>
</div>
<div class="ins-person-badge">👁 ${u.count}×</div>
</div>`).join('');
}
// ── Mini bar chart HTML (shared) ───────────────────────
function _miniBarChart(daily) {
const max = Math.max(...daily.map(d=>d.count), 1);
const today = new Date().toLocaleDateString('en-US',{month:'short',day:'numeric'});
return daily.map(d => {
const h = Math.round((d.count/max)*56);
const isT = d.label === today;
return `<div class="ins-bar-col">
<div class="ins-bar${isT?' ins-bar-today':''}" style="height:${Math.max(h,2)}px;" title="${d.label}: ${d.count}"></div>
<div class="ins-bar-label">${d.short}</div>
</div>`;
}).join('');
}
// ── Hourly chart HTML ──────────────────────────────────
function _hourlyChart(hourly) {
const max = Math.max(...hourly.map(h=>h.count), 1);
const peak = hourly.reduce((a,b)=>b.count>a.count?b:a, hourly[0]);
const bars = hourly.map(h => {
const ht = Math.round((h.count/max)*54);
const isPk = h.hour === peak.hour && peak.count > 0;
return `<div class="ins-hourly-col">
<div class="ins-hourly-bar${isPk?' peak':''}" style="height:${Math.max(ht,2)}px;" title="${h.label}: ${h.count}"></div>
<div class="ins-hourly-lbl">${h.hour%6===0?h.label:''}</div>
</div>`;
}).join('');
const peakH = peak.count > 0
? `<div style="margin-top:8px;"><span class="ins-peak-badge"><i class="bi bi-clock-fill"></i> Peak ${_fmtH(peak.hour)} · ${peak.count} views</span></div>`
: '';
return `<div class="ins-hourly-chart">${bars}</div>${peakH}`;
}
// ══════════════════════════════════════════════════════
// LOAD MAIN INSIGHTS
// ══════════════════════════════════════════════════════
function loadInsights() {
fetch('{{ route('videos.insights', $video) }}', { headers:{'Accept':'application/json','X-Requested-With':'XMLHttpRequest'} })
.then(r=>{ if(!r.ok) throw r; return r.json(); })
.then(d => { window._insLoaded=true; window._insData=d; renderInsights(d); })
.catch(() => {
document.getElementById('insightsContent').innerHTML =
'<p style="color:var(--text-secondary);font-size:13px;padding:8px 0;">Could not load insights. Try again later.</p>';
});
}
function renderInsights(d) {
const weekBadge = d.week_change>0
? `<span class="ins-card-sub up">↑ ${d.week_change}% vs last week</span>`
: d.week_change<0
? `<span class="ins-card-sub down">↓ ${Math.abs(d.week_change)}% vs last week</span>`
: `<span class="ins-card-sub neu">— same as last week</span>`;
const dlSub = (d.dl_video||d.dl_mp3)
? `<span class="ins-card-sub neu">🎬 ${_fmt(d.dl_video)} · 🎵 ${_fmt(d.dl_mp3)}</span>`
: `<span class="ins-card-sub neu">no downloads yet</span>`;
// ── Stat cards (open top-level modal on click) ──────
const cards = `
<p style="font-size:11px;color:var(--text-secondary);margin:0 0 8px;display:flex;align-items:center;gap:5px;">
<i class="bi bi-cursor-fill"></i> Tap any card, chart bar, or country for a detailed breakdown
</p>
<div class="ins-grid">
<div class="ins-card" onclick="openInsModal_views()">
<div class="ins-card-icon">👁️</div><div class="ins-card-val">${_fmt(d.total_views)}</div>
<div class="ins-card-label">Total Views</div>${weekBadge}
</div>
<div class="ins-card" onclick="openInsModal_viewers()">
<div class="ins-card-icon">👤</div><div class="ins-card-val">${_fmt(d.unique_viewers)}</div>
<div class="ins-card-label">Unique Viewers</div>
<span class="ins-card-sub neu">${_fmt(d.views_today)} today</span>
</div>
<div class="ins-card" onclick="openInsModal_downloads()">
<div class="ins-card-icon">⬇️</div><div class="ins-card-val">${_fmt(d.downloads)}</div>
<div class="ins-card-label">Downloads</div>${dlSub}
</div>
<div class="ins-card" onclick="openInsModal_shares()">
<div class="ins-card-icon">🔗</div><div class="ins-card-val">${_fmt(d.shares)}</div>
<div class="ins-card-label">Share Reach</div>
<span class="ins-card-sub neu">${_fmt(d.share_links)} link${d.share_links===1?'':'s'} created</span>
</div>
</div>`;
// ── Bar chart (each bar clickable) ──────────────────
const maxV = Math.max(...d.daily.map(x=>x.count),1);
const todayL = new Date().toLocaleDateString('en-US',{month:'short',day:'numeric'});
const bars = d.daily.map(day => {
const h = Math.round((day.count/maxV)*72);
const isT = day.label===todayL;
const tip = `${day.label}: ${day.count} views`;
return `<div class="ins-bar-col">
<div class="ins-bar clickable${isT?' ins-bar-today':''}" style="height:${Math.max(h,2)}px;"
title="${tip}" onclick="openDayDetail('${day.date}','${day.label}',${day.count})"></div>
<div class="ins-bar-label">${day.short}</div>
</div>`;
}).join('');
const peakHtml = d.peak_hour!==null
? `<div style="margin-top:10px;"><span class="ins-peak-badge"><i class="bi bi-clock-fill"></i> Peak hour: ${_fmtH(d.peak_hour)}</span></div>` : '';
const chartSection = `
<div>
<div class="ins-section-title"><i class="bi bi-graph-up"></i> Views — last 14 days <span style="font-size:10px;opacity:.5;font-weight:400;text-transform:none;">(tap bar for day detail)</span></div>
<div class="ins-chart">${bars}</div>
<div style="font-size:11px;color:var(--text-secondary);text-align:right;">${d.views_this_week} views this week</div>
${peakHtml}
</div>`;
// ── Countries (each row clickable) ──────────────────
let countriesHtml = '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No location data yet.</p>';
if (d.countries && d.countries.length) {
countriesHtml = d.countries.map(c => `
<div class="ins-country-row" onclick="openCountryDetail('${c.code}','${c.name}',${c.count})" title="View ${c.name||c.code} details">
<div class="ins-country-flag">${_cflag(c.code)}</div>
<div class="ins-country-name">${c.name||c.code}</div>
<div class="ins-country-bar-wrap"><div class="ins-country-bar" style="width:${c.pct}%;"></div></div>
<div class="ins-country-pct">${c.pct}%</div>
<div class="ins-country-cnt">${_fmt(c.count)}</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);flex-shrink:0;"></i>
</div>`).join('');
}
// ── Viewers section ─────────────────────────────────
const topViewerRows = (d.top_viewers || []).map((u, i) => `
<div class="ins-dl-user-row" onclick="window.location.href='/channel/${u.channel||u.id}'" title="View ${u.name}'s profile">
<div style="font-size:12px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<img src="${u.avatar}" alt="${u.name}" class="ins-dl-avatar">
<div style="flex:1;min-width:0;">
<div class="ins-dl-user-name">${u.name}</div>
<div class="ins-dl-user-meta">Last seen ${_ago(u.last_at)}</div>
</div>
<div class="ins-dl-count-badge">👁 ${u.count}×</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);flex-shrink:0;margin-left:4px;"></i>
</div>`).join('');
const recentViewerRows = (d.recent_viewers || []).map(r => {
const avatarSrc = r.user_avatar || `https://i.pravatar.cc/150?u=guest-${r.country||'unknown'}`;
const clickAttr = r.user_id ? `onclick="window.location.href='/channel/${r.user_channel||r.user_id}'" style="cursor:pointer;" title="View ${r.user_name}'s profile"` : '';
return `<div class="ins-recent-row" ${clickAttr}>
<img src="${avatarSrc}" class="ins-recent-avatar" alt="${r.user_name}">
<div class="ins-recent-name">${r.user_name}</div>
<span class="ins-recent-flag">${_cflag(r.country)}</span>
<span class="ins-recent-time">${_ago(r.at)}</span>
</div>`;
}).join('');
const viewersHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-dl-header">
<div class="ins-section-title" style="margin:0;"><i class="bi bi-people-fill"></i> Viewers — ${_fmt(d.unique_viewers)} registered · ${_fmt(d.guest_views||0)} guest</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">Top Viewers</div>
${topViewerRows || '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No registered viewers yet.</p>'}
</div>
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">Recent Activity</div>
${recentViewerRows || '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No views yet.</p>'}
</div>
</div>
</div>`;
// ── Downloads section ───────────────────────────────
let dlHtml = '';
if (d.downloads===0) {
dlHtml = `<div style="margin-top:20px;"><div class="ins-section-title"><i class="bi bi-download"></i> Downloads</div>
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No downloads yet.</p></div>`;
} else {
const pills = [
d.dl_video ? `<span class="ins-dl-pill video"><i class="bi bi-film"></i> ${_fmt(d.dl_video)} video</span>` : '',
d.dl_mp3 ? `<span class="ins-dl-pill mp3"><i class="bi bi-music-note"></i> ${_fmt(d.dl_mp3)} MP3</span>` : '',
d.dl_guests ? `<span class="ins-dl-pill guest"><i class="bi bi-person"></i> ${_fmt(d.dl_guests)} guest</span>` : '',
].filter(Boolean).join('');
const usersRows = d.dl_users && d.dl_users.length
? d.dl_users.map((u,i) => `
<div class="ins-dl-user-row" onclick="openDownloaderHistory(${u.id},'${u.name.replace(/'/g,"\\'")}','${u.avatar}')" title="See full download history">
<div style="font-size:12px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<img src="${u.avatar}" alt="${u.name}" class="ins-dl-avatar">
<div style="flex:1;min-width:0;">
<div class="ins-dl-user-name">${u.name}</div>
<div class="ins-dl-user-meta">Last: ${_ago(u.last_at)}</div>
</div>
<div class="ins-dl-count-badge">⬇️ ${u.count}×</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);flex-shrink:0;margin-left:4px;"></i>
</div>`).join('')
: '<p style="font-size:13px;color:var(--text-secondary);margin:0;">Guests only so far.</p>';
const recentRows = (d.dl_recent||[]).map(r => {
const avatarSrc = r.user_avatar || `https://i.pravatar.cc/150?u=guest-${r.country||'unknown'}`;
const av = `<img src="${avatarSrc}" class="ins-recent-avatar" alt="${r.user_name}">`;
return `<div class="ins-recent-row">${av}
<div class="ins-recent-name">${r.user_name}</div>
<span class="ins-recent-flag">${_cflag(r.country)}</span>
<span class="ins-recent-type ${r.type}">${r.type.toUpperCase()}</span>
<span class="ins-recent-time">${_ago(r.at)}</span>
</div>`;
}).join('');
dlHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-dl-header">
<div class="ins-section-title" style="margin:0;"><i class="bi bi-download"></i> Downloads — ${_fmt(d.downloads)} total</div>
<div class="ins-dl-type-pills">${pills}</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">
Top Downloaders <span style="font-weight:400;opacity:.5;">(tap to see history)</span>
</div>
${usersRows}
</div>
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">Recent Activity</div>
${recentRows}
</div>
</div>
</div>`;
}
// ── Gender breakdown ────────────────────────────────
let genderHtml;
if (d.genders && d.genders.length > 0) {
const gColors = { male: '#4a9eff', female: '#ff6eb0' };
const gSymbols = { male: '♂', female: '♀' };
genderHtml = d.genders.map(g => {
const color = gColors[g.gender] || '#aaa';
const sym = gSymbols[g.gender] || '⚧';
return `<div class="ins-demo-row">
<span class="ins-demo-sym" style="color:${color}">${sym}</span>
<span class="ins-demo-label">${g.gender.charAt(0).toUpperCase()+g.gender.slice(1)}</span>
<div class="ins-demo-bar-wrap"><div class="ins-demo-bar" style="width:${g.pct}%;background:${color}"></div></div>
<span class="ins-demo-pct">${g.pct}%</span>
<span class="ins-demo-cnt">${_fmt(g.count)}</span>
</div>`;
}).join('');
} else {
genderHtml = `<p style="font-size:13px;color:var(--text-secondary);margin:0;">No gender data yet — viewers need a profile to appear here.</p>`;
}
// ── Age group breakdown ──────────────────────────────
let ageHtml;
if (d.age_groups && d.age_groups.length > 0) {
const maxAge = Math.max(...d.age_groups.map(g => g.count), 1);
ageHtml = d.age_groups.map(g => {
const barW = Math.round(g.count / maxAge * 100);
return `<div class="ins-demo-row">
<span class="ins-demo-label ins-age-label">${g.label}</span>
<div class="ins-demo-bar-wrap"><div class="ins-demo-bar" style="width:${barW}%;background:var(--brand-red,#e61e1e)"></div></div>
<span class="ins-demo-pct">${g.pct}%</span>
<span class="ins-demo-cnt">${_fmt(g.count)}</span>
</div>`;
}).join('');
} else {
ageHtml = `<p style="font-size:13px;color:var(--text-secondary);margin:0;">No age data yet — viewers need a date of birth on their profile to appear here.</p>`;
}
const demographicsHtml = `
<div style="margin-top:18px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-body">
<div>
<div class="ins-section-title"><i class="bi bi-gender-ambiguous"></i> Viewers by Gender</div>
${genderHtml}
</div>
<div>
<div class="ins-section-title"><i class="bi bi-people-fill"></i> Viewers by Age Group</div>
${ageHtml}
</div>
</div>
</div>`;
// ── Share links breakdown ───────────────────────────
let shareHtml = '';
if (d.share_links > 0) {
const linkRows = (d.share_breakdown || []).map((s, i) => `
<div class="ins-dl-user-row" style="cursor:default;">
<div style="font-size:12px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<div style="flex:1;min-width:0;">
<div class="ins-dl-user-name">${s.sharer}</div>
<div class="ins-dl-user-meta">${_ago(s.created_at)}</div>
</div>
<div class="ins-dl-count-badge" title="Unique devices that opened this link">👁️ ${_fmt(s.reach)}</div>
</div>`).join('');
shareHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px;">
<div class="ins-section-title" style="margin:0;"><i class="bi bi-share"></i> Share Links — ${_fmt(d.share_links)} created · ${_fmt(d.shares)} unique devices reached</div>
</div>
<p style="font-size:11px;color:var(--text-secondary);margin:0 0 10px;line-height:1.5;">Each sharer gets a unique link. The same device opening the same link is counted only once — VPN IP changes don't affect the count.</p>
${linkRows}
</div>`;
} else {
shareHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-section-title"><i class="bi bi-share"></i> Share Links</div>
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No share links created yet. Tap the Share button to generate a unique tracked link.</p>
</div>`;
}
document.getElementById('insightsContent').innerHTML =
cards +
`<div class="ins-body">${chartSection}
<div>
<div class="ins-section-title"><i class="bi bi-globe2"></i> Audience by Country <span style="font-size:10px;opacity:.5;font-weight:400;text-transform:none;">(tap for viewer breakdown)</span></div>
${countriesHtml}
</div></div>` + viewersHtml + dlHtml + shareHtml + demographicsHtml;
}
// ══════════════════════════════════════════════════════
// STAT CARD MODALS (top-level summaries)
// ══════════════════════════════════════════════════════
function openInsModal_views() {
const d = window._insData; if(!d) return;
const changeColor = d.week_change>0?'#4ade80':d.week_change<0?'#f87171':'var(--text-secondary)';
const changeLabel = d.week_change>0?`↑ ${d.week_change}%`:d.week_change<0?`↓ ${Math.abs(d.week_change)}%`:'— 0%';
_openModal('👁️','View Analytics','All-time stats',`
<div class="ins-modal-hero">
<div class="ins-modal-hero-num">${_fmt(d.total_views)}</div>
<div class="ins-modal-hero-label">total views</div>
</div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-calendar-day"></i> Today</span><span class="ins-modal-stat-val">${_fmt(d.views_today)}</span></div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-calendar-week"></i> This week</span><span class="ins-modal-stat-val">${_fmt(d.views_this_week)}</span></div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-calendar2-minus"></i> Last week</span><span class="ins-modal-stat-val">${_fmt(d.views_last_week)}</span></div>
<div class="ins-modal-stat" style="border-bottom:none;margin-bottom:18px;">
<span class="ins-modal-stat-lbl"><i class="bi bi-graph-up-arrow"></i> Week over week</span>
<span class="ins-modal-stat-val" style="color:${changeColor};">${changeLabel}</span>
</div>
<div class="ins-modal-section"><i class="bi bi-graph-up"></i> Last 14 days — tap a bar to see that day</div>
<div class="ins-chart" style="height:72px;">${window._insData.daily.map(day=>{
const h=Math.round((day.count/Math.max(...window._insData.daily.map(x=>x.count),1))*68);
const isT=day.label===new Date().toLocaleDateString('en-US',{month:'short',day:'numeric'});
return `<div class="ins-bar-col"><div class="ins-bar clickable${isT?' ins-bar-today':''}" style="height:${Math.max(h,2)}px;" title="${day.label}: ${day.count}" onclick="closeInsModal();setTimeout(()=>openDayDetail('${day.date}','${day.label}',${day.count}),180)"></div><div class="ins-bar-label">${day.short}</div></div>`;
}).join('')}</div>
${d.peak_hour!==null?`<div style="margin-top:10px;"><span class="ins-peak-badge"><i class="bi bi-clock-fill"></i> Peak hour: ${_fmtH(d.peak_hour)}</span></div>`:''}`);
}
function openInsModal_viewers() {
const d = window._insData; if(!d) return;
const guestV = Math.max(0, d.total_views - d.unique_viewers);
const mPct = d.total_views>0 ? Math.round(d.unique_viewers/d.total_views*100) : 0;
_openModal('👤','Viewer Breakdown','Registered vs anonymous',`
<div class="ins-modal-hero">
<div class="ins-modal-hero-num">${_fmt(d.unique_viewers)}</div>
<div class="ins-modal-hero-label">unique registered viewers</div>
</div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-eye"></i> Total views</span><span class="ins-modal-stat-val">${_fmt(d.total_views)}</span></div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Registered</span><span class="ins-modal-stat-val">${mPct}% <span style="color:var(--text-secondary);font-weight:400;">(${_fmt(d.unique_viewers)} views)</span></span></div>
<div class="ins-modal-stat" style="border-bottom:none;margin-bottom:18px;"><span class="ins-modal-stat-lbl"><i class="bi bi-person-dash"></i> Anonymous</span><span class="ins-modal-stat-val">${100-mPct}% <span style="color:var(--text-secondary);font-weight:400;">(${_fmt(guestV)} views)</span></span></div>
<div class="ins-modal-section"><i class="bi bi-globe2"></i> Top countries — tap to see viewers</div>
${(d.countries||[]).slice(0,5).map(c=>`
<div class="ins-country-row" onclick="closeInsModal();setTimeout(()=>openCountryDetail('${c.code}','${c.name}',${c.count}),180)" style="cursor:pointer;">
<div class="ins-country-flag">${_cflag(c.code)}</div>
<div class="ins-country-name">${c.name||c.code}</div>
<div class="ins-country-bar-wrap"><div class="ins-country-bar" style="width:${c.pct}%;"></div></div>
<div class="ins-country-pct">${c.pct}%</div>
<div class="ins-country-cnt">${_fmt(c.count)}</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);"></i>
</div>`).join('')||'<p style="font-size:13px;color:var(--text-secondary);margin:0;">No location data.</p>'}`);
}
function openInsModal_downloads() {
const d = window._insData; if(!d) return;
if (d.downloads===0) {
_openModal('⬇️','Downloads','No downloads yet',`<div style="text-align:center;padding:30px 0;"><div style="font-size:48px;margin-bottom:12px;">📭</div><p style="color:var(--text-secondary);font-size:14px;margin:0;">No one has downloaded this video yet.</p></div>`);
return;
}
const engRate = d.total_views>0 ? ((d.downloads/d.total_views)*100).toFixed(1) : '0.0';
_openModal('⬇️','Download Summary',`${_fmt(d.downloads)} total downloads`,`
<div class="ins-modal-hero">
<div class="ins-modal-hero-num">${_fmt(d.downloads)}</div>
<div class="ins-modal-hero-label">total downloads</div>
</div>
<div style="display:flex;gap:8px;margin-bottom:18px;flex-wrap:wrap;">
${d.dl_video ? `<span class="ins-dl-pill video"><i class="bi bi-film"></i> ${_fmt(d.dl_video)} video</span>` : ''}
${d.dl_mp3 ? `<span class="ins-dl-pill mp3"><i class="bi bi-music-note"></i> ${_fmt(d.dl_mp3)} MP3</span>` : ''}
${d.dl_guests ? `<span class="ins-dl-pill guest"><i class="bi bi-person"></i> ${_fmt(d.dl_guests)} guest</span>` : ''}
</div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-eye"></i> Download rate</span><span class="ins-modal-stat-val">${engRate}% of viewers</span></div>
<div class="ins-modal-stat" style="border-bottom:none;margin-bottom:18px;"><span class="ins-modal-stat-lbl"><i class="bi bi-people"></i> Unique downloaders</span><span class="ins-modal-stat-val">${_fmt((d.dl_users||[]).length + (d.dl_guests>0?1:0))}</span></div>
${(d.dl_users||[]).length ? `
<div class="ins-modal-section"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Registered — tap for full history</div>
${d.dl_users.map((u,i)=>`
<div class="ins-person-row" style="cursor:pointer;" onclick="closeInsModal();setTimeout(()=>openDownloaderHistory(${u.id},'${u.name.replace(/'/g,"\\'")}','${u.avatar}'),180)">
<div style="font-size:11px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<img src="${u.avatar}" class="ins-person-avatar" alt="${u.name}">
<div style="flex:1;min-width:0;"><div class="ins-person-name">${u.name}</div><div class="ins-person-meta">Last: ${_ago(u.last_at)}</div></div>
<div class="ins-person-badge">⬇️ ${u.count}×</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);margin-left:6px;flex-shrink:0;"></i>
</div>`).join('')}` : ''}
`);
}
function openInsModal_shares() {
const d = window._insData; if(!d) return;
const engRate = d.total_views>0 ? (((d.shares)+d.likes)/d.total_views*100).toFixed(1) : '0.0';
const linkRows = (d.share_breakdown||[]).length
? (d.share_breakdown||[]).map((s,i) => `
<div class="ins-dl-user-row" style="cursor:default;">
<div style="font-size:12px;font-weight:700;color:var(--text-secondary);min-width:18px;text-align:center;">${i+1}</div>
<div style="flex:1;min-width:0;">
<div class="ins-dl-user-name">${s.sharer}</div>
<div class="ins-dl-user-meta">Created: ${_ago(s.created_at)}</div>
</div>
<div class="ins-dl-count-badge">👁️ ${_fmt(s.reach)}</div>
</div>`).join('')
: '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No share links created yet.</p>';
_openModal('🔗','Share Reach','Unique devices that opened your share links',`
<div class="ins-modal-hero">
<div class="ins-modal-hero-num">${_fmt(d.shares)}</div>
<div class="ins-modal-hero-label">unique devices reached</div>
</div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-link-45deg"></i> Links created</span><span class="ins-modal-stat-val">${_fmt(d.share_links)}</span></div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-hand-thumbs-up-fill" style="color:#ef4444;"></i> Likes</span><span class="ins-modal-stat-val">❤️ ${_fmt(d.likes)}</span></div>
<div class="ins-modal-stat"><span class="ins-modal-stat-lbl"><i class="bi bi-activity"></i> Engagement rate</span><span class="ins-modal-stat-val">${engRate}%</span></div>
<div class="ins-modal-section" style="margin-top:16px;"><i class="bi bi-people-fill"></i> Share links — top by reach</div>
${linkRows}
<p style="font-size:11px;color:var(--text-secondary);margin:12px 0 0;line-height:1.5;">Each person who shares gets a unique link. Same device opening the same link counts only once.</p>`);
}
// ══════════════════════════════════════════════════════
// DRILL-DOWN: COUNTRY
// ══════════════════════════════════════════════════════
function openCountryDetail(code, name, totalCount) {
const flag = _cflag(code);
_modalLoading(flag, name||code, `${_fmt(totalCount)} views from this country`);
fetch(`{{ url('/videos/' . $video->getRouteKey() . '/insights/country') }}/${code}`, {
headers:{'Accept':'application/json','X-Requested-With':'XMLHttpRequest'}
})
.then(r=>{ if(!r.ok) throw r; return r.json(); })
.then(d => {
const regHtml = _personListHtml(d.registered_users, 'No registered viewers yet from this country.');
const miniChart = _miniBarChart(d.daily);
document.getElementById('insModalBody').innerHTML = `
<div class="ins-modal-hero">
<div class="ins-modal-hero-flag">${flag}</div>
<div class="ins-modal-hero-num">${_fmt(d.total_views)}</div>
<div class="ins-modal-hero-label">total views from ${d.country_name||code}</div>
</div>
<div class="ins-modal-stat">
<span class="ins-modal-stat-lbl"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Registered viewers</span>
<span class="ins-modal-stat-val">${_fmt((d.registered_users||[]).length)}</span>
</div>
<div class="ins-modal-stat" style="border-bottom:none;margin-bottom:18px;">
<span class="ins-modal-stat-lbl"><i class="bi bi-person-dash"></i> Anonymous views</span>
<span class="ins-modal-stat-val">${_fmt(d.guest_count)}</span>
</div>
<div class="ins-modal-section"><i class="bi bi-graph-up"></i> 14-day trend from ${d.country_name||code}</div>
<div class="ins-chart" style="height:60px;">${miniChart}</div>
${(d.registered_users||[]).length ? `
<div class="ins-modal-section"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Registered viewers</div>
${regHtml}` : `
<div class="ins-modal-section"><i class="bi bi-people"></i> Viewers</div>
<p style="font-size:13px;color:var(--text-secondary);margin:0;">
All ${_fmt(d.total_views)} views from ${d.country_name||code} were anonymous.
</p>`}`;
document.getElementById('insModalTitle').textContent = d.country_name || code;
document.getElementById('insModalSubtitle').textContent = `${_fmt(d.total_views)} views from this country`;
})
.catch(() => {
document.getElementById('insModalBody').innerHTML =
'<p style="color:var(--text-secondary);font-size:13px;padding:8px 0;">Could not load country details.</p>';
});
}
// ══════════════════════════════════════════════════════
// DRILL-DOWN: SPECIFIC DAY
// ══════════════════════════════════════════════════════
function openDayDetail(date, label, totalCount) {
_modalLoading('📅', label, `${_fmt(totalCount)} views on this day`);
fetch(`{{ url('/videos/' . $video->getRouteKey() . '/insights/day') }}/${date}`, {
headers:{'Accept':'application/json','X-Requested-With':'XMLHttpRequest'}
})
.then(r=>{ if(!r.ok) throw r; return r.json(); })
.then(d => {
const hourlyHtml = _hourlyChart(d.hourly);
const regHtml = _personListHtml(d.registered_users, 'No registered viewers on this day.');
const topCountries = (d.countries||[]).map(c=>`
<div class="ins-country-row" style="cursor:pointer;" onclick="closeInsModal();setTimeout(()=>openCountryDetail('${c.code}','${c.name}',${c.count}),180)">
<div class="ins-country-flag">${_cflag(c.code)}</div>
<div class="ins-country-name">${c.name||c.code}</div>
<div class="ins-country-cnt" style="margin-left:auto;">${_fmt(c.count)}</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);"></i>
</div>`).join('') || '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No location data.</p>';
document.getElementById('insModalIcon').textContent = '📅';
document.getElementById('insModalTitle').textContent = `${d.day_of_week}, ${d.date}`;
document.getElementById('insModalSubtitle').textContent = `${_fmt(d.total_views)} views on this day`;
document.getElementById('insModalBody').innerHTML = `
<div class="ins-modal-hero">
<div class="ins-modal-hero-num">${_fmt(d.total_views)}</div>
<div class="ins-modal-hero-label">views on ${d.day_of_week}</div>
</div>
<div class="ins-modal-stat">
<span class="ins-modal-stat-lbl"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Registered</span>
<span class="ins-modal-stat-val">${_fmt((d.registered_users||[]).length)} viewers (${_fmt(d.total_views - d.guest_count)} views)</span>
</div>
<div class="ins-modal-stat" style="border-bottom:none;margin-bottom:18px;">
<span class="ins-modal-stat-lbl"><i class="bi bi-person-dash"></i> Anonymous</span>
<span class="ins-modal-stat-val">${_fmt(d.guest_count)} views</span>
</div>
<div class="ins-modal-section"><i class="bi bi-clock"></i> Views by Hour</div>
${hourlyHtml}
${(d.registered_users||[]).length ? `
<div class="ins-modal-section" style="margin-top:18px;"><i class="bi bi-person-check-fill" style="color:#4ade80;"></i> Who watched</div>
${regHtml}` : ''}
<div class="ins-modal-section" style="margin-top:18px;"><i class="bi bi-globe2"></i> Top countries — tap to explore</div>
${topCountries}`;
})
.catch(() => {
document.getElementById('insModalBody').innerHTML =
'<p style="color:var(--text-secondary);font-size:13px;padding:8px 0;">Could not load day details.</p>';
});
}
// ══════════════════════════════════════════════════════
// DRILL-DOWN: DOWNLOADER HISTORY
// ══════════════════════════════════════════════════════
function openDownloaderHistory(userId, userName, userAvatar) {
_modalLoading('⬇️', userName, 'Loading download history…');
document.getElementById('insModalIcon').innerHTML = `<img src="${userAvatar}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;">`;
fetch(`{{ url('/videos/' . $video->getRouteKey() . '/insights/downloader') }}/${userId}`, {
headers:{'Accept':'application/json','X-Requested-With':'XMLHttpRequest'}
})
.then(r=>{ if(!r.ok) throw r; return r.json(); })
.then(d => {
document.getElementById('insModalIcon').innerHTML = `<img src="${d.user.avatar}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;">`;
document.getElementById('insModalTitle').textContent = d.user.name;
document.getElementById('insModalSubtitle').textContent = `${d.total} download${d.total!==1?'s':''} of this video`;
const rows = d.records.map((r,i) => `
<div class="ins-dl-hist-row">
<div class="ins-dl-hist-num">${i+1}</div>
<span class="ins-dl-hist-type ${r.type}">${r.type.toUpperCase()}</span>
<div class="ins-dl-hist-time">${_fmtDt(r.at)}</div>
<div class="ins-dl-hist-flag" title="${r.country_name||r.country||'Unknown'}">${_cflag(r.country)}</div>
</div>`).join('');
document.getElementById('insModalBody').innerHTML = `
<div style="display:flex;align-items:center;gap:12px;padding:4px 0 20px;">
<img src="${d.user.avatar}" style="width:52px;height:52px;border-radius:50%;object-fit:cover;border:2px solid var(--border-color);">
<div>
<div style="font-size:16px;font-weight:700;color:var(--text-primary);">${d.user.name}</div>
<div style="font-size:13px;color:var(--text-secondary);margin-top:2px;">
${d.total} download${d.total!==1?'s':''} of this video
</div>
</div>
</div>
<div class="ins-modal-section"><i class="bi bi-clock-history"></i> Download History</div>
${rows || '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No records found.</p>'}`;
})
.catch(() => {
document.getElementById('insModalBody').innerHTML =
'<p style="color:var(--text-secondary);font-size:13px;padding:8px 0;">Could not load download history.</p>';
});
}
</script>
@endif