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 }