diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb7231f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Hatch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9a4a456..4afe07a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,10 @@ this is the repository for the backend and frontend of the hatch forums. ## routes - GET `/`: forums home page. -- POST `api/new/topic`: route create a new topic. user token should be in the `Token` header. body should contain `title`, `content` and `category` (as an ID) attributes. -- POST `api/new/post`: route create a new topic. user token should be in the `Token` header. body should contain `content` and `topic` (as an ID) attributes. +- POST `api/new/topic`: route to create a new topic. user token should be in the `Token` header. body should contain `title`, `content` and `category` (as an ID) attributes. +- POST `api/new/post`: route to create a new post. user token should be in the `Token` header. body should contain `content` and `topic` (as an ID) attributes. +- POST `api/new/reaction`: route to create a new reaction. user token should be in the `Token` header. body should contain the `reaction` (as an integer ranging from 1 to 5) and `post` (as an ID) attributes. +- POST `api/pin/topic`: route to pin a topic. user token should be in the `Token` header. body should contain `id` (topic ID) attributes. - GET `/category/`: category page. `` is a unique ID applied to each forum category. - GET `/topic/`: topic page. `` is a unique ID applied to each topic in the forums. diff --git a/index.js b/index.js index afa5ea8..2d58f2b 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +const version = "2.0.0"; + const express = require("express"); const path = require("path"); const sqlite = require("sqlite3"); @@ -5,9 +7,9 @@ const bodyParser = require("body-parser"); const app = express(); -const port = parseInt(process.env.PORT) || process.argv[3] || 3000; +const port = parseInt(process.env.PORT) || process.argv[3] || 8000; -const db = new sqlite.Database('./db.db', (err) => { +const db = new sqlite.Database("./db.db", (err) => { if (err) { console.error(err.message); } @@ -31,7 +33,7 @@ db.run(`CREATE TABLE IF NOT EXISTS topics ( name TEXT NOT NULL, author TEXT NOT NULL, category INTEGER NOT NULL, - pinned BOOLEAN + pinned BOOLEAN NOT NULL )`, (err) => { if (err) { console.error(err.message); @@ -42,7 +44,18 @@ db.run(`CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, content TEXT, - topic INTEGER NOT NULL + topic INTEGER NOT NULL, + timestamp INTEGER NOT NULL +)`, (err) => { + if (err) { + console.error(err.message); + } +}); + +db.run(`CREATE TABLE IF NOT EXISTS reactions ( + author TEXT NOT NULL, + type INTEGER NOT NULL, + post INTEGER NOT NULL )`, (err) => { if (err) { console.error(err.message); @@ -61,7 +74,9 @@ app.use(express.static(path.join(__dirname, "public"))) .set("view engine", "ejs"); app.get("/", (req, res) => { - res.render("index"); + res.render("index", { + version: version + }); }); app.get("/login", (req, res) => { @@ -88,16 +103,41 @@ app.get("/category/:category/new", (req, res) => { }); app.get("/topic/:topic", (req, res) => { - db.get("SELECT * FROM topics WHERE id = ?", [req.params.topic], (err, row) => { - db.all("SELECT * FROM posts WHERE topic = ?", [req.params.topic], (err, posts) => { + db.get("SELECT * FROM topics WHERE id = ?", [req.params.topic], (err, topic) => { + db.all("SELECT * FROM posts WHERE topic = ?", [req.params.topic], async (err, posts) => { + let get_reactions = (post) => new Promise((resolve) => { + db.all("SELECT * FROM reactions WHERE post = ?", [post.id], function(err, reactions) { + resolve(reactions); + }); + }); + let post_reactions = await Promise.all(posts.map(post => get_reactions(post))); + + let get_post_counts = (post) => new Promise((resolve) => { + db.get("SELECT COUNT(*) FROM posts WHERE author = ?", [post.author], (err, count) => { + if (!err) { + resolve(count["COUNT(*)"]); + } + }); + }); + let post_count = await Promise.all(posts.map(post => get_post_counts(post))); + + console.log(post_count); + res.render("topic", { - topic: row, - posts: posts + topic: topic, + posts: posts, + reactions: post_reactions, + post_count: post_count }); + }); }); }); +app.get("/*", (req, res) => { + res.render("404"); +}); + app.post("/api/new/topic", (req, res) => { fetch("https://api.hatch.lol/auth/me", { headers: { @@ -111,17 +151,23 @@ app.post("/api/new/topic", (req, res) => { res.sendStatus(fres.status); if (fres.status === 200) { fres.json().then(data => { - if (req.body.category === 1 && !data.hatchTeam) { + if (req.body.category === "1" && !data.hatchTeam) { return; } db.get("SELECT COUNT(*) FROM topics", (err, count) => { if (err) { console.error(err.message); } - db.run("INSERT INTO topics (name, author, category) VALUES (?, ?, ?)", [req.body.title, data.name, req.body.category], (err) => { if (err) { console.error(err.message); } }); - db.run("INSERT INTO posts (author, content, topic) VALUES (?, ?, ?)", [data.name, req.body.content, count["COUNT(*)"]+1], (err) => { if (err) { console.error(err.message); } }); + + let content = req.body.content; + if (req.body.category === "5") { + content = `User agent: ${req.headers["user-agent"]}\n\n${req.body.content}`; + } + + db.run("INSERT INTO topics (name, author, category, pinned) VALUES (?, ?, ?, false)", [req.body.title, data.name, req.body.category], (err) => { if (err) { console.error(err.message); } }); + db.run("INSERT INTO posts (author, content, topic, timestamp) VALUES (?, ?, ?, ?)", [data.name, content, count["COUNT(*)"]+1, Date.now()], (err) => { if (err) { console.error(err.message); } }); }); }); } - }) + }); }); app.post("/api/new/post", (req, res) => { @@ -137,12 +183,79 @@ app.post("/api/new/post", (req, res) => { res.sendStatus(fres.status); if (fres.status === 200) { fres.json().then(data => { - db.run("INSERT INTO posts (author, content, topic) VALUES (?, ?, ?)", [data.name, req.body.content, req.body.topic], (err) => { if (err) { console.error(err.message); } }); + db.run("INSERT INTO posts (author, content, topic, timestamp) VALUES (?, ?, ?, ?)", [data.name, req.body.content, req.body.topic, Date.now()], (err) => { if (err) { console.error(err.message); } }); }); } - }) + }); +}); + +app.post("/api/new/reaction", (req, res) => { + let post_reaction = parseInt(req.body.reaction); + if (post_reaction < 0 || post_reaction > 5) { + res.sendStatus(400); + return; + } + fetch("https://api.hatch.lol/auth/me", { + headers: { + "Token": req.header("Token") + } + }).then(fres => { + if (fres.status === 200) { + fres.json().then(data => { + db.get("SELECT * FROM reactions WHERE author = ? AND type = ?", [data.name, req.body.reaction], (err, reaction) => { + if (err) { + res.sendStatus(500); + console.error(err.message); + return; + } + if (reaction) { + db.run("DELETE FROM reactions WHERE author = ? AND type = ?", [data.name, req.body.reaction], (err) => { if (err) { res.sendStatus(500); console.error(err.message); return; } }); + } else { + db.run("INSERT INTO reactions (author, type, post) VALUES (?, ?, ?)", [data.name, post_reaction, req.body.post], (err) => { if (err) { res.sendStatus(500); console.error(err.message); return; } }) + } + res.sendStatus(200); + }); + }); + } else { + res.sendStatus(fres.status); + } + }); +}); + +app.post("/api/pin/topic", (req, res) => { + fetch("https://api.hatch.lol/auth/me", { + headers: { + "Token": req.header("Token") + } + }).then(fres => { + if (fres.status === 200) { + fres.json().then(data => { + if (!data.hatchTeam) { + res.sendStatus(403); + return; + } + db.get("SELECT * FROM topics WHERE id = ?", [req.body.id], (err, topic) => { + if (err) { + res.sendStatus(500); + console.error(err.message); + return; + } + db.run("UPDATE topics SET pinned = ? WHERE id = ?", [!topic.pinned, req.body.id], (err) => { + if (err) { + res.sendStatus(500); + console.error(err.message); + return; + } + }); + res.sendStatus(200); + }); + }); + } else { + res.sendStatus(fres.status); + } + }); }); app.listen(port, () => { console.log(`listening on http://localhost:${port}`); -}) +}); diff --git a/package-lock.json b/package-lock.json index 40972d0..0dab094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "ISC", "dependencies": { "body-parser": "^1.20.3", + "chokidar": "^4.0.3", "ejs": "^3.1.9", - "express": "^4.21.1", + "express": "^4.21.2", + "request-ip": "^3.3.0", "sqlite3": "^5.1.7" }, "devDependencies": { @@ -173,6 +175,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -243,6 +246,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -304,12 +308,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -411,27 +416,18 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { @@ -808,10 +804,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -883,6 +880,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -971,6 +969,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1280,6 +1279,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1292,6 +1292,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1311,6 +1312,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1330,6 +1332,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1711,6 +1714,31 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/nodemon/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1743,6 +1771,19 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1775,6 +1816,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1872,6 +1914,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2020,17 +2063,24 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, + "node_modules/request-ip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", + "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -2492,6 +2542,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index e28fbeb..02e01af 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "license": "ISC", "dependencies": { "body-parser": "^1.20.3", + "chokidar": "^4.0.3", "ejs": "^3.1.9", - "express": "^4.21.1", + "express": "^4.21.2", + "request-ip": "^3.3.0", "sqlite3": "^5.1.7" }, "devDependencies": { diff --git a/public/navbar/img/logo.png b/public/navbar/img/logo.png index c63bd45..169b994 100644 Binary files a/public/navbar/img/logo.png and b/public/navbar/img/logo.png differ diff --git a/public/styles.css b/public/styles.css index 29dd941..78113a5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -21,17 +21,55 @@ text-decoration: underline; } -.post { +.post-header { + margin-bottom: 10px; +} + +.post-date { + opacity: 0.6; + font-style: italic; +} + +.forum-version { + font-style: italic; + font-size: 1rem; +} + +.post-stuff { display: flex; gap: 10px; } -.post-content { +.post-content-container { flex: auto; + word-break: break-word; + width: calc(100% - 150px); +} + +.post-reactions { + height: fit-content; + position: sticky; + top: 10px; +} + +.post-reactions a.button { + user-select: none; + background-color: var(--innerBlock); + color: var(--univText) !important; + text-wrap: nowrap; + width: calc(100% - 0.8rem); + padding: 0.4rem; +} + +.post-reactions a.button:hover { + filter: brightness(1.3); } .post-user-info { width: 120px; + height: fit-content; + position: sticky; + top: 10px; } .post-pfp { @@ -48,7 +86,7 @@ display: inline-block; } -.post-content, textarea, input { +.post-content-container, textarea, input { padding: 10px; border-radius: 10px; background-color: var(--innerBlock2); @@ -90,3 +128,7 @@ input::placeholder { font-family: inherit; margin-bottom: 10px; } + +h1, h2, h3, p { + margin: 1rem 0 !important; +} \ No newline at end of file diff --git a/public/topic.js b/public/topic.js index 1002136..c81815b 100644 --- a/public/topic.js +++ b/public/topic.js @@ -1,6 +1,6 @@ document.addEventListener("DOMContentLoaded", () => { Array.from(document.getElementsByClassName("post")).forEach(element => { - fetch(`https://api.hatch.lol/users/${element.id}`).then(res => res.json()).then(data => { + fetch(`https://api.hatch.lol/users/${element.dataset.author}`).then(res => res.json()).then(data => { element.querySelector(".post-user-name").innerText = data.displayName; element.querySelector(".post-pfp").src = data.profilePicture.startsWith("data:image") ? data.profilePicture : `https://api.hatch.lol/${data.profilePicture}`; element.querySelector(".post-user-role").innerText = data.hatchTeam === true ? "Hatch Team" : "Hatchling"; @@ -11,4 +11,45 @@ document.addEventListener("DOMContentLoaded", () => { .replace(/(https?:\/\/\S+)/g, "$1") .replace(/@([a-z,A-Z,0-9,-,_]+)\b/g, "@$1"); }); + + Array.from(document.getElementsByClassName("post-reaction-button")).forEach(element => { + element.addEventListener("click", () => { + fetch("/api/new/reaction", { + method: "POST", + headers: { + "Content-type": "application/json", + "Token": localStorage.getItem("token") + }, + body: JSON.stringify({ + "reaction": element.dataset.reaction, + "post": element.dataset.post + }) + }); + }); + }); + + const pin_topic_button = document.querySelector("#pin-topic-button"); + + fetch("https://api.hatch.lol/auth/me", { + headers: { + "Token": localStorage.getItem("token") + } + }).then(fres => { + if (fres.status === 200) { + fres.json().then(data => { + pin_topic_button.addEventListener("click", () => { + fetch("/api/pin/topic", { + method: "POST", + headers: { + "Content-type": "application/json", + "Token": localStorage.getItem("token") + }, + body: JSON.stringify({ + "id": document.querySelector("#get-topic").content + }) + }); + }); + }); + } + }); }); diff --git a/views/404.ejs b/views/404.ejs new file mode 100644 index 0000000..213e096 --- /dev/null +++ b/views/404.ejs @@ -0,0 +1,24 @@ + + + + + + + + + + 404 | Hatch Forums + + + +
+

