ps99-api 2.1.0 → 2.3.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,20 +11,25 @@
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",
17
- "react-router-dom": "^6.23.1"
19
+ "react-router-dom": "^6.24.1"
18
20
  },
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
- "typescript": "^5.5.2",
26
- "webpack": "^5.92.0",
29
+ "typescript": "^5.5.3",
30
+ "webpack": "^5.92.1",
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 (
@@ -22,6 +23,7 @@ const App: React.FC = () => {
22
23
  element={<DynamicCollectionConfigData />}
23
24
  />
24
25
  </Routes>
26
+ <Footer />
25
27
  </Router>
26
28
  );
27
29
  };
@@ -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,18 +1,6 @@
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
6
  const { collectionName, configName } = useParams<{
@@ -20,109 +8,17 @@ const DynamicCollectionConfigData: React.FC = () => {
20
8
  configName: string;
21
9
  }>();
22
10
 
23
- if (!collectionName) {
24
- return <div>Invalid collection name</div>;
25
- }
26
-
27
- if (configName === "all") {
28
- return <RenderAllConfigs collectionName={collectionName} />;
11
+ if (!collectionName || !configName) {
12
+ return <div>Invalid collection or config name</div>;
29
13
  }
30
14
 
31
15
  const Component = lazy(() => import(`./${collectionName}Component`));
32
16
 
33
17
  return (
34
18
  <Suspense fallback={<div>Loading...</div>}>
35
- <Component />
19
+ <Component configName={configName} />
36
20
  </Suspense>
37
21
  );
38
22
  };
39
23
 
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
24
  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;
@@ -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,29 @@ 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(
31
+ const url = URL.createObjectURL(
33
32
  new Blob([imageBlob], { type: "image/png" }),
34
33
  );
35
- setImageUrl(imageUrl);
34
+ setImageUrl(url);
36
35
  } catch (error) {
37
- setError("Error fetching image");
38
36
  console.error("Error fetching image:", error);
39
37
  } finally {
40
- currentRequests--;
38
+ activeRequests--;
41
39
  processQueue();
42
40
  }
43
41
  };
44
42
 
45
- const load = () => {
43
+ const requestImage = () => {
46
44
  fetchImage();
47
45
  };
48
46
 
49
- queue.push(load);
47
+ requestQueue.push(requestImage);
50
48
  processQueue();
51
49
 
52
50
  return () => {
@@ -54,11 +52,7 @@ const ImageComponent: React.FC<ImageProps> = ({ src, alt }) => {
54
52
  URL.revokeObjectURL(imageUrl);
55
53
  }
56
54
  };
57
- }, [src, imageUrl]);
58
-
59
- if (error) {
60
- return <div>{error}</div>;
61
- }
55
+ }, [src]);
62
56
 
63
57
  return (
64
58
  <div>{imageUrl ? <img src={imageUrl} alt={alt} /> : <p>Loading...</p>}</div>
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  const path = require("path");
2
+ const CopyWebpackPlugin = require("copy-webpack-plugin");
2
3
 
3
4
  module.exports = {
4
5
  entry: "./src/index.tsx",
@@ -27,6 +28,11 @@ module.exports = {
27
28
  path: path.resolve(__dirname, "dist"),
28
29
  publicPath: "/",
29
30
  },
31
+ plugins: [
32
+ new CopyWebpackPlugin({
33
+ patterns: [{ from: path.resolve(__dirname, "public") }],
34
+ }),
35
+ ],
30
36
  devServer: {
31
37
  static: {
32
38
  directory: path.join(__dirname, "public"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ps99-api",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",
@@ -25,15 +25,15 @@
25
25
  "@semantic-release/git": "^10.0.1",
26
26
  "@tsconfig/node20": "^20.1.4",
27
27
  "@types/jest": "^29.5.12",
28
- "@types/node": "^20.14.2",
28
+ "@types/node": "^20.14.10",
29
29
  "cz-conventional-changelog": "^3.3.0",
30
30
  "dets": "^0.16.0",
31
31
  "esbuild": "0.21.5",
32
32
  "jest": "^29.7.0",
33
33
  "prettier": "^3.3.2",
34
34
  "semantic-release": "^24.0.0",
35
- "ts-jest": "^29.1.2",
36
- "typescript": "^5.4.5"
35
+ "ts-jest": "^29.1.5",
36
+ "typescript": "^5.5.3"
37
37
  },
38
38
  "dependencies": {
39
39
  "axios": "^1.7.2"