ps99-api 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,8 @@
11
11
  "author": "",
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
+ "date-fns": "^3.6.0",
15
+ "idb-keyval": "^6.2.1",
14
16
  "ps99-api": "file:../..",
15
17
  "react": "^18.3.1",
16
18
  "react-dom": "^18.3.1",
@@ -19,12 +21,15 @@
19
21
  "devDependencies": {
20
22
  "@types/react": "^18.3.3",
21
23
  "@types/react-dom": "^18.3.0",
24
+ "clean-webpack-plugin": "^4.0.0",
22
25
  "copy-webpack-plugin": "^12.0.2",
23
26
  "css-loader": "^7.1.2",
27
+ "html-webpack-plugin": "^5.6.0",
24
28
  "ts-loader": "^9.5.1",
25
29
  "typescript": "^5.5.2",
26
30
  "webpack": "^5.92.0",
27
31
  "webpack-cli": "^5.1.4",
28
- "webpack-dev-server": "^5.0.4"
32
+ "webpack-dev-server": "^5.0.4",
33
+ "workbox-webpack-plugin": "^7.1.0"
29
34
  }
30
35
  }
@@ -5,6 +5,7 @@
5
5
  "display": "standalone",
6
6
  "background_color": "#ffffff",
7
7
  "theme_color": "#000000",
