summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--BossDeer.html471
-rw-r--r--index.html16
-rw-r--r--lib/htmlbuilder.js46
-rw-r--r--lib/key.js16
-rw-r--r--lib/sd.js61
-rw-r--r--lib/stores.js12
-rw-r--r--main.js24
-rw-r--r--pages.js13
-rw-r--r--pages/login/page.html11
-rw-r--r--pages/login/page.js18
-rw-r--r--pages/main/page.html7
-rw-r--r--pages/main/page.js74
-rw-r--r--pages/test/page.html1
-rw-r--r--pages/test/page.js3
-rw-r--r--shitstylse.css61
15 files changed, 834 insertions, 0 deletions
diff --git a/BossDeer.html b/BossDeer.html
new file mode 100644
index 0000000..e8b3a95
--- /dev/null
+++ b/BossDeer.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<html lang="en-US"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+        <title>BossDeer</title>
+        <meta name="apple-mobile-web-app-capable" content="yes">
+        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+        <meta charset="UTF-8">
+        <style>
+            @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
+            body {
+                font-family: 'Noto Sans', sans-serif;
+                margin: 0px;
+                background-color: #2e2d2b;
+                color: #ede4d5;
+            }
+            button {
+                background-color: #4d4a45;
+                color: #ede4d5;
+                border: none;
+                border-radius: 4px;
+                padding: 8px;
+                font-family: 'Noto Sans', sans-serif;
+                margin-bottom: 4px;
+            }
+            button:hover {
+                background-color: #403d39;
+                cursor: pointer;
+            }
+            input {
+                background-color: #403d39;
+                color: #ede4d5;
+                border: none;
+                border-radius: 4px;
+                padding: 8px;
+                font-family: 'Noto Sans', sans-serif;
+                margin-bottom: 4px;
+            }
+            .scene {
+                margin: 8px;
+            }
+            #error-bar {
+                background-color: #f2b149;
+                color: #2e2d2b;
+                width: 100%;
+                padding: 4px;
+                box-sizing: border-box;
+            }
+            .text-clickable {
+                text-decoration: underline;
+                cursor: pointer;
+            }
+            .hidden {
+                display: none;
+            }
+            .post {
+                background-color: #403d39;
+                padding: 6px;
+                border-radius: 4px;
+                margin-bottom: 8px;
+            }
+            .mono {
+                font-family: monospace;
+            }
+            #ms-msg {
+                width:70vw;
+                box-sizing: border-box;
+            }
+            #ms-button-post {
+                box-sizing: border-box;
+            }
+            .pfp {
+                border-radius: 100%;
+                width: 36px;
+                height: 36px;
+                float: left;
+                margin-right: 8px;
+                margin-top: 2px;
+                border: 2px #383531 solid;
+            }
+            a {
+                color: #ede4d5;
+            }
+            .attachment {
+                margin-left:4px;
+                margin-right:4px;
+                max-height:15vw;
+            }
+            .rl-guidelines {
+                background-color: #999085;
+                color: black;
+                border: none;
+                border-radius: 4px;
+                padding: 8px;
+                font-family: serif;
+                margin: 4px;
+                width: 90vw;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="error-bar" class="hidden"><span onclick="closePopup();" class="text-clickable">Close</span> - <span id="error-text">Connecting...<span></span></span></div>
+        <div class="scene">
+        <div id="loading" class="hidden"><center>Taking too long to load? Try <span onclick="logOut();" class="text-clickable">a full reset</span>.</center></div>
+        <div id="connection-lost" class="hidden"><center>Connection was lost.<br><span onclick="window.location.reload();" class="text-clickable">Reload</span>.</center></div>
+        <div id="register-login" class=""><center>
+            <input id="rl-username" placeholder="Username..." maxlength="20"><br>
+            <input id="rl-password" placeholder="Password..." type="password"><br>
+            <input id="rl-invitecode" placeholder="Invite code..." maxlength="16"><br>
+            <button onclick="logIn();">Log in</button> 
+            <button onclick="register();">Register</button><br><br>
+            <small>(You only need to provide an invite code when registering.)<br>(Please ensure you have read the <a href="https://deer.meltland.dev/GUIDELINES.txt">GUIDELINES</a> (also provided below) before creating an account.)</small><br>
+             <embed class="rl-guidelines" type="text/plain" src="BossDeer_files/GUIDELINES.txt"><br> 
+            <small id="rl-version">1.0.5b - SOKTDEER-2024.11.30-02.07</small>
+        </center></div>
+        <div id="main-scene" class="hidden">
+            <button id="ms-name" disabled="disabled">@...</button> | <button onclick="switchScene('main-config');">Settings</button> <button onclick="alert('Not done yet!');">Inbox</button> <button id="ms-button-mod" class="hidden" onclick="switchScene('main-moderation');">Moderation</button><br>
+            <small id="ms-ulist">0 users online ()</small><br><br><center>
+            <button onclick="addAttachment();">+</button> <input id="ms-msg" maxlength="2000" onkeydown="if (event.keyCode == 13) {sendPost();}" placeholder="What's on your mind?"> <button id="ms-button-post" onclick="sendPost();">Post</button><br><small id="ms-details"></small></center><br>
+            <div id="ms-posts"><div class="post"><span><b>SoktDeer</b> (<span class="mono">@soktdeer</span>)</span><br><small>11/30/2024, 9:15:07 AM - <span class="text-clickable" onclick="reply(0);">Reply</span></small><br><span>Welcome to SoktDeer! Start chatting!</span></div></div>
+        </div>
+        <div id="main-config" class="hidden">
+            <button onclick="switchScene('main-scene');">Return to Home</button><br>
+            <h2>Profile</h2>
+            <input id="mc-display-name" placeholder="Display name..." maxlength="20"> <button onclick="setDisplayName();">Set display name</button><br>
+            <input id="mc-avatar" placeholder="Avatar URL..." maxlength="656"> <button onclick="setAvatar();">Set avatar URL</button>
+            <h2>Misc</h2>
+            <button onclick="updateStg('moderation')">Toggle Moderation tab</button><br><br>
+            <button onclick="logOut();">Log out</button><br>
+            <small id="mc-version">1.0.5b - SOKTDEER-2024.11.30-02.07</small>
+        </div>
+        <div id="main-moderation" class="hidden">
+            <button onclick="switchScene('main-scene');">Return to Home</button><br>
+            <h2>Ban</h2>
+            <input id="mm-username-ban" placeholder="Username..."><br>
+            <input id="mm-until-ban" type="datetime-local"><br>
+            <input id="mm-reason-ban" placeholder="Reason..."><br>
+            <button onclick="ban();">Ban User</button>
+            <h2>Invite code</h2>
+            <span id="mm-invite-code"></span><br>
+            <button onclick="genInviteCode();">Generate invite code</button><br>
+            <button onclick="resetInvites();">Reset invite codes</button>
+        </div>
+        </div>
+    
+    <script>
+// hello guys gals and gays!
+// keep in mind that unlike rome, this was infact built in a day
+// so please dont judge so much!
+
+// let us commence and define the lame shit here
+
+document.getElementById("rl-username").value = "";
+document.getElementById("rl-password").value = "";
+document.getElementById("rl-invitecode").value = "";
+
+function displayError (errText) {
+    document.getElementById("error-text").innerText = errText;
+    document.getElementById("error-bar").classList.remove("hidden");
+};
+
+function closePopup () {
+    document.getElementById("error-bar").classList.add("hidden");
+};
+
+const version = "1.0.5b";
+const serverVersion = "SOKTDEER-2024.11.30-02.07";
+let last_cmd = "";
+let username = "";
+let logged_in = false;
+let scene = "loading";
+let ulist = [];
+let posts = [];
+let replies = [];
+let attachments = [];
+
+if (localStorage.getItem("settings") == null) {
+    localStorage.setItem("settings", JSON.stringify({"moderation": false}))
+};
+
+let settings = JSON.parse(localStorage.getItem("settings"));
+
+function stgsTriggers() {
+    if (settings.moderation) {
+        document.getElementById("ms-button-mod").classList.remove("hidden");
+    } else {
+        document.getElementById("ms-button-mod").classList.add("hidden");
+    };
+};
+
+function updateStg(setting) {
+    if (setting == "moderation") {
+        if (settings.moderation) {
+            settings.moderation = false;
+        } else {
+            settings.moderation = true;
+        };
+    };
+    localStorage.setItem("settings", JSON.stringify(settings));
+    stgsTriggers();
+};
+
+stgsTriggers();
+
+// whatever, go my ws shit
+
+const ws = new WebSocket("wss://sokt.meltland.dev") // i hate const!! grr!! >:(
+
+ws.onmessage = function (event) {
+    let incoming = JSON.parse(event.data);
+    console.log(incoming);
+
+    if (incoming.command == "greet") {
+        closePopup();
+        document.getElementById("rl-version").innerText = `${version} - ${incoming.version}`;
+        document.getElementById("mc-version").innerText = `${version} - ${incoming.version}`;
+        if (incoming.version != serverVersion) {
+            displayError(`The server is on a different version than the client. Be wary of issues while using the client. (Expected "${serverVersion}", got "${incoming.version}")`);
+        };
+        ulist = incoming.ulist;
+        document.getElementById("ms-ulist").innerText = `${ulist.length} users online (${ulist.toString().replaceAll(",", ", ")})`;
+        posts = incoming.messages;
+        for (const i in incoming.messages) {
+            loadPost(incoming.messages[i], true);
+        };
+        if (localStorage.getItem("username") == null || localStorage.getItem("token") == null) {
+            scene = "register-login";
+            document.getElementById("loading").classList.toggle("hidden");
+            document.getElementById("register-login").classList.toggle("hidden")
+        } else {
+            username = localStorage.getItem("username");
+            last_cmd = "login_token";
+            ws.send(JSON.stringify({command: "login_token", username: username, token: localStorage.getItem("token")}))
+        };
+    } else if (incoming.command == "ulist") {
+        ulist = incoming.ulist;
+        document.getElementById("ms-ulist").innerText = `${ulist.length} users online (${ulist.toString().replaceAll(",", ", ")})`;
+    };
+    if ("error" in incoming) {
+        if (incoming.error) {
+            if (incoming.code == "banned") {
+                displayError(`Account is banned until ${new Date(incoming.banned_until * 1000).toLocaleString()} for "${incoming.ban_reason}"`)
+            } else {
+                displayError(`We hit an error. ("${incoming.code}" from ${incoming.form})`);
+            };
+        } else if (last_cmd == "login_token" || last_cmd == "login_pswd") {
+            if (scene == "register-login") {
+                document.getElementById("register-login").classList.toggle("hidden");
+            } else if (scene == "loading") {
+                document.getElementById("loading").classList.toggle("hidden");
+            };
+            scene = "main-scene";
+            document.getElementById("main-scene").classList.toggle("hidden");
+            document.getElementById("ms-name").innerText = `@${username}`
+        };
+    };
+    if ("token" in incoming && ["register","login_pswd"].includes(last_cmd)) {
+        localStorage.setItem("username", username);
+        localStorage.setItem("token", incoming.token);
+        if (last_cmd == "register") {
+            window.location.reload();
+        };
+        logged_in = true;
+    } else if (incoming.command == "new_post") {
+        loadPost(incoming.data, false);
+    } else if (last_cmd == "gen_invite" && "invite_code" in incoming) {
+        document.getElementById("mm-invite-code").innerText = `Your invite code is "${incoming.invite_code}". Use it on any SoktDeer client to sign up!\n\nCodes: ${incoming.invite_codes}`
+    };
+};
+
+ws.onclose = function (event) {
+    switchScene("connection-lost");
+};
+
+function switchScene (newScene) {
+    document.getElementById(scene).classList.toggle("hidden");
+    document.getElementById(newScene).classList.toggle("hidden");
+    scene = newScene;
+};
+
+function register() {
+    last_cmd = "register";
+    username = document.getElementById("rl-username").value;
+    ws.send(JSON.stringify({command: "register", username: username, password: document.getElementById("rl-password").value, invite_code: document.getElementById("rl-invitecode").value}))
+};
+
+function logIn() {
+    last_cmd = "login_pswd";
+    username = document.getElementById("rl-username").value;
+    ws.send(JSON.stringify({command: "login_pswd", username: username, password: document.getElementById("rl-password").value}))
+};
+
+function logOut() {
+    localStorage.clear();
+    window.location.reload();
+};
+
+function loadPost(resf, isFetch) {
+    console.log("Loading post " + resf.id)
+    var tsr = resf.created
+    var tsra = tsr * 1000
+    var tsrb = Math.trunc(tsra)
+    var ts = new Date();
+    ts.setTime(tsrb);
+    var sts = ts.toLocaleString();
+
+    var content = resf.content
+    for (const i in resf.replies) {
+        content = `→ ${resf.replies[i].author.display_name} (@${resf.replies[i].author.username}): ${resf.replies[i].content}\n${content}`
+    };
+
+    var post = document.createElement("div");
+    post.classList.add("post");
+
+    if (resf.author.avatar) {
+        var avatar = document.createElement("img");
+        avatar.src = resf.author.avatar;
+        avatar.classList.add("pfp");
+        post.appendChild(avatar);
+    };
+
+    var postUsername = document.createElement("span");
+    postUsername.innerHTML = `<b>${resf.author.display_name}</b> (<span class="mono">@${resf.author.username}</span>)`;
+    post.appendChild(postUsername);
+
+    var breaklineA = document.createElement("br");
+    post.appendChild(breaklineA);
+
+    var postDetails = document.createElement("small");
+    postDetails.innerHTML = `${sts} - <span class="text-clickable" onclick="reply(${resf.id});">Reply</span>`;
+    post.appendChild(postDetails);
+    
+    var breaklineB = document.createElement("br");
+    post.appendChild(breaklineB);
+
+    var postContent = document.createElement("span");
+    postContent.innerText = content;
+    post.appendChild(postContent);
+
+    if (resf.attachments.length != 0) {
+        var horline = document.createElement("hr");
+        post.appendChild(horline);
+        
+        var attachmentDetails = document.createElement("span");
+        for (const x in resf.attachments) {
+            attachmentDetails.innerHTML += `<a target="_blank" rel="noopener noreferrer" href="${resf.attachments[x]}">Attachment ${Number(x) + 1} (${resf.attachments[x]})</a><br>`
+        }
+        post.appendChild(attachmentDetails)
+
+        // i love making garbage code because im too lazy to learn how to make things properly
+
+        var attachmentA = document.createElement("img");
+        attachmentA.src = resf.attachments[0]
+        attachmentA.classList.add("attachment")
+        attachmentA.setAttribute("onerror", "this.remove();")
+        post.appendChild(attachmentA);
+        
+        if (resf.attachments.length >= 2) {
+            var attachmentB = document.createElement("img");
+            attachmentB.src = resf.attachments[1]
+            attachmentB.classList.add("attachment")
+            attachmentB.setAttribute("onerror", "this.remove();")
+            post.appendChild(attachmentB);
+        };
+
+        if (resf.attachments.length >= 3) {
+            var attachmentC = document.createElement("img");
+            attachmentC.src = resf.attachments[2]
+            attachmentC.classList.add("attachment")
+            attachmentC.setAttribute("onerror", "this.remove();")
+            post.appendChild(attachmentC);
+        };
+    };
+
+    if (isFetch) {
+        document.getElementById("ms-posts").appendChild(post);
+    } else {
+        document.getElementById("ms-posts").insertBefore(post, document.getElementById("ms-posts").firstChild);
+    }
+};
+
+function sendPost() {
+    last_cmd = "post";
+    ws.send(JSON.stringify({command: "post", content: document.getElementById("ms-msg").value, replies: replies, attachments: attachments}))
+    document.getElementById("ms-msg").value = "";
+    attachments = [];
+    replies = [];
+    updateDetailsMsg();
+};
+
+function ban() {
+    last_cmd = "post";
+    if (document.getElementById("mm-until-ban").value != "") {
+        var buntil = new Date(document.getElementById("mm-until-ban").value).getTime() / 1000
+    } else {
+        var buntil = 0
+    };
+    ws.send(JSON.stringify({command: "ban", username: document.getElementById("mm-username-ban").value, banned_until: buntil, ban_reason: document.getElementById("mm-reason-ban").value}))
+    document.getElementById("mm-username-ban").value = "";
+    document.getElementById("mm-until-ban").value = "";
+    document.getElementById("mm-reason-ban").value = "";
+};
+
+function genInviteCode() {
+    last_cmd = "gen_invite";
+    ws.send(JSON.stringify({command: "gen_invite"}))
+};
+
+function resetInvites() {
+    last_cmd = "reset_invites";
+    ws.send(JSON.stringify({command: "reset_invites"}))
+};
+
+function setDisplayName() {
+    last_cmd = "set_display_name";
+    ws.send(JSON.stringify({command: "set_display_name", display_name: document.getElementById("mc-display-name").value}))
+    document.getElementById("mc-display-name").value = "";
+};
+
+function setAvatar() {
+    last_cmd = "set_avatar";
+    ws.send(JSON.stringify({command: "set_avatar", avatar: document.getElementById("mc-avatar").value}))
+    document.getElementById("mc-avatar").value = "";
+};
+
+function updateDetailsMsg() {
+    if (replies.length == 0 && attachments.length == 0) {
+        document.getElementById("ms-details").innerText = ""
+    } else if (replies.length == 0) {
+        if (attachments.length == 1) {var plurals = ""} else {var plurals = "s"}
+        document.getElementById("ms-details").innerHTML = `${attachments.length} attachment${plurals} - <span class="text-clickable" onclick="clearAll();">Remove all</span>`
+    } else if (attachments.length == 0) {
+        if (replies.length == 1) {var plurals = "y"} else {var plurals = "ies"} 
+        document.getElementById("ms-details").innerHTML = `${replies.length} repl${plurals} - <span class="text-clickable" onclick="clearAll();">Remove all</span>`
+    } else {
+        if (replies.length == 1) {var plurals = "y"} else {var plurals = "ies"}
+        if (attachments.length == 1) {var plurals_b = ""} else {var plurals_b = "s"}
+        document.getElementById("ms-details").innerHTML = `${replies.length} repl${plurals}, ${attachments.length} attachment${plurals_b} - <span class="text-clickable" onclick="clearAll();">Remove all</span>`
+    };
+};
+
+function addAttachment() {
+    var ata = window.prompt("Add an attachment", "Put a whitelisted URL here...")
+    if (![null,""].includes(ata)) {
+        if (attachments.length != 3) {
+            attachments.push(ata);
+        };
+    };
+    updateDetailsMsg();
+};
+
+function reply(id) {
+    if (replies.length != 3) {
+        replies.push(id);
+    };
+    updateDetailsMsg();
+};
+
+function clearAll() {
+    replies = [];
+    attachments = [];
+    updateDetailsMsg();
+};
+
+function ping() {
+    ws.send(JSON.stringify({command: "ping"}))
+};
+
+setInterval(ping, 5000)
+    </script>
+
+</body></html>
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a094a56
--- /dev/null
+++ b/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>soktdeer wl client</title>
+    <!-- original name, ik -->
+    <script type="module" src="main.js"></script>
+    <link rel="stylesheet" href="shitstylse.css">
+</head>
+<body>
+    <div class="main">
+
+    </div>
+</body>
+</html>
diff --git a/lib/htmlbuilder.js b/lib/htmlbuilder.js
new file mode 100644
index 0000000..7343116
--- /dev/null
+++ b/lib/htmlbuilder.js
@@ -0,0 +1,46 @@
+function makeSigmaHtmlPlusElement(elem) {
+    elem.child = function (type) {
+        let childElem = makeSigmaHtmlPlusElement(document.createElement(type));
+        elem.appendChild(childElem)
+        return childElem;
+    }
+    elem.html = function (html) {
+        elem.innerHTML = html;
+        return elem;
+    }
+    elem.text = function (text) {
+        elem.innerText = text;
+        return elem;
+    }
+    elem.attr = function (attribute, value) {
+        elem.setAttribute(attribute, value);
+        return elem;
+    }
+    elem.class = function (className) {
+        elem.classList.add(className)
+        return elem;
+    }
+    elem.do = function (a, params) {
+        elem.a(...params)
+        return elem;
+    }
+    elem.ev = function (event, listener) {
+        elem.addEventListener(event, listener)
+        return elem;
+    }
+    elem.up = function () {
+        return elem.parentElement
+    }
+    elem.for = function (array, func) {
+        array.forEach(element => elem.appendChild(func(element)));
+        return elem
+    }
+    return elem;
+}
+
+/**
+ * @param {String} type HTML element type
+ */
+export default function make(type) {
+    return makeSigmaHtmlPlusElement(document.createElement(type));
+}
\ No newline at end of file
diff --git a/lib/key.js b/lib/key.js
new file mode 100644
index 0000000..7f7971a
--- /dev/null
+++ b/lib/key.js
@@ -0,0 +1,16 @@
+window.onkeydown = function (e) {
+	if (!e) e = window.event;
+	shiftHeld = e.shiftKey;
+};
+
+window.onkeyup = function (e) {
+	if (!e) e = window.event;
+	shiftHeld = e.shiftKey;
+};
+
+window.onmousemove = function (e) {
+	if (!e) e = window.event;
+	shiftHeld = e.shiftKey;
+};
+
+export let shiftHeld;
\ No newline at end of file
diff --git a/lib/sd.js b/lib/sd.js
new file mode 100644
index 0000000..69bd067
--- /dev/null
+++ b/lib/sd.js
@@ -0,0 +1,61 @@
+import "https://unpkg.com/eventemitter3@latest/dist/eventemitter3.umd.min.js"
+
+export default class SoktDeer {
+    /** @type {string} */
+    token = '';
+    /** @type {boolean} */
+    loggedIn = false;
+    /** @type {WebSocket} */
+    ws;
+    /** @type {import("node:events").EventEmitter} */
+    wsEvents = new EventEmitter3();
+
+    /** @type {any[]} */
+    messages = [];
+    constructor (wsUri="wss://sokt.meltland.dev") {
+        this.ws = new WebSocket(wsUri);
+        this.ws.onmessage = (rdata) => {
+            const data = JSON.parse(rdata.data.toString());
+            console.info("SD", "INCOMING", data)
+            if ('token' in data) return this.wsEvents.emit('token', data.token);
+            if ('command' in data) return this.wsEvents.emit(data.command, data);
+            if ('error' in data) return this.wsEvents.emit('error', data);
+        }
+        this.wsEvents.on('greet', greetp => {
+            this.messages = greetp.messages
+        })
+        this.wsEvents.on('new_post', ({data: post}) => {
+            this.messages.push(post)
+        })
+    }
+    login(username, password) {
+        return new Promise((resolve, reject) => {
+            this.ws.send(JSON.stringify({
+                command: "login_pswd",
+                username,
+                password
+            }))
+            this.wsEvents.once('token', token => {
+                this.token = token;
+                resolve(token)
+            })
+            this.wsEvents.once('error', error => {
+                if(error.error) {
+                    reject(error.code)
+                }
+            })
+        })
+    }
+    loginToken(token) {
+        return new Promise((resolve, reject) => {
+            this.ws.send(JSON.stringify({
+                command: "login_token",
+                token
+            }))
+            this.wsEvents.once('error', error => {
+                if(error.error) reject(error.code)
+                else resolve()
+            })
+        })
+    }
+}
\ No newline at end of file
diff --git a/lib/stores.js b/lib/stores.js
new file mode 100644
index 0000000..eacb5e2
--- /dev/null
+++ b/lib/stores.js
@@ -0,0 +1,12 @@
+export function set(store, value) {
+    window.stores[store] = value
+    Object.values(window.storesEvents).forEach(ev => {if(ev.store == store) cb()})
+}
+export function update(store) {
+    Object.values(window.storesEvents).forEach(ev => {if(ev.store == store) cb()})
+}
+export function onChange(store, cb) {
+    let id = nextStoresEventID++;
+    storesEvents[id] = {store, cb};
+    return id;
+}
\ No newline at end of file
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..e47e739
--- /dev/null
+++ b/main.js
@@ -0,0 +1,24 @@
+import * as pages from "./pages.js"
+import SoktDeer from "./lib/sd.js"
+
+window.pages = pages
+window.stores = {
+    sdlib: new SoktDeer(),
+    sendTokenToWlodekMsDMs: false,
+}
+window.storesEvents = {}
+let nextStoresEventID = 0
+window.stores.set = function (store, value) {
+    window.stores[store] = value
+    Object.values(window.storesEvents).forEach(ev => {if(ev.store == store) {ev.cb()}})
+}
+window.stores.update = function (store) {
+    Object.values(window.storesEvents).forEach(ev => {if(ev.store == store) {ev.cb()}})
+}
+window.stores.onChange = function (store, cb) {
+    let id = nextStoresEventID++;
+    window.storesEvents[id] = {store, cb};
+    return id;
+}
+window.sd = window.stores.sdlib
+pages.goToPage('login')
\ No newline at end of file
diff --git a/pages.js b/pages.js
new file mode 100644
index 0000000..1d48894
--- /dev/null
+++ b/pages.js
@@ -0,0 +1,13 @@
+export let page = ""
+
+let currPageData = null
+
+export async function goToPage(pageA) {
+    if(currPageData?.onunload) currPageData.onunload();
+    page = pageA;
+    let pageHTML = await (await fetch("./pages/"+pageA+"/page.html")).text();
+    document.querySelector(".main").innerHTML = pageHTML
+    let pageData = await import("./pages/"+pageA+"/page.js");
+    currPageData = pageData;
+    if(pageData?.onload) pageData.onload();
+}
\ No newline at end of file
diff --git a/pages/login/page.html b/pages/login/page.html
new file mode 100644
index 0000000..5fa188e
--- /dev/null
+++ b/pages/login/page.html
@@ -0,0 +1,11 @@
+<form id="loginForm">
+    <label for="username">Username:</label>
+    <input type="text" name="username" id="username">
+    <br>
+    <label for="password">Password:</label>
+    <input type="password" name="password" id="password">
+    <br>
+    <input type="submit" value="Log in">
+    <br>
+    <span id="error" style="color: red;"></span>
+</form>
\ No newline at end of file
diff --git a/pages/login/page.js b/pages/login/page.js
new file mode 100644
index 0000000..f0c5994
--- /dev/null
+++ b/pages/login/page.js
@@ -0,0 +1,18 @@
+async function fetchJSON(url, opts) {
+    let resp = await fetch(url, opts);
+    return await resp.json()
+}
+
+export function onload() {
+    document.getElementById("loginForm").addEventListener("submit", async function (ev) {
+        ev.preventDefault();
+        const username = document.getElementById('username').value;
+        const password = document.getElementById('password').value;
+        try {
+            await stores.sdlib.login(username, password)
+        } catch (error) {
+            return document.getElementById('error').innerText = 'An error occured\n' + error
+        }
+        pages.goToPage("main")
+    })
+}
\ No newline at end of file
diff --git a/pages/main/page.html b/pages/main/page.html
new file mode 100644
index 0000000..8b16e26
--- /dev/null
+++ b/pages/main/page.html
@@ -0,0 +1,7 @@
+<div class="channel">
+    <div class="messages" id="messages"></div>
+    <div class="messageInput disabled" id="messageForm">
+        <textarea name="message input" id="messageInput"></textarea>
+        <button id="send">Send</button>
+    </div>
+</div>
\ No newline at end of file
diff --git a/pages/main/page.js b/pages/main/page.js
new file mode 100644
index 0000000..c337888
--- /dev/null
+++ b/pages/main/page.js
@@ -0,0 +1,74 @@
+import { shiftHeld } from "../../lib/key.js"
+import html from "../../lib/htmlbuilder.js"
+
+async function fetchJSON(url, opts) {
+    let resp = await fetch(url, opts);
+    return await resp.json()
+}
+
+function scrollToBottomOfElement(element) {
+    element.scrollTo(0, element.scrollHeight);
+}
+
+export function onload() {
+    const msgArea = document.getElementById("messages");
+
+    function createMessage(msg) {
+        let elem = html('div')
+            .class('message')
+            .child('div')
+                .class('message-header')
+                .child('span')
+                    .class('username')
+                    .text(msg.author.display_name ? `${msg.author.display_name} (${msg.author.username})` : msg.author.username)
+                    .up()
+                .child('div')
+                    .class('action-buttons')
+                    .child('button').text('reply').up()
+                    .up()
+                .up()
+            .child('span')
+                .class('post-content')
+                .text(msg?.content)
+            .up()
+        msgArea.appendChild(elem)
+    }
+    document.getElementById("messageForm").classList.remove('disabled')
+    msgArea.innerHTML = "";
+    // :+1:
+
+    for (const msg of window.stores.sdlib.messages.reverse()) {
+        createMessage(msg)
+    }
+    scrollToBottomOfElement(msgArea.parentElement);
+
+    stores.sdlib.wsEvents.on("new_post", post => {
+        console.debug('posting of the poster', post)
+		let scrolledToBottom = msgArea.parentElement.scrollTopMax == msgArea.parentElement.scrollTop;
+        createMessage(post.data)
+		if(scrolledToBottom) scrollToBottomOfElement(msgArea.parentElement);
+    })
+
+    const submitBtn = document.getElementById("send")
+
+    submitBtn.onclick = function (event) { // using on(event) = ... instead of addEventListener("(event)", ...) because i cant be bothered to clear the event on channel change lol
+        let msg = document.getElementById("messageInput").value;
+        stores.sdlib.ws.send(JSON.stringify({
+            command: "post",
+            content: msg,
+            replies: [],
+            attachments: []
+        }))
+        document.getElementById("messageInput").value = ""
+    }
+
+    document.getElementById('messageInput').addEventListener('keydown', event => {
+        if (
+            event.key == "Enter" &&
+            !shiftHeld
+        ) {
+            event.preventDefault();
+            if (!submitBtn.disabled) submitBtn.click();
+        }
+    })
+}
\ No newline at end of file
diff --git a/pages/test/page.html b/pages/test/page.html
new file mode 100644
index 0000000..7f8bb38
--- /dev/null
+++ b/pages/test/page.html
@@ -0,0 +1 @@
+testing page (tm)
\ No newline at end of file
diff --git a/pages/test/page.js b/pages/test/page.js
new file mode 100644
index 0000000..f98f2fd
--- /dev/null
+++ b/pages/test/page.js
@@ -0,0 +1,3 @@
+export function onload() {
+    console.log("hi from onload")
+}
\ No newline at end of file
diff --git a/shitstylse.css b/shitstylse.css
new file mode 100644
index 0000000..6682846
--- /dev/null
+++ b/shitstylse.css
@@ -0,0 +1,61 @@
+/* i have no idea what im doing so */
+body {
+    /* why */
+    margin: 0;
+	height: 100vh;
+}
+
+.main {
+	height: 100%;
+	width: 100%;
+	line-break: anywhere;
+	word-break: break-all;
+}
+
+.channel {
+	flex-grow: 1;
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	overflow: auto;
+}
+
+.messages {
+	flex-grow: 1;
+}
+
+.messageInput {
+	background: white;
+	position: sticky;
+	bottom: 0;
+	display: flex;
+}
+
+#messageInput {
+	flex-grow: 1;
+}
+
+.messageInput.disabled {
+    display: none;
+}
+
+.message-header {
+	display: flex;
+	justify-content: space-between;
+}
+
+.action-buttons {
+	display: none;
+}
+
+.message:hover {
+	background: #0004;
+}
+
+.message:hover .action-buttons {
+	display: block;
+}
+
+.message-header .username {
+	font-size: 1.3em;
+}