-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDirectClientDemo.tsx
More file actions
112 lines (104 loc) · 4.17 KB
/
Copy pathDirectClientDemo.tsx
File metadata and controls
112 lines (104 loc) · 4.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { ApiError } from '@developerehsan/api-client';
/**
* DIRECT CLIENT USAGE
* -------------------
* Call the typed client straight from components. Every method is fully typed
* from the OpenAPI spec — inputs are checked, results are inferred:
*
* api.products.searchProducts({ q, limit }) -> Promise<ProductList>
* api.products.getProductById({ id }) -> Promise<Product>
*
* Shows: typed query params, typed path params, loading/error states, typed
* error handling (ApiError with a real HTTP status), and the debounce-cancel
* window (a newer keystroke auto-aborts the previous in-flight search).
*/
import { useCallback, useEffect, useState } from 'react';
import { Button, Panel, Spinner, StatusBadge } from '../components/ui';
import { api } from '../lib/api/api.config';
import type { Product } from '../lib/api/types/generated/api.types';
export function DirectClientDemo() {
const [term, setTerm] = useState('phone');
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selected, setSelected] = useState<Product | null>(null);
const [elapsed, setElapsed] = useState<number | null>(null);
const load = useCallback(async (q: string) => {
setLoading(true);
setError(null);
const started = performance.now();
try {
// Typed query params. A newer search within the 300ms `dedupeWindow`
// auto-cancels the previous in-flight one (see api.config.ts) — the
// AbortError is swallowed here so only the latest result wins.
const result = await api.products.searchProducts({ q, limit: 12 });
setProducts(result.products);
setElapsed(Math.round(performance.now() - started));
} catch (err) {
if (err instanceof ApiError && err.name === 'AbortError') return; // superseded
setError(err instanceof ApiError ? `${err.status ?? ''} ${err.message}` : String(err));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load(term);
}, [term, load]);
const openDetail = useCallback(async (id: number) => {
try {
// Path param `{id}` is typed and required (a wrong key is a TS error).
const product = await api.products.getProductById({ id });
setSelected(product);
} catch (err) {
setError(err instanceof ApiError ? `${err.status ?? ''} ${err.message}` : String(err));
}
}, []);
return (
<Panel
title="Search products (direct typed client)"
subtitle="api.products.searchProducts({ q }) → ProductList. Type quickly: earlier in-flight searches auto-cancel (300ms debounce window)."
>
<div className="toolbar">
<label>
Search
<input
value={term}
onChange={(e) => setTerm(e.target.value)}
placeholder="phone, laptop…"
/>
</label>
<Button onClick={() => load(term)}>Reload</Button>
{loading ? <Spinner /> : null}
{elapsed !== null ? <span className="muted">loaded in {elapsed} ms</span> : null}
</div>
{error ? <div className="alert">{error}</div> : null}
<div className="grid">
{products.map((p) => (
<button key={p.id} type="button" className="card" onClick={() => openDetail(p.id)}>
<div className="card__title">
{p.title} <StatusBadge status={p.category} />
</div>
<div className="muted">
#{p.id} · ${p.price}
</div>
{p.brand ? <div className="chip">{p.brand}</div> : null}
</button>
))}
{!loading && products.length === 0 ? (
<p className="muted">No products for “{term}”.</p>
) : null}
</div>
{selected ? (
<div className="detail">
<div className="detail__head">
<strong>{selected.title}</strong> <StatusBadge status={selected.category} />
<Button className="btn--ghost" onClick={() => setSelected(null)}>
close
</Button>
</div>
<pre className="code">{JSON.stringify(selected, null, 2)}</pre>
</div>
) : null}
</Panel>
);
}