dev-api-ui 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/package.json +40 -0
- package/ui/ThemeProvider.tsx +16 -0
- package/ui/components/ArticlePage.tsx +673 -0
- package/ui/components/AuthPage.tsx +109 -0
- package/ui/components/Callout.tsx +79 -0
- package/ui/components/Chart.tsx +100 -0
- package/ui/components/CodeTabs.tsx +145 -0
- package/ui/components/ComparePage.tsx +364 -0
- package/ui/components/DashboardPage.tsx +773 -0
- package/ui/components/DocsNav.tsx +80 -0
- package/ui/components/Footer.tsx +136 -0
- package/ui/components/HubPage.tsx +529 -0
- package/ui/components/Modal.tsx +412 -0
- package/ui/components/Nav.tsx +162 -0
- package/ui/components/OnThisPage.tsx +56 -0
- package/ui/components/ParamsTable.tsx +86 -0
- package/ui/components/RangeBar.tsx +68 -0
- package/ui/components/SeriesChart.tsx +218 -0
- package/ui/components/SeriesPage.tsx +461 -0
- package/ui/components/StatusMark.tsx +48 -0
- package/ui/components/publictrades/ExternalLink.tsx +37 -0
- package/ui/components/publictrades/FactsCard.tsx +40 -0
- package/ui/components/publictrades/LedgerFooter.tsx +22 -0
- package/ui/components/publictrades/LedgerNav.tsx +78 -0
- package/ui/components/publictrades/Mark.tsx +40 -0
- package/ui/components/publictrades/SourceBlock.tsx +91 -0
- package/ui/components/publictrades/Tape.tsx +164 -0
- package/ui/components/publictrades/ThreeStamps.tsx +47 -0
- package/ui/components/publictrades/types.ts +19 -0
- package/ui/data/series.ts +851 -0
- package/ui/index.ts +50 -0
- package/ui/publictrades.ts +16 -0
- package/ui/themes.css +81 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import StatusMark from './StatusMark';
|
|
6
|
+
|
|
7
|
+
type View = 'dashboard' | 'billing' | 'settings';
|
|
8
|
+
|
|
9
|
+
/* ── tiny helpers ────────────────────────────────────────────────── */
|
|
10
|
+
const A = 'var(--accent)';
|
|
11
|
+
const D = 'var(--down)';
|
|
12
|
+
const W = 'var(--warn)';
|
|
13
|
+
const T = 'var(--tertiary)';
|
|
14
|
+
const DOTS = '••••••••••••••••••••••••••';
|
|
15
|
+
|
|
16
|
+
function Label({ children }: { children: React.ReactNode }) {
|
|
17
|
+
return (
|
|
18
|
+
<span style={{
|
|
19
|
+
fontSize: 10, fontWeight: 600, textTransform: 'uppercase',
|
|
20
|
+
letterSpacing: '.07em', color: 'var(--tertiary)',
|
|
21
|
+
}}>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SectionHead({ children }: { children: React.ReactNode }) {
|
|
28
|
+
return (
|
|
29
|
+
<h2 style={{
|
|
30
|
+
fontFamily: 'var(--mono)', fontSize: 18, fontWeight: 600,
|
|
31
|
+
letterSpacing: '-.01em', margin: '30px 0 12px',
|
|
32
|
+
}}>
|
|
33
|
+
{children}
|
|
34
|
+
</h2>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Card({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) {
|
|
39
|
+
return (
|
|
40
|
+
<div style={{
|
|
41
|
+
background: 'var(--surface)', border: '0.5px solid var(--hairline-2)',
|
|
42
|
+
borderRadius: 8, ...style,
|
|
43
|
+
}}>
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function GhostBtn({ children, onClick, style }: {
|
|
50
|
+
children: React.ReactNode; onClick?: () => void; style?: React.CSSProperties;
|
|
51
|
+
}) {
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
onClick={onClick}
|
|
55
|
+
style={{
|
|
56
|
+
fontFamily: 'inherit', fontSize: 13, fontWeight: 500,
|
|
57
|
+
padding: '7px 13px', borderRadius: 4,
|
|
58
|
+
border: '0.5px solid var(--hairline-2)',
|
|
59
|
+
background: 'var(--surface)', color: 'var(--ink)', cursor: 'pointer',
|
|
60
|
+
...style,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── usage line/area chart (inline SVG, no lib) ──────────────────── */
|
|
69
|
+
function UsageChart() {
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
<svg viewBox="0 0 1040 200" width="100%" height="180" preserveAspectRatio="none" style={{ display: 'block' }} aria-hidden="true">
|
|
73
|
+
<line x1="0" y1="50" x2="1040" y2="50" stroke="var(--hairline)" strokeWidth="1" />
|
|
74
|
+
<line x1="0" y1="100" x2="1040" y2="100" stroke="var(--hairline)" strokeWidth="1" />
|
|
75
|
+
<line x1="0" y1="150" x2="1040" y2="150" stroke="var(--hairline)" strokeWidth="1" />
|
|
76
|
+
<polygon points="0,200 0,150 87,140 174,150 261,120 348,128 435,96 522,108 609,70 696,86 783,60 870,78 957,52 1040,64 1040,200" fill="var(--accent)" fillOpacity={0.07} />
|
|
77
|
+
<polyline points="0,150 87,140 174,150 261,120 348,128 435,96 522,108 609,70 696,86 783,60 870,78 957,52 1040,64" fill="none" stroke="var(--accent)" strokeWidth="2" />
|
|
78
|
+
</svg>
|
|
79
|
+
<div style={{
|
|
80
|
+
display: 'flex', justifyContent: 'space-between',
|
|
81
|
+
fontSize: 11, color: 'var(--tertiary)', padding: '8px 2px 2px',
|
|
82
|
+
}}>
|
|
83
|
+
<span>14:00 yesterday</span><span>02:00</span><span>14:00 today</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Sidebar ──────────────────────────────────────────────────────── */
|
|
90
|
+
interface NavItem { label: string; href: string; badge?: string; onClick?: () => void; active: boolean }
|
|
91
|
+
interface NavGroup { title: string; items: NavItem[] }
|
|
92
|
+
|
|
93
|
+
function Sidebar({ groups }: { groups: NavGroup[] }) {
|
|
94
|
+
return (
|
|
95
|
+
<aside style={{
|
|
96
|
+
borderRight: '0.5px solid var(--hairline)',
|
|
97
|
+
padding: '22px 16px',
|
|
98
|
+
position: 'sticky', top: 55,
|
|
99
|
+
alignSelf: 'start',
|
|
100
|
+
height: 'calc(100vh - 55px)',
|
|
101
|
+
overflowY: 'auto',
|
|
102
|
+
width: 222, flexShrink: 0,
|
|
103
|
+
}}>
|
|
104
|
+
{groups.map((g) => (
|
|
105
|
+
<div key={g.title} style={{ marginBottom: 20 }}>
|
|
106
|
+
<div style={{
|
|
107
|
+
fontSize: 11, fontWeight: 600, textTransform: 'uppercase',
|
|
108
|
+
letterSpacing: '.07em', color: 'var(--tertiary)',
|
|
109
|
+
marginBottom: 8, padding: '0 8px',
|
|
110
|
+
}}>
|
|
111
|
+
{g.title}
|
|
112
|
+
</div>
|
|
113
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
114
|
+
{g.items.map((it) => (
|
|
115
|
+
<a
|
|
116
|
+
key={it.label}
|
|
117
|
+
href={it.href}
|
|
118
|
+
onClick={it.onClick ? (e) => { e.preventDefault(); it.onClick!(); } : undefined}
|
|
119
|
+
aria-current={it.active ? 'page' : undefined}
|
|
120
|
+
style={{
|
|
121
|
+
display: 'flex', alignItems: 'center', gap: 9,
|
|
122
|
+
fontSize: 14, padding: '7px 8px', borderRadius: 4,
|
|
123
|
+
textDecoration: 'none',
|
|
124
|
+
color: it.active ? 'var(--ink)' : 'var(--secondary)',
|
|
125
|
+
background: it.active ? '#F2F2EF' : 'transparent',
|
|
126
|
+
fontWeight: it.active ? 500 : 400,
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{it.label}
|
|
130
|
+
{it.badge && (
|
|
131
|
+
<span style={{ marginLeft: 'auto', fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--tertiary)' }}>
|
|
132
|
+
{it.badge}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</a>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</aside>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ── Overview view ───────────────────────────────────────────────── */
|
|
145
|
+
function OverviewView({ pkgName, repoUrl }: { pkgName: string; repoUrl: string }) {
|
|
146
|
+
const [shown, setShown] = useState<Record<string, boolean>>({});
|
|
147
|
+
const toggle = (id: string) => setShown((s) => ({ ...s, [id]: !s[id] }));
|
|
148
|
+
|
|
149
|
+
const stats = [
|
|
150
|
+
{ label: 'Calls today', value: '84,210', note: '+12% vs yesterday', color: A },
|
|
151
|
+
{ label: 'Daily quota', value: '8.4%', note: 'well within limit', color: A },
|
|
152
|
+
{ label: 'Active keys', value: '2', note: '1 used in last hour', color: A },
|
|
153
|
+
{ label: 'p50 latency', value: '41ms', note: '+3ms vs last wk', color: W },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const keyDefs = [
|
|
157
|
+
{ id: 'live', env: 'LIVE', real: 'ck_live_8f3a2b9c7f0d4e1aa44', accent: true },
|
|
158
|
+
{ id: 'sandbox', env: 'SANDBOX', real: 'ck_test_2b9c7f0d4e1a8f3a210', accent: false },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const requests = [
|
|
162
|
+
{ time: '14:02:11', endpoint: 'GET /v1/fx/eurusd', code: '200', latency: '38 ms', color: A },
|
|
163
|
+
{ time: '14:02:09', endpoint: 'GET /v1/fx/gbpusd', code: '200', latency: '41 ms', color: A },
|
|
164
|
+
{ time: '14:01:52', endpoint: 'GET /v1/metals/xauusd', code: '200', latency: '55 ms', color: A },
|
|
165
|
+
{ time: '14:01:30', endpoint: 'GET /v1/power/pjm-rt', code: '429', latency: '12 ms', color: W },
|
|
166
|
+
{ time: '14:00:18', endpoint: 'GET /v1/fx/usdzar', code: '403', latency: '9 ms', color: D },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const webhooks = [
|
|
170
|
+
{ url: 'https://northwind.co/hooks/fx', event: 'series.threshold', status: 'Active', color: A },
|
|
171
|
+
{ url: 'https://northwind.co/hooks/status', event: 'feed.status', status: 'Failing', color: D },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const tbl = { borderBottom: '0.5px solid var(--hairline)', fontSize: 13 } as const;
|
|
175
|
+
const tHead = { fontSize: 10, fontWeight: 600, textTransform: 'uppercase' as const, letterSpacing: '.07em', color: 'var(--tertiary)' };
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
{/* Header row */}
|
|
180
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap', marginBottom: 20 }}>
|
|
181
|
+
<div>
|
|
182
|
+
<h1 style={{ fontFamily: 'var(--mono)', fontSize: 26, fontWeight: 600, letterSpacing: '-.02em' }}>Overview</h1>
|
|
183
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 4 }}>
|
|
184
|
+
Project <span style={{ fontFamily: 'var(--mono)', color: 'var(--ink)' }}>fx-prod</span> · last 24 hours
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
<a
|
|
188
|
+
href="#keys"
|
|
189
|
+
style={{
|
|
190
|
+
display: 'inline-flex', alignItems: 'center', gap: 7,
|
|
191
|
+
fontSize: 14, fontWeight: 500, padding: '9px 15px',
|
|
192
|
+
borderRadius: 4, border: '0.5px solid transparent',
|
|
193
|
+
background: 'var(--accent)', color: '#fff', textDecoration: 'none',
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
Create key
|
|
197
|
+
</a>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Stats row */}
|
|
201
|
+
<div id="overview" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 14, scrollMarginTop: 70 }}>
|
|
202
|
+
{stats.map((s) => (
|
|
203
|
+
<Card key={s.label} style={{ padding: '16px 18px' }}>
|
|
204
|
+
<Label>{s.label}</Label>
|
|
205
|
+
<div style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-.02em', marginTop: 7 }}>{s.value}</div>
|
|
206
|
+
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 500, marginTop: 7, color: s.color }}>
|
|
207
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: s.color, display: 'inline-block' }} />
|
|
208
|
+
{s.note}
|
|
209
|
+
</div>
|
|
210
|
+
</Card>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* API Keys */}
|
|
215
|
+
<h2 id="keys" style={{ fontFamily: 'var(--mono)', fontSize: 18, fontWeight: 600, letterSpacing: '-.01em', margin: '36px 0 6px', scrollMarginTop: 70 }}>
|
|
216
|
+
API keys
|
|
217
|
+
</h2>
|
|
218
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginBottom: 14 }}>
|
|
219
|
+
One key streams live and pulls history. Keep them server-side; rotate instantly if exposed.
|
|
220
|
+
</p>
|
|
221
|
+
<Card style={{ padding: '6px 18px' }}>
|
|
222
|
+
{keyDefs.map((k, i) => {
|
|
223
|
+
const isShown = !!shown[k.id];
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
key={k.id}
|
|
227
|
+
style={{
|
|
228
|
+
display: 'flex', alignItems: 'center', gap: 14,
|
|
229
|
+
padding: '14px 0',
|
|
230
|
+
borderBottom: i < keyDefs.length - 1 ? '0.5px solid var(--hairline)' : 'none',
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<span style={{
|
|
234
|
+
flexShrink: 0, width: 88, textAlign: 'center',
|
|
235
|
+
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.04em',
|
|
236
|
+
color: k.accent ? A : T,
|
|
237
|
+
border: `0.5px solid ${k.accent ? '#BFE3D0' : 'var(--hairline-2)'}`,
|
|
238
|
+
borderRadius: 4, padding: '5px 0',
|
|
239
|
+
}}>
|
|
240
|
+
{k.env}
|
|
241
|
+
</span>
|
|
242
|
+
<span style={{
|
|
243
|
+
flex: 1, minWidth: 0,
|
|
244
|
+
fontFamily: 'var(--mono)', fontSize: 13, color: 'var(--ink)',
|
|
245
|
+
border: '0.5px solid var(--hairline-2)', borderRadius: 4,
|
|
246
|
+
padding: '8px 12px', background: 'var(--paper)',
|
|
247
|
+
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
248
|
+
}}>
|
|
249
|
+
{isShown ? k.real : DOTS}
|
|
250
|
+
</span>
|
|
251
|
+
<GhostBtn onClick={() => toggle(k.id)}>{isShown ? 'Hide' : 'Reveal'}</GhostBtn>
|
|
252
|
+
<GhostBtn onClick={() => navigator.clipboard.writeText(k.real)}>Copy</GhostBtn>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
})}
|
|
256
|
+
</Card>
|
|
257
|
+
|
|
258
|
+
{/* REST + Install */}
|
|
259
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
|
|
260
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
261
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
262
|
+
<Label>REST API</Label>
|
|
263
|
+
<span style={{
|
|
264
|
+
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600,
|
|
265
|
+
color: '#fff', background: 'var(--ink)', borderRadius: 4, padding: '2px 7px',
|
|
266
|
+
}}>HTTP</span>
|
|
267
|
+
</div>
|
|
268
|
+
<h3 style={{ fontFamily: 'var(--mono)', fontSize: 17, fontWeight: 600, letterSpacing: '-.01em', marginTop: 12 }}>
|
|
269
|
+
Use your key over REST
|
|
270
|
+
</h3>
|
|
271
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 8, lineHeight: 1.55 }}>
|
|
272
|
+
The same key that streams live prices pulls history over plain HTTP — fx, metals, and power, point-in-time, with every response available as JSON or CSV.
|
|
273
|
+
</p>
|
|
274
|
+
<a
|
|
275
|
+
href="#"
|
|
276
|
+
style={{
|
|
277
|
+
display: 'inline-flex', alignItems: 'center', gap: 7,
|
|
278
|
+
fontSize: 14, fontWeight: 500, marginTop: 14,
|
|
279
|
+
padding: '8px 14px', borderRadius: 4,
|
|
280
|
+
border: '0.5px solid var(--hairline-2)',
|
|
281
|
+
background: 'var(--surface)', color: 'var(--ink)', textDecoration: 'none',
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
Read the API docs
|
|
285
|
+
<svg viewBox="0 0 24 24" width={14} height={14} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
286
|
+
<path d="M5 12h14" /><path d="M13 6l6 6-6 6" />
|
|
287
|
+
</svg>
|
|
288
|
+
</a>
|
|
289
|
+
</Card>
|
|
290
|
+
|
|
291
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
292
|
+
<Label>Install</Label>
|
|
293
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 12, lineHeight: 1.55 }}>
|
|
294
|
+
The client is open source and published to PyPI. One dependency, typed responses.
|
|
295
|
+
</p>
|
|
296
|
+
<div style={{
|
|
297
|
+
display: 'flex', alignItems: 'center', gap: 10, marginTop: 14,
|
|
298
|
+
border: '0.5px solid var(--hairline-2)', borderRadius: 4,
|
|
299
|
+
padding: '9px 12px', background: 'var(--paper)',
|
|
300
|
+
}}>
|
|
301
|
+
<span style={{ flex: 1, minWidth: 0, fontFamily: 'var(--mono)', fontSize: 13, color: 'var(--ink)' }}>
|
|
302
|
+
<span style={{ color: 'var(--accent)' }}>$</span> pip install {pkgName}
|
|
303
|
+
</span>
|
|
304
|
+
<GhostBtn style={{ fontSize: 12, padding: '5px 11px' }} onClick={() => navigator.clipboard.writeText('pip install console')}>Copy</GhostBtn>
|
|
305
|
+
</div>
|
|
306
|
+
<a
|
|
307
|
+
href="#"
|
|
308
|
+
style={{
|
|
309
|
+
display: 'inline-flex', alignItems: 'center', gap: 7,
|
|
310
|
+
fontSize: 13, color: 'var(--secondary)', marginTop: 12, textDecoration: 'none',
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<svg viewBox="0 0 24 24" width={15} height={15} fill="currentColor" aria-hidden="true">
|
|
314
|
+
<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85l-.01 2.75c0 .27.18.58.69.48A10 10 0 0 0 12 2z" />
|
|
315
|
+
</svg>
|
|
316
|
+
{repoUrl}
|
|
317
|
+
</a>
|
|
318
|
+
</Card>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Usage */}
|
|
322
|
+
<h2 id="usage" style={{ fontFamily: 'var(--mono)', fontSize: 18, fontWeight: 600, letterSpacing: '-.01em', margin: '34px 0 14px', scrollMarginTop: 70 }}>
|
|
323
|
+
Usage
|
|
324
|
+
</h2>
|
|
325
|
+
<Card style={{ padding: '18px 20px 12px' }}>
|
|
326
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
|
327
|
+
<div style={{ fontSize: 14, fontWeight: 500 }}>Requests · last 24h</div>
|
|
328
|
+
<div style={{ fontSize: 12, color: 'var(--tertiary)' }}>84,210 of 1,000,000 daily</div>
|
|
329
|
+
</div>
|
|
330
|
+
<UsageChart />
|
|
331
|
+
</Card>
|
|
332
|
+
|
|
333
|
+
{/* Recent requests */}
|
|
334
|
+
<div style={{ margin: '24px 0 14px' }}>
|
|
335
|
+
<h3 style={{ fontFamily: 'var(--mono)', fontSize: 15, fontWeight: 600, letterSpacing: '-.01em' }}>Recent requests</h3>
|
|
336
|
+
</div>
|
|
337
|
+
<Card style={{ overflow: 'hidden' }}>
|
|
338
|
+
<div style={{ display: 'grid', gridTemplateColumns: '150px 1fr 90px 90px', alignItems: 'center', gap: 16, padding: '9px 18px', borderBottom: '0.5px solid var(--hairline-2)', background: '#F4F4F1' }}>
|
|
339
|
+
{['Time', 'Endpoint', 'Status', 'Latency'].map((h, i) => (
|
|
340
|
+
<span key={h} style={{ ...tHead, textAlign: i === 3 ? 'right' : undefined }}>{h}</span>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
{requests.map((r) => (
|
|
344
|
+
<div key={r.time + r.endpoint} style={{ display: 'grid', gridTemplateColumns: '150px 1fr 90px 90px', alignItems: 'center', gap: 16, padding: '11px 18px', ...tbl }}>
|
|
345
|
+
<span style={{ color: 'var(--secondary)' }}>{r.time}</span>
|
|
346
|
+
<span style={{ fontFamily: 'var(--mono)', color: 'var(--ink)' }}>{r.endpoint}</span>
|
|
347
|
+
<span>
|
|
348
|
+
<StatusMark color={r.color} label={r.code} size={6} fontSize={13} />
|
|
349
|
+
</span>
|
|
350
|
+
<span style={{ textAlign: 'right', color: 'var(--secondary)' }}>{r.latency}</span>
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</Card>
|
|
354
|
+
|
|
355
|
+
{/* Webhooks */}
|
|
356
|
+
<h2 id="webhooks" style={{ fontFamily: 'var(--mono)', fontSize: 18, fontWeight: 600, letterSpacing: '-.01em', margin: '36px 0 6px', scrollMarginTop: 70 }}>
|
|
357
|
+
Webhooks
|
|
358
|
+
</h2>
|
|
359
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginBottom: 14 }}>
|
|
360
|
+
Get a POST when a series crosses a threshold or a feed changes status.
|
|
361
|
+
</p>
|
|
362
|
+
<Card style={{ overflow: 'hidden' }}>
|
|
363
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr 110px', alignItems: 'center', gap: 16, padding: '9px 18px', borderBottom: '0.5px solid var(--hairline-2)', background: '#F4F4F1' }}>
|
|
364
|
+
{['Endpoint', 'Event', 'Status'].map((h) => (
|
|
365
|
+
<span key={h} style={tHead}>{h}</span>
|
|
366
|
+
))}
|
|
367
|
+
</div>
|
|
368
|
+
{webhooks.map((w) => (
|
|
369
|
+
<div key={w.url} style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr 110px', alignItems: 'center', gap: 16, padding: '12px 18px', ...tbl }}>
|
|
370
|
+
<span style={{ fontFamily: 'var(--mono)', color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.url}</span>
|
|
371
|
+
<span style={{ color: 'var(--secondary)' }}>{w.event}</span>
|
|
372
|
+
<span><StatusMark color={w.color} label={w.status} size={6} fontSize={13} /></span>
|
|
373
|
+
</div>
|
|
374
|
+
))}
|
|
375
|
+
<div style={{ padding: '13px 18px' }}>
|
|
376
|
+
<GhostBtn>
|
|
377
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
|
378
|
+
<svg viewBox="0 0 24 24" width={14} height={14} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
379
|
+
<path d="M12 5v14" /><path d="M5 12h14" />
|
|
380
|
+
</svg>
|
|
381
|
+
Add endpoint
|
|
382
|
+
</span>
|
|
383
|
+
</GhostBtn>
|
|
384
|
+
</div>
|
|
385
|
+
</Card>
|
|
386
|
+
</>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── Billing view ────────────────────────────────────────────────── */
|
|
391
|
+
function BillingView() {
|
|
392
|
+
const plan = { price: '$49', cycle: '/mo', renews: 'Jul 1, 2026', calls: '84,210', quota: '1,000,000', pct: 8 };
|
|
393
|
+
const invoices = [
|
|
394
|
+
{ date: '2026-06-01', amount: '$49.00', status: 'Paid', color: A },
|
|
395
|
+
{ date: '2026-05-01', amount: '$49.00', status: 'Paid', color: A },
|
|
396
|
+
{ date: '2026-04-01', amount: '$49.00', status: 'Paid', color: A },
|
|
397
|
+
{ date: '2026-03-01', amount: '$19.00', status: 'Paid', color: A },
|
|
398
|
+
];
|
|
399
|
+
const tHead = { fontSize: 10, fontWeight: 600, textTransform: 'uppercase' as const, letterSpacing: '.07em', color: 'var(--tertiary)' };
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div style={{ maxWidth: 760 }}>
|
|
403
|
+
<h1 style={{ fontFamily: 'var(--mono)', fontSize: 26, fontWeight: 600, letterSpacing: '-.02em' }}>Billing</h1>
|
|
404
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 4 }}>
|
|
405
|
+
Workspace <span style={{ fontFamily: 'var(--mono)', color: 'var(--ink)' }}>Northwind</span> · plan, usage, and invoices
|
|
406
|
+
</p>
|
|
407
|
+
|
|
408
|
+
{/* Current plan */}
|
|
409
|
+
<Card style={{ padding: '20px 22px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 18, flexWrap: 'wrap', marginTop: 24 }}>
|
|
410
|
+
<div>
|
|
411
|
+
<Label>Current plan</Label>
|
|
412
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 8 }}>
|
|
413
|
+
<span style={{ fontFamily: 'var(--mono)', fontSize: 24, fontWeight: 600, letterSpacing: '-.02em' }}>Pro</span>
|
|
414
|
+
<span style={{ fontSize: 15, color: 'var(--secondary)' }}>{plan.price}{plan.cycle}</span>
|
|
415
|
+
</div>
|
|
416
|
+
<div style={{ fontSize: 13, color: 'var(--tertiary)', marginTop: 4 }}>Renews {plan.renews}</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
|
419
|
+
<a
|
|
420
|
+
href="#"
|
|
421
|
+
style={{
|
|
422
|
+
display: 'inline-flex', alignItems: 'center', gap: 7,
|
|
423
|
+
fontSize: 14, fontWeight: 500, padding: '9px 15px',
|
|
424
|
+
borderRadius: 4, border: '0.5px solid transparent',
|
|
425
|
+
background: 'var(--accent)', color: '#fff', textDecoration: 'none',
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
Manage plan
|
|
429
|
+
</a>
|
|
430
|
+
<GhostBtn style={{ fontSize: 14, padding: '9px 15px' }}>Cancel plan</GhostBtn>
|
|
431
|
+
</div>
|
|
432
|
+
</Card>
|
|
433
|
+
|
|
434
|
+
<SectionHead>Usage this cycle</SectionHead>
|
|
435
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
436
|
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
|
|
437
|
+
<span style={{ fontSize: 14, color: 'var(--ink)' }}>
|
|
438
|
+
<span style={{ fontWeight: 500 }}>{plan.calls}</span> of {plan.quota} calls
|
|
439
|
+
</span>
|
|
440
|
+
<span style={{ fontSize: 13, color: 'var(--tertiary)' }}>{plan.pct}% used</span>
|
|
441
|
+
</div>
|
|
442
|
+
<div style={{ height: 8, borderRadius: 999, background: '#EFEFEC', marginTop: 12, overflow: 'hidden' }}>
|
|
443
|
+
<div style={{ height: '100%', width: `${plan.pct}%`, background: 'var(--accent)', borderRadius: 999 }} />
|
|
444
|
+
</div>
|
|
445
|
+
<div style={{ fontSize: 13, color: 'var(--tertiary)', marginTop: 10 }}>
|
|
446
|
+
Overage is billed at $0.40 per 1,000 calls beyond your daily quota.
|
|
447
|
+
</div>
|
|
448
|
+
</Card>
|
|
449
|
+
|
|
450
|
+
<SectionHead>Payment method</SectionHead>
|
|
451
|
+
<Card style={{ padding: '18px 22px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 18, flexWrap: 'wrap' }}>
|
|
452
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
453
|
+
<span style={{
|
|
454
|
+
flexShrink: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
455
|
+
width: 38, height: 26, border: '0.5px solid var(--hairline-2)', borderRadius: 4, color: 'var(--secondary)',
|
|
456
|
+
}}>
|
|
457
|
+
<svg viewBox="0 0 24 24" width={18} height={18} fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
|
|
458
|
+
<rect x="2" y="5" width="20" height="14" rx="2" /><path d="M2 10h20" />
|
|
459
|
+
</svg>
|
|
460
|
+
</span>
|
|
461
|
+
<div>
|
|
462
|
+
<div style={{ fontSize: 14, fontWeight: 500 }}>Visa ending 4242</div>
|
|
463
|
+
<div style={{ fontSize: 13, color: 'var(--tertiary)' }}>Expires 04/28</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
<GhostBtn style={{ fontSize: 14, padding: '9px 15px' }}>Update</GhostBtn>
|
|
467
|
+
</Card>
|
|
468
|
+
|
|
469
|
+
<SectionHead>Invoices</SectionHead>
|
|
470
|
+
<Card style={{ overflow: 'hidden' }}>
|
|
471
|
+
<div style={{ display: 'grid', gridTemplateColumns: '170px 120px 1fr 30px', alignItems: 'center', gap: 16, padding: '9px 18px', borderBottom: '0.5px solid var(--hairline-2)', background: '#F4F4F1' }}>
|
|
472
|
+
{['Date', 'Amount', 'Status', ''].map((h, i) => (
|
|
473
|
+
<span key={i} style={tHead}>{h}</span>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
{invoices.map((v) => (
|
|
477
|
+
<div key={v.date} style={{ display: 'grid', gridTemplateColumns: '170px 120px 1fr 30px', alignItems: 'center', gap: 16, padding: '12px 18px', borderBottom: '0.5px solid var(--hairline)', fontSize: 13 }}>
|
|
478
|
+
<span style={{ color: 'var(--secondary)' }}>{v.date}</span>
|
|
479
|
+
<span style={{ fontWeight: 500 }}>{v.amount}</span>
|
|
480
|
+
<span><StatusMark color={v.color} label={v.status} size={6} fontSize={13} /></span>
|
|
481
|
+
<span style={{ textAlign: 'right' }}>
|
|
482
|
+
<a href="#" aria-label={'Download invoice ' + v.date} style={{ display: 'inline-flex', color: 'var(--accent)' }}>
|
|
483
|
+
<svg viewBox="0 0 24 24" width={14} height={14} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
484
|
+
<path d="M12 3v12" /><path d="M7 11l5 5 5-5" /><path d="M5 21h14" />
|
|
485
|
+
</svg>
|
|
486
|
+
</a>
|
|
487
|
+
</span>
|
|
488
|
+
</div>
|
|
489
|
+
))}
|
|
490
|
+
</Card>
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ── Settings view ───────────────────────────────────────────────── */
|
|
496
|
+
function SettingsView() {
|
|
497
|
+
const [twofa, setTwofa] = useState(true);
|
|
498
|
+
|
|
499
|
+
const sessions = [
|
|
500
|
+
{ device: 'MacBook Pro · Chrome', loc: 'London, UK', last: 'Active now', current: true },
|
|
501
|
+
{ device: 'iPhone 15 · Safari', loc: 'London, UK', last: '2 hours ago', current: false },
|
|
502
|
+
{ device: 'Linux · Firefox', loc: 'Frankfurt, DE', last: '3 days ago', current: false },
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
const inputStyle: React.CSSProperties = {
|
|
506
|
+
width: '100%', fontFamily: 'inherit', fontSize: 14,
|
|
507
|
+
padding: '9px 11px', border: '0.5px solid var(--hairline-2)',
|
|
508
|
+
borderRadius: 4, background: 'var(--surface)', color: 'var(--ink)',
|
|
509
|
+
outline: 'none',
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<div style={{ maxWidth: 720 }}>
|
|
514
|
+
<h1 style={{ fontFamily: 'var(--mono)', fontSize: 26, fontWeight: 600, letterSpacing: '-.02em' }}>Settings</h1>
|
|
515
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 4 }}>
|
|
516
|
+
Workspace <span style={{ fontFamily: 'var(--mono)', color: 'var(--ink)' }}>Northwind</span> · profile, security, and defaults
|
|
517
|
+
</p>
|
|
518
|
+
|
|
519
|
+
<SectionHead>Profile</SectionHead>
|
|
520
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
521
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
522
|
+
<label style={{ display: 'block' }}>
|
|
523
|
+
<span style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--secondary)', marginBottom: 6 }}>Full name</span>
|
|
524
|
+
<input defaultValue="Dana Khoury" style={inputStyle} />
|
|
525
|
+
</label>
|
|
526
|
+
<label style={{ display: 'block' }}>
|
|
527
|
+
<span style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--secondary)', marginBottom: 6 }}>Email</span>
|
|
528
|
+
<input defaultValue="dana@northwind.co" style={inputStyle} />
|
|
529
|
+
</label>
|
|
530
|
+
</div>
|
|
531
|
+
<div style={{ marginTop: 16 }}>
|
|
532
|
+
<GhostBtn style={{ fontSize: 14, padding: '8px 15px' }}>Save changes</GhostBtn>
|
|
533
|
+
</div>
|
|
534
|
+
</Card>
|
|
535
|
+
|
|
536
|
+
<SectionHead>Security</SectionHead>
|
|
537
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
538
|
+
{/* Password change */}
|
|
539
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
540
|
+
<label style={{ display: 'block' }}>
|
|
541
|
+
<span style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--secondary)', marginBottom: 6 }}>Current password</span>
|
|
542
|
+
<input type="password" style={inputStyle} />
|
|
543
|
+
</label>
|
|
544
|
+
<label style={{ display: 'block' }}>
|
|
545
|
+
<span style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--secondary)', marginBottom: 6 }}>New password</span>
|
|
546
|
+
<input type="password" style={inputStyle} />
|
|
547
|
+
</label>
|
|
548
|
+
</div>
|
|
549
|
+
<div style={{ marginTop: 16 }}>
|
|
550
|
+
<GhostBtn style={{ fontSize: 14, padding: '8px 15px' }}>Update password</GhostBtn>
|
|
551
|
+
</div>
|
|
552
|
+
<div style={{ marginTop: 18, borderTop: '0.5px solid var(--hairline)' }} />
|
|
553
|
+
|
|
554
|
+
{/* 2FA toggle */}
|
|
555
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, paddingTop: 18 }}>
|
|
556
|
+
<div>
|
|
557
|
+
<div style={{ fontSize: 14, fontWeight: 500 }}>Two-factor authentication</div>
|
|
558
|
+
<div style={{ fontSize: 13, color: 'var(--secondary)', marginTop: 2 }}>Require a one-time code at sign-in.</div>
|
|
559
|
+
</div>
|
|
560
|
+
{/* Real stateful toggle */}
|
|
561
|
+
<button
|
|
562
|
+
onClick={() => setTwofa((v) => !v)}
|
|
563
|
+
role="switch"
|
|
564
|
+
aria-checked={twofa}
|
|
565
|
+
aria-label="Two-factor authentication"
|
|
566
|
+
style={{
|
|
567
|
+
flexShrink: 0, width: 40, height: 23, borderRadius: 999,
|
|
568
|
+
border: '0.5px solid var(--hairline-2)',
|
|
569
|
+
background: twofa ? 'var(--accent)' : 'var(--hairline-2)',
|
|
570
|
+
position: 'relative', cursor: 'pointer', padding: 0,
|
|
571
|
+
transition: 'background .15s',
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
<span style={{
|
|
575
|
+
position: 'absolute', top: 2,
|
|
576
|
+
left: twofa ? 18 : 2,
|
|
577
|
+
width: 17, height: 17, borderRadius: '50%',
|
|
578
|
+
background: '#fff', border: '0.5px solid var(--hairline-2)',
|
|
579
|
+
transition: 'left .15s',
|
|
580
|
+
}} />
|
|
581
|
+
</button>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
{/* Active sessions */}
|
|
585
|
+
<div style={{ paddingTop: 18, marginTop: 18, borderTop: '0.5px solid var(--hairline)' }}>
|
|
586
|
+
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)', marginBottom: 10 }}>Active sessions</div>
|
|
587
|
+
<div style={{ border: '0.5px solid var(--hairline-2)', borderRadius: 6, overflow: 'hidden' }}>
|
|
588
|
+
{sessions.map((s, i) => (
|
|
589
|
+
<div key={s.device} style={{
|
|
590
|
+
display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr', gap: 14,
|
|
591
|
+
alignItems: 'center', padding: '11px 14px',
|
|
592
|
+
borderBottom: i < sessions.length - 1 ? '0.5px solid var(--hairline)' : 'none',
|
|
593
|
+
fontSize: 13,
|
|
594
|
+
}}>
|
|
595
|
+
<span style={{ color: 'var(--ink)' }}>
|
|
596
|
+
{s.device}
|
|
597
|
+
{s.current && <span style={{ color: 'var(--accent)', fontWeight: 500 }}> · this device</span>}
|
|
598
|
+
</span>
|
|
599
|
+
<span style={{ color: 'var(--secondary)' }}>{s.loc}</span>
|
|
600
|
+
<span style={{ color: 'var(--tertiary)', textAlign: 'right' }}>{s.last}</span>
|
|
601
|
+
</div>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</Card>
|
|
606
|
+
|
|
607
|
+
<SectionHead>API defaults</SectionHead>
|
|
608
|
+
<Card style={{ padding: '20px 22px' }}>
|
|
609
|
+
<label style={{ display: 'block', maxWidth: 320 }}>
|
|
610
|
+
<span style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--secondary)', marginBottom: 6 }}>Usage alert threshold</span>
|
|
611
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
612
|
+
<input defaultValue="80" style={{ ...inputStyle, width: 90 }} />
|
|
613
|
+
<span style={{ fontSize: 14, color: 'var(--secondary)' }}>% of daily quota</span>
|
|
614
|
+
</div>
|
|
615
|
+
</label>
|
|
616
|
+
<div style={{ marginTop: 16 }}>
|
|
617
|
+
<GhostBtn style={{ fontSize: 14, padding: '8px 15px' }}>Save changes</GhostBtn>
|
|
618
|
+
</div>
|
|
619
|
+
</Card>
|
|
620
|
+
|
|
621
|
+
<SectionHead>
|
|
622
|
+
<span style={{ color: 'var(--down)' }}>Danger zone</span>
|
|
623
|
+
</SectionHead>
|
|
624
|
+
<div style={{
|
|
625
|
+
background: 'var(--surface)', border: '0.5px solid #E7C4C0', borderRadius: 8,
|
|
626
|
+
padding: '18px 22px', display: 'flex', alignItems: 'center',
|
|
627
|
+
justifyContent: 'space-between', gap: 18, flexWrap: 'wrap',
|
|
628
|
+
}}>
|
|
629
|
+
<div>
|
|
630
|
+
<div style={{ fontSize: 14, fontWeight: 500 }}>Delete project fx-prod</div>
|
|
631
|
+
<div style={{ fontSize: 13, color: 'var(--secondary)', marginTop: 2 }}>
|
|
632
|
+
Permanently removes keys, webhooks, and usage history. This cannot be undone.
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
<button style={{
|
|
636
|
+
flexShrink: 0, fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
|
|
637
|
+
padding: '9px 15px', borderRadius: 4,
|
|
638
|
+
border: '0.5px solid var(--down)', background: 'transparent',
|
|
639
|
+
color: 'var(--down)', cursor: 'pointer',
|
|
640
|
+
}}>
|
|
641
|
+
Delete project
|
|
642
|
+
</button>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* ── Dashboard topbar ────────────────────────────────────────────── */
|
|
649
|
+
function DashTopbar({ brand, onSignOut }: { brand: string; onSignOut: () => void }) {
|
|
650
|
+
return (
|
|
651
|
+
<header style={{
|
|
652
|
+
borderBottom: '0.5px solid var(--hairline)',
|
|
653
|
+
background: 'var(--surface)',
|
|
654
|
+
position: 'sticky', top: 0, zIndex: 20,
|
|
655
|
+
}}>
|
|
656
|
+
<div style={{
|
|
657
|
+
padding: '12px 22px', display: 'flex',
|
|
658
|
+
alignItems: 'center', justifyContent: 'space-between', gap: 24,
|
|
659
|
+
}}>
|
|
660
|
+
{/* Left */}
|
|
661
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flex: 1, minWidth: 0, maxWidth: 520 }}>
|
|
662
|
+
<div style={{
|
|
663
|
+
display: 'flex', alignItems: 'center', gap: 9,
|
|
664
|
+
fontFamily: 'var(--mono)', fontWeight: 600, fontSize: 16,
|
|
665
|
+
letterSpacing: '-.01em', flexShrink: 0,
|
|
666
|
+
}}>
|
|
667
|
+
<span style={{ width: 9, height: 9, background: 'var(--accent)', transform: 'rotate(45deg)', display: 'inline-block' }} />
|
|
668
|
+
{brand}
|
|
669
|
+
</div>
|
|
670
|
+
<span style={{
|
|
671
|
+
display: 'flex', alignItems: 'center', gap: 7,
|
|
672
|
+
fontSize: 12, color: 'var(--secondary)',
|
|
673
|
+
border: '0.5px solid var(--hairline-2)', borderRadius: 4,
|
|
674
|
+
padding: '5px 10px', flexShrink: 0,
|
|
675
|
+
}}>
|
|
676
|
+
Northwind
|
|
677
|
+
<svg viewBox="0 0 24 24" width={12} height={12} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
678
|
+
<path d="M6 9l6 6 6-6" />
|
|
679
|
+
</svg>
|
|
680
|
+
</span>
|
|
681
|
+
<span style={{
|
|
682
|
+
display: 'flex', alignItems: 'center', gap: 9, flex: 1, minWidth: 0,
|
|
683
|
+
fontSize: 13, color: 'var(--tertiary)',
|
|
684
|
+
border: '0.5px solid var(--hairline-2)', borderRadius: 4,
|
|
685
|
+
padding: '7px 11px', background: 'var(--paper)',
|
|
686
|
+
}}>
|
|
687
|
+
<svg viewBox="0 0 24 24" width={14} height={14} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
|
688
|
+
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4-4" />
|
|
689
|
+
</svg>
|
|
690
|
+
<span style={{ flex: 1 }}>Jump to…</span>
|
|
691
|
+
<span style={{ border: '0.5px solid var(--hairline-2)', borderRadius: 3, padding: '1px 6px', fontSize: 11 }}>⌘K</span>
|
|
692
|
+
</span>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
{/* Right */}
|
|
696
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0 }}>
|
|
697
|
+
<Link href="/docs" style={{ fontSize: 13, color: 'var(--secondary)', textDecoration: 'none' }}>Docs</Link>
|
|
698
|
+
<StatusMark color="var(--accent)" label="Operational" pulse size={6} fontSize={12} />
|
|
699
|
+
<button
|
|
700
|
+
onClick={onSignOut}
|
|
701
|
+
style={{
|
|
702
|
+
display: 'inline-flex', alignItems: 'center', gap: 8,
|
|
703
|
+
fontFamily: 'inherit', fontSize: 13, padding: '5px 10px 5px 6px',
|
|
704
|
+
borderRadius: 4, border: '0.5px solid var(--hairline-2)',
|
|
705
|
+
background: 'var(--surface)', color: 'var(--ink)', cursor: 'pointer',
|
|
706
|
+
}}
|
|
707
|
+
>
|
|
708
|
+
<span style={{
|
|
709
|
+
width: 22, height: 22, borderRadius: '50%',
|
|
710
|
+
background: 'var(--ink)', color: '#fff',
|
|
711
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
712
|
+
fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 600,
|
|
713
|
+
}}>DK</span>
|
|
714
|
+
Sign out
|
|
715
|
+
</button>
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
</header>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* ── Root export ─────────────────────────────────────────────────── */
|
|
723
|
+
export interface DashboardPageProps {
|
|
724
|
+
brand?: string;
|
|
725
|
+
pkgName?: string;
|
|
726
|
+
repoUrl?: string;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export default function DashboardPage({
|
|
730
|
+
brand = 'Console',
|
|
731
|
+
pkgName = 'console',
|
|
732
|
+
repoUrl = 'github.com/console/console-py',
|
|
733
|
+
}: DashboardPageProps = {}) {
|
|
734
|
+
const [view, setView] = useState<View>('dashboard');
|
|
735
|
+
const [section, setSection] = useState<string>('overview');
|
|
736
|
+
|
|
737
|
+
const mk = (label: string, href: string, badge: string | undefined, onClick: () => void, active: boolean): NavItem => ({
|
|
738
|
+
label, href, badge, onClick, active,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const navGroups: NavGroup[] = [
|
|
742
|
+
{
|
|
743
|
+
title: 'Project',
|
|
744
|
+
items: [
|
|
745
|
+
mk('Overview', '#overview', undefined, () => { setView('dashboard'); setSection('overview'); }, view === 'dashboard' && section === 'overview'),
|
|
746
|
+
mk('API keys', '#keys', '2', () => { setView('dashboard'); setSection('keys'); }, view === 'dashboard' && section === 'keys'),
|
|
747
|
+
mk('Usage', '#usage', undefined, () => { setView('dashboard'); setSection('usage'); }, view === 'dashboard' && section === 'usage'),
|
|
748
|
+
mk('Webhooks', '#webhooks', '2', () => { setView('dashboard'); setSection('webhooks'); }, view === 'dashboard' && section === 'webhooks'),
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
title: 'Account',
|
|
753
|
+
items: [
|
|
754
|
+
mk('Billing', '#', undefined, () => setView('billing'), view === 'billing'),
|
|
755
|
+
mk('Settings', '#', undefined, () => setView('settings'), view === 'settings'),
|
|
756
|
+
],
|
|
757
|
+
},
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
762
|
+
<DashTopbar brand={brand} onSignOut={() => {}} />
|
|
763
|
+
<div style={{ display: 'grid', gridTemplateColumns: '222px minmax(0,1fr)', flex: 1, alignItems: 'start' }}>
|
|
764
|
+
<Sidebar groups={navGroups} />
|
|
765
|
+
<main style={{ padding: '26px 30px 64px', minWidth: 0 }}>
|
|
766
|
+
{view === 'dashboard' && <OverviewView pkgName={pkgName} repoUrl={repoUrl} />}
|
|
767
|
+
{view === 'billing' && <BillingView />}
|
|
768
|
+
{view === 'settings' && <SettingsView />}
|
|
769
|
+
</main>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
);
|
|
773
|
+
}
|