apiComponentWrapper.jsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. 'use client'
  2. import apiMethods from '@/lib/api/aura/methods'
  3. import { useEffect, useState } from 'react'
  4. import { Fence } from '../Fence'
  5. import Spinner from '../icons/spinner'
  6. import { Totem, TotemAccordion } from '../Totem'
  7. import ApiParameterDisplay from './apiParams'
  8. import ApiExampleSelector from './exampleSelector'
  9. import LanguageRenderer from './languageRenderer'
  10. import Responce from './responce'
  11. const ApiComponentWrapper = (args) => {
  12. const api = apiMethods[args.method]
  13. const [screenWidth, setScreenWidth] = useState(null)
  14. const [responce, setResponce] = useState(null)
  15. const [isLoading, setIsLoading] = useState(false)
  16. const [selectedExample, setSelectedExample] = useState(-1)
  17. const [activeEndpoint, setActiveEndpoint] = useState("https://api.devnet.solana.com")
  18. useEffect(() => {
  19. // Load saved endpoint from localStorage on component mount
  20. try {
  21. const savedEndpoint = localStorage.getItem('customEndPoint')
  22. if (savedEndpoint) {
  23. setActiveEndpoint(savedEndpoint)
  24. }
  25. } catch (error) {
  26. console.warn('Failed to load endpoint from localStorage:', error)
  27. }
  28. }, [])
  29. const handleSetExample = (index) => {
  30. if (index == -1) {
  31. setBody((prev) => {
  32. let newBody = { ...prev }
  33. newBody.params = {}
  34. return newBody
  35. })
  36. setSelectedExample(index)
  37. return
  38. }
  39. setBody((prev) => {
  40. let newBody = { ...prev }
  41. newBody.params = api.examples[index].body.params
  42. return newBody
  43. })
  44. setSelectedExample(index)
  45. // Update endpoint based on example chain if available
  46. if (api.examples[index].chain) {
  47. const chainEndpoint = api.examples[index].chain === 'devnet'
  48. ? 'https://api.devnet.solana.com'
  49. : api.examples[index].chain === 'mainnet'
  50. ? 'https://api.mainnet-beta.solana.com'
  51. : activeEndpoint
  52. setActiveEndpoint(chainEndpoint)
  53. try {
  54. localStorage.setItem('customEndPoint', chainEndpoint)
  55. } catch (error) {
  56. console.warn('Failed to save endpoint to localStorage:', error)
  57. }
  58. }
  59. }
  60. const [body, setBody] = useState({
  61. jsonrpc: '2.0',
  62. id: '1',
  63. method: api.method,
  64. params: {},
  65. })
  66. // Track screen width on resize (client-side only)
  67. useEffect(() => {
  68. const handleResize = () => {
  69. setScreenWidth(window.innerWidth)
  70. }
  71. // Set the initial screen width when the component mounts
  72. if (screenWidth === null) {
  73. setScreenWidth(window.innerWidth)
  74. }
  75. window.addEventListener('resize', handleResize)
  76. return () => window.removeEventListener('resize', handleResize)
  77. }, [screenWidth])
  78. if (screenWidth === null) {
  79. return null
  80. }
  81. const isLargeScreen = screenWidth >= 1536 // 2xl breakpoint
  82. const handleSetParam = (path, value) => {
  83. setBody((prev) => {
  84. const newBody = { ...prev }
  85. // Recursive function to traverse and update or clean up fields
  86. const updateField = (obj, path) => {
  87. const key = path[0]
  88. if (path.length === 1) {
  89. // If we are at the target field
  90. if (
  91. !value ||
  92. (Array.isArray(value) && value.every((v) => v === ''))
  93. ) {
  94. // Delete the field if value is null, undefined, or an empty key-value pair
  95. delete obj[key]
  96. } else {
  97. // Otherwise, set the value
  98. obj[key] = value
  99. }
  100. } else {
  101. // Ensure the parent key exists and is an object
  102. if (!obj[key] || typeof obj[key] !== 'object') {
  103. obj[key] = {}
  104. }
  105. // Recurse into the nested object
  106. updateField(obj[key], path.slice(1))
  107. // Remove the parent key if it becomes empty after the update
  108. if (Object.keys(obj[key]).length === 0) {
  109. delete obj[key]
  110. }
  111. }
  112. }
  113. updateField(newBody.params, path)
  114. return newBody
  115. })
  116. }
  117. const handleTryItOut = async () => {
  118. setResponce(null)
  119. setIsLoading(true)
  120. try {
  121. if (!activeEndpoint) {
  122. throw new Error('Endpoint URL is required')
  123. }
  124. try {
  125. new URL(activeEndpoint)
  126. } catch {
  127. throw new Error('Invalid endpoint URL. Please enter a valid URL starting with http:// or https://')
  128. }
  129. const res = await fetch(activeEndpoint, {
  130. method: 'POST',
  131. headers: {
  132. 'Content-Type': 'application/json',
  133. },
  134. body: JSON.stringify(body),
  135. })
  136. if (!res.ok) {
  137. throw new Error(`HTTP error! status: ${res.status}`)
  138. }
  139. const resJson = await res.json()
  140. setResponce(resJson)
  141. } catch (error) {
  142. setResponce({
  143. error: {
  144. message: error.message || 'Failed to fetch. Please check your endpoint URL and try again.'
  145. }
  146. })
  147. } finally {
  148. setIsLoading(false)
  149. }
  150. }
  151. return (
  152. <div>
  153. <div className="flex w-full flex-col-reverse gap-8 2xl:flex-row ">
  154. <div className="flex w-full flex-col gap-8 2xl:w-1/2">
  155. {api.examples && isLargeScreen && (
  156. <ApiExampleSelector
  157. examples={api.examples}
  158. selectedExample={selectedExample}
  159. handleSetExample={(index) => handleSetExample(index)}
  160. />
  161. )}
  162. <ApiParameterDisplay
  163. params={api.params}
  164. body={body.params}
  165. setParam={(path, value) => {
  166. handleSetParam(path, value)
  167. }}
  168. />
  169. <button
  170. className="block min-w-[150px] rounded-lg border border-gray-200 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 disabled:pointer-events-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:placeholder-neutral-500 2xl:hidden"
  171. onClick={handleTryItOut}
  172. >
  173. {isLoading ? <Spinner className="h-6 w-6" /> : 'Try it out'}
  174. </button>
  175. {responce && !isLargeScreen && <Responce responce={responce} />}
  176. </div>
  177. <div className="flex w-full flex-col items-end gap-4 2xl:w-1/2">
  178. {api.examples && !isLargeScreen && (
  179. <ApiExampleSelector
  180. examples={api.examples}
  181. selectedExample={selectedExample}
  182. handleSetExample={(index) => handleSetExample(index)}
  183. />
  184. )}
  185. <LanguageRenderer
  186. api={api}
  187. body={body}
  188. activeEndpoint={activeEndpoint}
  189. setActiveEndpoint={(endpoint) => setActiveEndpoint(endpoint)}
  190. noUmi={args.noUmi}
  191. />
  192. <button
  193. className="hidden min-w-[150px] items-center justify-center rounded-lg border border-gray-200 px-4 py-3 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 disabled:pointer-events-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:placeholder-neutral-500 2xl:flex"
  194. onClick={handleTryItOut}
  195. >
  196. {isLoading ? <Spinner className="h-6 w-6" /> : 'Try it out'}
  197. </button>
  198. {responce && isLargeScreen && <Responce responce={responce} />}
  199. </div>
  200. </div>
  201. {api.exampleResponse && (
  202. <>
  203. <hr className="my-8 border-gray-200 dark:border-neutral-700" />
  204. <Totem className="w-full">
  205. <TotemAccordion title="Example Response">
  206. <Fence className="w-full" language="python">
  207. {JSON.stringify(api.exampleResponse, null, 2)}
  208. </Fence>
  209. </TotemAccordion>
  210. </Totem>
  211. </>
  212. )}
  213. </div>
  214. )
  215. }
  216. export default ApiComponentWrapper