8
+ "description": "Pet Simulator 99 PWA",
8
9
  "icons": [
9
10
  {
10
11
  "src": "/icons/icon-192x192.png",
@@ -5,6 +5,7 @@ import Header from "./components/Header";
5
5
  import CollectionsIndex from "./components/CollectionsIndex";
6
6
  import CollectionConfigIndex from "./components/CollectionConfigIndex";
7
7
  import DynamicCollectionConfigData from "./components/DynamicCollectionConfigData";
8
+ import Footer from "./components/Footer";
8
9
 
9
10
  const App: React.FC = () => {
10
11
  return (
@@ -13,15 +14,10 @@ const App: React.FC = () => {
13
14
  <Routes>
14
15
  <Route path="/" element={<HomePage />} />
15
16
  <Route path="/collections" element={<CollectionsIndex />} />
16
- <Route
17
- path="/collections/:collectionName"
18
- element={<CollectionConfigIndex />}
19
- />
20
- <Route
21
- path="/collections/:collectionName/:configName"
22
- element={<DynamicCollectionConfigData />}
23
- />
17
+ <Route path="/collections/:collectionName" element={<CollectionConfigIndex />} />
18
+ <Route path="/collections/:collectionName/:configName" element={<DynamicCollectionConfigData />} />
24
19
  </Routes>
20
+ <Footer />
25
21
  </Router>
26
22
  );
27
23
  };
@@ -33,9 +33,6 @@ const CollectionConfigIndex: React.FC = () => {
33
33
  <div>
34
34
  <h2>{collectionName} Configurations</h2>
35
35
  <ul>
36
- <li>
37
- <Link to={`/collections/${collectionName}/all`}>All</Link>
38
- </li>
39
36
  {configNames.map((configName, index) => (
40
37
  <li key={index}>
41
38
  <Link
@@ -1,128 +1,21 @@
1
- import React, {
2
- lazy,
3
- Suspense,
4
- useEffect,
5
- useState,
6
- useRef,
7
- useCallback,
8
- } from "react";
1
+ import React, { lazy, Suspense } from "react";
9
2
  import { useParams } from "react-router-dom";
10
- import {
11
- PetSimulator99API,
12
- CollectionName,
13
- Collection,
14
- CollectionConfigData,
15
- } from "ps99-api";
3
+ import { CollectionName } from "ps99-api";
16
4
 
17
5
  const DynamicCollectionConfigData: React.FC = () => {
18
- const { collectionName, configName } = useParams<{
19
- collectionName: CollectionName;
20
- configName: string;
21
- }>();
6
+ const { collectionName, configName } = useParams<{ collectionName: CollectionName; configName: string }>();
22
7
 
23
- if (!collectionName) {
24
- return <div>Invalid collection name</div>;
25
- }
26
-
27
- if (configName === "all") {
28
- return <RenderAllConfigs collectionName={collectionName} />;
8
+ if (!collectionName || !configName) {
9
+ return <div>Invalid collection or config name</div>;
29
10
  }
30
11
 
31
12
  const Component = lazy(() => import(`./${collectionName}Component`));
32
13
 
33
14
  return (
34
15
  <Suspense fallback={<div>Loading...</div>}>
35
- <Component />
16
+ <Component configName={configName} />
36
17
  </Suspense>
37
18
  );
38
19
  };
39
20
 
40
- const RenderAllConfigs: React.FC<{ collectionName: CollectionName }> = ({
41
- collectionName,
42
- }) => {
43
- const [configDataList, setConfigDataList] = useState<
44
- Array<Collection<CollectionName>>
45
- >([]);
46
- const [page, setPage] = useState(0);
47
- const [error, setError] = useState<string | null>(null);
48
- const observer = useRef<IntersectionObserver | null>(null);
49
- const loadMoreRef = useRef<HTMLDivElement | null>(null);
50
-
51
- const fetchData = async (page: number) => {
52
- try {
53
- const api = new PetSimulator99API();
54
- const response = await api.getCollection(collectionName);
55
- if (response.status === "ok") {
56
- const start = page * 20;
57
- const end = start + 20;
58
- setConfigDataList((prev) => [
59
- ...prev,
60
- ...response.data.slice(start, end),
61
- ]);
62
- } else {
63
- setError(response.error.message);
64
- }
65
- } catch (error) {
66
- setError("Error fetching data");
67
- console.error("Error fetching data:", error);
68
- }
69
- };
70
-
71
- useEffect(() => {
72
- fetchData(page);
73
- }, [page]);
74
-
75
- const lastElementRef = useCallback((node: HTMLDivElement | null) => {
76
- if (observer.current) observer.current.disconnect();
77
- observer.current = new IntersectionObserver((entries) => {
78
- if (entries[0].isIntersecting) {
79
- setPage((prevPage) => prevPage + 1);
80
- }
81
- });
82
- if (node) observer.current.observe(node);
83
- }, []);
84
-
85
- if (error) {
86
- return <div>Error: {error}</div>;
87
- }
88
-
89
- return (
90
- <div>
91
- {configDataList.map((configData, index) => {
92
- const Component = lazy(() => import(`./${collectionName}Component`));
93
- if (index === configDataList.length - 1) {
94
- return (
95
- <div ref={lastElementRef} key={index}>
96
- <Suspense fallback={<div>Loading...</div>}>
97
- <Component
98
- configData={
99
- configData.configData as CollectionConfigData<
100
- typeof collectionName
101
- >
102
- }
103
- />
104
- </Suspense>
105
- </div>
106
- );
107
- } else {
108
- return (
109
- <div key={index}>
110
- <Suspense fallback={<div>Loading...</div>}>
111
- <Component
112
- configData={
113
- configData.configData as CollectionConfigData<
114
- typeof collectionName
115
- >
116
- }
117
- />
118
- </Suspense>
119
- </div>
120
- );
121
- }
122
- })}
123
- <div ref={loadMoreRef}></div>
124
- </div>
125
- );
126
- };
127
-
128
21
  export default DynamicCollectionConfigData;
@@ -0,0 +1,60 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { useOnlineStatus } from "../hooks/useOnlineStatus";
3
+
4
+ const Footer: React.FC = () => {
5
+ const isOnline = useOnlineStatus();
6
+ const [lastUpdate, setLastUpdate] = useState<string | null>(null);
7
+ const [loading, setLoading] = useState(false);
8
+
9
+ const updateLastUpdate = () => {
10
+ setLastUpdate(new Date().toLocaleString());
11
+ };
12
+
13
+ useEffect(() => {
14
+ const fetchData = async () => {
15
+ setLoading(true);
16
+ // Simulate a fetch call to update lastUpdate time
17
+ await new Promise((resolve) => setTimeout(resolve, 1000));
18
+ updateLastUpdate();
19
+ setLoading(false);
20
+ };
21
+
22
+ fetchData();
23
+
24
+ window.addEventListener("online", updateLastUpdate);
25
+ window.addEventListener("offline", updateLastUpdate);
26
+
27
+ return () => {
28
+ window.removeEventListener("online", updateLastUpdate);
29
+ window.removeEventListener("offline", updateLastUpdate);
30
+ };
31
+ }, []);
32
+
33
+ return (
34
+ <footer
35
+ style={{
36
+ display: "flex",
37
+ justifyContent: "space-between",
38
+ padding: "1em",
39
+ borderTop: "1px solid #ccc",
40
+ }}
41
+ >
42
+ <div>
43
+ {loading ? (
44
+ <span>♻️ Loading...</span>
45
+ ) : (
46
+ <span>Last update: {lastUpdate}</span>
47
+ )}
48
+ </div>
49
+ <div>
50
+ {isOnline ? (
51
+ <span style={{ color: "green" }}>● Online</span>
52
+ ) : (
53
+ <span style={{ color: "red" }}>● Offline</span>
54
+ )}
55
+ </div>
56
+ </footer>
57
+ );
58
+ };
59
+
60
+ export default Footer;
@@ -9,10 +9,10 @@ interface GenericFetchComponentProps<T> {
9
9
  }
10
10
 
11
11
  export const GenericFetchComponent = <T,>({
12
- collectionName,
13
- render,
14
- configData,
15
- }: GenericFetchComponentProps<T>) => {
12
+ collectionName,
13
+ render,
14
+ configData,
15
+ }: GenericFetchComponentProps<T>) => {
16
16
  const { configName } = useParams<{ configName: string }>();
17
17
  const [data, setData] = useState<T | null>(configData || null);
18
18
  const [error, setError] = useState<string | null>(null);
@@ -23,12 +23,9 @@ export const GenericFetchComponent = <T,>({
23
23
  const fetchData = async () => {
24
24
  if (!configName) return;
25
25
  const api = new PetSimulator99API();
26
- const response: ApiResponseBody<any[]> =
27
- await api.getCollection(collectionName);
26
+ const response: ApiResponseBody<any[]> = await api.getCollection(collectionName);
28
27
  if (response.status === "ok") {
29
- const item = response.data.find(
30
- (item) => item.configName === configName,
31
- );
28
+ const item = response.data.find((item) => item.configName === configName);
32
29
  if (item) {
33
30
  setData(item.configData);
34
31
  } else {
@@ -7,14 +7,14 @@ interface ImageProps {
7
7
  }
8
8
 
9
9
  const MAX_CONCURRENT_REQUESTS = 5;
10
- let currentRequests = 0;
11
- const queue: (() => void)[] = [];
10
+ const requestQueue: Array<() => void> = [];
11
+ let activeRequests = 0;
12
12
 
13
13
  const processQueue = () => {
14
- if (queue.length > 0 && currentRequests < MAX_CONCURRENT_REQUESTS) {
15
- const nextRequest = queue.shift();
14
+ if (activeRequests < MAX_CONCURRENT_REQUESTS && requestQueue.length > 0) {
15
+ const nextRequest = requestQueue.shift();
16
16
  if (nextRequest) {
17
- currentRequests++;
17
+ activeRequests++;
18
18
  nextRequest();
19
19
  }
20
20
  }
@@ -22,31 +22,27 @@ const processQueue = () => {
22
22
 
23
23
  const ImageComponent: React.FC<ImageProps> = ({ src, alt }) => {
24
24
  const [imageUrl, setImageUrl] = useState<string | null>(null);
25
- const [error, setError] = useState<string | null>(null);
26
25
 
27
26
  useEffect(() => {
28
27
  const fetchImage = async () => {
28
+ const api = new PetSimulator99API();
29
29
  try {
30
- const api = new PetSimulator99API();
31
30
  const imageBlob = await api.getImage(src);
32
- const imageUrl = URL.createObjectURL(
33
- new Blob([imageBlob], { type: "image/png" }),
34
- );
35
- setImageUrl(imageUrl);
31
+ const url = URL.createObjectURL(new Blob([imageBlob], { type: "image/png" }));
32
+ setImageUrl(url);
36
33
  } catch (error) {
37
- setError("Error fetching image");
38
34
  console.error("Error fetching image:", error);
39
35
  } finally {
40
- currentRequests--;
36
+ activeRequests--;
41
37
  processQueue();
42
38
  }
43
39
  };
44
40
 
45
- const load = () => {
41
+ const requestImage = () => {
46
42
  fetchImage();
47
43
  };
48
44
 
49
- queue.push(load);
45
+ requestQueue.push(requestImage);
50
46
  processQueue();
51
47
 
52
48
  return () => {
@@ -54,14 +50,12 @@ const ImageComponent: React.FC<ImageProps> = ({ src, alt }) => {
54
50
  URL.revokeObjectURL(imageUrl);
55
51
  }
56
52
  };
57
- }, [src, imageUrl]);
58
-
59
- if (error) {
60
- return <div>{error}</div>;
61
- }
53
+ }, [src]);
62
54
 
63
55
  return (
64
- <div>{imageUrl ? <img src={imageUrl} alt={alt} /> : <p>Loading...</p>}</div>
56
+ <div>
57
+ {imageUrl ? <img src={imageUrl} alt={alt} /> : <p>Loading...</p>}
58
+ </div>
65
59
  );
66
60
  };
67
61
 
@@ -3,7 +3,9 @@ import { CollectionConfigData } from "ps99-api";
3
3
  import { GenericFetchComponent } from "./GenericFetchComponent";
4
4
  import ImageComponent from "./ImageComponent";
5
5
 
6
- const XPPotionsComponent: React.FC<{ configData?: CollectionConfigData<"XPPotions"> }> = ({ configData }) => {
6
+ const XPPotionsComponent: React.FC<{
7
+ configData?: CollectionConfigData<"XPPotions">;
8
+ }> = ({ configData }) => {
7
9
  return (
8
10
  <GenericFetchComponent<CollectionConfigData<"XPPotions">>
9
11
  collectionName="XPPotions"
@@ -0,0 +1,20 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export const useOnlineStatus = () => {
4
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
5
+
6
+ useEffect(() => {
7
+ const handleOnline = () => setIsOnline(true);
8
+ const handleOffline = () => setIsOnline(false);
9
+
10
+ window.addEventListener("online", handleOnline);
11
+ window.addEventListener("offline", handleOffline);
12
+
13
+ return () => {
14
+ window.removeEventListener("online", handleOnline);
15
+ window.removeEventListener("offline", handleOffline);
16
+ };
17
+ }, []);
18
+
19
+ return isOnline;
20
+ };
@@ -1,7 +1,11 @@
1
1
  import React from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import App from "./App";
4
+ import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
4
5
 
5
6
  const container = document.getElementById("root");
6
7
  const root = createRoot(container!);
7
8
  root.render(<App />);
9
+
10
+ // Register the service worker
11
+ serviceWorkerRegistration.register();
@@ -0,0 +1,110 @@
1
+ const isLocalhost = Boolean(
2
+ window.location.hostname === "localhost" ||
3
+ window.location.hostname === "[::1]" ||
4
+ window.location.hostname.match(/^127(?:\.\d+){0,2}\.\d+$/),
5
+ );
6
+
7
+ type Config = {
8
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
9
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
10
+ };
11
+
12
+ export function register(config?: Config) {
13
+ if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
14
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
15
+ if (publicUrl.origin !== window.location.origin) {
16
+ return;
17
+ }
18
+
19
+ window.addEventListener("load", () => {
20
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
21
+
22
+ if (isLocalhost) {
23
+ checkValidServiceWorker(swUrl, config);
24
+
25
+ navigator.serviceWorker.ready.then(() => {
26
+ console.log(
27
+ "This web app is being served cache-first by a service " +
28
+ "worker. To learn more, visit https://cra.link/PWA",
29
+ );
30
+ });
31
+ } else {
32
+ registerValidSW(swUrl, config);
33
+ }
34
+ });
35
+ }
36
+ }
37
+
38
+ function registerValidSW(swUrl: string, config?: Config) {
39
+ navigator.serviceWorker
40
+ .register(swUrl)
41
+ .then((registration) => {
42
+ registration.onupdatefound = () => {
43
+ const installingWorker = registration.installing;
44
+ if (installingWorker == null) {
45
+ return;
46
+ }
47
+ installingWorker.onstatechange = () => {
48
+ if (installingWorker.state === "installed") {
49
+ if (navigator.serviceWorker.controller) {
50
+ console.log(
51
+ "New content is available and will be used when all " +
52
+ "tabs for this page are closed. See https://cra.link/PWA.",
53
+ );
54
+
55
+ if (config && config.onUpdate) {
56
+ config.onUpdate(registration);
57
+ }
58
+ } else {
59
+ console.log("Content is cached for offline use.");
60
+
61
+ if (config && config.onSuccess) {
62
+ config.onSuccess(registration);
63
+ }
64
+ }
65
+ }
66
+ };
67
+ };
68
+ })
69
+ .catch((error) => {
70
+ console.error("Error during service worker registration:", error);
71
+ });
72
+ }
73
+
74
+ function checkValidServiceWorker(swUrl: string, config?: Config) {
75
+ fetch(swUrl, {
76
+ headers: { "Service-Worker": "script" },
77
+ })
78
+ .then((response) => {
79
+ const contentType = response.headers.get("content-type");
80
+ if (
81
+ response.status === 404 ||
82
+ (contentType != null && contentType.indexOf("javascript") === -1)
83
+ ) {
84
+ navigator.serviceWorker.ready.then((registration) => {
85
+ registration.unregister().then(() => {
86
+ window.location.reload();
87
+ });
88
+ });
89
+ } else {
90
+ registerValidSW(swUrl, config);
91
+ }
92
+ })
93
+ .catch(() => {
94
+ console.log(
95
+ "No internet connection found. App is running in offline mode.",
96
+ );
97
+ });
98
+ }
99
+
100
+ export function unregister() {
101
+ if ("serviceWorker" in navigator) {
102
+ navigator.serviceWorker.ready
103
+ .then((registration) => {
104
+ registration.unregister();
105
+ })
106
+ .catch((error) => {
107
+ console.error(error.message);
108
+ });
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ps99-api",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Pet Simulator Public API wrapper written in Typescript.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",