diff --git a/README.md b/README.md index 145f248..a9b286b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ## QR Sync Admin Web + This is the new web interface for the QR Sync Admin app. The goal of this admin app is for controlling multiple computers using a quick and easy QR scanning sync process. diff --git a/package-lock.json b/package-lock.json index b7abb13..f71865e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "0.1.0", "dependencies": { "@ionic/react": "^7.7.2", + "@ionic/react-router": "7.8.0", "@yudiel/react-qr-scanner": "^1.2.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-qr-code": "^2.0.12", "react-responsive-carousel": "^3.2.23", - "typescript": "^4" + "react-router": "^5.3.4", + "react-router-dom": "^5.3.4", + "typescript": "^4.9.5" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -23,6 +26,7 @@ "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@types/react-qr-reader": "^2.1.7", + "@types/react-router-dom": "^5.3.3", "eslint-utils": "^3.0.0", "react-scripts": "^5.0.1" } @@ -2238,7 +2242,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2734,9 +2737,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.7.2.tgz", - "integrity": "sha512-cH92OSqJBTaW8AAqh+M6NjzltVoAZCXqsHAOQMmZgrY4KgXNU+Wh+fs2La/UrFxTob9pZf30EpRddUG5rQYIFw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.0.tgz", + "integrity": "sha512-rogQw6lWH367E5XQnovbAIB4pT1YmuTz7OvyQm0cp4pO2/64faKyTGteSxc99stG01CoARW+pjJN1K09hfKFPw==", "dependencies": { "@stencil/core": "^4.12.2", "ionicons": "^7.2.2", @@ -2744,11 +2747,11 @@ } }, "node_modules/@ionic/react": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-7.7.2.tgz", - "integrity": "sha512-oUR49V7XpfZf3kLu7lAZLFiK5B5HMiL5UigWYalGqSCxrK4Jj7Tt4OZEUoAoDnbdDx67S490w/PcIUIw8eE45Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-7.8.0.tgz", + "integrity": "sha512-OpP3WWnbD0cdP98afJk/Z6DmXk4t1ZBOLzZvBaNlRUMbnkTZOjt/PufCK+qvaPVGc2OKfKhv+XRckbx9uX5Few==", "dependencies": { - "@ionic/core": "7.7.2", + "@ionic/core": "7.8.0", "ionicons": "^7.0.0", "tslib": "*" }, @@ -2757,6 +2760,21 @@ "react-dom": ">=16.8.6" } }, + "node_modules/@ionic/react-router": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-k0KdikXwZAxJF4PZXNlVFPCQqBGj35Tz20E/g/KsML6G3I3pB1HIhyioQfRecZrDWRxCYUh3RPAObY+vWxpC1w==", + "dependencies": { + "@ionic/react": "7.8.0", + "tslib": "*" + }, + "peerDependencies": { + "react": ">=16.8.6", + "react-dom": ">=16.8.6", + "react-router": "^5.0.1", + "react-router-dom": "^5.0.1" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3743,9 +3761,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.2.tgz", - "integrity": "sha512-WEMpoqwMV4hY/ab2z9NxRhSeZwuKEugjyn6Vd+qA9xqZh6VNUL27QbP8vCa7IeqD4Zql4JBtKu3lVuBHutWE6w==", + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.6.tgz", + "integrity": "sha512-15JO2TdaxGVKNdLZb/2TtDa+juj3XGD/V0y/disgdzYYSnajgSh06nwODfdHz9eTUh1Hisz+KIo857I1rCZrfg==", "bin": { "stencil": "bin/stencil" }, @@ -4143,6 +4161,12 @@ "@types/node": "*" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4367,6 +4391,27 @@ "@types/react": "*" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -9416,6 +9461,32 @@ "he": "bin/he" } }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -15334,6 +15405,60 @@ "react-easy-swipe": "^0.0.21" } }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/react-router/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/react-router/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -15496,8 +15621,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -15663,6 +15787,11 @@ "node": ">=4" } }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "node_modules/resolve-url-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", @@ -17198,6 +17327,16 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -17703,6 +17842,11 @@ "node": ">=10.12.0" } }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 15c5f58..11b2e1b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "react-dom": "^18.2.0", "react-qr-code": "^2.0.12", "react-responsive-carousel": "^3.2.23", - "typescript": "^4" + "typescript": "^4.9.5", + "@ionic/react-router": "7.8.0", + "react-router": "^5.3.4", + "react-router-dom": "^5.3.4" }, "devDependencies": { "@types/jest": "^29.5.12", @@ -17,6 +20,7 @@ "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@types/react-qr-reader": "^2.1.7", + "@types/react-router-dom": "^5.3.3", "eslint-utils": "^3.0.0", "react-scripts": "^5.0.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11" @@ -42,4 +46,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 747221a..11efc9a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -16,9 +16,9 @@ "src": "logo512.png", "type": "image/png", "sizes": "512x512" - } + } ], "start_url": ".", "display": "standalone", "theme_color": "#107C76" -} +} \ No newline at end of file diff --git a/src/App.css b/src/App.css index ee53c3f..b9b9c1f 100644 --- a/src/App.css +++ b/src/App.css @@ -1,5 +1,5 @@ body { - --ion-color-primary: #107C76; + --ion-color-primary: #107c76; --ion-background-color: rgb(39, 39, 39); --ion-text-color: white; --ion-text-color-rgb: 255, 255, 255; @@ -80,4 +80,4 @@ body { to { transform: rotate(360deg); } -} \ No newline at end of file +} diff --git a/src/App.test.tsx b/src/App.test.tsx index a754b20..1729d67 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom"; +import { App } from "./App"; -it('renders without crashing', () => { - const div = document.createElement('div'); +it("renders without crashing", () => { + const div = document.createElement("div"); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); diff --git a/src/App.tsx b/src/App.tsx index cbe7ff8..2c4b0ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,115 +1,119 @@ -import React from 'react'; -import './App.css'; -import { ServerTypes } from './ServerTypes'; -import { Header } from './feature/header/Header'; -import { SessionPage } from './feature/session/SessionPage'; -import { HomePage } from './feature/home/HomePage'; -import { WSClient } from './WSClient'; -import { IonApp } from '@ionic/react'; +import React, { useState } from "react"; +import "./App.css"; +import { IonApp, IonPage, IonRouterOutlet } from "@ionic/react"; +import { IonReactRouter } from "@ionic/react-router"; +import { Redirect, Route } from "react-router"; +import { HomePage } from "./feature/home/HomePage"; +import { WSClient } from "./WSClient"; +import { ServerTypes } from "./ServerTypes"; +import { SessionPage } from "./feature/session/SessionPage"; +import { NavigateOnStateChange } from "./NavigateOnStateChange"; -interface AppState { - result: string; - goToURL: string; - clientMap: {[key: string]: ServerTypes.Client}; - scanModalOpen: boolean; - ourClientId: string | null; - sessionId: string | null; - sessionOwnerId: string | null; -} +export const App: React.FC = () => { + let wsUrl = "wss://qrsync.org/api/v1/ws"; + // let wsUrl = "ws://localhost:4010/api/v1/ws"; + const [wsClient] = useState(new WSClient(wsUrl)); + const [ourClientId, setOurClientId] = useState(); + const [sessionOwnerId, setSessionOwnerId] = useState(); + const [sessionId, setSessionId] = useState(); + const [clientMap, setClientMap] = useState< + Record + >({}); + // State used to navigate to route via a server sent event (not user link click) + const [changeToRoute, setChangeToRoute] = useState(); -class App extends React.Component<{}, AppState> { - wsClient: WSClient; + const onClientJoinedSessionMsg = ( + msg: ServerTypes.ClientJoinedSessionMsg + ) => { + if (msg.clientId === ourClientId) { + setSessionId(msg.sessionId); + setSessionOwnerId(msg.sessionOwnerId); + setClientMap(msg.clientMap); + setChangeToRoute("/session"); + } + }; - constructor(props: {}) { - super(props); - this.state = { - result: "", - goToURL: "", - clientMap: {}, - scanModalOpen: false, - ourClientId: null, - sessionId: null, - sessionOwnerId: null - }; - let wsUrl = "wss://qrsync.org/api/v1/ws"; - this.wsClient = new WSClient(wsUrl); - this.wsClient.addMessageHandler("main", this.onReceiveWebsocketMsg); - } + const onClientConnectMsg = (msg: ServerTypes.ClientConnectMsg) => { + setOurClientId(msg.client.id); + }; - onReceiveWebsocketMsg = (msg: ServerTypes.Msg) => { + const onReceiveWebsocketMsg = (msg: ServerTypes.Msg) => { if (msg.type) { switch (msg.type) { - case "ClientConnect": return this.onClientConnectMsg(msg); - case "ClientJoinedSession": return this.onClientJoinedSessionMsg(msg); - case "BroadcastFromSession": return this.onBroadcastFromSessionMsg(msg); + case "ClientConnect": + return onClientConnectMsg(msg); + case "ClientJoinedSession": + return onClientJoinedSessionMsg(msg); + case "BroadcastFromSession": + return onBroadcastFromSessionMsg(msg); } } else { console.warn("No message type ", msg); } - } - - onClientJoinedSessionMsg = (msg: ServerTypes.ClientJoinedSessionMsg) => { - if (msg.clientId === this.state.ourClientId) { - this.setState({ - sessionId: msg.sessionId, - sessionOwnerId: msg.sessionOwnerId, - clientMap: msg.clientMap - }); - } - } + }; - onClientConnectMsg = (msg: ServerTypes.ClientConnectMsg) => { - this.setState({ ourClientId: msg.client.id }); - } + wsClient.addMessageHandler("main", onReceiveWebsocketMsg); - onScanClient = (clientId: string | null) => { - this.setState({result: "" + clientId}); + const onScanClient = (clientId: string | null) => { if (clientId) { console.log("Client id scanned ", clientId); - if (!this.state.sessionId) { - this.wsClient.sendMessage({ + if (!sessionId) { + wsClient.sendMessage({ type: "CreateSession", - addClientId: clientId + addClientId: clientId, }); } else { - this.wsClient.sendMessage({ - type: "AddSessionClient", + wsClient.sendMessage({ + type: "AddClientToSession", addClientId: clientId, - sessionId: this.state.sessionId + sessionId: sessionId, }); } } - } - - onBroadcastFromSessionMsg = (msg: ServerTypes.BroadcastFromSessionMsg) => { - - } + }; - onLeaveSession = () => { - this.wsClient.leaveSession(); - this.setState({ sessionId: null }); - } + const onBroadcastFromSessionMsg = ( + msg: ServerTypes.BroadcastFromSessionMsg + ) => {}; - render() { - return
- -
- {this.state.sessionId ? - : - - } -
-
- } -} + const onLeaveSession = () => { + wsClient.leaveSession(); + setSessionId(undefined); + setChangeToRoute("/index.html"); + }; -export default App; \ No newline at end of file + return ( + + + {/* + Way to change react router route based on websocket events without + having to create second state wrapper inside of IonReactRouter + */} + setChangeToRoute(undefined)}/> + + + + + + + + + +

