/* Page: Keyword rankings + page mapping (SERanking-style centerpiece) */
const { useState: useStateR } = React;
function RankingsView({ client }) {
const { data, loading, error } = useApi(
`/api/clients/${client.id}/rankings`,
[client.id]
);
const [selectedKw, setSelectedKw] = useStateR(null);
const [tab, setTab] = useStateR('keywords');
const [groupBy, setGroupBy] = useStateR('flat');
if (loading) return
Loading rankings...
;
if (error) return Failed to load rankings: {error}
;
const KEYWORDS = data?.keywords || [];
const PAGES = data?.pages || [];
if (!selectedKw && KEYWORDS.length) {
// Can't call setSelectedKw here (would be a side effect in render)
// The table will render and first-row click sets the selection
}
const activeKw = selectedKw || KEYWORDS[0] || null;
const visibility = 38.4;
const tracked = KEYWORDS.length * 12; // pretend total is bigger
return (
<>
Rankings · 142 keywords across 38 pages
Keyword rankings
Tracked keywords mapped to landing pages. Position history, SERP features, intent. Drill into any keyword for daily detail.
Visibility score
{visibility}%
+4.6pp
Semrush · weighted
Top-3
38
+11
142 tracked · PH desktop
Top-10
76
+18
vs prev 30d
Avg position
11.4
−2.7
Lower is better
SERP features
28
Map pack · 14 · Snippet · 6
Semrush · daily
Tracked keywords
setTab('keywords')}>By keyword
setTab('pages')}>By page
setTab('intent')}>By intent
{/* Left: keyword table */}
| Keyword |
Mapped page |
Pos |
Δ |
Target |
History · 90d |
Vol |
KD |
SERP features |
{KEYWORDS.map((k, i) => {
const sel = activeKw?.kw === k.kw;
const posClass = k.pos <= 3 ? 'top3' : k.pos <= 10 ? 'top10' : k.pos <= 30 ? 'top30' : 'beyond';
const delta = k.posPrev - k.pos; // positive = improved
return (
setSelectedKw(k)}
style={{cursor:'pointer', background: sel ? 'var(--gold-dim)' : undefined}}>
|
{k.kw}
{k.intent} · {k.tags.join(' · ')}
|
{k.page} |
{k.pos} |
{delta !== 0 ? (
0 ? 'up' : 'down'}`}>
0 ? 'arrowUp' : 'arrowDown'} size={10}/>
{Math.abs(delta)}
) : —}
|
≤{k.target} |
|
{k.vol.toLocaleString()} |
{k.kd} |
{k.serp.length === 0 ? — :
k.serp.map((s, j) => (
{s}
))}
|
);
})}
{/* Right: keyword detail panel */}
Position · today
{activeKw?.pos || 0}
0 ? 'up' : (activeKw?.posPrev || 0) - (activeKw?.pos || 0) < 0 ? 'down' : 'flat'}`} style={{marginTop: 6}}>
{(activeKw?.posPrev || 0) - (activeKw?.pos || 0) > 0 ? : (activeKw?.posPrev || 0) - (activeKw?.pos || 0) < 0 ? : null}
{(activeKw?.posPrev || 0) - (activeKw?.pos || 0) === 0 ? 'No change' : `${Math.abs((activeKw?.posPrev || 0) - (activeKw?.pos || 0))} from prev week`}
Best · 90d
{(activeKw?.history?.length ? Math.min(...activeKw.history) : 0)}
Target · ≤{activeKw?.target || 0}
Mapped page
SERVICE
karatenation.ph{activeKw?.page || ''}
38 keywords · 4 820 sessions/mo
SERP composition
{(activeKw?.serp || []).length === 0 && No SERP features detected.}
{(activeKw?.serp || []).map((s, i) => (
{s}
))}
10 organic
3 ads
Top-10 competitors
{[
{ d: 'karatenation.ph', p: activeKw?.pos || 0, you: true },
{ d: 'manilamartialarts.com', p: 1 },
{ d: 'philippinekarate.org', p: 2 },
{ d: 'fitnessfirst.com.ph', p: 4 },
{ d: 'rappler.com', p: 6 },
].sort((a,b) => a.p - b.p).map((c, i) => (
{c.p}
{c.d} {c.you && YOU}
))}
>
);
}
/* Position history line — for the detail panel */
function PosHistoryChart({ values }) {
const w = 420, h = 140;
const padL = 28, padR = 8, padT = 8, padB = 22;
const innerW = w - padL - padR;
const innerH = h - padT - padB;
const max = Math.max(20, Math.max(...values));
const min = 1;
const stepX = innerW / (values.length - 1);
// Top of chart = best position (1)
const yFor = (v) => padT + ((v - min) / (max - min)) * innerH;
const pts = values.map((v, i) => [padL + i * stepX, yFor(v)]);
const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
const last = values[values.length - 1];
const lastColor = last <= 3 ? 'var(--gold)' : last <= 10 ? 'var(--accent)' : 'var(--text-muted)';
const yTicks = [1, 3, 10, 20].filter(t => t <= max);
return (
);
}
window.RankingsView = RankingsView;