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) { return

No rows for this range.

} return (
{sorted.map((row) => ( ))}
{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)}
) } export default function AdsenseIndex({ connection, configured, pendingAccounts = [], recommendedPlacements = [], range, dashboard, routes, }) { const flash = usePage().props.flash ?? {} const [chartMetric, setChartMetric] = useState('estimated_earnings') const status = statusCopy(connection) const query = new URLSearchParams() query.set('period', range.period) if (range.period === 'custom') { query.set('from', range.from) query.set('to', range.to) } const applyPeriod = (period, from = null, to = null) => { const params = { period } if (period === 'custom' && from && to) { params.from = from params.to = to } router.get(routes.index, params, { preserveState: true, replace: true }) } return (
{flash.success ? (
{flash.success}
) : null} {flash.warning ? (
{flash.warning}
) : null} {flash.error ? (
{flash.error}
) : null}

Google AdSense

{status.label}

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}
{connection?.needs_reconnect || connection?.status === 'reconnect_required' ? 'Reconnect' : 'Connect Google AdSense'} {connection?.status === 'connected' ? ( ) : null} {connection && connection.status !== 'disconnected' ? ( ) : null}
{pendingAccounts.length ? (

Select AdSense account

{pendingAccounts.map((account) => ( ))}
) : null}

Period

{range.from} to {range.to}

{[ ['today', 'Today'], ['yesterday', 'Yesterday'], ['7d', 'Last 7 days'], ['30d', 'Last 30 days'], ].map(([value, label]) => ( ))}
{ event.preventDefault() const form = new FormData(event.currentTarget) applyPeriod('custom', form.get('from'), form.get('to')) }} >

Today's AdSense data is estimated and may change.

{dashboard ? ( <>
{dashboard.kpis.map((kpi) => (

{kpi.label}

{formatKpi(kpi)}

{formatChange(kpi.change_percent)}

))}

Daily trend

{[ ['estimated_earnings', 'Revenue'], ['page_views', 'Page Views'], ['page_views_rpm', 'Page RPM'], ['clicks', 'Clicks'], ].map(([value, label]) => ( ))}

Placement performance

Platform breakdown

Top countries by revenue

) : null}

Recommended AdSense unit names

Rename units inside Google AdSense. Skinbase imports names and does not modify inventory.

    {recommendedPlacements.map((name) => (
  • {name}
  • ))}
) }