Rework Discord: dedicated /discord page + Lanyard friend cards, top-left nav, drop corner badges, merge player.css (#2)
This commit is contained in:
parent
fb91d15607
commit
414eddea42
|
|
@ -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>
|
||||
|
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
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);
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue