' ); add_menu_page( __( 'PulseViews', 'pulseviews' ), __( 'PulseViews', 'pulseviews' ), 'manage_options', 'pulseviews', [ $this, 'render_dashboard' ], $icon, 30 ); add_submenu_page( 'pulseviews', __( 'Dashboard', 'pulseviews' ), __( 'Dashboard', 'pulseviews' ), 'manage_options', 'pulseviews', [ $this, 'render_dashboard' ] ); add_submenu_page( 'pulseviews', __( 'Settings', 'pulseviews' ), __( 'Settings', 'pulseviews' ), 'manage_options', 'pulseviews-settings', [ new SettingsPage(), 'render' ] ); } public function render_dashboard(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'pulseviews' ) ); } $repo = new \PulseViews\Database\ViewsRepository(); $totals = $repo->get_site_totals(); $top = $repo->get_top_posts( 8, '', 'total' ); $series = $this->get_calendar_month_series(); $top_posts = array_map( static function ( array $item ): array { $post = get_post( $item['post_id'] ); return [ 'post_id' => $item['post_id'], 'views' => $item['views'], 'title' => $post ? get_the_title( $post ) : "Post #{$item['post_id']}", 'edit_link' => $post ? ( get_edit_post_link( $post, 'raw' ) ?: '' ) : '', 'type' => $post ? $post->post_type : '', ]; }, $top ); $max_views = ! empty( $top_posts ) ? max( array_column( $top_posts, 'views' ) ) : 1; ?>

— PulseViews

👁
fmt( $totals['today'] ) ); ?>
📅
fmt( $totals['week'] ) ); ?>
📈
fmt( $totals['month'] ) ); ?>
fmt( $totals['total'] ) ); ?>

