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