diff --git a/code/static/index.html b/code/static/index.html
index 786fcb4..060f94d 100644
--- a/code/static/index.html
+++ b/code/static/index.html
@@ -4,7 +4,6 @@
Go Chat Application
-
diff --git a/demo/hub.go b/demo/hub.go
new file mode 100644
index 0000000..0e770ef
--- /dev/null
+++ b/demo/hub.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+var usernames = make(map[*websocket.Conn]string)
+var nameToConn = make(map[string]*websocket.Conn)
+
+func handleIncomingMessage(sender *websocket.Conn, msg string) {
+ if _, ok := usernames[sender]; ok {
+ sendChatMessage(sender, msg)
+ return
+ }
+
+ // Registering a new user
+ username := strings.TrimSpace(msg)
+ if username == "" || username == "server" {
+ sender.WriteJSON(newError("You have an illegal username."))
+ return
+ }
+
+ if _, ok := nameToConn[username]; ok {
+ sender.WriteJSON(newError("The specified username is already taken."))
+ return
+ }
+
+ sendUserList(sender)
+
+ usernames[sender] = username
+ nameToConn[username] = sender
+
+ m := newMessage(msgJoin, "server", username)
+ m.dispatch()
+}
+
+func handleDisconnection(sender *websocket.Conn) {
+ username, ok := usernames[sender]
+ if !ok {
+ return
+ }
+
+ m := newMessage(msgLeave, "server", username)
+ m.dispatch()
+ delete(usernames, sender)
+ delete(nameToConn, username)
+}
+
+type messageType string
+
+const (
+ msgChat messageType = "message"
+ msgJoin messageType = "join"
+ msgLeave messageType = "leave"
+ msgErr messageType = "error"
+ msgUserList messageType = "users"
+)
+
+type message struct {
+ Type messageType `json:"type"`
+ Sender string `json:"sender"`
+ Content interface{} `json:"content"`
+ Date time.Time `json:"date"`
+ Success bool `json:"success"`
+}
+
+func newError(content string) message {
+ return message{
+ Type: msgErr,
+ Sender: "",
+ Content: content,
+ Date: time.Now().UTC(),
+ Success: false,
+ }
+}
+
+func newMessage(msgType messageType, sender string, content string) message {
+ return message{
+ Type: msgType,
+ Sender: sender,
+ Content: content,
+ Date: time.Now().UTC(),
+ Success: true,
+ }
+}
+
+func (m message) dispatch() {
+ for client := range usernames {
+ _ = client.WriteJSON(m)
+ }
+}
+
+func sendUserList(who *websocket.Conn) {
+ list := []string{}
+ for _, username := range usernames {
+ list = append(list, username)
+ }
+
+ m := message{
+ Type: msgUserList,
+ Sender: "",
+ Content: list,
+ Date: time.Now().UTC(),
+ Success: true,
+ }
+
+ _ = who.WriteJSON(m)
+}
+
+func sendChatMessage(sender *websocket.Conn, msg string) {
+ m := newMessage(msgChat, usernames[sender], msg)
+ m.dispatch()
+}
diff --git a/demo/main.go b/demo/main.go
new file mode 100644
index 0000000..3bce483
--- /dev/null
+++ b/demo/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/websocket"
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+func websocketConnection(w http.ResponseWriter, r *http.Request) {
+ ws, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ _, _ = fmt.Fprint(w, "You must use the web socket protocol to connect to this endpoint. ", err)
+ return
+ }
+
+ defer ws.Close()
+
+ for {
+ _, p, err := ws.ReadMessage()
+ if err != nil {
+ handleDisconnection(ws)
+ break
+ }
+
+ msg := string(p)
+ handleIncomingMessage(ws, msg)
+ }
+}
+
+const port = 8080
+
+func main() {
+ http.HandleFunc("/websocket", websocketConnection)
+ http.Handle("/", http.FileServer(http.Dir("./static")))
+
+ fmt.Println("Chat server opening on port", port)
+ err := http.ListenAndServe(":"+strconv.Itoa(port), nil)
+ if err != nil {
+ _ = fmt.Errorf("Server could not start up: %s", err.Error())
+ }
+}
diff --git a/demo/static/css/style.css b/demo/static/css/style.css
new file mode 100644
index 0000000..9354941
--- /dev/null
+++ b/demo/static/css/style.css
@@ -0,0 +1,253 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;0,900;1,400&display=swap');
+
+body {
+ width: 100vw;
+ height: 100vh;
+ padding: 0;
+ margin: 0;
+ font-family: 'Roboto', sans-serif;
+}
+
+#snackbar {
+ visibility: hidden;
+ min-width: 250px;
+ margin-left: -125px;
+ background-color: #4D5359;
+ color: #BBC4FF;
+ text-align: center;
+ border-radius: 4px;
+ padding: 16px;
+ position: fixed;
+ z-index: 2;
+ left: 50%;
+ bottom: 30px;
+ font-size: 15px;
+}
+
+#snackbar.snackshow {
+ visibility: visible;
+ -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
+ animation: fadein 0.5s, fadeout 0.5s 2.5s;
+}
+
+.container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.lhs {
+ width: calc(100% - 300px);
+ display: flex;
+ flex-direction: column-reverse;
+ background-color: #39363F;
+}
+
+.rhs {
+ width: calc(300px - 24px);
+ background-color: #28272D;
+ padding: 12px;
+ color: #eeeeee;
+}
+
+.messages {
+ overflow: auto;
+ max-height: calc(100vh - 128px);
+ flex-grow: 1;
+}
+
+.messages::-webkit-scrollbar {
+ width: 8px;
+}
+
+.messages::-webkit-scrollbar-track {
+ background: #322F37;
+ border-radius: 12px;
+}
+
+.messages::-webkit-scrollbar-thumb {
+ background: #504D56;
+ border-radius: 12px;
+}
+
+.messages::-webkit-scrollbar-thumb:hover {
+ background: #5C5766;
+}
+
+
+/* */
+.chat {
+ height: 64px;
+ padding: 32px;
+}
+
+.chat-inner {
+ height:64px;
+ display: flex;
+ align-items: center;
+}
+
+#chatI {
+ height: 64px;
+ width: 100%;
+ padding: 0 12px;
+ font-size: 20px;
+ color: #eeeeee;
+ border: none;
+ border-radius: 6px 0 0 6px;
+ background: #28272D;
+}
+
+#chatI:focus {
+ outline: none;
+}
+
+#chatI::placeholder {
+ color: #908D9B;
+}
+
+#chatB {
+ height: 64px;
+ padding: 0;
+ width: 128px;
+ font-size: 24px;
+ color: #000;
+ border: none;
+ border-radiuS: 0 6px 6px 0;
+ background: #8EFFD6;
+}
+
+i {
+ font-size: 22px;
+ transition: color 0.25s;
+ color: #AAB3FF;
+}
+
+i:hover {
+ color: #EAF0CE;
+}
+
+/* Chat Messages */
+.message {
+ margin: 32px 32px;
+}
+
+.server-message {
+ color: #908D9B;
+ font-style: italic;
+ font-size: 21px;
+}
+
+.author {
+ color: #fcfcfc;
+ font-weight: 700;
+ display: inline-block;
+ margin-right: 8px;
+}
+
+.author.self {
+ color: #8EFFD6;
+}
+
+.timestamp {
+ display: inline-block;
+ font-weight: 400;
+ font-size: 14px;
+ color: #7D7A87;
+}
+
+.content {
+ color: #eee;
+ margin-top: 2px;
+}
+
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1;
+ padding-top: 100px;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #00000088;
+ animation-name: animatetop;
+ animation-duration: 0.6s;
+}
+
+.modal-content {
+ color: #eeeeee;
+ position: relative;
+ background-color: #39363F;
+ margin: auto;
+ padding: 18px;
+ padding-bottom: 36px;
+ border-radius: 4px;
+ width: 60%;
+ -webkit-animation-name: animatetop;
+ -webkit-animation-duration: 0.6s;
+}
+
+.modal-content h1 {
+ padding: 18px 0;
+}
+
+#nickI {
+ height: 48px;
+ padding: 0 12px;
+ font-size: 16px;
+ color: #eeeeee;
+ border: none;
+ border-radius: 6px;
+ background: #28272D;
+}
+
+#nickI:focus {
+ outline: none;
+}
+
+#nickI::placeholder {
+ color: #908D9B;
+}
+
+#nickB {
+ height: 48px;
+ padding: 0;
+ width: 96px;
+ font-size: 18px;
+ color: #000;
+ border: none;
+ border-radiuS: 6px;
+ background: #8EFFD6;
+}
+@-webkit-keyframes animatetop {
+ from {top:-300px; opacity:0}
+ to {top:0; opacity:1}
+}
+
+@keyframes animatetop {
+ from {top:-300px; opacity:0}
+ to {top:0; opacity:1}
+}
+
+@-webkit-keyframes fadein {
+ from {bottom: 0; opacity: 0;}
+ to {bottom: 30px; opacity: 1;}
+}
+
+@keyframes fadein {
+ from {bottom: 0; opacity: 0;}
+ to {bottom: 30px; opacity: 1;}
+}
+
+@-webkit-keyframes fadeout {
+ from {bottom: 30px; opacity: 1;}
+ to {bottom: 0; opacity: 0;}
+}
+
+@keyframes fadeout {
+ from {bottom: 30px; opacity: 1;}
+ to {bottom: 0; opacity: 0;}
+}
diff --git a/demo/static/index.html b/demo/static/index.html
new file mode 100644
index 0000000..060f94d
--- /dev/null
+++ b/demo/static/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+ Go Chat Application
+
+
+
+
+
+
+
Enter a username:
+
+
+
+
+
+
+
+
diff --git a/demo/static/js/index.js b/demo/static/js/index.js
new file mode 100644
index 0000000..2da2748
--- /dev/null
+++ b/demo/static/js/index.js
@@ -0,0 +1,196 @@
+//Saved nickname from previous usage
+let nickname = window.localStorage.getItem("nickname");
+
+//Nickname button and input
+let nickI = document.getElementById("nickI");
+let nickB = document.getElementById("nickB");
+
+//Chatting button and input
+let chatI = document.getElementById("chatI");
+let chatB = document.getElementById("chatB");
+
+let registered = false;
+let modal = document.getElementById("modal");
+
+if (nickname != null) {
+ nickI.value = nickname;
+}
+
+//Gets websocket url by replacing http with ws and https with wss, and appending /websocket to the host
+let protocol = location.protocol == "http:" ? "ws:" : "wss:";
+let url = protocol + "//" + location.host + "/websocket";
+
+let ws = new WebSocket(url);
+
+ws.onopen = function() {
+ modal.style.display = "block";
+ nickI.focus();
+}
+
+ws.onmessage = function(info) {
+ let data = JSON.parse(info.data);
+ if(!data.success) {
+ toast("Error: " + data.content);
+ return;
+ }
+
+ // Not Registered + Non error message means we have a new message!
+ if (!registered) {
+ registered = true;
+ window.localStorage.setItem("nickname", nickname);
+ modal.style.display = "none";
+ chatI.focus()
+ let users = data.content;
+ for(let i = 0; i < users.length; i++) {
+ userJoin(users[i]);
+ }
+ return;
+ }
+
+ newMessage(data);
+}
+
+/*
+ * Message receive handling
+ */
+
+function newMessage(data) {
+ switch(data.type) {
+ case "join":
+ userJoin(data.content, data.date);
+ break;
+ case "leave":
+ userLeave(data.content, data.date);
+ break;
+ case "message":
+ chatMessage(data.sender, data.content, data.date)
+ break;
+ }
+}
+
+/**
+ * Join/leave events
+ */
+function userJoin(user, date) {
+ let p = document.createElement("p");
+ p.className = user;
+ p.innerText = user;
+ document.getElementById("users").appendChild(p);
+ serverMessage(data.content + " has joined the server", data.date);
+}
+
+function userLeave(user, date) {
+ users = document.getElementById("users");
+ users.removeChild(users.getElementsByClassName(user)[0]);
+ serverMessage(user + " has left the server", date);
+}
+
+function chatMessage(sender, content, date) {
+ let msg = create("div", "message");
+
+ aClasses = "author"
+ if (sender === nickname) {
+ aClasses += " self"
+ }
+ let author = create("div", aClasses);
+ author.innerText = sender;
+
+ let timestamp = create("div", "timestamp");
+ timestamp.innerText = dateToString(new Date(date));
+
+ let text = create("div", "content");
+ text.innerText = content;
+
+ msg.append(author, timestamp, text);
+ insertMessage(msg);
+}
+
+function serverMessage(content, date) {
+ let msg = create("div", "message");
+
+ let text = create("div", "server-message");
+ text.innerText = content;
+
+ msg.append(text);
+ insertMessage(msg);
+}
+
+function insertMessage(div) {
+ //If message scrolling at the bottom, move to bottom again to update
+ let scroller = document.getElementsByClassName("messages")[0];
+ let currentScroll = scroller.scrollTop;
+ let maxScroll = scroller.scrollHeight - scroller.clientHeight;
+
+ document.getElementById("messages").append(div);
+
+ if (currentScroll === maxScroll) {
+ div.scrollIntoView();
+ }
+}
+
+/*
+ * Registration
+ */
+function register() {
+ let nick = nickI.value;
+ if (nick.trim().length == 0) return;
+ nickname = nick;
+ ws.send(nick);
+}
+
+nickI.onkeydown = function(ev) {
+ if(ev.key !== "Enter") {
+ return;
+ }
+ register();
+}
+
+nickB.onclick = function() {
+ register();
+}
+
+/**
+ * Message sending
+ */
+
+chatI.onkeydown = function(ev) {
+ if(ev.key !== "Enter") {
+ return;
+ }
+ if(chatI.value.trim().length == 0) return;
+
+ ws.send(chatI.value);
+ chatI.value = "";
+}
+chatB.onclick = function() {
+ if(chatI.value.trim().length == 0) return;
+
+ ws.send(chatI.value);
+ chatI.value = "";
+}
+
+/*
+ * Util functions
+ */
+function toast(message) {
+ var x = document.getElementById("snackbar");
+ x.innerText = message;
+ x.className = "snackshow";
+ setTimeout(function(){ x.className = x.className.replace("snackshow", ""); }, 3000);
+}
+
+let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+function dateToString(date) {
+ let year = date.getFullYear();
+ let month = months[date.getMonth()];
+ let day = date.getDate();
+ let hour = date.getHours();
+ let minute = date.getMinutes();
+ return ` ${day} ${month} ${year} at ${hour}:${minute}`;
+}
+
+function create(name, classes) {
+ let x = document.createElement(name);
+ x.className = classes;
+ return x;
+}
diff --git a/demo/static/verify.txt b/demo/static/verify.txt
new file mode 100644
index 0000000..a042389
--- /dev/null
+++ b/demo/static/verify.txt
@@ -0,0 +1 @@
+hello world!