Skip to content

Commit df8be38

Browse files
committed
fix: tool
1 parent 18ad6c9 commit df8be38

File tree

9 files changed

+1087
-248
lines changed

9 files changed

+1087
-248
lines changed

docs/api/api/index.mdx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,68 @@
22
sidebar_position: 3
33
---
44

5+
import ApiEndpoint from "@site/src/components/ApiEndpoint";
6+
57
# API
8+
9+
## 地震報告
10+
11+
### v1
12+
13+
<ApiEndpoint
14+
method="GET"
15+
baseUrls={[
16+
{ name: "Production", url: "https://api.exptech.dev/api" },
17+
{ name: "Staging", url: "https://staging-api.exptech.dev/api" },
18+
]}
19+
endpoint="/v1/report"
20+
params={[
21+
{
22+
name: "limit",
23+
type: "number",
24+
optional: true,
25+
description: "限制回傳數量,最大為 100",
26+
default: "10",
27+
},
28+
{
29+
name: "offset",
30+
type: "number",
31+
optional: true,
32+
description: "偏移量",
33+
default: "0",
34+
},
35+
]}
36+
responses={{
37+
"200": {
38+
description: "成功",
39+
data: {
40+
code: 200,
41+
message: "success",
42+
data: [],
43+
},
44+
},
45+
"400": {
46+
description: "錯誤請求",
47+
data: {
48+
code: 400,
49+
message: "Invalid parameters",
50+
error: "limit must be a positive number",
51+
},
52+
},
53+
"403": {
54+
description: "無權限",
55+
data: {
56+
code: 403,
57+
message: "Forbidden",
58+
error: "API key required",
59+
},
60+
},
61+
"500": {
62+
description: "伺服器錯誤",
63+
data: {
64+
code: 500,
65+
message: "Internal server error",
66+
},
67+
},
68+
}}
69+
/>

docs/api/start/server.mdx

Lines changed: 35 additions & 196 deletions
Large diffs are not rendered by default.

docs/api/verify/index.mdx

Lines changed: 15 additions & 52 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { JSX, useState } from "react";
2+
import styles from "./styles.module.css";
3+
4+
type ApiEndpointProps = {
5+
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
6+
baseUrls: Array<{
7+
name?: string;
8+
url: string;
9+
}> | string[];
10+
endpoint: string;
11+
params?: Array<{
12+
name: string;
13+
type?: string;
14+
optional?: boolean;
15+
description: string;
16+
default?: string;
17+
}>;
18+
responses?: Record<string, {
19+
description?: string;
20+
data: any;
21+
}>;
22+
}
23+
24+
export default function ApiEndpoint({
25+
method = "GET",
26+
baseUrls,
27+
endpoint,
28+
params = [],
29+
responses = {}
30+
}: ApiEndpointProps): JSX.Element {
31+
const normalizedBaseUrls = baseUrls.map(url =>
32+
typeof url === 'string' ? { url } : url
33+
);
34+
35+
const [selectedBaseUrl, setSelectedBaseUrl] = useState(0);
36+
const [selectedStatus, setSelectedStatus] = useState<string>(
37+
Object.keys(responses)[0] || "200"
38+
);
39+
const [copied, setCopied] = useState(false);
40+
41+
const currentBaseUrl = normalizedBaseUrls[selectedBaseUrl]?.url || '';
42+
const fullUrl = `${currentBaseUrl}${endpoint}`;
43+
44+
const handleCopyUrl = () => {
45+
navigator.clipboard.writeText(fullUrl);
46+
setCopied(true);
47+
setTimeout(() => setCopied(false), 2000);
48+
};
49+
50+
const getStatusColor = (status: string) => {
51+
const code = parseInt(status);
52+
if (code >= 200 && code < 300) return styles.statusSuccess;
53+
if (code >= 400 && code < 500) return styles.statusError;
54+
if (code >= 500) return styles.statusServerError;
55+
return styles.statusDefault;
56+
};
57+
58+
return (
59+
<div className={styles.apiEndpoint}>
60+
<div className={styles.endpointHeader}>
61+
<span className={`${styles.method} ${styles[method.toLowerCase()]}`}>
62+
{method}
63+
</span>
64+
<div className={styles.urlContainer}>
65+
{normalizedBaseUrls.length > 1 && (
66+
<select
67+
className={styles.baseUrlSelect}
68+
value={selectedBaseUrl}
69+
onChange={(e) => setSelectedBaseUrl(Number(e.target.value))}
70+
>
71+
{normalizedBaseUrls.map((base, index) => (
72+
<option key={index} value={index}>
73+
{base.name || `Server ${index + 1}`}
74+
</option>
75+
))}
76+
</select>
77+
)}
78+
<code className={styles.url}>
79+
<span className={styles.baseUrl}>{currentBaseUrl}</span>
80+
<span className={styles.path}>{endpoint}</span>
81+
</code>
82+
<button
83+
className={styles.copyButton}
84+
onClick={handleCopyUrl}
85+
title="複製 URL"
86+
>
87+
{copied ? (
88+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
89+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
90+
</svg>
91+
) : (
92+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
93+
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/>
94+
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/>
95+
</svg>
96+
)}
97+
</button>
98+
</div>
99+
</div>
100+
101+
{params.length > 0 && (
102+
<div className={styles.section}>
103+
<h4 className={styles.sectionTitle}>參數</h4>
104+
<div className={styles.paramsList}>
105+
{params.map((param) => (
106+
<div key={param.name} className={styles.param}>
107+
<div className={styles.paramHeader}>
108+
<code className={styles.paramName}>{param.name}</code>
109+
{param.type && <span className={styles.paramType}>{param.type}</span>}
110+
{param.optional && <span className={styles.optional}>optional</span>}
111+
</div>
112+
<div className={styles.paramDescription}>
113+
{param.description}
114+
{param.default && <span className={styles.default}> (預設: {param.default})</span>}
115+
</div>
116+
</div>
117+
))}
118+
</div>
119+
</div>
120+
)}
121+
122+
{Object.keys(responses).length > 0 && (
123+
<div className={styles.section}>
124+
<div className={styles.responseHeader}>
125+
<h4 className={styles.sectionTitle}>回傳</h4>
126+
<select
127+
className={`${styles.statusSelect} ${getStatusColor(selectedStatus)}`}
128+
value={selectedStatus}
129+
onChange={(e) => setSelectedStatus(e.target.value)}
130+
>
131+
{Object.keys(responses).map((status) => (
132+
<option key={status} value={status}>
133+
{status} {responses[status].description && `- ${responses[status].description}`}
134+
</option>
135+
))}
136+
</select>
137+
</div>
138+
<pre className={styles.codeBlock}>
139+
<code>{JSON.stringify(responses[selectedStatus]?.data, null, 2)}</code>
140+
</pre>
141+
</div>
142+
)}
143+
</div>
144+
);
145+
}

0 commit comments

Comments
 (0)