const express = require("express"), router = express.Router(), fs = require("fs"), courseCardComponent = fs.readFileSync("components/course_card.html", "utf-8"); const { sanitize, isCourseFileValid } = require("../utils/functions"); router.get("/", async (req, res) => { const courses = await req.app.locals.db.getData("/courses"); const codes = Object.keys(courses); let sortType = "none"; if (req.query.sort) sortType = req.query.sort.toString(); let page = 1; if (req.query.page) page = req.query.page.toString(); let search = ""; if (req.query.search) search = req.query.search.toString(); /* Pages Count */ const coursesCount = Object.keys(courses).length; const pagesCount = Math.max(Math.ceil(coursesCount / 20), 0); if (page === "Page") page = 1; if (page * 20 - coursesCount >= 20) page = 1; /* Pages Dropdown */ let pagesDropdown = ""; for (let i = 1; i <= pagesCount; i++) { if (page === i.toString()) pagesDropdown += ``; else pagesDropdown += ``; } /* Sort Dropdown */ const sortOptions = { time: "Date", name: "Course Name", map: "Map Name", elements: "Element count", scoresmart: "Rating", plays: "Plays", }; let sortDropdown = ""; for (const [key, value] of Object.entries(sortOptions)) { if (sortType === key) sortDropdown += ``; else sortDropdown += ``; } /* Courses Cards */ const usernames = await req.app.locals.db.getData("/usernames"); const ratings = await req.app.locals.db.getData("/rating"); const codesData = []; const cards = []; codes.forEach(async code => { const codeData = courses[code]; let codeFile = ""; try { codeFile = fs.readFileSync(`public/${codeData.path}`, "utf-8"); } catch (e) { return console.log(`[WARNING] Not found file for: ${code}`); } const parsedCodeFile = JSON.parse(codeFile); if (!isCourseFileValid(parsedCodeFile)) console.log(`[WARNING] Course is not valid: ${code}`); let codeMapImage = "img/unknown.jpg"; const codeMap = codeData.map; if (codeMap === "gm_flatgrass" || codeMap === "gm_construct") if (fs.existsSync(`public/img/${codeMap}.jpg`)) codeMapImage = `img/${codeMap}.jpg`; if (codeData.mapimg) codeMapImage = codeData.mapimg; const codeMapId = codeData.mapid !== "0" && codeData.mapid !== "" ? codeData.mapid : "2818480138"; // le troll const codeUserId = codeData.uploader.userid; const codeUsername = usernames[codeUserId] ? usernames[codeUserId] : codeUserId; if (!ratings[code]) { ratings[code] = {}; } const rating = getCourseRating(ratings[code]); codesData.push({ name: parsedCodeFile[4], code: code, map: codeData.map, username: codeUsername, userid: codeUserId, elements: parsedCodeFile[5].length + parsedCodeFile[0].length, download: codeData.path, likes: rating.likes, dislikes: rating.dislikes, rates: rating.ratings, scoresmart: rating.rateSmart, mapimg: codeMapImage, mapwid: codeMapId, time: codeData.time, plays: codeData.plays || 0, }); }); let sortedCodesData = []; const sortKeys = { time: "DESC", name: "STRING", map: "STRING", elements: "DESC", scoresmart: "DESC", plays: "DESC", }; if (sortType === "none" || sortType === "Sort by") sortedCodesData = codesData.sort((a, b) => { return b.time - a.time; }); else { const sortKey = sortKeys[sortType]; if (sortKey === "STRING") sortedCodesData = codesData.sort((a, b) => { return a[sortType].localeCompare(b[sortType]); }); else if (sortKey === "ASC") sortedCodesData = codesData.sort((a, b) => { return a[sortType] - b[sortType]; }); else if (sortKey === "DESC") sortedCodesData = codesData.sort((a, b) => { return b[sortType] - a[sortType]; }); } if (search) sortedCodesData = codesData.filter(c => { const query = sanitize(search, true, false); let searchString = ""; searchString += c.name; searchString += c.code; searchString += c.userid; searchString += c.username; searchString += c.map; searchString += c.mapwid; return searchString.toLowerCase().includes(query); }); sortedCodesData.forEach(data => cards.push(generateCourseCard(data))); const sortedCards = cards.slice(Math.max((page - 1) * 20 - 1, 0), Math.max((page - 1) * 20 - 1, 0) + 20).join("\n"); res.render("index", { user: req.user, locals: req.app.locals, pagesDropdown: pagesDropdown, sortDropdown: sortDropdown, coursesList: sortedCards, searchText: search, sanitize, }); await req.app.locals.db.push("/rating", ratings); }); /** * Calculates rating statistics for a course based on rating data. * * @param {Object} data - Object containing rating data where keys are user IDs * and values are true for like or false for dislike * @returns {Object} Object containing calculated rating stats: * - likes: Number of likes * - dislikes: Number of dislikes * - ratings: Total number of ratings * - rateSmart: Likes minus dislikes */ function getCourseRating(data) { const ratings = Object.keys(data).length; if (ratings <= 0) return { likes: 0, dislikes: 0, ratings: 0, rateSmart: 0 }; let likes = 0, dislikes = 0, rateSmart = 0; for (const r in data) if (data[r]) likes += 1; dislikes = ratings - likes; rateSmart = ratings + likes - dislikes; return { likes: likes, dislikes: dislikes, ratings: ratings, rateSmart: rateSmart }; } /** * Generates an HTML string for a course card component. * * @param {Object} course The course data object * @returns {String} The generated HTML string for the course card */ function generateCourseCard(course) { const templates = { "{courseName}": course.name, "{courseCode}": course.code, "{uploaderID}": course.userid, "{uploaderName}": course.username, "{courseMap}": course.map, "{downloadLink}": course.download, "{likesCount}": course.likes, "{dislikesCount}": course.dislikes, "{ratesmart}": course.scoresmart, "{mapImage}": course.mapimg, "{mapID}": course.mapwid, "{elementsCount}": course.elements === 1 ? "1 element" : `${course.elements} elements`, "{uploadDate}": new Date(course.time).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", year: "2-digit" }), "{plays}": course.plays === 1 ? "1 play" : `${course.plays || 0} plays`, }; let output = courseCardComponent; for (const t in templates) output = output.replace(new RegExp(t, "g"), templates[t]); return output; } module.exports = router;