Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 import { CSSProperties, useCallback, useEffect, useRef, useState } from "react"; | |
| 2 | |
| 3 interface PersonJson { | |
| 4 name: string; | |
| 5 height: string; | |
| 6 mass: string; | |
| 7 hair_color: string; | |
| 8 skin_color: string; | |
| 9 eye_color: string; | |
| 10 birth_year: string; | |
| 11 gender: string; | |
| 12 homeworld: string; | |
| 13 films: string[]; | |
| 14 species: string; | |
| 15 vehicles: string[]; | |
| 16 starships: string[]; | |
| 17 created: string; | |
| 18 edited: string; | |
| 19 url: string; | |
| 20 } | |
| 21 | |
| 22 interface Person extends PersonJson {} | |
| 23 | |
| 24 interface APIResponse { | |
| 25 next: string | null; | |
| 26 previous: string | null; | |
| 27 results: PersonJson[]; | |
| 28 } | |
| 29 | |
| 30 interface PageData extends APIResponse { | |
| 31 curr: string; | |
| 32 } | |
| 33 | |
| 34 const BASE_URL = "https://swapi.py4e.com/api/people/"; | |
| 35 | |
| 36 const cacheMap: Map<string, Promise<PageData>> = new Map() | |
| 37 | |
| 38 const getPeople = async ( | |
| 39 url: string, | |
| 40 signal: AbortSignal, | |
| 41 ): Promise<PageData> => { | |
| 42 const cacheValue = cacheMap.get(url); | |
| 43 if (cacheValue) return cacheValue; | |
| 44 | |
| 45 const response = await fetch(url, { signal }); | |
| 46 if (!response.ok) throw new Error("Server Error"); | |
| 47 const pageData = await response.json(); | |
| 48 | |
| 49 cacheMap.set(url, pageData); | |
| 50 return { ...pageData, curr: url }; | |
| 51 } | |
| 52 | |
| 53 interface PersonRowProp { | |
| 54 person: Person; | |
| 55 } | |
| 56 | |
| 57 interface StylesProp { | |
| 58 row: CSSProperties, | |
| 59 } | |
| 60 | |
| 61 const styles: StylesProp = { | |
| 62 row: { | |
| 63 display: "flex", | |
| 64 justifyContent: "space-between", | |
| 65 borderBottom: "1px solid black", | |
| 66 marginBottom: 10 | |
| 67 } | |
| 68 } | |
| 69 | |
| 70 const PersonRow = ({person}: PersonRowProp) => { | |
| 71 return ( | |
| 72 <div style={styles.row}> | |
| 73 <p>{ person.name }</p> | |
| 74 <p>{ person.birth_year }</p> | |
| 75 </div> | |
| 76 ) | |
| 77 } | |
| 78 | |
| 79 const constructSearchParam = (pageData: PageData) => { | |
| 80 const sp = new URLSearchParams(); | |
| 81 if (pageData.next) { | |
| 82 sp.set("next", pageData.next) | |
| 83 } | |
| 84 if (pageData.previous) { | |
| 85 sp.set("previous", pageData.previous) | |
| 86 } | |
| 87 sp.set("curr", pageData.curr) | |
| 88 return sp; | |
| 89 } | |
| 90 | |
| 91 const constructUrl = (url: string, sp: URLSearchParams) => { | |
| 92 return `${url}?${sp.toString()}` | |
| 93 } | |
| 94 | |
| 95 type Direction = "next" | "previous" | |
| 96 | |
| 97 type APIType = "page" | "offset" | "cursor" | "link" | |
| 98 | |
| 99 const constructUrlFromSP = (apiType: APIType, sp: URLSearchParams): string | null => { | |
| 100 switch(apiType) { | |
| 101 case "page": | |
| 102 case "offset": | |
| 103 case "cursor": | |
| 104 return `${BASE_URL}?${sp.toString()}` | |
| 105 case "link": | |
| 106 return sp.get("curr"); | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 const updateSP = ( | |
| 111 apiType: APIType, | |
| 112 direction: Direction, | |
| 113 sp: URLSearchParams | |
| 114 ) => { | |
| 115 switch(apiType) { | |
| 116 case "page": | |
| 117 sp.set( | |
| 118 "page", | |
| 119 direction === "next" ? | |
| 120 String(Number(sp.get("page") as string) + 1) : | |
| 121 String(Math.max(Number(sp.get("page") as string) - 1, 1)) | |
| 122 ); | |
| 123 return sp; | |
| 124 case "offset": | |
| 125 sp.set("offset", | |
| 126 direction === "next" ? | |
| 127 String(Number(sp.get("offset") as string) + Number(sp.get("offset") as string)) : | |
| 128 String(Number(sp.get("offset") as string) - Math.min(Number(sp.get("offset") as string))) | |
| 129 ); | |
| 130 return sp; | |
| 131 case "cursor": | |
| 132 // TODO get id from people: | |
| 133 return sp; | |
| 134 case "link": | |
| 135 const curr = sp.get("curr") as string; | |
| 136 const next = sp.get("next"); | |
| 137 const previous = sp.get("previous"); | |
| 138 if (direction === "next") { | |
| 139 sp.delete("next"); | |
| 140 if (next) { | |
| 141 sp.set("curr", next); | |
| 142 } | |
| 143 sp.set("previous", curr); | |
| 144 } else { | |
| 145 sp.delete("previous"); | |
| 146 if (previous) { | |
| 147 sp.set("curr", previous); | |
| 148 } | |
| 149 sp.set("next", curr); | |
| 150 } | |
| 151 sp.set("offset", String(Number(sp.get("offset")) as number + 1)); | |
| 152 return sp; | |
| 153 } | |
| 154 } | |
| 155 | |
| 156 const usePageData = (api: APIType) => { | |
| 157 const [apiType] = useState<APIType>(api); | |
| 158 const [people, setPeople] = useState<Person[]>([]); | |
| 159 const [hasNext, setHasNext] = useState(true); | |
| 160 const [hasPrevious, setHasPrevious] = useState(true); | |
| 161 const abortRef = useRef<AbortController | null>(null); | |
| 162 | |
| 163 /** Single path for: fetch -> set state -> reflect URL */ | |
| 164 const load = useCallback(async (url: string, replace = false) => { | |
| 165 // cancel any prior request | |
| 166 abortRef.current?.abort(); | |
| 167 const ac = new AbortController(); | |
| 168 abortRef.current = ac; | |
| 169 | |
| 170 const page = await getPeople(url, ac.signal); | |
| 171 | |
| 172 // state | |
| 173 setPeople(page.results); | |
| 174 | |
| 175 if (apiType === "link") { | |
| 176 const sp = constructSearchParam(page); | |
| 177 setHasNext(Boolean(page.next)); | |
| 178 setHasPrevious(Boolean(page.previous)); | |
| 179 const target = `${location.pathname}?${sp.toString()}`; | |
| 180 replace ? history.replaceState(null, "", target) : history.pushState(null, "", target); | |
| 181 } else { | |
| 182 const sp = new URL(url).searchParams; | |
| 183 const target = `${location.pathname}?${sp.toString()}`; | |
| 184 replace ? history.replaceState(null, "", target) : history.pushState(null, "", target); | |
| 185 } | |
| 186 }, [apiType]); | |
| 187 | |
| 188 // Initial hydrate: reuse load() | |
| 189 useEffect(() => { | |
| 190 const sp = new URLSearchParams(location.search); | |
| 191 const url = constructUrlFromSP(apiType, sp) ?? BASE_URL; | |
| 192 load(url, /* replace */ true); | |
| 193 return () => abortRef.current?.abort(); | |
| 194 }, [apiType, load]); | |
| 195 | |
| 196 // Button handler: reuse load() | |
| 197 const handleOnClick = useCallback((direction: Direction) => { | |
| 198 const sp = new URLSearchParams(location.search); | |
| 199 const nextSP = updateSP(apiType, direction, sp); | |
| 200 const nextUrl = constructUrlFromSP(apiType, nextSP); | |
| 201 if (nextUrl) load(nextUrl); | |
| 202 }, [apiType, load]); | |
| 203 | |
| 204 return { people, hasNext, hasPrevious, handleOnClick }; | |
| 205 }; | |
| 206 | |
| 207 const PageTableComponent = () => { | |
| 208 const {hasNext, hasPrevious, people, handleOnClick} = usePageData("link"); | |
| 209 return ( | |
| 210 <> | |
| 211 <div style={{...styles.row, width: "30%", borderBottom: "none" }}> | |
| 212 <button disabled={!hasNext} onClick={() => handleOnClick("next")}> Next </button> | |
| 213 <button disabled={!hasPrevious} onClick={() => handleOnClick("previous")}> Previous </button> | |
| 214 </div> | |
| 215 <div style={styles.row}> | |
| 216 <p>Name</p> | |
| 217 <p>Birth_year</p> | |
| 218 </div> | |
| 219 {people.map((person) => (<PersonRow person={person} />) )} | |
| 220 </> | |
| 221 ) | |
| 222 } | |
| 223 | |
| 224 export { | |
| 225 PageTableComponent, | |
| 226 } |