Skip to content

Commit 54db727

Browse files
committed
feat: add improved keywords breakdown table
1 parent cc870be commit 54db727

1 file changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useMemo, useState } from 'react';
2+
3+
import { DeltaBadge } from '@/components/recharts-poc/kpi/DeltaBadge';
4+
import { IMPROVED_KEYWORDS } from '@/components/recharts-poc/mocks/improvedKeywords';
5+
import { periodOverPeriod } from '@/components/recharts-poc/primitives/periodOverPeriod';
6+
7+
type SortKey = 'keyword' | 'positionNow' | 'positionBefore' | 'delta';
8+
type SortDir = 'asc' | 'desc';
9+
10+
export function BreakdownTable() {
11+
const [sortKey, setSortKey] = useState<SortKey>('delta');
12+
const [sortDir, setSortDir] = useState<SortDir>('desc');
13+
14+
const rows = useMemo(() => {
15+
const enriched = IMPROVED_KEYWORDS.map((row) => {
16+
const result = periodOverPeriod(row.positionBefore, row.positionNow);
17+
return { ...row, delta: result.absDelta, result };
18+
});
19+
enriched.sort((a, b) => {
20+
const dir = sortDir === 'asc' ? 1 : -1;
21+
if (sortKey === 'keyword')
22+
return a.keyword.localeCompare(b.keyword) * dir;
23+
const av = a[sortKey];
24+
const bv = b[sortKey];
25+
return (Number(av) - Number(bv)) * dir;
26+
});
27+
return enriched;
28+
}, [sortKey, sortDir]);
29+
30+
const maxDelta = useMemo(
31+
() => Math.max(...rows.map((r) => Math.abs(r.delta))),
32+
[rows],
33+
);
34+
35+
const onHeaderClick = (key: SortKey) => {
36+
if (sortKey === key) {
37+
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
38+
} else {
39+
setSortKey(key);
40+
setSortDir('desc');
41+
}
42+
};
43+
44+
return (
45+
<section className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
46+
<h3 className="mb-3 text-base font-semibold text-gray-900 dark:text-gray-100">
47+
Improved Keywords
48+
</h3>
49+
<div className="overflow-x-auto">
50+
<table className="w-full text-sm">
51+
<thead>
52+
<tr className="text-left text-gray-500 dark:text-gray-400">
53+
<Th
54+
onClick={() => onHeaderClick('keyword')}
55+
active={sortKey === 'keyword'}
56+
dir={sortDir}
57+
>
58+
Keyword
59+
</Th>
60+
<Th
61+
onClick={() => onHeaderClick('positionNow')}
62+
active={sortKey === 'positionNow'}
63+
dir={sortDir}
64+
align="right"
65+
>
66+
Position now
67+
</Th>
68+
<Th
69+
onClick={() => onHeaderClick('positionBefore')}
70+
active={sortKey === 'positionBefore'}
71+
dir={sortDir}
72+
align="right"
73+
>
74+
Position before
75+
</Th>
76+
<Th
77+
onClick={() => onHeaderClick('delta')}
78+
active={sortKey === 'delta'}
79+
dir={sortDir}
80+
align="right"
81+
>
82+
Δ
83+
</Th>
84+
</tr>
85+
</thead>
86+
<tbody>
87+
{rows.map((row) => {
88+
const intensity =
89+
maxDelta === 0 ? 0 : Math.abs(row.delta) / maxDelta;
90+
const tint = `rgba(74, 144, 226, ${(intensity * 0.25).toFixed(3)})`;
91+
return (
92+
<tr
93+
key={row.keyword}
94+
className="border-t border-gray-100 dark:border-gray-800"
95+
>
96+
<td className="py-2 text-gray-800 dark:text-gray-200">
97+
{row.keyword}
98+
</td>
99+
<td
100+
className="py-2 text-right text-gray-800 dark:text-gray-200"
101+
style={{ background: tint }}
102+
>
103+
{row.positionNow}
104+
</td>
105+
<td className="py-2 text-right text-gray-800 dark:text-gray-200">
106+
{row.positionBefore}
107+
</td>
108+
<td className="py-2 text-right">
109+
<DeltaBadge result={row.result} />
110+
</td>
111+
</tr>
112+
);
113+
})}
114+
</tbody>
115+
</table>
116+
</div>
117+
</section>
118+
);
119+
}
120+
121+
interface ThProps {
122+
onClick: () => void;
123+
active: boolean;
124+
dir: SortDir;
125+
align?: 'left' | 'right';
126+
children: React.ReactNode;
127+
}
128+
129+
function Th(props: ThProps) {
130+
const { onClick, active, dir, align = 'left', children } = props;
131+
const arrow = active ? (dir === 'asc' ? ' ▲' : ' ▼') : '';
132+
return (
133+
<th
134+
className={`py-2 font-medium ${align === 'right' ? 'text-right' : 'text-left'}`}
135+
>
136+
<button
137+
type="button"
138+
onClick={onClick}
139+
className="hover:text-gray-900 dark:hover:text-gray-100"
140+
>
141+
{children}
142+
{arrow}
143+
</button>
144+
</th>
145+
);
146+
}

0 commit comments

Comments
 (0)