diff --git a/src/component/ErrorBoundary.jsx b/src/component/ErrorBoundary.jsx
new file mode 100644
index 0000000..553a3e6
--- /dev/null
+++ b/src/component/ErrorBoundary.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+
+const crashStyle = {
+ minHeight: '100vh',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '24px',
+ backgroundColor: 'var(--bg-primary)',
+ color: 'var(--text-primary)',
+ textAlign: 'center',
+};
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ render() {
+ const { hasError } = this.state;
+ const { children } = this.props;
+
+ if (hasError) {
+ return (
+
+
+
Something went wrong
+
+ The app hit an unexpected error. Reload to continue.
+
+
+
+
+ );
+ }
+
+ return children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/src/graph-builder/local-storage-manager.js b/src/graph-builder/local-storage-manager.js
index 1b4a93f..12af2df 100644
--- a/src/graph-builder/local-storage-manager.js
+++ b/src/graph-builder/local-storage-manager.js
@@ -1,5 +1,28 @@
import { toast } from 'react-toastify';
+const encodeBase64 = (value) => {
+ const bytes = new TextEncoder().encode(value);
+ let binary = '';
+ bytes.forEach((byte) => {
+ binary += String.fromCharCode(byte);
+ });
+ return window.btoa(binary);
+};
+
+const decodeBase64 = (value) => {
+ const binary = window.atob(value);
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
+ return new TextDecoder().decode(bytes);
+};
+
+const parseStoredJson = (raw) => {
+ try {
+ return JSON.parse(decodeBase64(raw));
+ } catch (e) {
+ return null;
+ }
+};
+
const localStorageGet = (key) => {
try {
return window.localStorage.getItem(key);
@@ -27,11 +50,16 @@ const localStorageRemove = (key) => {
const getSet = (ALL_GRAPHS) => {
if (!localStorageGet(ALL_GRAPHS)) {
- localStorageSet(ALL_GRAPHS, window.btoa(JSON.stringify([])));
+ localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
}
const raw = localStorageGet(ALL_GRAPHS);
if (!raw) return new Set();
- return new Set(JSON.parse(window.atob(raw)));
+ const parsed = parseStoredJson(raw);
+ if (!Array.isArray(parsed)) {
+ localStorageSet(ALL_GRAPHS, encodeBase64(JSON.stringify([])));
+ return new Set();
+ }
+ return new Set(parsed);
};
const localStorageManager = {
@@ -41,24 +69,29 @@ const localStorageManager = {
allgs: getSet(window.btoa('ALL_GRAPHS')),
saveAllgs() {
- localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Array.from(this.allgs))));
+ localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Array.from(this.allgs))));
},
addEmptyIfNot() {
if (!localStorageGet(this.ALL_GRAPHS)) {
- localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify([])));
+ localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
}
},
get(id) {
const raw = localStorageGet(id);
if (raw === null) return null;
- return JSON.parse(window.atob(raw));
+ const parsed = parseStoredJson(raw);
+ if (parsed === null) {
+ localStorageRemove(id);
+ return null;
+ }
+ return parsed;
},
save(id, graphContent) {
this.addGraph(id);
const serializedJson = JSON.stringify(graphContent);
- localStorageSet(id, window.btoa(serializedJson));
+ localStorageSet(id, encodeBase64(serializedJson));
},
remove(id) {
if (this.allgs.delete(id)) this.saveAllgs();
@@ -72,16 +105,25 @@ const localStorageManager = {
getAllGraphs() {
const raw = localStorageGet(this.ALL_GRAPHS);
if (!raw) return [];
- return JSON.parse(window.atob(raw));
+ const parsed = parseStoredJson(raw);
+ if (!Array.isArray(parsed)) {
+ localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify([])));
+ return [];
+ }
+ return parsed;
},
addToFront(id) {
if (this.allgs.has(id)) return;
this.allgs.add(id);
const raw = localStorageGet(this.ALL_GRAPHS);
if (!raw) return;
- const Garr = JSON.parse(window.atob(raw));
+ const Garr = parseStoredJson(raw);
+ if (!Array.isArray(Garr)) {
+ this.saveAllgs();
+ return;
+ }
Garr.unshift(id);
- localStorageSet(this.ALL_GRAPHS, window.btoa(JSON.stringify(Garr)));
+ localStorageSet(this.ALL_GRAPHS, encodeBase64(JSON.stringify(Garr)));
},
getAuthorName() {
return localStorageGet(this.AUTHOR_NAME) || '';
diff --git a/src/index.jsx b/src/index.jsx
index afefa85..7156d15 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -2,12 +2,15 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
+import ErrorBoundary from './component/ErrorBoundary';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
-
+
+
+
,
document.getElementById('root'),
);
diff --git a/src/serverCon/crud_http.js b/src/serverCon/crud_http.js
index de75afa..00df3a7 100644
--- a/src/serverCon/crud_http.js
+++ b/src/serverCon/crud_http.js
@@ -1,7 +1,14 @@
import ec from './config';
+function readTextOrThrow(x) {
+ return x.text().then((text) => {
+ if (x.ok) return text;
+ throw new Error(text || `Request failed with status ${x.status}`);
+ });
+}
+
function getGraph(serverID) {
- return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => x.text());
+ return fetch(`${ec.baseURL + ec.getGraph(serverID)}`).then((x) => readTextOrThrow(x));
}
function getGraphWithHashCheck(serverID, latestHash) {
@@ -9,10 +16,7 @@ function getGraphWithHashCheck(serverID, latestHash) {
headers: {
'X-Latest-Hash': latestHash,
},
- }).then((x) => {
- if (x.status === 200) return x.text();
- return Promise.reject(x.text());
- });
+ }).then((x) => readTextOrThrow(x));
}
function postGraph(graphml) {
@@ -22,10 +26,7 @@ function postGraph(graphml) {
},
method: 'POST',
body: graphml,
- }).then((x) => {
- if (!x.ok) return Promise.reject(x.text());
- return x.text();
- });
+ }).then((x) => readTextOrThrow(x));
}
function updateGraph(serverID, graphml) {
@@ -35,10 +36,7 @@ function updateGraph(serverID, graphml) {
'Content-Type': 'application/xml',
},
body: graphml,
- }).then((x) => {
- if (!x.ok) return Promise.reject(x.text());
- return x.text();
- });
+ }).then((x) => readTextOrThrow(x));
}
function forceUpdateGraph(serverID, graphml) {
@@ -48,10 +46,7 @@ function forceUpdateGraph(serverID, graphml) {
'Content-Type': 'application/xml',
},
body: graphml,
- }).then((x) => {
- if (!x.ok) return Promise.reject(x.text());
- return x.text();
- });
+ }).then((x) => readTextOrThrow(x));
}
export {