Just a quick test...

+
+
+ +
+
+
+ ); +}; diff --git a/src/NavigateOnStateChange.tsx b/src/NavigateOnStateChange.tsx new file mode 100644 index 0000000..ed14632 --- /dev/null +++ b/src/NavigateOnStateChange.tsx @@ -0,0 +1,23 @@ +import { useIonRouter } from "@ionic/react"; +import React, { useEffect } from "react"; + +type NavigateOnStateChangeProps = { route?: string, onNavigate?: () => void} + +export const NavigateOnStateChange: React.FC = ({route, onNavigate}) => { + const ionRouter = useIonRouter(); + console.log("NavigateOnStateChange", ionRouter); + useEffect(() => { + if (route && ionRouter) { + console.log("Route changed ", route); + setTimeout(() => { + ionRouter.push(route); + }); + if (onNavigate) { + onNavigate(); + } + } + /* This useEffect should ONLY run when route changes */ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route]); + return <> + } \ No newline at end of file diff --git a/src/ServerTypes.ts b/src/ServerTypes.ts index 0c2543d..426672d 100644 --- a/src/ServerTypes.ts +++ b/src/ServerTypes.ts @@ -1,5 +1,5 @@ export namespace ServerTypes { - export type Msg = ClientConnectMsg | CreateSessionMsg | UpdateClientMsg | AddSessionClientMsg | ClientJoinedSessionMsg | ClientLeftSessionMsg | BroadcastToSessionMsg | BroadcastFromSessionMsg | ErrorMsg | InfoMsg + export type Msg = ClientConnectMsg | CreateSessionMsg | UpdateClientMsg | AddClientToSessionMsg | ClientJoinedSessionMsg | ClientLeftSessionMsg | BroadcastToSessionMsg | BroadcastFromSessionMsg | ErrorMsg | InfoMsg export interface Client { id: string; @@ -22,8 +22,8 @@ export namespace ServerTypes { type: "UpdateClient"; name: string; } - export interface AddSessionClientMsg { - type: "AddSessionClient"; + export interface AddClientToSessionMsg { + type: "AddClientToSession"; sessionId: string; addClientId: string; } @@ -32,14 +32,14 @@ export namespace ServerTypes { clientId: string; sessionId: string; sessionOwnerId: string; - clientMap: {[key: string]: Client}; + clientMap: { [key: string]: Client }; } export interface ClientLeftSessionMsg { type: "ClientLeftSession"; clientId: string; sessionId: string; sessionOwnerId: string; - clientMap: {[key: string]: Client}; + clientMap: { [key: string]: Client }; } export interface BroadcastToSessionMsg { type: "BroadcastToSession"; diff --git a/src/WSClient.ts b/src/WSClient.ts index b28ebd8..84118e0 100644 --- a/src/WSClient.ts +++ b/src/WSClient.ts @@ -4,7 +4,7 @@ export type WSMessageHandler = (msg: ServerTypes.Msg) => any; export class WSClient { ws: WebSocket; - messageHandlerMap: {[id: string]: WSMessageHandler} = {}; + messageHandlerMap: { [id: string]: WSMessageHandler } = {}; allMessages: ServerTypes.Msg[] = []; public clientId: string | null = null; clientName: string = ""; @@ -69,7 +69,7 @@ export class WSClient { const prevSessionId = localStorage.getItem("prevSessionId"); if (prevSessionId) { this.sendMessage({ - type: "AddSessionClient", + type: "AddClientToSession", sessionId: prevSessionId, addClientId: this.clientId }); diff --git a/src/feature/header/Header.tsx b/src/feature/header/Header.tsx index eb2fb6c..b36109b 100644 --- a/src/feature/header/Header.tsx +++ b/src/feature/header/Header.tsx @@ -1,18 +1,17 @@ -import logo_fg from './logo_fg1.svg'; -import logo_bg from './sync_arrows1.svg'; -import React from 'react'; +import logo_fg from "./logo_fg1.svg"; +import logo_bg from "./sync_arrows1.svg"; +import React from "react"; export class Header extends React.Component { - - render = () => (
-
-
- logo - logo-bg -
-

QR Sync

+ render = () => ( +
+
+
+ logo + logo-bg
-
) - +

QR Sync

+
+
+ ); } - diff --git a/src/feature/home/HomePage.tsx b/src/feature/home/HomePage.tsx index d805248..90cb2b2 100644 --- a/src/feature/home/HomePage.tsx +++ b/src/feature/home/HomePage.tsx @@ -1,6 +1,14 @@ import React, { useState } from "react"; import QRCode from "react-qr-code"; -import { IonButton, IonModal } from "@ionic/react"; +import { + IonButton, + IonContent, + IonHeader, + IonModal, + IonPage, + IonToolbar, + useIonRouter, +} from "@ionic/react"; import { ScanClientModal } from "../scan-client/ScanClientModal"; import { IntroSlides } from "../intro-slides/IntroSlides"; @@ -15,6 +23,8 @@ export const HomePage: React.FC = ({ }) => { const [scanModalOpen, setScanModalOpen] = useState(false); + const router = useIonRouter(); + const closeScannerModal = () => { console.log("Close scanner"); setScanModalOpen(false); @@ -24,36 +34,50 @@ export const HomePage: React.FC = ({ if (result) { closeScannerModal(); onScanClient(result); + router.push("session"); } }; return ( -
-
- {ourClientId && ( - <> - -

Client id {ourClientId}

- - )} - setScanModalOpen(true)}>Open Scanner -
- - - - -
+ + + +

QR Sync

+
+
+ +
+
+ {ourClientId && ( + <> + +

Client id {ourClientId}

+ + )} + setScanModalOpen(true)}> + Open Scanner + +
+ + + + +
+
+
); }; diff --git a/src/feature/intro-slides/slides.css b/src/feature/intro-slides/slides.css index 9008e4b..d53a293 100644 --- a/src/feature/intro-slides/slides.css +++ b/src/feature/intro-slides/slides.css @@ -1,12 +1,12 @@ .introSlider { - margin-bottom: 20px; + margin-bottom: 20px; } .slideDescription { - width: 100%; - margin-bottom: 40px; + width: 100%; + margin-bottom: 40px; } .introSlide img { - padding: 40px; -} \ No newline at end of file + padding: 40px; +} diff --git a/src/feature/session-actions/OpenWebsiteModal.tsx b/src/feature/session-actions/OpenWebsiteModal.tsx index 2f357b4..cc27ccc 100644 --- a/src/feature/session-actions/OpenWebsiteModal.tsx +++ b/src/feature/session-actions/OpenWebsiteModal.tsx @@ -3,36 +3,43 @@ import React from "react"; import { SessionMessage } from "../session/SessionMessage"; import { SessionActionModal } from "./SessionActionModal"; -export class OpenWebsiteModal extends SessionActionModal<{}, { urlInput: string }> { - - state = { - urlInput: "" - }; +export class OpenWebsiteModal extends SessionActionModal< + {}, + { urlInput: string } +> { + state = { + urlInput: "", + }; - onOpenWebsiteClick = () => { - let id = this.props.wsClient.getId(); - if (this.props.wsClient && id != null) { - let sessionMsg: SessionMessage = { - type: "OPEN_WEBSITE", - text: this.state.urlInput, - senderId: id, - senderName: this.props.wsClient.getName() - }; - this.props.wsClient.sendMessage({ - type: "BroadcastToSession", - payload: sessionMsg - }); - } - this.props.closeModal(); + onOpenWebsiteClick = () => { + let id = this.props.wsClient.getId(); + if (this.props.wsClient && id != null) { + let sessionMsg: SessionMessage = { + type: "OPEN_WEBSITE", + text: this.state.urlInput, + senderId: id, + senderName: this.props.wsClient.getName(), + }; + this.props.wsClient.sendMessage({ + type: "BroadcastToSession", + payload: sessionMsg, + }); } + this.props.closeModal(); + }; - render() { - return <>

Open Website

-

Open a website on devices controlled by this session

- this.setState({ urlInput: e.detail.value! })}> - Open Website - - } - -} \ No newline at end of file + render() { + return ( + <> +

Open Website

+

Open a website on devices controlled by this session

+ this.setState({ urlInput: e.detail.value! })} + > + Open Website + + ); + } +} diff --git a/src/feature/session-actions/SessionActionsList.tsx b/src/feature/session-actions/SessionActionsList.tsx index 8debc96..7ce4683 100644 --- a/src/feature/session-actions/SessionActionsList.tsx +++ b/src/feature/session-actions/SessionActionsList.tsx @@ -1,82 +1,107 @@ import React from "react"; -import { globeOutline } from 'ionicons/icons'; +import { globeOutline } from "ionicons/icons"; import { IonIcon, IonModal } from "@ionic/react"; import { OpenWebsiteModal } from "./OpenWebsiteModal"; import { WSClient } from "../../WSClient"; import { SessionActionModal } from "./SessionActionModal"; export interface SessionActionsListProps { - wsClient: WSClient; - userIsSessionOwner: boolean; + wsClient: WSClient; + userIsSessionOwner: boolean; } export interface SessionActionsListState { - actionModalOpen: boolean; - actionModalComponent?: typeof SessionActionModal; + actionModalOpen: boolean; + actionModalComponent?: typeof SessionActionModal; } export interface SessionActionSummary { - name: string; - ionicon: string; - ownerOnly: boolean; - modalComponent?: typeof SessionActionModal; + name: string; + ionicon: string; + ownerOnly: boolean; + modalComponent?: typeof SessionActionModal; } const sessionActions: SessionActionSummary[] = [ - { - name: "Open Website", - ionicon: globeOutline, - ownerOnly: true, - modalComponent: OpenWebsiteModal - } + { + name: "Open Website", + ionicon: globeOutline, + ownerOnly: true, + modalComponent: OpenWebsiteModal, + }, ]; -export class SessionActionsList extends React.Component { - - state: SessionActionsListState = { - actionModalOpen: false - }; +export class SessionActionsList extends React.Component< + SessionActionsListProps, + SessionActionsListState +> { + state: SessionActionsListState = { + actionModalOpen: false, + }; - render() { - console.log("User is session owner", this.props.userIsSessionOwner); - return
-

User is session owner? {this.props.userIsSessionOwner ? "yes" : "no"}

- {sessionActions.filter((action) => this.props.userIsSessionOwner || !action.ownerOnly) - .map((action) => this.renderActionButton(action)) - } - - {this.renderModal()} - -
- } + render() { + console.log("User is session owner", this.props.userIsSessionOwner); + return ( +
+

+ User is session owner? {this.props.userIsSessionOwner ? "yes" : "no"} +

+ {sessionActions + .filter( + (action) => this.props.userIsSessionOwner || !action.ownerOnly + ) + .map((action) => this.renderActionButton(action))} + + {this.renderModal()} + +
+ ); + } - renderActionButton = (action: SessionActionSummary) => { - return
this.onActionClicked(action)} - style={{borderWidth: 1, borderStyle: "solid", borderColor: "white", borderRadius: 20, display: "inline-block"}} - > -

{action.name}

- -
- } + renderActionButton = (action: SessionActionSummary) => { + return ( +
this.onActionClicked(action)} + style={{ + borderWidth: 1, + borderStyle: "solid", + borderColor: "white", + borderRadius: 20, + display: "inline-block", + }} + > +

{action.name}

+ +
+ ); + }; - renderModal = () => { - let ModalComponent = this.state.actionModalComponent; - if (ModalComponent) { - return - } + renderModal = () => { + let ModalComponent = this.state.actionModalComponent; + if (ModalComponent) { + return ( + + ); } + }; - closeModal = () => { - this.setState({ actionModalOpen: false }); - } + closeModal = () => { + this.setState({ actionModalOpen: false }); + }; - onActionClicked = (action: SessionActionSummary) => { - if (action.name === "Open Website") { - this.setState({ actionModalOpen: true, actionModalComponent: action.modalComponent }) - } + onActionClicked = (action: SessionActionSummary) => { + if (action.name === "Open Website") { + this.setState({ + actionModalOpen: true, + actionModalComponent: action.modalComponent, + }); } - -} \ No newline at end of file + }; +} diff --git a/src/feature/session/SessionPage.tsx b/src/feature/session/SessionPage.tsx index 9be5e59..500197a 100644 --- a/src/feature/session/SessionPage.tsx +++ b/src/feature/session/SessionPage.tsx @@ -1,70 +1,77 @@ import React from "react"; import { ServerTypes } from "../../ServerTypes"; -import { IonButton } from "@ionic/react"; +import { IonButton, IonPage } from "@ionic/react"; import { WSClient } from "../../WSClient"; import { SessionMessage } from "./SessionMessage"; import { SessionActionsList } from "../session-actions/SessionActionsList"; export interface SessionPageProps { - sessionId: string | null; - sessionOwnerId: string | null; - clientMap: Record; - wsClient: WSClient; - onLeaveSession: () => any; + sessionId: string | undefined; + sessionOwnerId: string | undefined; + clientMap: Record; + wsClient: WSClient; + onLeaveSession: () => any; } export interface SessionPageState { - urlInput: string; - sessionMessages: SessionMessage[]; + urlInput: string; + sessionMessages: SessionMessage[]; } -export class SessionPage extends React.Component { +export class SessionPage extends React.Component< + SessionPageProps, + SessionPageState +> { + state = { + urlInput: "", + sessionMessages: [], + } as SessionPageState; - state = { - urlInput: "", - sessionMessages: [] - } as SessionPageState; + constructor(props: SessionPageProps) { + super(props); + props.wsClient.addMessageHandler("session_page", this.onWebsocketMessage); + } - constructor(props: SessionPageProps) { - super(props); - props.wsClient.addMessageHandler("session_page", this.onWebsocketMessage); - } - - render() { - return
- Leave Session -

Session Page

-

{this.props.sessionId}

-

Session Messages

-
    - {this.state.sessionMessages.map((sessionMsg) => (
  • {sessionMsg.text}
  • ))} -
-

Session Actions:

- -
- } + render() { + return ( + + Leave Session +

Session Page

+

{this.props.sessionId}

+

Session Messages

+
    + {this.state.sessionMessages.map((sessionMsg) => ( +
  • {sessionMsg.text}
  • + ))} +
+

Session Actions:

+ +
+ ); + } - onWebsocketMessage = (msg: ServerTypes.Msg) => { - if (msg.type === "BroadcastFromSession") { - let payload: SessionMessage = msg.payload; - if (payload.type && payload.text) { - let sessionMsgs: SessionMessage[] = []; - if (this.state.sessionMessages) { - sessionMsgs = this.state.sessionMessages; - } - sessionMsgs.push(payload); - this.setState({ sessionMessages: sessionMsgs}); - } - if (msg.senderId !== this.props.wsClient.getId()) { - if (payload.type === "OPEN_WEBSITE") { - console.log("Opening url ", payload.text); - window.open(payload.text, "_blank"); - } - } + onWebsocketMessage = (msg: ServerTypes.Msg) => { + if (msg.type === "BroadcastFromSession") { + let payload: SessionMessage = msg.payload; + if (payload.type && payload.text) { + let sessionMsgs: SessionMessage[] = []; + if (this.state.sessionMessages) { + sessionMsgs = this.state.sessionMessages; } + sessionMsgs.push(payload); + this.setState({ sessionMessages: sessionMsgs }); + } + if (msg.senderId !== this.props.wsClient.getId()) { + if (payload.type === "OPEN_WEBSITE") { + console.log("Opening url ", payload.text); + window.open(payload.text, "_blank"); + } + } } - -} \ No newline at end of file + }; +} diff --git a/src/index.css b/src/index.css index ec2585e..4a1df4d 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,13 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/src/index.tsx b/src/index.tsx index d4ec660..b0abd4e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,14 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App'; -import * as serviceWorker from './serviceWorker'; -import '@ionic/react/css/core.css'; -import { setupIonicReact } from '@ionic/react'; +import React from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import { App } from "./App"; +import * as serviceWorker from "./serviceWorker"; +import "@ionic/react/css/core.css"; +import { setupIonicReact } from "@ionic/react"; setupIonicReact(); - -const container = document.getElementById('root'); +const container = document.getElementById("root"); const root = createRoot(container!); root.render(); diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 15d90cb..b3793f8 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -12,12 +12,12 @@ const isLocalhost = Boolean( window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) ); type Config = { @@ -51,7 +51,7 @@ export function register(config?: Config) { navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' + 'worker. To learn more, visit https://bit.ly/CRA-PWA' ); }); } else { @@ -79,7 +79,7 @@ function registerValidSW(swUrl: string, config?: Config) { // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' ); // Execute callback diff --git a/tsconfig.json b/tsconfig.json index 5c05ace..889be38 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -21,7 +17,5 @@ "jsx": "react", "noFallthroughCasesInSwitch": true }, - "include": [ - "src" - ] + "include": ["src"] }