📊
render_posts_table( $top_posts, $max_views ); ?>
render_pro_sections(); ?>
$this->get_hourly_series() ] ); return; } if ( 'month' === $days_raw ) { wp_send_json( [ 'series' => $this->get_calendar_month_series() ] ); return; } $days = min( 365, max( 2, (int) $days_raw ) ); wp_send_json( [ 'series' => $this->get_site_series( $days ) ] ); } if ( 'top_posts' === $type ) { $period = in_array( sanitize_key( wp_unslash( $_GET['period'] ?? '' ) ), [ 'total', 'today', 'week', 'month' ], true ) ? sanitize_key( $_GET['period'] ) : 'total'; $repo = new ViewsRepository(); $top = $repo->get_top_posts( 8, '', $period ); $top_posts = array_map( static function ( array $item ): array { $post = get_post( $item['post_id'] ); return [ 'post_id' => $item['post_id'], 'views' => $item['views'], 'title' => $post ? get_the_title( $post ) : "Post #{$item['post_id']}", 'edit_link' => $post ? ( get_edit_post_link( $post, 'raw' ) ?: '' ) : '', 'type' => $post ? $post->post_type : '', ]; }, $top ); $max = ! empty( $top_posts ) ? max( array_column( $top_posts, 'views' ) ) : 1; ob_start(); $this->render_posts_table( $top_posts, $max ); wp_send_json( [ 'html' => ob_get_clean() ] ); } wp_die( '', 400 ); } private function get_dashboard_css(): string { return <<<'PVDASHCSS' #pv-app,#pv-app *{box-sizing:border-box;font-family:'Inter',system-ui,sans-serif} #pv-app{ --surface:#f8f9ff; --surface-lowest:#ffffff; --surface-low:#f2f3f9; --surface-container:#eceef4; --surface-high:#e6e8ee; --surface-highest:#e1e2e8; --outline:#717881; --outline-variant:#c1c7d2; --on-surface:#191c20; --on-surface-var:#414750; --primary:#005891; --primary-container:#2271b1; --on-primary:#ffffff; --on-primary-container:#e9f1ff; --secondary:#1b6296; --secondary-container:#8bc6ff; --success:#166534; --success-bg:#dcfce7; --error:#ba1a1a; --error-bg:#ffdad6; --shadow:0px 2px 4px rgba(0,0,0,0.05); background:transparent;color:var(--on-surface);padding:0;margin:0;-webkit-font-smoothing:antialiased } .pv-wrap{max-width:1080px;margin:0;padding:24px 0 64px} .pv-hd{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px} .pv-hd h1{font-size:24px;font-weight:600;letter-spacing:-0.02em;line-height:32px;margin:0;color:var(--on-surface)} .pv-hd p{font-size:13px;color:var(--on-surface-var);margin:4px 0 0;line-height:20px} .pv-live{display:inline-flex;align-items:center;gap:7px;background:#dcfce7;border:1px solid #bbf7d0;color:var(--success);font-size:10px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;padding:5px 12px;border-radius:9999px;white-space:nowrap;align-self:flex-start} .pv-live::before{content:'';width:6px;height:6px;background:#16a34a;border-radius:50%;animation:pvpulse 1.6s ease infinite} @keyframes pvpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}} .pv-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-bottom:20px} @media(max-width:900px){.pv-cards{grid-template-columns:repeat(2,1fr)}} @media(max-width:560px){.pv-cards{grid-template-columns:1fr}} .pv-card{background:var(--surface-lowest);border:1px solid var(--outline-variant);border-radius:12px;padding:24px;position:relative;box-shadow:var(--shadow);transition:box-shadow .2s} .pv-card:hover{box-shadow:0px 4px 12px rgba(0,0,0,0.08)} .pv-card-hd{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px} .pv-cl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--on-surface-var);line-height:16px} .pv-card-icon{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1} .pv-card-body{display:flex;align-items:baseline;gap:8px;margin-bottom:16px} .pv-cv{font-size:32px;font-weight:700;letter-spacing:-0.03em;line-height:40px;font-variant-numeric:tabular-nums;color:var(--on-surface)} .pv-cc{display:flex;align-items:center;gap:2px;font-size:13px;font-weight:700} .pv-cc.up{color:#16a34a}.pv-cc.dn{color:var(--error)} .pv-card-bar{height:4px;width:100%;background:var(--surface-container);border-radius:9999px;overflow:hidden} .pv-card-bar-fill{height:100%;border-radius:9999px;background:var(--primary)} .pv-card-bar-fill.sec{background:var(--secondary)} .pv-main-grid{display:grid;grid-template-columns:1fr 340px;gap:20px;margin-bottom:20px} @media(max-width:1020px){.pv-main-grid{grid-template-columns:1fr}} .pv-panel{background:var(--surface-lowest);border:1px solid var(--outline-variant);border-radius:12px;box-shadow:var(--shadow);overflow:hidden;margin-bottom:0} .pv-panel-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--outline-variant);background:var(--surface)} .pv-panel-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--on-surface-var)} .pv-panel-body{padding:24px} .pv-chart-wrap{height:300px;position:relative} .pv-chart-btns{display:flex;gap:4px} .pv-chart-btn{font-size:11px;font-weight:700;padding:4px 10px;border-radius:4px;border:none;cursor:pointer;transition:all .15s;font-family:inherit;background:transparent;color:var(--on-surface-var)} .pv-chart-btn.on{background:var(--on-primary-container);color:var(--primary);font-weight:700} .pv-chart-btn:hover:not(.on){background:var(--surface-container)} .pv-sidebar{display:flex;flex-direction:column;gap:20px} .pv-health{display:flex;align-items:center;gap:16px} .pv-health-ring{position:relative;width:64px;height:64px;flex-shrink:0} .pv-health-ring svg{width:100%;height:100%;transform:rotate(-90deg)} .pv-health-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;transform:rotate(0deg)} .pv-cache-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;font-size:13px} .pv-cache-row+.pv-cache-row{border-top:1px solid var(--surface-high)} .pv-cache-ok{font-weight:600;color:var(--primary)} .pv-realtime{background:var(--primary);color:var(--on-primary);border-radius:12px;padding:24px;position:relative;overflow:hidden;box-shadow:0 4px 16px rgba(0,88,145,.3)} .pv-realtime-hd{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:11px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;opacity:.85} .pv-realtime-dot{width:8px;height:8px;background:#fff;border-radius:50%;animation:pvpulse 1.5s ease infinite;box-shadow:0 0 8px rgba(255,255,255,.8)} .pv-realtime-num{font-size:32px;font-weight:700;letter-spacing:-0.03em;line-height:1;margin-bottom:6px;font-variant-numeric:tabular-nums} .pv-realtime-sub{font-size:13px;opacity:.85} .pv-realtime-bg{position:absolute;bottom:-16px;right:-16px;font-size:80px;opacity:.1;line-height:1} .pv-table-wrap{overflow-x:auto} .pv-table{width:100%;border-collapse:collapse;font-size:13px} .pv-table thead th{padding:0 0 12px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--outline);border-bottom:1px solid var(--outline-variant);text-align:left} .pv-table tbody tr{border-bottom:1px solid var(--surface-high);transition:background .15s;cursor:pointer} .pv-table tbody tr:last-child{border-bottom:none} .pv-table tbody tr:hover{background:var(--surface-low)} .pv-table td{padding:14px 0} .pv-table-title{font-weight:600;color:var(--on-surface);font-size:13px} .pv-table-views{font-size:13px;color:var(--on-surface-var);font-variant-numeric:tabular-nums} .pv-table-trend{font-size:12px;font-weight:700} .pv-table-trend.up{color:#16a34a}.pv-table-trend.dn{color:var(--error)} .pv-bottom-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px} @media(max-width:760px){.pv-bottom-grid{grid-template-columns:1fr}} .pv-mini-bar-bg{height:4px;background:var(--surface-highest);border-radius:2px;overflow:hidden;flex-shrink:0;width:48px} .pv-mini-bar-fill{height:100%;border-radius:2px;background:var(--primary)} .pv-author-row{display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--surface-high)} .pv-author-row:last-child{border-bottom:none} .pv-avatar{width:40px;height:40px;border-radius:9999px;background:var(--secondary-container);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;color:var(--on-surface);flex-shrink:0} .pv-author-name{font-size:13px;font-weight:600;color:var(--on-surface)} .pv-author-views{font-size:11px;color:var(--outline)} .pv-device-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center} .pv-device-icon{font-size:24px;margin-bottom:4px} .pv-device-pct{font-size:12px;font-weight:700;color:var(--on-surface)} .pv-empty{text-align:center;padding:48px 20px} .pv-empty-icon{font-size:38px;margin-bottom:10px} .pv-empty-t{font-size:14px;font-weight:600;color:var(--on-surface-var);margin-bottom:4px} .pv-empty-s{font-size:12px;color:var(--outline);line-height:1.6;max-width:300px;margin:0 auto} #pv-tip{position:absolute;background:var(--surface-lowest);border:1px solid var(--outline-variant);border-radius:8px;padding:8px 12px;font-size:12px;pointer-events:none;opacity:0;transition:opacity .15s;white-space:nowrap;z-index:9;box-shadow:0 4px 16px rgba(0,0,0,.12)} #pv-tip strong{color:var(--on-surface);display:block;font-variant-numeric:tabular-nums;font-weight:600} #pv-tip span{color:var(--on-surface-var);font-size:10px} .pv-badge-green{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:2px 8px;border-radius:9999px;background:#dcfce7;color:#166534;letter-spacing:0.03em} .pv-pro-section-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--outline);margin:24px 0 16px;display:flex;align-items:center;gap:10px} .pv-pro-section-label::after{content:'';flex:1;height:1px;background:var(--outline-variant)} .pv-pro-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-bottom:20px} @media(max-width:900px){.pv-pro-grid{grid-template-columns:1fr}} .pv-pro-card{background:var(--surface-lowest);border:1px solid var(--outline-variant);border-radius:12px;overflow:hidden;position:relative;box-shadow:var(--shadow)} .pv-pro-card-inner{padding:24px;filter:blur(2px);pointer-events:none;user-select:none;opacity:.5} .pv-pro-card-hd{display:flex;align-items:center;gap:10px;margin-bottom:16px} .pv-pro-card-icon{width:36px;height:36px;border-radius:8px;background:var(--on-primary-container);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0} .pv-pro-card-title{font-size:14px;font-weight:600;color:var(--on-surface)} .pv-pro-card-sub{font-size:12px;color:var(--on-surface-var);margin-top:1px} .pv-pro-card-desc{font-size:12px;color:var(--on-surface-var);line-height:1.5;margin:12px 0} .pv-pro-fake-row{height:10px;background:var(--surface-container);border-radius:3px;margin-bottom:8px} .pv-pro-fake-chart{height:64px;background:var(--surface-container);border-radius:6px;overflow:hidden;margin-bottom:12px} .pv-pro-fake-bars{display:flex;align-items:flex-end;gap:5px;height:56px;padding:0 6px} .pv-pro-bar{background:var(--outline-variant);border-radius:3px 3px 0 0;flex:1} .pv-pro-lock{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;z-index:2;background:rgba(248,249,255,.7);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)} .pv-pro-lock-badge{background:var(--primary-container);color:var(--on-primary);font-size:10px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;padding:4px 12px;border-radius:9999px} .pv-pro-lock-text{font-size:13px;font-weight:600;color:var(--on-surface);text-align:center;line-height:1.4;max-width:180px} .pv-pro-lock-sub{font-size:11px;color:var(--on-surface-var);text-align:center;max-width:190px;line-height:1.5} .pv-pro-lock-btn{display:inline-flex;align-items:center;gap:6px;background:var(--primary-container);color:var(--on-primary);font-size:12px;font-weight:600;padding:8px 18px;border-radius:8px;text-decoration:none;transition:opacity .15s} .pv-pro-lock-btn:hover{opacity:.88;color:#fff} .pv-pro-unlocked .pv-pro-card-inner{filter:none;opacity:1;pointer-events:auto;user-select:auto} .pv-pro-unlocked{border-color:var(--outline-variant)} .pv-pro-open-btn{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;color:var(--primary-container);text-decoration:none;padding:7px 14px;border-radius:8px;border:1px solid var(--outline-variant);background:var(--surface-low);transition:all .15s;margin-top:8px} .pv-pro-open-btn:hover{background:var(--on-primary-container);border-color:var(--primary-container);color:var(--primary)} .pv-pro-banner{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap;background:var(--on-primary-container);border:1px solid var(--outline-variant);border-radius:12px;padding:20px 24px;margin-bottom:20px;box-shadow:var(--shadow)} .pv-pro-active-banner{background:#f0fdf4;border-color:#bbf7d0} .pv-pro-banner-l{display:flex;align-items:center;gap:16px} .pv-pro-banner-icon{font-size:32px;flex-shrink:0} .pv-pro-banner-title{font-size:15px;font-weight:600;color:var(--on-surface);margin-bottom:4px} .pv-pro-banner-desc{font-size:12px;color:var(--on-surface-var);line-height:1.6} .pv-pro-banner-desc ul{margin:4px 0 0 14px;list-style:disc} .pv-pro-banner-desc li{margin-bottom:2px} .pv-pro-banner-desc a{color:var(--primary);font-weight:600} .pv-pro-banner-btn{display:inline-flex;align-items:center;gap:8px;background:var(--primary-container);color:var(--on-primary);font-size:13px;font-weight:700;padding:10px 22px;border-radius:8px;text-decoration:none;white-space:nowrap;transition:opacity .15s,box-shadow .15s;flex-shrink:0} .pv-pro-banner-btn:hover{opacity:.9;color:#fff;box-shadow:0 4px 12px rgba(34,113,177,.35)} .pv-pro-active-btn{background:#166534} /* WP Dashboard Widget Styles */ .pv-widget { padding: 4px 0; font-size: 13px; } .pv-widget-empty { text-align: center; padding: 20px; color: #50575e; } .pv-widget-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f1; } .pv-widget-stat-label { font-size: 11px; text-transform: uppercase; color: #646970; font-weight: 600; margin-bottom: 2px; } .pv-widget-stat-val { font-size: 20px; font-weight: 700; color: #1d2327; } .pv-widget-section-hd { font-size: 11px; font-weight: 700; text-transform: uppercase; color: #646970; margin-bottom: 10px; } .pv-widget-post { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .pv-widget-post-title { flex: 1; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; color: #2271b1; text-decoration: none; font-weight: 500; } .pv-widget-post-title:hover { text-decoration: underline; } .pv-widget-post-bar { width: 60px; height: 4px; background: #f0f0f1; border-radius: 2px; overflow: hidden; flex-shrink: 0; } .pv-widget-post-bar-fill { height: 100%; background: #2271b1; border-radius: 2px; } .pv-widget-post-views { font-size: 12px; font-weight: 600; color: #50575e; min-width: 32px; text-align: right; } .pv-widget-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 14px; padding-top: 10px; border-top: 1px solid #f0f0f1; font-size: 12px; } .pv-widget-live { color: #166534; background: #dcfce7; font-weight: 700; font-size: 10px; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; } .pv-widget-footer a { color: #2271b1; text-decoration: none; font-weight: 600; } PVDASHCSS; } private function get_dashboard_js(): string { return <<<'PVJS' (function(){ function pvInit(){ var cfg = window.pulseviewsDashData || window.pvDashData || {}; var series = cfg.series || []; var ajaxUrl = cfg.ajaxUrl || ''; var nonce = cfg.nonce || ''; var activeDays = 30; var wrap = document.getElementById('pv-chart-wrap'); var tip = document.getElementById('pv-tip'); var tipV = document.getElementById('pv-tip-v'); var tipD = document.getElementById('pv-tip-d'); if (!wrap) return; function getAccent(){ var app = document.getElementById('pv-app'); if (!app) return '#2271b1'; var col = getComputedStyle(app).getPropertyValue('--primary-container').trim(); return col || '#2271b1'; } function buildChart(pts){ var old = wrap.querySelector('svg'); if (old) old.remove(); if (!pts || pts.length < 2) return; var W = wrap.offsetWidth || wrap.parentElement.offsetWidth || 600; var H = 260, P = 14; var vals = pts.map(function(s){ return parseInt(s.views, 10) || 0; }); var mx = Math.max.apply(null, vals.concat([1])); var ac = getAccent(); var ns = 'http://www.w3.org/2000/svg'; var svg = document.createElementNS(ns, 'svg'); svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H); svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible'; var defs = document.createElementNS(ns, 'defs'); var grad = document.createElementNS(ns, 'linearGradient'); grad.id = 'pvChartGrad'; grad.setAttribute('x1','0'); grad.setAttribute('y1','0'); grad.setAttribute('x2','0'); grad.setAttribute('y2','1'); function mkStop(offset, opacity){ var s = document.createElementNS(ns, 'stop'); s.setAttribute('offset', offset); s.setAttribute('stop-color', ac); s.setAttribute('stop-opacity', opacity); return s; } grad.appendChild(mkStop('0%', '0.18')); grad.appendChild(mkStop('100%', '0')); defs.appendChild(grad); svg.appendChild(defs); [0.25, 0.5, 0.75, 1].forEach(function(f){ var y = H - P - f * (H - P * 2); var line = document.createElementNS(ns, 'line'); line.setAttribute('x1', P); line.setAttribute('x2', W - P); line.setAttribute('y1', y); line.setAttribute('y2', y); line.setAttribute('stroke', 'rgba(0,0,0,0.05)'); line.setAttribute('stroke-width', '1'); svg.appendChild(line); }); var coords = pts.map(function(s, i){ var v = parseInt(s.views, 10) || 0; return { x: P + (i / (pts.length - 1)) * (W - P * 2), y: H - P - (v / mx) * (H - P * 2), v: v, d: s.date || '' }; }); var pathD = 'M' + coords[0].x + ',' + coords[0].y; for (var i = 0; i < coords.length - 1; i++){ var cx = (coords[i].x + coords[i+1].x) / 2; pathD += ' C' + cx + ',' + coords[i].y + ' ' + cx + ',' + coords[i+1].y + ' ' + coords[i+1].x + ',' + coords[i+1].y; } var area = document.createElementNS(ns, 'path'); area.setAttribute('d', pathD + ' L' + (W - P) + ',' + H + ' L' + P + ',' + H + ' Z'); area.setAttribute('fill', 'url(#pvChartGrad)'); svg.appendChild(area); var stroke = document.createElementNS(ns, 'path'); stroke.setAttribute('d', pathD); stroke.setAttribute('fill', 'none'); stroke.setAttribute('stroke', ac); stroke.setAttribute('stroke-width', '2'); stroke.setAttribute('stroke-linejoin', 'round'); stroke.setAttribute('stroke-linecap', 'round'); svg.appendChild(stroke); var colW = (W - P * 2) / Math.max(coords.length, 1); coords.forEach(function(pt){ var rect = document.createElementNS(ns, 'rect'); rect.setAttribute('x', pt.x - colW / 2); rect.setAttribute('y', 0); rect.setAttribute('width', colW); rect.setAttribute('height', H); rect.setAttribute('fill', 'transparent'); rect.style.cursor = 'crosshair'; rect.addEventListener('mouseenter', function(){ if (tipV) tipV.textContent = pt.v.toLocaleString() + ' views'; if (tipD) tipD.textContent = pt.d; if (tip) tip.style.opacity = '1'; var dot = svg.querySelector('.pv-dot'); if (dot) dot.remove(); var c = document.createElementNS(ns, 'circle'); c.setAttribute('cx', pt.x); c.setAttribute('cy', pt.y); c.setAttribute('r', '5'); c.setAttribute('fill', ac); c.setAttribute('stroke', '#fff'); c.setAttribute('stroke-width', '2'); c.classList.add('pv-dot'); svg.appendChild(c); }); rect.addEventListener('mousemove', function(e){ if (!tip) return; var b = wrap.getBoundingClientRect(); var tx = e.clientX - b.left + 14; var ty = e.clientY - b.top - 48; if (tx + 140 > b.width) tx = e.clientX - b.left - 155; tip.style.left = tx + 'px'; tip.style.top = Math.max(0, ty) + 'px'; }); rect.addEventListener('mouseleave', function(){ if (tip) tip.style.opacity = '0'; var dot = svg.querySelector('.pv-dot'); if (dot) dot.remove(); }); svg.appendChild(rect); }); var maxLabels = 7; var step = Math.max(1, Math.ceil(coords.length / maxLabels)); var labelIndexes = []; for (var li = 0; li < coords.length; li += step) labelIndexes.push(li); var last = coords.length - 1; var prevLabel = labelIndexes[labelIndexes.length - 1]; if (prevLabel !== last && (last - prevLabel) >= Math.ceil(step * 0.6)) { labelIndexes.push(last); } var labelSet = {}; labelIndexes.forEach(function(idx){ labelSet[idx] = true; }); coords.forEach(function(pt, i){ if (!labelSet[i]) return; var t = document.createElementNS(ns, 'text'); t.setAttribute('x', pt.x); t.setAttribute('y', H + 14); t.setAttribute('text-anchor', 'middle'); t.setAttribute('font-size', '10'); t.setAttribute('fill', 'rgba(0,0,0,0.3)'); t.setAttribute('font-family', 'inherit'); t.textContent = pt.d.indexOf(':') !== -1 ? pt.d : pt.d.slice(5); svg.appendChild(t); }); wrap.appendChild(svg); } buildChart(series.slice(-activeDays)); document.querySelectorAll('[data-days]').forEach(function(btn){ btn.addEventListener('click', function(){ document.querySelectorAll('[data-days]').forEach(function(b){ b.classList.remove('on'); }); btn.classList.add('on'); var daysVal = btn.dataset.days; activeDays = parseInt(daysVal, 10) || 30; if (daysVal === '1') { if (!ajaxUrl) return; fetch(ajaxUrl + '?action=pulseviews_dashboard_data&type=series&days=1&_wpnonce=' + nonce) .then(function(r){ return r.json(); }) .then(function(d){ if (d.series && d.series.length >= 2){ buildChart(d.series); } }) .catch(function(){}); return; } if (daysVal === 'month') { if (!ajaxUrl) return; fetch(ajaxUrl + '?action=pulseviews_dashboard_data&type=series&days=month&_wpnonce=' + nonce) .then(function(r){ return r.json(); }) .then(function(d){ if (d.series){ buildChart(d.series); } }) .catch(function(){}); return; } if (series.length >= activeDays){ buildChart(series.slice(-activeDays)); return; } if (!ajaxUrl) return; fetch(ajaxUrl + '?action=pulseviews_dashboard_data&type=series&days=' + activeDays + '&_wpnonce=' + nonce) .then(function(r){ return r.json(); }) .then(function(d){ if (d.series){ series = d.series; buildChart(d.series); } }) .catch(function(){}); }); }); document.querySelectorAll('[data-period]').forEach(function(btn){ btn.addEventListener('click', function(){ document.querySelectorAll('[data-period]').forEach(function(b){ b.classList.remove('on'); }); btn.classList.add('on'); var list = document.getElementById('pv-posts'); if (!list) return; list.style.opacity = '0.4'; fetch(ajaxUrl + '?action=pulseviews_dashboard_data&type=top_posts&period=' + btn.dataset.period + '&_wpnonce=' + nonce) .then(function(r){ return r.json(); }) .then(function(d){ if (d.html) list.innerHTML = d.html; list.style.opacity = '1'; animBars(); }) .catch(function(){ list.style.opacity = '1'; }); }); }); function animBars(){ setTimeout(function(){ document.querySelectorAll('.pv-bar-fill').forEach(function(b){ b.style.width = (b.dataset.w || '0') + '%'; }); }, 50); } animBars(); var resizeTimer; window.addEventListener('resize', function(){ clearTimeout(resizeTimer); resizeTimer = setTimeout(function(){ buildChart(series.slice(-activeDays)); }, 150); }); } if (document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', pvInit); } else { requestAnimationFrame(function(){ setTimeout(pvInit, 0); }); } })(); PVJS; } private function render_pro_sections(): void { $is_pro = (bool) apply_filters( 'pulseviews/is_pro', false ); $reports_url = admin_url( 'admin.php?page=pulseviews-reports' ); ?>
📊
Detailed Reports
Custom date ranges

View detailed stats about the popularity of your content for any period.

Open Reports →
📅
Date Range Reports
Any period you need

Generate reports for any custom date range — 7 days, 90 days, or a full year.

View by Month →
📥
Export Views
CSV & XML

Export, download and share your website views data in CSV or XML format.

Go to Export →
✅
PulseViews Pro — Active
📊 Open Reports
📊
Detailed Reports
Any date range
PRO
Detailed Reports
View detailed stats about the popularity of your content
Upgrade →
📅
Custom Date Range
Any period you need
PRO
Custom Date Reports
Generate reports in any date range you need
Upgrade →
📥
Export Views
CSV & XML
PRO
Export Data
Export, download and share your views data as CSV or XML
Upgrade →
⚡
Unlock PulseViews Pro
  • View detailed stats about the popularity of your content
  • Generate views reports in any date range you need
  • Export, download and share your website views data
⚡ Get PulseViews Pro
📊
' . '
' . esc_html__( 'No views recorded yet', 'pulseviews' ) . '
' . '
' . esc_html__( 'Views will appear here once the tracker starts recording pageviews.', 'pulseviews' ) . '
'; return; } echo '
' . '' . '' . '' . '' . ''; $total = max( array_sum( array_column( $posts, 'views' ) ), 1 ); foreach ( $posts as $p ) { $share = round( $p['views'] / $total * 100, 1 ); printf( '' . '' . '' . '', esc_url( $p['edit_link'] ), esc_html( $p['title'] ), esc_html( ucfirst( $p['type'] ) ), esc_html( number_format_i18n( $p['views'] ) ), esc_html( $share ) ); } echo '
' . esc_html__( 'TITLE', 'pulseviews' ) . '' . esc_html__( 'TYPE', 'pulseviews' ) . '' . esc_html__( 'VIEWS', 'pulseviews' ) . '' . esc_html__( 'SHARE', 'pulseviews' ) . '
%s%s%s%s%%
'; } private function render_posts_list( array $posts, int $max_views ): void { if ( empty( $posts ) ) { echo '
📊
' . '
No views recorded yet
' . '
Views will appear here once the tracker starts recording pageviews.
'; return; } foreach ( $posts as $i => $p ) { $pct = $max_views > 0 ? (int) round( ( $p['views'] / $max_views ) * 100 ) : 0; printf( '
' . '%d' . '' . '
' . '%s' . '
', (int) ( $i + 1 ), esc_url( $p['edit_link'] ), esc_html( $p['title'] ), esc_html( $p['type'] ), (int) $pct, esc_html( $this->fmt( $p['views'] ) ) ); } } private function get_hourly_series(): array { global $wpdb; $views_table = $wpdb->prefix . 'pulseviews_views'; $today = current_time( 'Y-m-d' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT HOUR(viewed_at) AS hour, COUNT(*) AS views FROM %i WHERE is_bot = 0 AND DATE(viewed_at) = %s GROUP BY HOUR(viewed_at) ORDER BY hour ASC", $views_table, $today ), ARRAY_A ) ?? []; $indexed = []; foreach ( (array) $rows as $row ) { $indexed[ (int) $row['hour'] ] = (int) $row['views']; } $series = []; for ( $h = 0; $h < 24; $h++ ) { $series[] = [ 'date' => gmdate( 'H:00', mktime( $h, 0, 0 ) ), 'views' => $indexed[ $h ] ?? 0, ]; } return $series; } private function get_calendar_month_series(): array { global $wpdb; $today = current_time( 'Y-m-d' ); $month_start = current_time( 'Y-m' ) . '-01'; $day_of_month = (int) current_time( 'j' ); $agg_table = $wpdb->prefix . 'pulseviews_aggregates'; $views_table = $wpdb->prefix . 'pulseviews_views'; $agg_rows = $wpdb->get_results( $wpdb->prepare( "SELECT period_date AS date, SUM(view_count) AS views FROM %i WHERE period_type='day' AND period_date >= %s AND period_date < %s GROUP BY period_date ORDER BY period_date ASC", $agg_table, $month_start, $today ), ARRAY_A ) ?? []; if ( empty( $agg_rows ) ) { $agg_rows = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(viewed_at) AS date, COUNT(*) AS views FROM %i WHERE is_bot=0 AND DATE(viewed_at) >= %s AND DATE(viewed_at) < %s GROUP BY DATE(viewed_at) ORDER BY date ASC", $views_table, $month_start, $today ), ARRAY_A ) ?? []; } $today_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE is_bot=0 AND DATE(viewed_at) = %s", $views_table, $today ) ); $indexed = array_column( (array) $agg_rows, 'views', 'date' ); $indexed[$today] = $today_count; $series = []; for ( $i = 0; $i < $day_of_month; $i++ ) { $d = gmdate( 'Y-m-d', strtotime( $month_start . " +{$i} days" ) ); $series[] = [ 'date' => $d, 'views' => (int) ( $indexed[ $d ] ?? 0 ) ]; } return $series; } private function get_site_series( int $days ): array { global $wpdb; $today = current_time( 'Y-m-d' ); $start = gmdate( 'Y-m-d', strtotime( "-{$days} days", current_time( 'timestamp' ) ) ); $agg_table = $wpdb->prefix . 'pulseviews_aggregates'; $views_table = $wpdb->prefix . 'pulseviews_views'; $agg_rows = $wpdb->get_results( $wpdb->prepare( "SELECT period_date AS date, SUM(view_count) AS views FROM %i WHERE period_type='day' AND period_date >= %s AND period_date < %s GROUP BY period_date ORDER BY period_date ASC", $agg_table, $start, $today ), ARRAY_A ) ?? []; if ( empty( $agg_rows ) ) { $agg_rows = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(viewed_at) AS date, COUNT(*) AS views FROM %i WHERE is_bot=0 AND DATE(viewed_at) >= %s AND DATE(viewed_at) < %s GROUP BY DATE(viewed_at) ORDER BY date ASC", $views_table, $start, $today ), ARRAY_A ) ?? []; } $today_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE is_bot=0 AND DATE(viewed_at) = %s", $views_table, $today ) ); $indexed = array_column( (array) $agg_rows, 'views', 'date' ); $indexed[ $today ] = $today_count; $series = []; for ( $i = $days - 1; $i >= 0; $i-- ) { $d = gmdate( 'Y-m-d', strtotime( "-{$i} days", current_time( 'timestamp' ) ) ); $series[] = [ 'date' => $d, 'views' => (int) ( $indexed[ $d ] ?? 0 ) ]; } return $series; } private function fmt( int $n ): string { if ( $n >= 1_000_000 ) { return number_format_i18n( $n / 1_000_000, 1 ) . 'M'; } if ( $n >= 1_000 ) { return number_format_i18n( $n / 1_000, 1 ) . 'K'; } return number_format_i18n( $n ); } public function register_dashboard_widget(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } wp_add_dashboard_widget( 'pulseviews_dashboard_widget', __( 'PulseViews — Analytics', 'pulseviews' ), [ $this, 'render_dashboard_widget' ] ); } public function render_dashboard_widget(): void { $repo = new \PulseViews\Database\ViewsRepository(); $totals = $repo->get_site_totals(); $top = $repo->get_top_posts( 5, '', 'total' ); $top_posts = array_map( static function ( array $item ): array { $post = get_post( $item['post_id'] ); return [ 'views' => $item['views'], 'title' => $post ? get_the_title( $post ) : "Post #{$item['post_id']}", 'link' => $post ? ( get_edit_post_link( $post, 'raw' ) ?: '' ) : '', ]; }, $top ); $max = ! empty( $top_posts ) ? max( array_column( $top_posts, 'views' ) ) : 1; $dashboard_url = admin_url( 'admin.php?page=pulseviews' ); ?>
📊
fmt( $totals['today'] ) ); ?>
fmt( $totals['week'] ) ); ?>
fmt( $totals['total'] ) ); ?>
0 ? (int) round( ( $p['views'] / $max ) * 100 ) : 0; ?>
fmt( $p['views'] ) ); ?>
get_dashboard_css() ); } else { wp_add_inline_style( 'pulseviews-admin', $settings_obj->get_settings_css() ); } wp_enqueue_script( 'pulseviews-admin-js', PULSEVIEWS_URL . 'assets/admin/loader.js', [], PULSEVIEWS_VERSION, true ); if ( str_contains( $hook, 'toplevel_page_pulseviews' ) ) { $series = $this->get_calendar_month_series(); wp_localize_script( 'pulseviews-admin-js', 'pulseviewsDashData', [ 'series' => $series, 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'pulseviews_dashboard' ), ] ); wp_add_inline_script( 'pulseviews-admin-js', $this->get_dashboard_js() ); } else { wp_add_inline_script( 'pulseviews-admin-js', $settings_obj->get_settings_js() ); } } public function add_views_column( array $columns ): array { $columns['pulseviews_views'] = __( 'Views', 'pulseviews' ); return $columns; } public function render_views_column( string $column, int $post_id ): void { if ( 'pulseviews_views' !== $column ) { return; } global $wpdb; $table = $wpdb->prefix . 'pulseviews_post_totals'; $views = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT total_views FROM %i WHERE post_id=%d', $table, $post_id ) ); echo '' . esc_html( number_format_i18n( $views ) ) . ''; } public function make_views_sortable( array $cols ): array { $cols['pulseviews_views'] = 'pulseviews_views'; return $cols; } public function sort_by_views( \WP_Query $query ): void { if ( ! is_admin() || ! $query->is_main_query() ) { return; } if ( 'pulseviews_views' !== $query->get( 'orderby' ) ) { return; } global $wpdb; $t = $wpdb->prefix . 'pulseviews_post_totals'; add_filter( 'posts_join', static fn( $j ) => $j . " LEFT JOIN `{$t}` pvt ON pvt.post_id={$wpdb->posts}.ID " ); add_filter( 'posts_orderby', static function ( $ob, $q ) use ( $query ) { if ( $q !== $query ) { return $ob; } return 'COALESCE(pvt.total_views,0) ' . ( strtoupper( $q->get( 'order' ) ) === 'ASC' ? 'ASC' : 'DESC' ); }, 10, 2 ); } public function handle_activation_redirect(): void { if ( ! get_transient( 'pulseviews_activation_redirect' ) ) { return; } delete_transient( 'pulseviews_activation_redirect' ); if ( ! is_multisite() ) { wp_safe_redirect( admin_url( 'admin.php?page=pulseviews' ) ); exit; } } }