diff --git a/discord/index.html b/discord/index.html new file mode 100644 index 0000000..5681005 --- /dev/null +++ b/discord/index.html @@ -0,0 +1,74 @@ + + + + + + + Clove Twilight - Discord + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Discord

+

What fae is up to, live via Lanyard.

+
+ +
+
+ + + + + + + + + + diff --git a/js/friends.js b/js/friends.js new file mode 100644 index 0000000..7c0a314 --- /dev/null +++ b/js/friends.js @@ -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, """); + } + 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 = '' + esc(b.description || b.id) + ''; + return b.link + ? '' + img + "" + : 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 = + '
' + + '
' + + '' + + '' + esc(m.name) + '' + + '' + + '' + + '' + + '<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '' + + '' + + '' + + '' + + '
'; + + 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); + } +})();