diff --git a/cmd/database/database.go b/cmd/database/database.go index 1851192..bc73cd5 100644 --- a/cmd/database/database.go +++ b/cmd/database/database.go @@ -58,8 +58,16 @@ func insertToDB(d amqp.Delivery, c *pgx.Conn) bool { log.Fatalf("insert into database: %s", err) } - // Alternatively, send Cache messages from here - // instead of server and backend services + // An alternative query that returns the id of the inserted row. + // var id int64 + // err = c.QueryRow(context.Background(), "insert into messages (message, created) values ($1, to_timestamp($2)) returning id", message.Text, message.Time/1000).Scan(&id) + // if err != nil { + // log.Fatalf("insert into database: %s", err) + // } + // fmt.Println(id) + + // For cache, could send messages from here instead + // of doing it from the server and backend services. // err = .Publish(conf.Exchange, conf.KeyCache, d.Body) return true diff --git a/cmd/server/template/template.html b/cmd/server/template/template.html index c01af98..354f43b 100644 --- a/cmd/server/template/template.html +++ b/cmd/server/template/template.html @@ -8,8 +8,20 @@
+
-
+
@@ -23,12 +35,12 @@
-
+
- - + + diff --git a/web/bootstrap/scss/style.scss b/web/bootstrap/scss/style.scss index df308c2..f7cc4d6 100644 --- a/web/bootstrap/scss/style.scss +++ b/web/bootstrap/scss/style.scss @@ -21,7 +21,7 @@ $input-btn-focus-width: 0rem; // @import "bootstrap/scss/images"; // @import "bootstrap/scss/code"; @import "bootstrap/scss/grid"; -// @import "bootstrap/scss/tables"; +@import "bootstrap/scss/tables"; @import "bootstrap/scss/forms"; @import "bootstrap/scss/buttons"; // @import "bootstrap/scss/transitions"; @@ -29,8 +29,8 @@ $input-btn-focus-width: 0rem; // @import "bootstrap/scss/button-group"; // @import "bootstrap/scss/input-group"; // Requires forms // @import "bootstrap/scss/custom-forms"; -// @import "bootstrap/scss/nav"; -// @import "bootstrap/scss/navbar"; // Requires nav +@import "bootstrap/scss/nav"; +@import "bootstrap/scss/navbar"; // Requires nav // @import "bootstrap/scss/card"; // @import "bootstrap/scss/breadcrumb"; // @import "bootstrap/scss/pagination"; diff --git a/web/react/package-lock.json b/web/react/package-lock.json index 8a26515..00ff1a6 100644 --- a/web/react/package-lock.json +++ b/web/react/package-lock.json @@ -4,11 +4,45 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "esbuild": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.5.tgz", "integrity": "sha512-vcuP53pA5XiwUU4FnlXM+2PnVjTfHGthM7uP1gtp+9yfheGvFFbq/KyuESThmtoHPUrfZH5JpxGVJIFDVD1Egw==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@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" + } + }, + "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==", + "requires": { + "react-is": "^16.7.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -22,11 +56,38 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "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==", + "requires": { + "isarray": "0.0.1" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, "react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -46,6 +107,52 @@ "scheduler": "^0.20.2" } }, + "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==" + }, + "react-router": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", + "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "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" + } + }, + "react-router-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "requires": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.1", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "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==" + }, "scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -54,6 +161,21 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } + }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "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==" + }, + "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==" } } } diff --git a/web/react/package.json b/web/react/package.json index 3082269..647cadd 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -13,6 +13,7 @@ "dependencies": { "esbuild": "^0.12.5", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "react-router-dom": "^5.3.0" } } diff --git a/web/react/src/alert.jsx b/web/react/src/alert.jsx new file mode 100644 index 0000000..6367651 --- /dev/null +++ b/web/react/src/alert.jsx @@ -0,0 +1,17 @@ +import React from "react"; + +function Alert({alert}) { + const alertType = alert.source == "back" ? "success" : "primary"; + const alertLabel = alert.source == "back" ? "Received" : "Sent"; + + return ( +
+ {alertLabel}: {alert.text} +
+ ); +} + +export default Alert; diff --git a/web/react/src/app.jsx b/web/react/src/app.jsx index 727c8cd..b96074b 100644 --- a/web/react/src/app.jsx +++ b/web/react/src/app.jsx @@ -1,141 +1,38 @@ import React from "react"; +import { + BrowserRouter as Router, + Switch, + Route, + NavLink + } from "react-router-dom"; + +import Home from './home'; +import Messages from './messages'; function App() { - const [alerts, setAlerts] = React.useState([]); - const ws = React.useRef(null); - const timeout = React.useRef(null); - - const sendMessage = (message) => { - if (ws.current.readyState != 1) { - return false; - } - - let msg = JSON.stringify({ - text: message, - source: "front", - time: Date.now() - }); - - ws.current.send(msg); - addAlert(msg); - - return true; - } - - const addAlert = (msg) => { - let item = JSON.parse(msg); - setAlerts([item, ...alerts.slice(0, 2)]); - } - - // Sets up a WebSocket connection - React.useEffect(() => { - ws.current = new WebSocket("ws://localhost:8080/ws"); - ws.current.onopen = () => { - console.log('Connected'); - // ws.current.send("Hello from the client!"); - } - ws.current.onclose = () => console.log('Disconnected'); - ws.current.onerror = () => console.log('Websocket error') - - return () => { - ws.current.close(); - } - }, []); - - // Sets a received message handler, only once. - React.useEffect(() => { - if (!ws.current) return; - - ws.current.onmessage = e => { - const msg = e.data; - addAlert(msg); - } - }, [alerts]); - - // Clears out alerts with a delay, - // resets after each message - React.useEffect(() => { - clearTimeout(timeout.current); - - timeout.current = setTimeout(() => {setAlerts([])}, 20000); - - return () => { - clearTimeout(timeout.current); - } - }, [alerts]); - return ( -
-
-
-
- -
+ +
-
-
-
- {alerts.map((alert) => ( - - ))} -
-
-
-
- ) -} - -function Alert({alert}) { - const alertType = alert.source == "back" ? "success" : "primary"; - const alertLabel = alert.source == "back" ? "Received" : "Sent"; - - return ( -
- {alertLabel}: {alert.text} -
- ); -} - -function Form({sendMessage}) { - const [message, setMessage] = React.useState(''); - - const submitMessage = (e) => { - e.preventDefault(); - - if (!message) { - return false; - } - - if (sendMessage(message)) { - setMessage(''); - } - } - - return ( - -
- setMessage(e.target.value)} - /> - -
- + + + + + + + + + + ) } diff --git a/web/react/src/form.jsx b/web/react/src/form.jsx new file mode 100644 index 0000000..23fd3d0 --- /dev/null +++ b/web/react/src/form.jsx @@ -0,0 +1,42 @@ +import React from "react"; + +function Form({sendMessage}) { + const [message, setMessage] = React.useState(''); + + const submitMessage = (e) => { + e.preventDefault(); + + if (!message) { + return false; + } + + if (sendMessage(message)) { + setMessage(''); + } + } + + return ( +
+
+ setMessage(e.target.value)} + /> + +
+
+ ) +} + +export default Form; diff --git a/web/react/src/home.jsx b/web/react/src/home.jsx new file mode 100644 index 0000000..cfa3b7e --- /dev/null +++ b/web/react/src/home.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import Form from './form'; +import Alert from './alert'; + +function Home() { + const [alerts, setAlerts] = React.useState([]); + const ws = React.useRef(null); + const timeout = React.useRef(null); + + const sendMessage = (message) => { + if (ws.current.readyState != 1) { + return false; + } + + let msg = JSON.stringify({ + text: message, + source: "front", + time: Date.now() + }); + + ws.current.send(msg); + addAlert(msg); + + return true; + } + + const addAlert = (msg) => { + let item = JSON.parse(msg); + setAlerts([item, ...alerts.slice(0, 2)]); + } + + // Sets up a WebSocket connection + React.useEffect(() => { + ws.current = new WebSocket("ws://" + window.location.host + "/ws"); + ws.current.onopen = () => { + console.log('Connected'); + // ws.current.send("Hello from the client!"); + } + ws.current.onclose = () => console.log('Disconnected'); + ws.current.onerror = () => console.log('Websocket error') + + return () => { + ws.current.close(); + } + }, []); + + // Sets a received message handler, only once. + React.useEffect(() => { + if (!ws.current) return; + + ws.current.onmessage = e => { + const msg = e.data; + addAlert(msg); + } + }, [alerts]); + + // Clears out alerts with a delay, + // resets after each message + React.useEffect(() => { + clearTimeout(timeout.current); + timeout.current = setTimeout(() => {setAlerts([])}, 20000); + + return () => { + clearTimeout(timeout.current); + } + }, [alerts]); + + return ( +
+
+
+
+
+
+
+
+
+
+
+ {alerts.map((alert) => ( + + ))} +
+
+
+
+ ) +} + +export default Home; \ No newline at end of file diff --git a/web/react/src/messages.jsx b/web/react/src/messages.jsx new file mode 100644 index 0000000..150e33f --- /dev/null +++ b/web/react/src/messages.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +function Messages() { + return ( +
+

Recent messages (3/67)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TimeMessageSource
Mar 3, 2021Hello there from the frontendFront end
Mar 3, 2021Hello there from the back endBack end
Mar 3, 2021This is good!Front end
+
+ ) +} + +export default Messages;