view react_games/src/API_works/pagination_2.tsx @ 186:8cf4ec5e2191 hg-web

Fixed merge conflict.
author MrJuneJune <me@mrjunejune.com>
date Fri, 23 Jan 2026 22:38:59 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

import { CSSProperties, useCallback, useEffect, useRef, useState } from "react";

interface PersonJson {
  name: string;
  height: string;
  mass: string;
  hair_color: string;
  skin_color: string;
  eye_color: string;
  birth_year: string;
  gender: string;
  homeworld: string;
  films: string[];
  species: string;
  vehicles: string[];
  starships: string[]; 
  created: string;
  edited: string;
  url: string;
}

interface Person extends PersonJson {}

interface APIResponse {
  next: string | null;
  previous: string | null;
  results: PersonJson[];
}

interface PageData extends APIResponse {
  curr: string;
}

const BASE_URL = "https://swapi.py4e.com/api/people/";

const cacheMap: Map<string, Promise<PageData>> = new Map()

const getPeople = async (
  url: string,
  signal: AbortSignal,
): Promise<PageData> => {
  const cacheValue =  cacheMap.get(url);
  if (cacheValue) return cacheValue;

  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error("Server Error");
  const pageData = await response.json();

  cacheMap.set(url, pageData);
  return { ...pageData, curr: url };
}

interface PersonRowProp {
  person: Person;
}

interface StylesProp {
  row: CSSProperties,
}

const styles: StylesProp = {
  row: {
    display: "flex",
    justifyContent: "space-between",
    borderBottom: "1px solid black",
    marginBottom: 10
  }
}

const PersonRow = ({person}: PersonRowProp) => {
  return (
    <div style={styles.row}> 
      <p>{ person.name }</p>
      <p>{ person.birth_year }</p>
    </div>
  )
}

const constructSearchParam = (pageData: PageData) => {
  const sp = new URLSearchParams();
  if (pageData.next) {
    sp.set("next", pageData.next)
  }
  if (pageData.previous) {
    sp.set("previous", pageData.previous)
  }
  sp.set("curr", pageData.curr)
  return sp;
}

const constructUrl = (url: string, sp: URLSearchParams) => {
  return `${url}?${sp.toString()}`
}

type Direction = "next" | "previous"

type APIType = "page" | "offset" | "cursor" | "link"

const constructUrlFromSP = (apiType: APIType, sp: URLSearchParams): string | null => {
  switch(apiType) {
    case "page":
    case "offset":
    case "cursor":
      return `${BASE_URL}?${sp.toString()}`
    case "link":
      return sp.get("curr");
  }
}

const updateSP = (
  apiType: APIType,
  direction: Direction,
  sp: URLSearchParams
) => {
  switch(apiType) {
    case "page":
      sp.set(
        "page",
        direction === "next" ? 
          String(Number(sp.get("page") as string)  + 1) : 
          String(Math.max(Number(sp.get("page") as string) - 1, 1))
      );
      return sp;
    case "offset":
      sp.set("offset",
        direction === "next" ? 
          String(Number(sp.get("offset") as string)  + Number(sp.get("offset") as string)) : 
          String(Number(sp.get("offset") as string) - Math.min(Number(sp.get("offset") as string)))
      );
      return sp;
    case "cursor":
      // TODO get id from people:
      return sp;
    case "link":
      const curr = sp.get("curr") as string;
      const next = sp.get("next");
      const previous = sp.get("previous");
      if (direction === "next") {
        sp.delete("next");
        if (next) {
          sp.set("curr", next);
        }
        sp.set("previous", curr);
      } else {
        sp.delete("previous");
        if (previous) {
          sp.set("curr", previous);
        }
        sp.set("next", curr);
      }
      sp.set("offset", String(Number(sp.get("offset")) as number + 1));
      return sp;
  }
}

const usePageData = (api: APIType) => {
  const [apiType] = useState<APIType>(api);
  const [people, setPeople] = useState<Person[]>([]);
  const [hasNext, setHasNext] = useState(true);
  const [hasPrevious, setHasPrevious] = useState(true);
  const abortRef = useRef<AbortController | null>(null);

  /** Single path for: fetch -> set state -> reflect URL */
  const load = useCallback(async (url: string, replace = false) => {
    // cancel any prior request
    abortRef.current?.abort();
    const ac = new AbortController();
    abortRef.current = ac;

    const page = await getPeople(url, ac.signal);

    // state
    setPeople(page.results);

    if (apiType === "link") {
      const sp = constructSearchParam(page);
      setHasNext(Boolean(page.next));
      setHasPrevious(Boolean(page.previous));
      const target = `${location.pathname}?${sp.toString()}`;
      replace ? history.replaceState(null, "", target) : history.pushState(null, "", target);
    } else {
      const sp = new URL(url).searchParams;
      const target = `${location.pathname}?${sp.toString()}`;
      replace ? history.replaceState(null, "", target) : history.pushState(null, "", target);
    }
  }, [apiType]);

  // Initial hydrate: reuse load()
  useEffect(() => {
    const sp = new URLSearchParams(location.search);
    const url = constructUrlFromSP(apiType, sp) ?? BASE_URL;
    load(url, /* replace */ true);
    return () => abortRef.current?.abort();
  }, [apiType, load]);

  // Button handler: reuse load()
  const handleOnClick = useCallback((direction: Direction) => {
    const sp = new URLSearchParams(location.search);
    const nextSP = updateSP(apiType, direction, sp);
    const nextUrl = constructUrlFromSP(apiType, nextSP);
    if (nextUrl) load(nextUrl);
  }, [apiType, load]);

  return { people, hasNext, hasPrevious, handleOnClick };
};

const PageTableComponent = () => {
  const {hasNext, hasPrevious, people, handleOnClick} = usePageData("link");
  return (
    <> 
      <div style={{...styles.row, width: "30%", borderBottom: "none" }}>
         <button disabled={!hasNext} onClick={() => handleOnClick("next")}> Next </button>
         <button disabled={!hasPrevious} onClick={() => handleOnClick("previous")}> Previous </button>
      </div>
      <div style={styles.row}> 
        <p>Name</p>
        <p>Birth_year</p>
      </div>
      {people.map((person) => (<PersonRow person={person} />) )}
    </>
  )
}

export {
  PageTableComponent,
}