Mercurial
view react_games/src/API_works/pagination_2.tsx @ 200:90dfcef375fb
Added my own s3 bucket uploader url to mrjunejune.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 14 Feb 2026 16:32:24 -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, }