diff --git a/.gitignore b/.gitignore index 3f0b6ea..5030290 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bun.lock .env logs/ .vscode/ +robots.txt diff --git a/public/css/index.css b/public/css/index.css index 1593bf9..b8df88e 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -884,6 +884,13 @@ ul { flex-wrap: wrap; } +.review-header-inner { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + .review-username { font-weight: 600; color: var(--text-color); @@ -907,6 +914,23 @@ ul { flex-wrap: wrap; } +.emoji { + width: 20px; + height: 20px; + vertical-align: middle; + margin: 0 2px; + display: inline-block; + transition: transform 0.3s ease; +} + +.emoji:hover { + transform: scale(1.2); +} + +.review-content img.emoji { + vertical-align: middle; +} + @media (max-width: 600px) { .reviews { max-width: 100%; @@ -965,4 +989,9 @@ ul { .review-badges { justify-content: center; } + + .emoji { + width: 16px; + height: 16px; + } } diff --git a/public/js/index.js b/public/js/index.js index 1193627..4c0bfc4 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -142,7 +142,7 @@ async function populateReviews(userId) { const data = await res.json(); if (!data.success || !Array.isArray(data.reviews)) { - if (page === 1) reviewSection.classList.add("hidden"); + if (currentReviewOffset === 0) reviewSection.classList.add("hidden"); isLoadingReviews = false; return; } @@ -152,7 +152,14 @@ async function populateReviews(userId) { const sender = review.sender; const username = sender.username; const avatar = sender.profilePhoto; - const comment = review.comment; + let comment = review.comment; + + comment = comment.replace( + /<(a?):\w+:(\d+)>/g, + (_, animated, id) => + `emoji`, + ); + const timestamp = review.timestamp ? new Date(review.timestamp * 1000).toLocaleString(undefined, { hour12: false, @@ -172,18 +179,20 @@ async function populateReviews(userId) { .join(""); return ` -
  • - ${username}'s avatar -
    -
    - ${username} - ${timestamp} +
  • + ${username}'s avatar +
    +
    +
    + ${username} + ${badges} +
    + ${timestamp} +
    +
    ${comment}
    -
    ${badges}
    -
    ${comment}
    - -
  • - `; + + `; }) .join(""); @@ -193,7 +202,6 @@ async function populateReviews(userId) { reviewSection.classList.remove("hidden"); hasMoreReviews = data.hasNextPage; - currentReviewPage = page; isLoadingReviews = false; } catch (err) { console.error("Failed to fetch reviews", err); diff --git a/src/index.ts b/src/index.ts index 2836a3f..89b3704 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import { serverHandler } from "@/server"; +import { verifyRequiredVariables } from "@config/environment"; import { logger } from "@creations.works/logger"; async function main(): Promise { + verifyRequiredVariables(); + serverHandler.initialize(); } diff --git a/src/server.ts b/src/server.ts index 2bc28ae..c936afa 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ import { resolve } from "node:path"; -import { environment } from "@config/environment"; +import { environment, robotstxtPath } from "@config/environment"; import { logger } from "@creations.works/logger"; import { type BunFile, @@ -65,10 +65,15 @@ class ServerHandler { } } - private async serveStaticFile(pathname: string): Promise { - try { - let filePath: string; + private async serveStaticFile( + request: ExtendedRequest, + pathname: string, + ip: string, + ): Promise { + let filePath: string; + let response: Response; + try { if (pathname === "/favicon.ico") { filePath = resolve("public", "assets", "favicon.ico"); } else { @@ -81,16 +86,37 @@ class ServerHandler { const fileContent: ArrayBuffer = await file.arrayBuffer(); const contentType: string = file.type || "application/octet-stream"; - return new Response(fileContent, { + response = new Response(fileContent, { headers: { "Content-Type": contentType }, }); + } else { + logger.warn(`File not found: ${filePath}`); + response = new Response("Not Found", { status: 404 }); } - logger.warn(`File not found: ${filePath}`); - return new Response("Not Found", { status: 404 }); } catch (error) { logger.error([`Error serving static file: ${pathname}`, error as Error]); - return new Response("Internal Server Error", { status: 500 }); + response = new Response("Internal Server Error", { status: 500 }); } + + this.logRequest(request, response, ip); + return response; + } + + private logRequest( + request: ExtendedRequest, + response: Response, + ip: string | undefined, + ): void { + logger.custom( + `[${request.method}]`, + `(${response.status})`, + [ + request.url, + `${(performance.now() - request.startPerf).toFixed(2)}ms`, + ip || "unknown", + ], + "90", + ); } private async handleRequest( @@ -100,16 +126,52 @@ class ServerHandler { const extendedRequest: ExtendedRequest = request as ExtendedRequest; extendedRequest.startPerf = performance.now(); + const headers = request.headers; + let ip = server.requestIP(request)?.address; + let response: Response; + + if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { + ip = + headers.get("CF-Connecting-IP")?.trim() || + headers.get("X-Real-IP")?.trim() || + headers.get("X-Forwarded-For")?.split(",")[0].trim() || + "unknown"; + } + const pathname: string = new URL(request.url).pathname; + if (pathname === "/robots.txt" && robotstxtPath) { + try { + const file: BunFile = Bun.file(robotstxtPath); + + if (await file.exists()) { + const fileContent: ArrayBuffer = await file.arrayBuffer(); + const contentType: string = file.type || "text/plain"; + + response = new Response(fileContent, { + headers: { "Content-Type": contentType }, + }); + } else { + logger.warn(`File not found: ${robotstxtPath}`); + response = new Response("Not Found", { status: 404 }); + } + } catch (error) { + logger.error([ + `Error serving robots.txt: ${robotstxtPath}`, + error as Error, + ]); + response = new Response("Internal Server Error", { status: 500 }); + } + + this.logRequest(extendedRequest, response, ip); + return response; + } + if (pathname.startsWith("/public") || pathname === "/favicon.ico") { - return await this.serveStaticFile(pathname); + return await this.serveStaticFile(extendedRequest, pathname, ip); } const match: MatchedRoute | null = this.router.match(request); let requestBody: unknown = {}; - let response: Response; - - let logRequest = true; if (match) { const { filePath, params, query } = match; @@ -121,8 +183,6 @@ class ServerHandler { ? contentType.split(";")[0].trim() : null; - logRequest = routeModule.routeDef.log !== false; - if ( routeModule.routeDef.needsBody === "json" && actualContentType === "application/json" @@ -231,30 +291,6 @@ class ServerHandler { ); } - if (logRequest) { - const headers = request.headers; - let ip = server.requestIP(request)?.address; - - if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { - ip = - headers.get("CF-Connecting-IP")?.trim() || - headers.get("X-Real-IP")?.trim() || - headers.get("X-Forwarded-For")?.split(",")[0].trim() || - "unknown"; - } - - logger.custom( - `[${request.method}]`, - `(${response.status})`, - [ - request.url, - `${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`, - ip || "unknown", - ], - "90", - ); - } - return response; } }