Mercurial
view react_games/src/API_works/starwars_characters.tsx @ 80:d55157451947
[MrJuneJune] Updating my homepage.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Thu, 01 Jan 2026 08:23:46 -0800 |
| parents | fb9bcd3145cb |
| children |
line wrap: on
line source
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, }