import React, { useMemo, useState } from 'react' import { Head, router, usePage } from '@inertiajs/react' import AdminLayout from '../../../Layouts/AdminLayout' function formatNumber(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return 'โ' } return Number(value).toLocaleString() } function formatMoney(value, currency) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return 'โ' } try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD', maximumFractionDigits: 2, }).format(Number(value)) } catch { return `${Number(value).toFixed(2)} ${currency || ''}`.trim() } } function formatPercent(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return 'โ' } return `${(Number(value) * 100).toFixed(2)}%` } function formatKpi(kpi) { if (kpi.format === 'money') { return formatMoney(kpi.value, kpi.currency_code) } if (kpi.format === 'percent') { return formatPercent(kpi.value) } return formatNumber(kpi.value) } function formatChange(change) { if (change === null || change === undefined) { return 'No previous period' } const prefix = Number(change) > 0 ? '+' : '' return `${prefix}${Number(change).toFixed(1)}% vs previous` } function formatTimestamp(value) { if (!value) { return 'Never' } return new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeStyle: 'short', }).format(new Date(value)) } function statusCopy(connection) { if (!connection) { return { label: 'Not connected', className: 'border-slate-400/20 bg-slate-500/10 text-slate-200' } } if (connection.needs_reconnect || connection.status === 'reconnect_required') { return { label: 'Reconnect required', className: 'border-amber-400/20 bg-amber-500/10 text-amber-200' } } if (connection.status === 'error') { return { label: 'Synchronization error', className: 'border-rose-400/20 bg-rose-500/10 text-rose-200' } } if (connection.status === 'connected') { return { label: 'Connected', className: 'border-emerald-400/20 bg-emerald-500/10 text-emerald-200' } } if (connection.status === 'pending') { return { label: 'Pending account selection', className: 'border-sky-400/20 bg-sky-500/10 text-sky-200' } } return { label: 'Not connected', className: 'border-slate-400/20 bg-slate-500/10 text-slate-200' } } function Sparkline({ points, metric }) { if (!points.length) { return
No chart data for this range.
} const values = points.map((point) => Number(point[metric] || 0)) const max = Math.max(...values, 0.0001) const width = 640 const height = 180 const step = values.length === 1 ? width : width / (values.length - 1) const path = values .map((value, index) => { const x = index * step const y = height - (value / max) * (height - 16) - 8 return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}` }) .join(' ') return ( ) } function SortableTable({ rows, currency }) { const [sortKey, setSortKey] = useState('revenue') const [direction, setDirection] = useState('desc') const sorted = useMemo(() => { return [...rows].sort((a, b) => { const left = a[sortKey] const right = b[sortKey] const numeric = typeof left === 'number' || typeof right === 'number' || sortKey !== 'label' if (numeric) { return direction === 'asc' ? Number(left || 0) - Number(right || 0) : Number(right || 0) - Number(left || 0) } return direction === 'asc' ? String(left || '').localeCompare(String(right || '')) : String(right || '').localeCompare(String(left || '')) }) }, [rows, sortKey, direction]) const toggle = (key) => { if (sortKey === key) { setDirection(direction === 'asc' ? 'desc' : 'asc') return } setSortKey(key) setDirection(key === 'label' ? 'asc' : 'desc') } const header = (key, label) => ( ) if (!rows.length) { returnNo rows for this range.
} return (| {header('label', 'Placement')} | {header('page_views', 'Page Views')} | {header('impressions', 'Impressions')} | {header('clicks', 'Clicks')} | {header('ctr', 'CTR')} | {header('rpm', 'RPM')} | {header('revenue', 'Revenue')} |
|---|---|---|---|---|---|---|
| {row.label} | {formatNumber(row.page_views)} | {formatNumber(row.impressions)} | {formatNumber(row.clicks)} | {formatPercent(row.ctr)} | {formatMoney(row.rpm, currency)} | {formatMoney(row.revenue, currency)} |
Google AdSense
Account: {connection?.account_display_name || 'Not selected'} {connection?.account_resource_name ? ` / ${connection.account_resource_name.replace('accounts/', '')}` : ''}
Last synchronized: {formatTimestamp(connection?.last_successful_sync_at)}
{connection?.last_error ?{connection.last_error}
: null} {connection?.needs_reconnect ? (AdSense authorization expired or was revoked. Reconnect Google AdSense.
) : null} {!configured ?ADSENSE_CLIENT_ID and ADSENSE_CLIENT_SECRET are not configured.
: null}Select AdSense account
Period
{range.from} to {range.to}
Today's AdSense data is estimated and may change.
{kpi.label}
{formatKpi(kpi)}
{formatChange(kpi.change_percent)}
Daily trend
Placement performance
Platform breakdown
Top countries by revenue
Recommended AdSense unit names
Rename units inside Google AdSense. Skinbase imports names and does not modify inventory.