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