diff --git a/index.js b/index.js index 498265a..13c247c 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ import bcrypt from "bcrypt"; import fileUpload from "express-fileupload"; import path from "path"; import childProcess from "child_process"; +import levenshtein from "js-levenshtein"; const users = []; let sessions = []; @@ -32,12 +33,12 @@ app.post("/api/register", async (req, res) => { return res.status(400).json({ ok: false, error: "bad request" }); } const existingUser = users.find(user => user.username === username); - if (existingUser === undefined) { + if (existingUser !== undefined) { return res.status(400).json({ ok: false, error: "username taken" }); } const passwordHash = await bcrypt.hash(password, 10); const id = users.length; - const user = { id, username, passwordHash }; + const user = { id, username, password: passwordHash }; users.push(user); return res.status(200).json({ ok: true, user }); }); @@ -63,6 +64,28 @@ app.post("/api/login", async (req, res) => { return res.status(200).json({ ok: true, session }); }); + +app.get("/api/search", async (req, res) => { + const page = +req.query.page || 0; + const search = req.query.query; + if (!search) { + return res.status(400).json({ ok: false, error: "bad request" }); + } + const [start, end] = [20 * page, 20 * (page + 1)]; + const returnedVideos = videos + .map(video => ({dist: levenshtein(search, video.title), ...video})) + .toSorted((a, b) => a.dist - b.dist) + .slice(start, end) + .map(video => { + const user = users.find(user => user.id === v.userId); + if (!user) { + return {...video, author: "[Liberal]"}; + } + return {...video, author: user.username}; + }); + return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length }); +}); + function authorized() { return (req, res, next) => { const token = (() => { diff --git a/package-lock.json b/package-lock.json index 0d304d4..88e4b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "express": "^4.18.2", - "express-fileupload": "^1.4.3" + "express-fileupload": "^1.4.3", + "js-levenshtein": "^1.1.6" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -668,6 +669,14 @@ "node": ">=8" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index 75792e9..e114af8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "express": "^4.18.2", - "express-fileupload": "^1.4.3" + "express-fileupload": "^1.4.3", + "js-levenshtein": "^1.1.6" } } diff --git a/public/index.html b/public/index.html index 24c799b..438ec15 100644 --- a/public/index.html +++ b/public/index.html @@ -4,11 +4,10 @@ MaoTube -

MaoTube

-
+
diff --git a/public/login/index.html b/public/login/index.html new file mode 100644 index 0000000..a1fd655 --- /dev/null +++ b/public/login/index.html @@ -0,0 +1,35 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + + +
+
+ +
+
+
+ + +
+ + + + diff --git a/public/login/script.js b/public/login/script.js new file mode 100644 index 0000000..f689adc --- /dev/null +++ b/public/login/script.js @@ -0,0 +1,46 @@ +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + window.location.href = "/"; +} + +function click() { + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + if (username.trim().length === 0) { + error("username cannot be empty"); + return; + } else if (password.trim().length === 0) { + error("password cannot be empty"); + return; + } + const method = "POST"; + const body = JSON.stringify({ username, password }); + const headers = new Headers({ "Content-Type": "application/json" }); + return fetch("/api/login", { + method, + body, + headers, + }) + .then(v => v.json()) + .then(displayResponse); + +} + +function main() { + const submit = document.getElementById("submit"); + submit.addEventListener("click", () => click()) +} + +main(); diff --git a/public/register/index.html b/public/register/index.html new file mode 100644 index 0000000..1a311d5 --- /dev/null +++ b/public/register/index.html @@ -0,0 +1,35 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + + +
+
+ +
+
+
+ + +
+ + + + diff --git a/public/register/script.js b/public/register/script.js new file mode 100644 index 0000000..ca6b8a1 --- /dev/null +++ b/public/register/script.js @@ -0,0 +1,46 @@ +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + window.location.href = "/login"; +} + +function click() { + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + if (username.trim().length === 0) { + error("username cannot be empty"); + return; + } else if (password.trim().length === 0) { + error("password cannot be empty"); + return; + } + const method = "POST"; + const body = JSON.stringify({ username, password }); + const headers = new Headers({ "Content-Type": "application/json" }); + return fetch("/api/register", { + method, + body, + headers, + }) + .then(v => v.json()) + .then(displayResponse); + +} + +function main() { + const submit = document.getElementById("submit"); + submit.addEventListener("click", () => click()) +} + +main(); diff --git a/public/script.js b/public/script.js deleted file mode 100644 index b28b04f..0000000 --- a/public/script.js +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/search/index.html b/public/search/index.html new file mode 100644 index 0000000..a8b63e4 --- /dev/null +++ b/public/search/index.html @@ -0,0 +1,30 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + +
+
+
+ + +
+ + + + diff --git a/public/search/script.js b/public/search/script.js new file mode 100644 index 0000000..e154ab0 --- /dev/null +++ b/public/search/script.js @@ -0,0 +1,38 @@ + +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + const { videos, total } = response; + if (videos.length === 0) { + error("search returned no results"); + return; + } + const resultElement = document.getElementById("result"); + resultElement.innerHTML = `

Showing ${videos.length}/${total} results.

` + + ""; +} + +function main() { + const params = new URLSearchParams(window.location.search); + return fetch(`/api/search?${params.toString()}`) + .then(v => v.json()) + .then(displayResponse); +} + +main(); diff --git a/public/style.css b/public/style.css index 4751a62..a4d0111 100644 --- a/public/style.css +++ b/public/style.css @@ -12,3 +12,44 @@ body { padding: 2rem; text-align: center; } + +.mao-error { + display: flex; + flex-direction: column; + background: url("/chairman_1.jpg"); + background-size: auto 600px; + background-repeat: no-repeat; + background-position: center center; + height: 600px; +} + +.hidden { + display: none; +} + +.mao-error p { + color: #fff; + text-transform: uppercase; + font-weight: bold; + font-family: "Impact", "Bebas", "League Gothic", "Oswald", "Coluna", "Ubuntu Condensed", system-ui, sans-serif; + text-shadow: + 3px 3px 0 black, + -3px -3px 0 black, + 3px -3px 0 black, + -3px 3px 0 black, + + -3px 0px 0 black, + 3px 0px 0 black, + 0px 3px 0 black, + 0px -3px 0 black; + font-size: 2em; +} + +.mao-error p:last-child { + margin-top: auto; +} + +ul#video-list { + padding: 0; + list-style: none; +}