takeone-youtube-clone/resources/views/components/video-insights.blade.php
ghassan c160242dbc WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:15:20 +03:00

822 lines
56 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{--
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; }
/* ── Two-column grid (collapses to 1 col on mobile) ─── */
.ins-two-col { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media(max-width:600px){ .ins-two-col { grid-template-columns:1fr; } }
/* ── Who Liked rows ──────────────────────────────────── */
.ins-liker-row { display:flex; align-items:center; gap:10px; padding:7px 6px; border-bottom:1px solid rgba(255,255,255,.04); cursor:pointer; border-radius:8px; transition:background .12s; }
.ins-liker-row:last-child { border-bottom:none; }
.ins-liker-row:hover { background:rgba(255,255,255,.05); }
.ins-liker-avatar { width:34px; height:34px; border-radius:50%; object-fit:cover; flex-shrink:0; background:rgba(255,255,255,.08); }
.ins-liker-name { flex:1; font-size:13px; font-weight:600; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ins-liker-time { font-size:11px; color:var(--text-secondary); flex-shrink:0; }
/* ── 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 class="ins-two-col">
<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>`;
// ── Who Liked section ───────────────────────────────
let likersHtml = '';
if (d.likes > 0 && d.likers && d.likers.length) {
const likerRows = d.likers.map(u => `
<div class="ins-liker-row" onclick="window.location.href='/channel/${u.channel||u.id}'" title="View ${u.name}'s profile">
<img src="${u.avatar}" alt="${u.name}" class="ins-liker-avatar">
<div class="ins-liker-name">${u.name}</div>
<div class="ins-liker-time">${_ago(u.liked_at)}</div>
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);flex-shrink:0;margin-left:4px;"></i>
</div>`).join('');
likersHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-section-title"><i class="bi bi-heart-fill" style="color:#ef4444;"></i> Liked by — ${_fmt(d.likes)} ${d.likes===1?'person':'people'}</div>
${likerRows}
</div>`;
} else if (d.likes === 0) {
likersHtml = `
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
<div class="ins-section-title"><i class="bi bi-heart" style="color:var(--text-secondary);"></i> Likes</div>
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No likes yet.</p>
</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 class="ins-two-col">
<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 + likersHtml + 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