From 9aa58ae23fc26c48f9af9ba932870cdf208bbc8f Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 10 May 2025 12:46:58 -0400 Subject: [PATCH] add review db, fix issues with spamming css url and readme whenever status updated --- .env.example | 4 ++ config/environment.ts | 5 ++ public/css/index.css | 152 ++++++++++++++++++++++++++++++++++++++++++ public/js/index.js | 105 ++++++++++++++++++++++++++++- src/routes/[id].ts | 11 ++- src/views/index.html | 6 ++ 6 files changed, 281 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 92211b4..626a366 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,10 @@ LANYARD_INSTANCE=https://lanyard.rest # Required if you want to enable badges BADGE_API_URL=http://localhost:8081 +# Required if you want to enable reviews from reviewdb +REVIEW_DB=true + + # https://www.steamgriddb.com/api/v2, if you want games to have images STEAMGRIDDB_API_KEY=steamgrid_api_key diff --git a/config/environment.ts b/config/environment.ts index 4d48b88..dec203b 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -14,6 +14,11 @@ export const lanyardConfig: LanyardConfig = { instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest", }; +export const reviewDb = { + enabled: process.env.REVIEW_DB === "true" || process.env.REVIEW_DB === "1", + url: "https://manti.vendicated.dev/api/reviewdb", +}; + export const badgeApi: string | null = process.env.BADGE_API_URL || null; export const steamGridDbKey: string | undefined = process.env.STEAMGRIDDB_API_KEY; diff --git a/public/css/index.css b/public/css/index.css index 63fe0c2..1593bf9 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -814,3 +814,155 @@ ul { font-size: 0.95rem; } } + +/* reviews */ +.reviews { + width: 100%; + max-width: 700px; + margin-top: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + background-color: var(--card-bg); + padding: 1rem; + border-radius: 10px; + border: 1px solid var(--border-color); + box-sizing: border-box; +} + +.reviews h2 { + margin: 0 0 1rem; + font-size: 2rem; + font-weight: 600; + text-align: center; +} + +.reviews-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.review { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 1rem; + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid var(--border-color); + transition: background-color 0.3s ease; +} + +.review:hover { + background-color: var(--card-hover-bg); +} + +.review-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.review-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.review-username { + font-weight: 600; + color: var(--text-color); +} + +.review-timestamp { + font-size: 0.8rem; + color: var(--text-muted); +} + +.review-content { + color: var(--text-secondary); + font-size: 0.95rem; + word-break: break-word; + white-space: pre-wrap; +} + +.review-badges { + display: flex; + gap: 0.3rem; + flex-wrap: wrap; +} + +@media (max-width: 600px) { + .reviews { + max-width: 100%; + padding: 1rem; + border-radius: 0; + border: none; + background-color: transparent; + } + + .reviews h2 { + font-size: 1.4rem; + text-align: center; + margin-bottom: 1rem; + } + + .reviews-list { + gap: 0.75rem; + } + + .review { + flex-direction: column; + align-items: center; + text-align: center; + padding: 1rem; + border-radius: 0; + } + + .review-avatar { + width: 64px; + height: 64px; + } + + .review-body { + width: 100%; + align-items: center; + } + + .review-header { + flex-direction: column; + align-items: center; + gap: 0.25rem; + } + + .review-username { + font-size: 1rem; + } + + .review-timestamp { + font-size: 0.75rem; + } + + .review-content { + font-size: 0.9rem; + } + + .review-badges { + justify-content: center; + } +} diff --git a/public/js/index.js b/public/js/index.js index a32bbd8..d80afa7 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -2,10 +2,18 @@ const head = document.querySelector("head"); const userId = head?.dataset.userId; const activityProgressMap = new Map(); +const reviewURL = head?.dataset.reviewDb; let instanceUri = head?.dataset.instanceUri; let badgeURL = head?.dataset.badgeUrl; let socket; + let badgesLoaded = false; +let readmeLoaded = false; +let cssLoaded = false; + +let currentReviewPage = 1; +let hasMoreReviews = true; +let isLoadingReviews = false; function formatTime(ms) { const totalSecs = Math.floor(ms / 1000); @@ -119,6 +127,93 @@ function resolveActivityImage(img, applicationId) { return `https://cdn.discordapp.com/app-assets/${applicationId}/${img}.png`; } +async function populateReviews(userId, page = 1) { + if (!reviewURL || !userId || isLoadingReviews || !hasMoreReviews) return; + const reviewSection = document.querySelector(".reviews"); + const reviewList = reviewSection?.querySelector(".reviews-list"); + if (!reviewList) return; + + isLoadingReviews = true; + + try { + const url = `${reviewURL}/users/${userId}/reviews?page=${page}`; + const res = await fetch(url); + const data = await res.json(); + + if (!data.success || !Array.isArray(data.reviews)) { + if (page === 1) reviewSection.classList.add("hidden"); + isLoadingReviews = false; + return; + } + + const reviewsHTML = data.reviews + .slice(1) + .map((review) => { + const sender = review.sender; + const username = sender.username; + const avatar = sender.profilePhoto; + const comment = review.comment; + const timestamp = review.timestamp + ? new Date(review.timestamp * 1000).toLocaleString() + : "N/A"; + + const badges = (sender.badges || []) + .map( + (b) => + `${b.name}`, + ) + .join(""); + + return ` +
  • + ${username}'s avatar +
    +
    + ${username} + ${timestamp} +
    +
    ${badges}
    +
    ${comment}
    +
    +
  • + `; + }) + .join(""); + + if (page === 1) reviewList.innerHTML = reviewsHTML; + else reviewList.insertAdjacentHTML("beforeend", reviewsHTML); + + reviewSection.classList.remove("hidden"); + + hasMoreReviews = data.hasNextPage; + currentReviewPage = page; + isLoadingReviews = false; + } catch (err) { + console.error("Failed to fetch reviews", err); + isLoadingReviews = false; + } +} + +function setupReviewScrollObserver(userId) { + const sentinel = document.createElement("div"); + sentinel.className = "review-scroll-sentinel"; + document.querySelector(".reviews").appendChild(sentinel); + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMoreReviews) { + populateReviews(userId, currentReviewPage + 1); + } + }, + { + rootMargin: "200px", + threshold: 0, + }, + ); + + observer.observe(sentinel); +} + function buildActivityHTML(activity) { const start = activity.timestamps?.start; const end = activity.timestamps?.end; @@ -334,6 +429,8 @@ async function loadBadges(userId, options = {}) { } async function populateReadme(data) { + if (readmeLoaded) return; + const readmeSection = document.querySelector(".readme"); const kv = data.kv || {}; @@ -347,6 +444,7 @@ async function populateReadme(data) { readmeSection.innerHTML = `
    ${text}
    `; readmeSection.classList.remove("hidden"); + readmeLoaded = true; } catch (err) { console.error("Failed to load README", err); readmeSection.classList.add("hidden"); @@ -396,7 +494,7 @@ async function updatePresence(initialData) { } const cssLink = kv.css; - if (cssLink) { + if (cssLink && !cssLoaded) { try { const res = await fetch(`/api/css?url=${encodeURIComponent(cssLink)}`); if (!res.ok) throw new Error("Failed to fetch CSS"); @@ -405,6 +503,7 @@ async function updatePresence(initialData) { const style = document.createElement("style"); style.textContent = cssText; document.head.appendChild(style); + cssLoaded = true; } catch (err) { console.error("Failed to load CSS", err); } @@ -462,6 +561,10 @@ async function updatePresence(initialData) { } updateClanBadge(data); + if (kv.reviews !== "false") { + populateReviews(userId, 1); + setupReviewScrollObserver(userId); + } const platform = { mobile: data.active_on_discord_mobile, diff --git a/src/routes/[id].ts b/src/routes/[id].ts index 55fe2e8..28921ec 100644 --- a/src/routes/[id].ts +++ b/src/routes/[id].ts @@ -1,5 +1,10 @@ import { resolve } from "node:path"; -import { badgeApi, lanyardConfig, plausibleScript } from "@config/environment"; +import { + badgeApi, + lanyardConfig, + plausibleScript, + reviewDb, +} from "@config/environment"; import { file } from "bun"; const routeDef: RouteDef = { @@ -24,6 +29,10 @@ async function handler(request: ExtendedRequest): Promise { head.setAttribute("data-instance-uri", instance); head.setAttribute("data-badge-url", badgeApi || ""); + if (reviewDb.enabled) { + head.setAttribute("data-review-db", reviewDb.url); + } + if (plausibleScript) { head.append(plausibleScript, { html: true }); } diff --git a/src/views/index.html b/src/views/index.html index b5e313b..79ec446 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -62,6 +62,12 @@ + +