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,
+}