Not Found

+

Sorry, the page you're looking for could not be located. Home

+
+
+ + + + diff --git a/views/category.ejs b/views/category.ejs index 2052be7..3ff9699 100644 --- a/views/category.ejs +++ b/views/category.ejs @@ -9,6 +9,7 @@ + <%= category.name %> | Hatch Forums @@ -21,15 +22,21 @@ <% let pinnedlist = []; %> <% let topiclist = []; %> <% for (let i = 0; i < topics.length; i++) { %> - <% if (topics[i].pinned === "true") { %> + <% if (topics[i].pinned === 1) { %> <% pinnedlist.push(topics[i]); %> <% } else { %> - <% topiclist.unshift(topics[i]); %> + <% topiclist.unshift(topics[i]); %> <%} %> <% } %> + <% if (pinnedlist.length > 0) { %> +

Pinned topics

+ <% } %> <% for (let i = 0; i < pinnedlist.length; i++) { %> <%= pinnedlist[i].name %> <% } %> + <% if (topiclist.length > 0) { %> +

Topics

+ <% } %> <% for (let i = 0; i < topiclist.length; i++) { %> <%= topiclist[i].name %> <% } %> diff --git a/views/index.ejs b/views/index.ejs index 6413551..ae0bf30 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -9,11 +9,12 @@ + Hatch Forums
-

Hatch Forums

+

Hatch Forums <%= version %>

Announcements Suggestions Questions about Hatch diff --git a/views/topic.ejs b/views/topic.ejs index f519d22..63478c5 100644 --- a/views/topic.ejs +++ b/views/topic.ejs @@ -10,28 +10,56 @@ + <%= topic.name %> by @<%= topic.author %> | Hatch Forums
back

<%= topic.name %>

-

by @<%= topic.author %>

+

by <%= topic.author %>

+ <%= topic.pinned === 1 ? "Unpin" : "Pin" %>
<% for (let i = 0; i < posts.length; i++) { %> -
- -
- <%= - posts[i].content - %> +
+ <% let date = new Date(posts[i].timestamp); %> +
#<%= i+1 %>
+
+
+ +
+
+
+ <% if (posts[i].content.includes("\n")) { %> + <% let paragraphs = posts[i].content.split(/\r?\n/) %> + <% for (let j = 0; j < paragraphs.length; j++) { %> + <%= paragraphs[j] %>
+ <% } %> + <% } else { %> + <%= posts[i].content %> + <% } %> +
+
+
+
+ <% let post_reactions = [ [], [], [], [], [] ]; %> + <% for (let j = 0; j < reactions[i].length; j++) { %> + <% post_reactions[reactions[i][j].type].push(reactions[i][j].author); %> + <% } %> + + <% for (let j = 0; j < 5; j++) { %> + "><%= ["🎉", "👍", "👎", "❓", "️‍🔥"][j] %> <%= post_reactions[j].length > 0 ? post_reactions[j].length : "" %> + <% } %> +
+
<% } %>