view react_games/src/API_works/starwars_characters.tsx @ 98:5c47eab8032d

Updated deploy script
author June Park <parkjune1995@gmail.com>
date Fri, 02 Jan 2026 20:25:47 -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,
}