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 {