Mercurial
comparison react_games/src/API_works/starwars_characters.tsx @ 37:fb9bcd3145cb
[ReactGames] Few games I made using react just to practice few things.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:22:47 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 import { CSSProperties, FormEvent, KeyboardEvent, useCallback, useRef, useState } from "react"; | |
| 2 import ReactDOM from "react-dom/client"; | |
| 3 | |
| 4 /** | |
| 5 * | |
| 6 * Implement an app that searches for Star Wars characters. | |
| 7 * | |
| 8 * User can type in texts to search for the matching characters through this | |
| 9 * API endpoint: | |
| 10 * | |
| 11 * https://swapi.tech/api/people?name=luke | |
| 12 * | |
| 13 * Users can also tap a search result row and enter the profile detail screen. | |
| 14 * The information displayed in the detail screen should already be returned | |
| 15 * from the search API. | |
| 16 * | |
| 17 */ | |
| 18 | |
| 19 interface Person { | |
| 20 created: string; | |
| 21 edited: string; | |
| 22 name: string; | |
| 23 gender: string; | |
| 24 skin_color: string; | |
| 25 hair_color: string; | |
| 26 height: string; | |
| 27 eye_color: string; | |
| 28 mass: string; | |
| 29 homeworld: string; | |
| 30 birth_year: string; | |
| 31 vehicles: string[]; | |
| 32 starships: string[]; | |
| 33 films: string[]; | |
| 34 url: string; | |
| 35 } | |
| 36 | |
| 37 interface APIResponseJSON { | |
| 38 message: string; | |
| 39 result: { properties: Person; uid: string; }[]; | |
| 40 } | |
| 41 | |
| 42 const BASE_URL = "https://swapi.tech/api/people" ; | |
| 43 const constructAPIUrl = (url: string, params: URLSearchParams) => { | |
| 44 return `${url}?${params.toString()}`; | |
| 45 } | |
| 46 | |
| 47 const map: Map<string, Promise<Person[]>> = new Map(); | |
| 48 | |
| 49 const fetchPeople = async (name: string, signal: AbortSignal): Promise<Person[]> => { | |
| 50 const sp = new URLSearchParams(); | |
| 51 sp.set("name", name); | |
| 52 const url = constructAPIUrl(BASE_URL, sp); | |
| 53 const cacheValue = map.get(url); | |
| 54 if (cacheValue) return cacheValue; | |
| 55 | |
| 56 const fetchPromise = (async () => { | |
| 57 const response = await fetch(url, { signal }); | |
| 58 | |
| 59 if (!response.ok) throw new Error("Server error"); | |
| 60 const json: APIResponseJSON = await response.json(); | |
| 61 if (json.message != "ok") throw new Error("Response does not have ok"); | |
| 62 | |
| 63 const result = json.result.map( | |
| 64 ({properties}) => properties | |
| 65 ) | |
| 66 return result; | |
| 67 })(); | |
| 68 | |
| 69 map.set(url, fetchPromise); | |
| 70 setTimeout(() => map.delete(url), 1000 * 60 * 5); | |
| 71 | |
| 72 try { | |
| 73 return await fetchPromise; | |
| 74 } catch (err) { | |
| 75 map.delete(url); | |
| 76 throw err; | |
| 77 } | |
| 78 } | |
| 79 | |
| 80 interface PersonRowProp { | |
| 81 person: Person; | |
| 82 idx: number; | |
| 83 onSelect: (i: number) => void; | |
| 84 | |
| 85 } | |
| 86 | |
| 87 const PersonRow = ({person, idx, onSelect}: PersonRowProp) => { | |
| 88 const handleOnKeyDown = useCallback((event: KeyboardEvent) => { | |
| 89 if (event.key != "Enter") return; | |
| 90 event.preventDefault(); | |
| 91 onSelect(idx); | |
| 92 }, []); | |
| 93 | |
| 94 return ( | |
| 95 <div | |
| 96 role="button" | |
| 97 style={styles.detailPerson} | |
| 98 tabIndex={0} | |
| 99 onClick={()=>onSelect(idx)} | |
| 100 onKeyDown={handleOnKeyDown} | |
| 101 > | |
| 102 {person.name} | |
| 103 </div> | |
| 104 ) | |
| 105 } | |
| 106 | |
| 107 interface StyleProp { | |
| 108 form: CSSProperties; | |
| 109 searchResult: CSSProperties; | |
| 110 detailPerson: CSSProperties; | |
| 111 } | |
| 112 | |
| 113 const styles: StyleProp = { | |
| 114 form: { | |
| 115 display: "flex", | |
| 116 gap: 5, | |
| 117 }, | |
| 118 searchResult: { | |
| 119 overflowY: "scroll", | |
| 120 height: 400, | |
| 121 }, | |
| 122 detailPerson: { | |
| 123 }, | |
| 124 } | |
| 125 | |
| 126 interface PersonDetialComponentProp { | |
| 127 person: Person; | |
| 128 } | |
| 129 | |
| 130 const PersonDetialComponent = ({person}: PersonDetialComponentProp) => { | |
| 131 return ( | |
| 132 <div> | |
| 133 <p> <span> Name: </span> { person.name }</p> | |
| 134 <p> <span> Mass: </span> { person.mass }</p> | |
| 135 <p> <span> Gender: </span> { person.gender }</p> | |
| 136 <p> <span> Height: </span> { person.height }</p> | |
| 137 <p> <span> Homeworld: </span>{ person.homeworld }</p> | |
| 138 <p> <span> Hair_color: </span> { person.hair_color }</p> | |
| 139 </div> | |
| 140 ) | |
| 141 } | |
| 142 | |
| 143 const StarWarsCharactersComponent = () => { | |
| 144 const [people, setPeople] = useState<Person[]>([]); | |
| 145 const [isLoading, setIsLoading] = useState<boolean>(false); | |
| 146 const [personIdx, setPersonIdx] = useState<number | null>(null); | |
| 147 const nameInputRef = useRef<HTMLInputElement | null>(null) | |
| 148 const abortController = useRef<AbortController | null>(null); | |
| 149 | |
| 150 const personOnSelect = useCallback((idx: number) => { | |
| 151 setPersonIdx(idx); | |
| 152 }, []) | |
| 153 | |
| 154 const handleOnSubmit = useCallback((event: FormEvent) => { | |
| 155 event.preventDefault(); | |
| 156 | |
| 157 // if exist abort the request; | |
| 158 abortController.current?.abort(); | |
| 159 const currAborController = new AbortController(); | |
| 160 abortController.current = currAborController; | |
| 161 | |
| 162 const nameInputEL = nameInputRef.current; | |
| 163 if (!nameInputEL || nameInputEL.value.trim() == "") return; | |
| 164 setIsLoading(true); | |
| 165 fetchPeople(nameInputEL.value.trim(), currAborController.signal) | |
| 166 .then( | |
| 167 (res) => { | |
| 168 console.log("??"); | |
| 169 setPeople(res); | |
| 170 setPersonIdx(null); | |
| 171 } | |
| 172 ).finally( | |
| 173 () => setIsLoading(false) | |
| 174 ); | |
| 175 nameInputEL.value = ""; | |
| 176 return () => currAborController.abort(); | |
| 177 },[]) | |
| 178 | |
| 179 return ( | |
| 180 <> | |
| 181 <form style={styles.form} onSubmit={handleOnSubmit}> | |
| 182 <input ref={nameInputRef}></input> | |
| 183 <button type="submit"> Search </button> | |
| 184 </form> | |
| 185 {isLoading && (<div> Loading.... </div>)} | |
| 186 <div style={styles.searchResult}> | |
| 187 { | |
| 188 people.length > 0 && people.map( | |
| 189 (person, idx) => (<PersonRow key={idx} idx={idx} onSelect={personOnSelect} person={person} />)) | |
| 190 } | |
| 191 </div> | |
| 192 { | |
| 193 personIdx != null && | |
| 194 ( | |
| 195 <PersonDetialComponent person={people[personIdx]}/> | |
| 196 ) | |
| 197 } | |
| 198 </> | |
| 199 ) | |
| 200 } | |
| 201 | |
| 202 export { | |
| 203 StarWarsCharactersComponent, | |
| 204 } |