Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/react_games/src/API_works/starwars_characters.tsx Mon Dec 01 20:22:47 2025 -0800 @@ -0,0 +1,204 @@ +import { CSSProperties, FormEvent, KeyboardEvent, useCallback, useRef, useState } from "react"; +import ReactDOM from "react-dom/client"; + +/** + * + * Implement an app that searches for Star Wars characters. + * + * User can type in texts to search for the matching characters through this + * API endpoint: + * + * https://swapi.tech/api/people?name=luke + * + * Users can also tap a search result row and enter the profile detail screen. + * The information displayed in the detail screen should already be returned + * from the search API. + * + */ + +interface Person { + created: string; + edited: string; + name: string; + gender: string; + skin_color: string; + hair_color: string; + height: string; + eye_color: string; + mass: string; + homeworld: string; + birth_year: string; + vehicles: string[]; + starships: string[]; + films: string[]; + url: string; +} + +interface APIResponseJSON { + message: string; + result: { properties: Person; uid: string; }[]; +} + +const BASE_URL = "https://swapi.tech/api/people" ; +const constructAPIUrl = (url: string, params: URLSearchParams) => { + return `${url}?${params.toString()}`; +} + +const map: Map<string, Promise<Person[]>> = new Map(); + +const fetchPeople = async (name: string, signal: AbortSignal): Promise<Person[]> => { + const sp = new URLSearchParams(); + sp.set("name", name); + const url = constructAPIUrl(BASE_URL, sp); + const cacheValue = map.get(url); + if (cacheValue) return cacheValue; + + const fetchPromise = (async () => { + const response = await fetch(url, { signal }); + + if (!response.ok) throw new Error("Server error"); + const json: APIResponseJSON = await response.json(); + if (json.message != "ok") throw new Error("Response does not have ok"); + + const result = json.result.map( + ({properties}) => properties + ) + return result; + })(); + + map.set(url, fetchPromise); + setTimeout(() => map.delete(url), 1000 * 60 * 5); + + try { + return await fetchPromise; + } catch (err) { + map.delete(url); + throw err; + } +} + +interface PersonRowProp { + person: Person; + idx: number; + onSelect: (i: number) => void; + +} + +const PersonRow = ({person, idx, onSelect}: PersonRowProp) => { + const handleOnKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key != "Enter") return; + event.preventDefault(); + onSelect(idx); + }, []); + + return ( + <div + role="button" + style={styles.detailPerson} + tabIndex={0} + onClick={()=>onSelect(idx)} + onKeyDown={handleOnKeyDown} + > + {person.name} + </div> + ) +} + +interface StyleProp { + form: CSSProperties; + searchResult: CSSProperties; + detailPerson: CSSProperties; +} + +const styles: StyleProp = { + form: { + display: "flex", + gap: 5, + }, + searchResult: { + overflowY: "scroll", + height: 400, + }, + detailPerson: { + }, +} + +interface PersonDetialComponentProp { + person: Person; +} + +const PersonDetialComponent = ({person}: PersonDetialComponentProp) => { + return ( + <div> + <p> <span> Name: </span> { person.name }</p> + <p> <span> Mass: </span> { person.mass }</p> + <p> <span> Gender: </span> { person.gender }</p> + <p> <span> Height: </span> { person.height }</p> + <p> <span> Homeworld: </span>{ person.homeworld }</p> + <p> <span> Hair_color: </span> { person.hair_color }</p> + </div> + ) +} + +const StarWarsCharactersComponent = () => { + const [people, setPeople] = useState<Person[]>([]); + const [isLoading, setIsLoading] = useState<boolean>(false); + const [personIdx, setPersonIdx] = useState<number | null>(null); + const nameInputRef = useRef<HTMLInputElement | null>(null) + const abortController = useRef<AbortController | null>(null); + + const personOnSelect = useCallback((idx: number) => { + setPersonIdx(idx); + }, []) + + const handleOnSubmit = useCallback((event: FormEvent) => { + event.preventDefault(); + + // if exist abort the request; + abortController.current?.abort(); + const currAborController = new AbortController(); + abortController.current = currAborController; + + const nameInputEL = nameInputRef.current; + if (!nameInputEL || nameInputEL.value.trim() == "") return; + setIsLoading(true); + fetchPeople(nameInputEL.value.trim(), currAborController.signal) + .then( + (res) => { + console.log("??"); + setPeople(res); + setPersonIdx(null); + } + ).finally( + () => setIsLoading(false) + ); + nameInputEL.value = ""; + return () => currAborController.abort(); + },[]) + + return ( + <> + <form style={styles.form} onSubmit={handleOnSubmit}> + <input ref={nameInputRef}></input> + <button type="submit"> Search </button> + </form> + {isLoading && (<div> Loading.... </div>)} + <div style={styles.searchResult}> + { + people.length > 0 && people.map( + (person, idx) => (<PersonRow key={idx} idx={idx} onSelect={personOnSelect} person={person} />)) + } + </div> + { + personIdx != null && + ( + <PersonDetialComponent person={people[personIdx]}/> + ) + } + </> + ) +} + +export { + StarWarsCharactersComponent, +}