add review db, fix issues with spamming css url and readme whenever status updated
All checks were successful
Code quality checks / biome (push) Successful in 9s
All checks were successful
Code quality checks / biome (push) Successful in 9s
This commit is contained in:
parent
5ad5d7181f
commit
9aa58ae23f
6 changed files with 281 additions and 2 deletions
|
@ -12,6 +12,10 @@ LANYARD_INSTANCE=https://lanyard.rest
|
||||||
# Required if you want to enable badges
|
# Required if you want to enable badges
|
||||||
BADGE_API_URL=http://localhost:8081
|
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
|
# https://www.steamgriddb.com/api/v2, if you want games to have images
|
||||||
STEAMGRIDDB_API_KEY=steamgrid_api_key
|
STEAMGRIDDB_API_KEY=steamgrid_api_key
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ export const lanyardConfig: LanyardConfig = {
|
||||||
instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest",
|
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 badgeApi: string | null = process.env.BADGE_API_URL || null;
|
||||||
export const steamGridDbKey: string | undefined =
|
export const steamGridDbKey: string | undefined =
|
||||||
process.env.STEAMGRIDDB_API_KEY;
|
process.env.STEAMGRIDDB_API_KEY;
|
||||||
|
|
|
@ -814,3 +814,155 @@ ul {
|
||||||
font-size: 0.95rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,18 @@ const head = document.querySelector("head");
|
||||||
const userId = head?.dataset.userId;
|
const userId = head?.dataset.userId;
|
||||||
const activityProgressMap = new Map();
|
const activityProgressMap = new Map();
|
||||||
|
|
||||||
|
const reviewURL = head?.dataset.reviewDb;
|
||||||
let instanceUri = head?.dataset.instanceUri;
|
let instanceUri = head?.dataset.instanceUri;
|
||||||
let badgeURL = head?.dataset.badgeUrl;
|
let badgeURL = head?.dataset.badgeUrl;
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
let badgesLoaded = false;
|
let badgesLoaded = false;
|
||||||
|
let readmeLoaded = false;
|
||||||
|
let cssLoaded = false;
|
||||||
|
|
||||||
|
let currentReviewPage = 1;
|
||||||
|
let hasMoreReviews = true;
|
||||||
|
let isLoadingReviews = false;
|
||||||
|
|
||||||
function formatTime(ms) {
|
function formatTime(ms) {
|
||||||
const totalSecs = Math.floor(ms / 1000);
|
const totalSecs = Math.floor(ms / 1000);
|
||||||
|
@ -119,6 +127,93 @@ function resolveActivityImage(img, applicationId) {
|
||||||
return `https://cdn.discordapp.com/app-assets/${applicationId}/${img}.png`;
|
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) =>
|
||||||
|
`<img src="${b.icon}" class="badge" title="${b.description}" alt="${b.name}" />`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="review">
|
||||||
|
<img class="review-avatar" src="${avatar}" alt="${username}'s avatar"/>
|
||||||
|
<div class="review-body">
|
||||||
|
<div class="review-header">
|
||||||
|
<span class="review-username">${username}</span>
|
||||||
|
<span class="review-timestamp">${timestamp}</span>
|
||||||
|
</div>
|
||||||
|
<div class="review-badges">${badges}</div>
|
||||||
|
<div class="review-content">${comment}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.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) {
|
function buildActivityHTML(activity) {
|
||||||
const start = activity.timestamps?.start;
|
const start = activity.timestamps?.start;
|
||||||
const end = activity.timestamps?.end;
|
const end = activity.timestamps?.end;
|
||||||
|
@ -334,6 +429,8 @@ async function loadBadges(userId, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateReadme(data) {
|
async function populateReadme(data) {
|
||||||
|
if (readmeLoaded) return;
|
||||||
|
|
||||||
const readmeSection = document.querySelector(".readme");
|
const readmeSection = document.querySelector(".readme");
|
||||||
const kv = data.kv || {};
|
const kv = data.kv || {};
|
||||||
|
|
||||||
|
@ -347,6 +444,7 @@ async function populateReadme(data) {
|
||||||
|
|
||||||
readmeSection.innerHTML = `<div class="markdown-body">${text}</div>`;
|
readmeSection.innerHTML = `<div class="markdown-body">${text}</div>`;
|
||||||
readmeSection.classList.remove("hidden");
|
readmeSection.classList.remove("hidden");
|
||||||
|
readmeLoaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load README", err);
|
console.error("Failed to load README", err);
|
||||||
readmeSection.classList.add("hidden");
|
readmeSection.classList.add("hidden");
|
||||||
|
@ -396,7 +494,7 @@ async function updatePresence(initialData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssLink = kv.css;
|
const cssLink = kv.css;
|
||||||
if (cssLink) {
|
if (cssLink && !cssLoaded) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/css?url=${encodeURIComponent(cssLink)}`);
|
const res = await fetch(`/api/css?url=${encodeURIComponent(cssLink)}`);
|
||||||
if (!res.ok) throw new Error("Failed to fetch CSS");
|
if (!res.ok) throw new Error("Failed to fetch CSS");
|
||||||
|
@ -405,6 +503,7 @@ async function updatePresence(initialData) {
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.textContent = cssText;
|
style.textContent = cssText;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
cssLoaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load CSS", err);
|
console.error("Failed to load CSS", err);
|
||||||
}
|
}
|
||||||
|
@ -462,6 +561,10 @@ async function updatePresence(initialData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClanBadge(data);
|
updateClanBadge(data);
|
||||||
|
if (kv.reviews !== "false") {
|
||||||
|
populateReviews(userId, 1);
|
||||||
|
setupReviewScrollObserver(userId);
|
||||||
|
}
|
||||||
|
|
||||||
const platform = {
|
const platform = {
|
||||||
mobile: data.active_on_discord_mobile,
|
mobile: data.active_on_discord_mobile,
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { badgeApi, lanyardConfig, plausibleScript } from "@config/environment";
|
import {
|
||||||
|
badgeApi,
|
||||||
|
lanyardConfig,
|
||||||
|
plausibleScript,
|
||||||
|
reviewDb,
|
||||||
|
} from "@config/environment";
|
||||||
import { file } from "bun";
|
import { file } from "bun";
|
||||||
|
|
||||||
const routeDef: RouteDef = {
|
const routeDef: RouteDef = {
|
||||||
|
@ -24,6 +29,10 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
head.setAttribute("data-instance-uri", instance);
|
head.setAttribute("data-instance-uri", instance);
|
||||||
head.setAttribute("data-badge-url", badgeApi || "");
|
head.setAttribute("data-badge-url", badgeApi || "");
|
||||||
|
|
||||||
|
if (reviewDb.enabled) {
|
||||||
|
head.setAttribute("data-review-db", reviewDb.url);
|
||||||
|
}
|
||||||
|
|
||||||
if (plausibleScript) {
|
if (plausibleScript) {
|
||||||
head.append(plausibleScript, { html: true });
|
head.append(plausibleScript, { html: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,12 @@
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<section class="reviews hidden" aria-label="User Reviews">
|
||||||
|
<h2>User Reviews</h2>
|
||||||
|
<ul class="reviews-list">
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script src="/public/js/index.js" type="module"></script>
|
<script src="/public/js/index.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Add table
Reference in a new issue