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 }