Rework Discord: dedicated /discord page + Lanyard friend cards, top-left nav, drop corner badges, merge player.css (#2)

This commit is contained in:
Clove 2026-06-16 09:47:36 +01:00
parent fb91d15607
commit 414eddea42
2 changed files with 322 additions and 0 deletions

74
discord/index.html Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clove Twilight - Discord</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now." />
<meta name="keywords" content="Portfolio, Personal, Developer, Discord, presence, Lanyard" />
<meta name="author" content="doughmination" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<!-- Canonical URL -->
<link rel="canonical" href="https://clove.is-a.dev/discord" />
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://clove.is-a.dev/discord" />
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7" />
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://clove.is-a.dev/assets/favicon/favicon.png" />
<meta property="og:site_name" content="clove.is-a.dev" />
<meta property="og:title" content="Clove Twilight | Discord" />
<meta property="og:description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://clove.is-a.dev/discord" />
<meta property="og:locale" content="en_GB" />
<!-- Twitter Card -->
<meta name="twitter:image" content="https://clove.is-a.dev/assets/favicon/favicon.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Clove Twilight | Discord" />
<meta name="twitter:description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now." />
</head>
<body>
<header class="nav">
<nav class="nav-links">
<a class="nav-link" data-href="/">Link Center</a>
<a class="nav-link" data-href="/cool-people">Cool People</a>
<a class="nav-link" data-href="/dev-info">Dev Info</a>
<a class="nav-link selected" data-href="/discord">Discord</a>
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
<a class="nav-link" data-href="/music">Music</a>
<a class="nav-link" data-href="/88x31">88x31</a>
</nav>
</header>
<main class="presence-stage">
<div class="presence-intro">
<h1>Discord</h1>
<p>What fae is up to, live via Lanyard.</p>
</div>
<div id="now-playing"></div>
</main>
<script src="/js/cat.js" data-cat="/assets/oneko/classics/classic.png"></script>
<script src="/js/nav.js"></script>
<script src="/js/now-playing.js" data-user="1464890289922641993"></script>
<script src="/js/flavors.js"></script>
<script src="/js/dev-mode.js"></script>
<script src="/js/site-switcher.js"></script>
</body>
</html>

248
js/friends.js Normal file
View File

@ -0,0 +1,248 @@
(function friends() {
"use strict";
// =====================================================================
// FRIENDS CONFIG ✏️ EDIT ME
// ---------------------------------------------------------------------
// Each friend is one card. Fields:
// name (required) display name
// img (required) static fallback image (used until/unless Lanyard
// gives a live avatar, and forever if they're not
// in the Lanyard server or the API is down)
// discordId (optional) Discord user ID. If set AND they're in the
// Lanyard server (https://discord.gg/lanyard),
// the card goes live: real avatar, status dot
// (green/idle/dnd/offline), badges + Nitro banner.
// Leave null for friends who aren't in the server —
// they get a blue "not connected" dot + static img.
// link (optional) where the card links to (their site, etc.)
//
// To make Camilla (etc.) live, paste her Discord ID into discordId.
// IMPORTANT: keep IDs as STRINGS in quotes ("123..."), not bare numbers —
// Discord IDs are too big for a JS number and get rounded (wrong user!).
// =====================================================================
var FRIENDS = [
{
title: "Fiancée",
members: [
{ name: "Aria", img: "/assets/friends/ari.png", tier: "wife", discordId: "1305215902685597797", link: null }
]
},
{
title: "Close Friends",
members: [
{ name: "Ria", img: "/assets/friends/ria.png", tier: "close", discordId: "1513506390088618145", link: null },
{ name: "Camilla", img: "/assets/friends/camilla.png", tier: "close", discordId: "1110542429838397471", link: "https://cammy-the-cat.com" },
{ name: "Saphie", img: "/assets/friends/saphie.png", tier: "close", discordId: "527709099186716673", link: null },
{ name: "Ari", img: "/assets/friends/meowhem.png", tier: "close", discordId: "1474568910736199825", link: "https://a.stupid.cat" }
]
},
{
title: "Friends",
members: [
{ name: "Fin", img: "/assets/friends/fin.png", tier: "friend", discordId: "867818211574808607", link: null },
{ name: "Meme", img: "/assets/friends/meme.png", tier: "friend", discordId: "812998699667161098", link: null },
{ name: "N", img: "/assets/friends/n.png", tier: "friend", discordId: "639399972407869450", link: null },
{ name: "Lylla", img: "/assets/friends/lylla.png", tier: "friend", discordId: "1009889543878611016", link: null },
{ name: "Simon", img: "/assets/friends/simon.png", tier: "friend", discordId: "758466783354814514", link: null }
]
},
{
title: "Other Peeps",
subtitle: "You can request to be added here!",
members: [
{ name: "Aureal", img: "/assets/friends/aureal.gif", tier: "known", discordId: "1498977251134279900", link: "https://aureal.dev/" }
]
}
];
var REFRESH_MS = 60000; // re-poll live friends once a minute
var root = document.getElementById("friends-root");
if (!root) return;
// ---- helpers (mirrors now-playing.js) -------------------------------
function esc(str) {
return String(str == null ? "" : str)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function intToHex(n) {
return "#" + (Number(n) >>> 0).toString(16).padStart(6, "0").slice(-6);
}
// Re-serve Discord CDN images cookieless via wsrv.nl (same trick as the
// presence card) so no third-party cookies are set.
function proxyImg(url, opts) {
if (!url) return url;
if (!/^https:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(url)) return url;
var src = url.replace(/^https:\/\//, "");
var q = "https://wsrv.nl/?url=" + encodeURIComponent(src) + "&output=webp";
if (opts && opts.w) q += "&w=" + opts.w + "&dpr=2&fit=cover";
return q;
}
function avatarUrl(u) {
if (!u || !u.avatar) {
var def = (Number(u && u.discriminator) || 0) % 5;
return proxyImg("https://cdn.discordapp.com/embed/avatars/" + def + ".png");
}
var ext = String(u.avatar).startsWith("a_") ? "gif" : "png";
return proxyImg("https://cdn.discordapp.com/avatars/" + u.id + "/" + u.avatar + "." + ext + "?size=128", { w: 96 });
}
function bannerUrl(id, hash) {
if (!id || !hash) return null;
var ext = String(hash).startsWith("a_") ? "gif" : "png";
return proxyImg("https://cdn.discordapp.com/banners/" + id + "/" + hash + "." + ext + "?size=480", { w: 480 });
}
// dstn.to badge list → small icon row (same source the presence card uses)
function renderDstnBadges(badges) {
if (!Array.isArray(badges)) return "";
return badges.map(function (b) {
var img = '<img class="fc-badge" src="' +
proxyImg("https://cdn.discordapp.com/badge-icons/" + esc(b.icon) + ".png") +
'" alt="' + esc(b.description || b.id) + '" title="' + esc(b.description || b.id) +
'" onerror="this.remove()">';
return b.link
? '<a class="fc-badge-link" href="' + esc(b.link) + '" target="_blank" rel="noopener">' + img + "</a>"
: img;
}).join("");
}
// ---- build one card -------------------------------------------------
function buildCard(m) {
var card = document.createElement("article");
card.className = "friend-card" + (m.tier ? " tier-" + m.tier : "");
card.dataset.status = "unconnected"; // until proven otherwise
var nameTag = m.link ? "a" : "span";
var nameAttrs = m.link ? ' href="' + esc(m.link) + '" target="_blank" rel="noopener"' : "";
card.innerHTML =
'<div class="fc-banner"></div>' +
'<div class="fc-main">' +
'<span class="fc-avatar">' +
'<img class="fc-pfp" src="' + esc(m.img) + '" alt="' + esc(m.name) + '" loading="lazy">' +
'<span class="fc-status" title="not connected to Lanyard"></span>' +
'</span>' +
'<span class="fc-id">' +
'<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '</' + nameTag + '>' +
'<span class="fc-user"></span>' +
'<span class="fc-badges"></span>' +
'</span>' +
'</div>';
return {
el: card,
pfp: card.querySelector(".fc-pfp"),
statusDot: card.querySelector(".fc-status"),
user: card.querySelector(".fc-user"),
badges: card.querySelector(".fc-badges"),
banner: card.querySelector(".fc-banner")
};
}
var STATUS_TITLE = {
online: "Online",
idle: "Idle",
dnd: "Do Not Disturb",
offline: "Offline"
};
// ---- live data: Lanyard (status + avatar) ---------------------------
function loadLanyard(m, refs) {
return fetch("https://api.lanyard.rest/v1/users/" + m.discordId)
.then(function (r) { return r.json().catch(function () { return null; }); })
.then(function (j) {
if (!j || !j.success || !j.data) {
// in config but not in the Lanyard server → stays "unconnected"
return false;
}
var d = j.data;
var u = d.discord_user || {};
var status = d.discord_status || "offline";
refs.el.dataset.status = status;
refs.statusDot.title = STATUS_TITLE[status] || "Offline";
if (u.avatar) refs.pfp.src = avatarUrl(u);
if (u.username) refs.user.textContent = "@" + u.username;
return true;
})
.catch(function () {
// API down / network error → static fallback, blue dot
return false;
});
}
// ---- live data: dstn.to (badges + Nitro banner) ---------------------
function loadDstn(m, refs) {
return fetch("https://dcdn.dstn.to/profile/" + m.discordId)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (j) {
if (!j) return;
var u = j.user || {};
var prof = j.user_profile || {};
if (Array.isArray(j.badges) && j.badges.length) {
refs.badges.innerHTML = renderDstnBadges(j.badges);
}
var bUrl = bannerUrl(u.id || m.discordId, prof.banner || u.banner);
if (bUrl) {
refs.banner.style.backgroundImage = "url('" + bUrl + "')";
refs.el.classList.add("has-banner");
} else if (u.banner_color) {
refs.banner.style.background = u.banner_color;
refs.el.classList.add("has-banner");
} else if (typeof u.accent_color === "number") {
refs.banner.style.background = intToHex(u.accent_color);
refs.el.classList.add("has-banner");
}
})
.catch(function () {});
}
function refreshMember(m, refs) {
loadLanyard(m, refs).then(function (live) {
if (live) loadDstn(m, refs);
});
}
// ---- render ---------------------------------------------------------
var liveMembers = []; // {m, refs} for those with an id, for polling
FRIENDS.forEach(function (group) {
var section = document.createElement("section");
section.className = "section";
var h2 = document.createElement("h2");
h2.className = "section-title";
h2.textContent = group.title;
section.appendChild(h2);
if (group.subtitle) {
var sub = document.createElement("p");
sub.className = "section-subtitle";
sub.textContent = group.subtitle;
section.appendChild(sub);
}
var grid = document.createElement("div");
grid.className = "friend-grid";
group.members.forEach(function (m) {
var refs = buildCard(m);
grid.appendChild(refs.el);
if (m.discordId) {
liveMembers.push({ m: m, refs: refs });
refreshMember(m, refs);
}
});
section.appendChild(grid);
root.appendChild(section);
});
// ---- poll live members ----------------------------------------------
if (liveMembers.length) {
setInterval(function () {
if (document.hidden) return;
liveMembers.forEach(function (x) { refreshMember(x.m, x.refs); });
}, REFRESH_MS);
}
})();