diff react_games/src/API_works/pagination_2.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/pagination_2.tsx	Mon Dec 01 20:22:47 2025 -0800
@@ -0,0 +1,226 @@
+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,
+}