diff --git a/README.md b/README.md index 396d1b3..a7a1269 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ These can be defined in Lanyard's KV store to customize the page: | `stars` | Enables starfield background (`true` / `false`) | | `badges` | Enables badge fetching (`true` / `false`) | | `readme` | URL to a README displayed on the profile (`.md` or `.html`) | -| `colors` | Enables avatar-based color theming (uses `node-vibrant`) | +| `css` | URL to a css to change styles on the page, no import or require allowed | --- diff --git a/biome.json b/biome.json index fa06b32..46ee8c9 100644 --- a/biome.json +++ b/biome.json @@ -26,7 +26,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "correctness": { + "noUnusedImports": "error" + } } }, "javascript": { diff --git a/package.json b/package.json index 1a81a77..96f4946 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@creations.works/logger": "^1.0.3", "ejs": "^3.1.10", "isomorphic-dompurify": "^2.23.0", - "marked": "^15.0.7", - "node-vibrant": "^4.0.3" + "marked": "^15.0.7" } } diff --git a/public/css/index.css b/public/css/index.css index b10bdc8..145949e 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -40,6 +40,39 @@ } } +#loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + + transition: opacity 0.5s ease; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 5px solid var(--border-color); + border-top: 5px solid var(--progress-fill); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + /* actual styles below */ body { font-family: system-ui, sans-serif; diff --git a/public/css/root.css b/public/css/root.css new file mode 100644 index 0000000..dec65f3 --- /dev/null +++ b/public/css/root.css @@ -0,0 +1,29 @@ +:root { + --background: #0e0e10; + --readme-bg: #1a1a1d; + --card-bg: #1e1f22; + --card-hover-bg: #2a2a2d; + --border-color: #2e2e30; + + --text-color: #ffffff; + --text-subtle: #bbb; + --text-secondary: #b5bac1; + --text-muted: #888; + --link-color: #00b0f4; + + --button-bg: #5865f2; + --button-hover-bg: #4752c4; + --button-disabled-bg: #2d2e31; + + --progress-bg: #f23f43; + --progress-fill: #5865f2; + + --status-online: #23a55a; + --status-idle: #f0b232; + --status-dnd: #e03e3e; + --status-offline: #747f8d; + --status-streaming: #b700ff; + + --blockquote-color: #aaa; + --code-bg: #2e2e30; +} diff --git a/public/js/index.js b/public/js/index.js index 0181d5c..0dcd03e 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -353,13 +353,45 @@ if (badgeURL && badgeURL !== "null" && userId) { }); } -function updatePresence(data) { - const avatarWrapper = document.querySelector(".avatar-wrapper"); - const statusIndicator = avatarWrapper?.querySelector(".status-indicator"); - const mobileIcon = avatarWrapper?.querySelector(".platform-icon.mobile-only"); +async function updatePresence(data) { + const cssLink = data.kv?.css; - const userInfo = document.querySelector(".user-info"); - const customStatus = userInfo?.querySelector(".custom-status"); + if (cssLink) { + try { + const res = await fetch(`/api/css?url=${encodeURIComponent(cssLink)}`); + if (!res.ok) throw new Error("Failed to fetch CSS"); + + const cssText = await res.text(); + const style = document.createElement("style"); + style.textContent = cssText; + document.head.appendChild(style); + } catch (err) { + console.error("Failed to load CSS", err); + } + } + + const avatarWrapper = document.querySelector(".avatar-wrapper"); + + const avatarImg = document.querySelector(".avatar-wrapper .avatar"); + const usernameEl = document.querySelector(".username"); + + if (avatarImg && data.discord_user?.avatar) { + const newAvatarUrl = `https://cdn.discordapp.com/avatars/${data.discord_user.id}/${data.discord_user.avatar}`; + avatarImg.src = newAvatarUrl; + avatarImg.classList.remove("hidden"); + + const siteIcon = document.getElementById("site-icon"); + + if (siteIcon) { + siteIcon.href = newAvatarUrl; + } + } + if (usernameEl) { + const username = + data.discord_user.global_name || data.discord_user.username; + usernameEl.textContent = username; + document.title = username; + } const platform = { mobile: data.active_on_discord_mobile, @@ -374,37 +406,51 @@ function updatePresence(data) { status = data.discord_status; } - if (statusIndicator) { - statusIndicator.className = `status-indicator ${status}`; - } + let updatedStatusIndicator = avatarWrapper.querySelector(".status-indicator"); + const updatedMobileIcon = avatarWrapper.querySelector( + ".platform-icon.mobile-only", + ); - if (platform.mobile && !mobileIcon) { + if (platform.mobile && !updatedMobileIcon) { avatarWrapper.innerHTML += ` `; - } else if (!platform.mobile && mobileIcon) { - mobileIcon.remove(); + } else if (!platform.mobile && updatedMobileIcon) { + updatedMobileIcon.remove(); avatarWrapper.innerHTML += `
`; } - const custom = data.activities?.find((a) => a.type === 4); - if (customStatus && custom) { - let emojiHTML = ""; - const emoji = custom.emoji; - if (emoji?.id) { - const emojiUrl = `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? "gif" : "png"}`; - emojiHTML = `+ ${emojiHTML}${custom.state ? `${custom.state}` : ""} +
+ `; + + if (customStatus) { + customStatus.outerHTML = html; + } else { + userInfoInner.insertAdjacentHTML("beforeend", html); + } + } else if (customStatus) { + customStatus.remove(); + } } async function getAllNoAsset() { @@ -454,3 +541,13 @@ async function getAllNoAsset() { } } } + +function loadEffectScript(effect) { + const existing = document.querySelector(`script[data-effect="${effect}"]`); + if (existing) return; + + const script = document.createElement("script"); + script.src = `/public/js/${effect}.js`; + script.dataset.effect = effect; + document.head.appendChild(script); +} diff --git a/public/js/snow.js b/public/js/snow.js index 05048a8..e1027fc 100644 --- a/public/js/snow.js +++ b/public/js/snow.js @@ -1,84 +1,82 @@ -document.addEventListener("DOMContentLoaded", () => { - const snowContainer = document.createElement("div"); - snowContainer.style.position = "fixed"; - snowContainer.style.top = "0"; - snowContainer.style.left = "0"; - snowContainer.style.width = "100vw"; - snowContainer.style.height = "100vh"; - snowContainer.style.pointerEvents = "none"; - document.body.appendChild(snowContainer); +const snowContainer = document.createElement("div"); +snowContainer.style.position = "fixed"; +snowContainer.style.top = "0"; +snowContainer.style.left = "0"; +snowContainer.style.width = "100vw"; +snowContainer.style.height = "100vh"; +snowContainer.style.pointerEvents = "none"; +document.body.appendChild(snowContainer); - const maxSnowflakes = 60; - const snowflakes = []; - const mouse = { x: -100, y: -100 }; +const maxSnowflakes = 60; +const snowflakes = []; +const mouse = { x: -100, y: -100 }; - document.addEventListener("mousemove", (e) => { - mouse.x = e.clientX; - mouse.y = e.clientY; - }); +document.addEventListener("mousemove", (e) => { + mouse.x = e.clientX; + mouse.y = e.clientY; +}); - const createSnowflake = () => { - if (snowflakes.length >= maxSnowflakes) { - const oldestSnowflake = snowflakes.shift(); - snowContainer.removeChild(oldestSnowflake); - } - - const snowflake = document.createElement("div"); - snowflake.classList.add("snowflake"); - snowflake.style.position = "absolute"; - snowflake.style.width = `${Math.random() * 3 + 2}px`; - snowflake.style.height = snowflake.style.width; - snowflake.style.background = "white"; - snowflake.style.borderRadius = "50%"; - snowflake.style.opacity = Math.random(); - snowflake.style.left = `${Math.random() * window.innerWidth}px`; - snowflake.style.top = `-${snowflake.style.height}`; - snowflake.speed = Math.random() * 3 + 2; - snowflake.directionX = (Math.random() - 0.5) * 0.5; - snowflake.directionY = Math.random() * 0.5 + 0.5; - - snowflakes.push(snowflake); - snowContainer.appendChild(snowflake); - }; - - setInterval(createSnowflake, 80); - - function updateSnowflakes() { - snowflakes.forEach((snowflake, index) => { - const rect = snowflake.getBoundingClientRect(); - - const dx = rect.left + rect.width / 2 - mouse.x; - const dy = rect.top + rect.height / 2 - mouse.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < 30) { - snowflake.directionX += (dx / distance) * 0.02; - snowflake.directionY += (dy / distance) * 0.02; - } else { - snowflake.directionX += (Math.random() - 0.5) * 0.01; - snowflake.directionY += (Math.random() - 0.5) * 0.01; - } - - snowflake.style.left = `${rect.left + snowflake.directionX * snowflake.speed}px`; - snowflake.style.top = `${rect.top + snowflake.directionY * snowflake.speed}px`; - - if (rect.top + rect.height >= window.innerHeight) { - snowContainer.removeChild(snowflake); - snowflakes.splice(index, 1); - } - - if ( - rect.left > window.innerWidth || - rect.top > window.innerHeight || - rect.left < 0 - ) { - snowflake.style.left = `${Math.random() * window.innerWidth}px`; - snowflake.style.top = `-${snowflake.style.height}`; - } - }); - - requestAnimationFrame(updateSnowflakes); +const createSnowflake = () => { + if (snowflakes.length >= maxSnowflakes) { + const oldestSnowflake = snowflakes.shift(); + snowContainer.removeChild(oldestSnowflake); } - updateSnowflakes(); -}); + const snowflake = document.createElement("div"); + snowflake.classList.add("snowflake"); + snowflake.style.position = "absolute"; + snowflake.style.width = `${Math.random() * 3 + 2}px`; + snowflake.style.height = snowflake.style.width; + snowflake.style.background = "white"; + snowflake.style.borderRadius = "50%"; + snowflake.style.opacity = Math.random(); + snowflake.style.left = `${Math.random() * window.innerWidth}px`; + snowflake.style.top = `-${snowflake.style.height}`; + snowflake.speed = Math.random() * 3 + 2; + snowflake.directionX = (Math.random() - 0.5) * 0.5; + snowflake.directionY = Math.random() * 0.5 + 0.5; + + snowflakes.push(snowflake); + snowContainer.appendChild(snowflake); +}; + +setInterval(createSnowflake, 80); + +function updateSnowflakes() { + snowflakes.forEach((snowflake, index) => { + const rect = snowflake.getBoundingClientRect(); + + const dx = rect.left + rect.width / 2 - mouse.x; + const dy = rect.top + rect.height / 2 - mouse.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 30) { + snowflake.directionX += (dx / distance) * 0.02; + snowflake.directionY += (dy / distance) * 0.02; + } else { + snowflake.directionX += (Math.random() - 0.5) * 0.01; + snowflake.directionY += (Math.random() - 0.5) * 0.01; + } + + snowflake.style.left = `${rect.left + snowflake.directionX * snowflake.speed}px`; + snowflake.style.top = `${rect.top + snowflake.directionY * snowflake.speed}px`; + + if (rect.top + rect.height >= window.innerHeight) { + snowContainer.removeChild(snowflake); + snowflakes.splice(index, 1); + } + + if ( + rect.left > window.innerWidth || + rect.top > window.innerHeight || + rect.left < 0 + ) { + snowflake.style.left = `${Math.random() * window.innerWidth}px`; + snowflake.style.top = `-${snowflake.style.height}`; + } + }); + + requestAnimationFrame(updateSnowflakes); +} + +updateSnowflakes(); diff --git a/public/js/stars.js b/public/js/stars.js index 093b21a..d35d995 100644 --- a/public/js/stars.js +++ b/public/js/stars.js @@ -1,67 +1,65 @@ -document.addEventListener("DOMContentLoaded", () => { - const container = document.createElement("div"); - container.style.position = "fixed"; - container.style.top = "0"; - container.style.left = "0"; - container.style.width = "100vw"; - container.style.height = "100vh"; - container.style.pointerEvents = "none"; - container.style.overflow = "hidden"; - container.style.zIndex = "9999"; - document.body.appendChild(container); +const container = document.createElement("div"); +container.style.position = "fixed"; +container.style.top = "0"; +container.style.left = "0"; +container.style.width = "100vw"; +container.style.height = "100vh"; +container.style.pointerEvents = "none"; +container.style.overflow = "hidden"; +container.style.zIndex = "9999"; +document.body.appendChild(container); - for (let i = 0; i < 60; i++) { - const star = document.createElement("div"); - const size = Math.random() * 2 + 1; - star.style.position = "absolute"; - star.style.width = `${size}px`; - star.style.height = `${size}px`; - star.style.background = "white"; - star.style.borderRadius = "50%"; - star.style.opacity = Math.random(); - star.style.top = `${Math.random() * 100}vh`; - star.style.left = `${Math.random() * 100}vw`; - star.style.animation = `twinkle ${Math.random() * 3 + 2}s infinite alternate ease-in-out`; - container.appendChild(star); - } +for (let i = 0; i < 60; i++) { + const star = document.createElement("div"); + const size = Math.random() * 2 + 1; + star.style.position = "absolute"; + star.style.width = `${size}px`; + star.style.height = `${size}px`; + star.style.background = "white"; + star.style.borderRadius = "50%"; + star.style.opacity = Math.random(); + star.style.top = `${Math.random() * 100}vh`; + star.style.left = `${Math.random() * 100}vw`; + star.style.animation = `twinkle ${Math.random() * 3 + 2}s infinite alternate ease-in-out`; + container.appendChild(star); +} - function createShootingStar() { - const star = document.createElement("div"); - star.classList.add("shooting-star"); +function createShootingStar() { + const star = document.createElement("div"); + star.classList.add("shooting-star"); - let x = Math.random() * window.innerWidth * 0.8; - let y = Math.random() * window.innerHeight * 0.3; - const angle = (Math.random() * Math.PI) / 6 + Math.PI / 8; - const speed = 10; - const totalFrames = 60; + let x = Math.random() * window.innerWidth * 0.8; + let y = Math.random() * window.innerHeight * 0.3; + const angle = (Math.random() * Math.PI) / 6 + Math.PI / 8; + const speed = 10; + const totalFrames = 60; - const deg = angle * (180 / Math.PI); + const deg = angle * (180 / Math.PI); + star.style.left = `${x}px`; + star.style.top = `${y}px`; + star.style.transform = `rotate(${deg}deg)`; + + container.appendChild(star); + + let frame = 0; + function animate() { + x += Math.cos(angle) * speed; + y += Math.sin(angle) * speed; star.style.left = `${x}px`; star.style.top = `${y}px`; - star.style.transform = `rotate(${deg}deg)`; + star.style.opacity = `${1 - frame / totalFrames}`; - container.appendChild(star); - - let frame = 0; - function animate() { - x += Math.cos(angle) * speed; - y += Math.sin(angle) * speed; - star.style.left = `${x}px`; - star.style.top = `${y}px`; - star.style.opacity = `${1 - frame / totalFrames}`; - - frame++; - if (frame < totalFrames) { - requestAnimationFrame(animate); - } else { - container.removeChild(star); - } + frame++; + if (frame < totalFrames) { + requestAnimationFrame(animate); + } else { + container.removeChild(star); } - - animate(); } - setInterval(() => { - if (Math.random() < 0.3) createShootingStar(); - }, 1000); -}); + animate(); +} + +setInterval(() => { + if (Math.random() < 0.3) createShootingStar(); +}, 1000); diff --git a/src/helpers/colors.ts b/src/helpers/colors.ts deleted file mode 100644 index 43a74e7..0000000 --- a/src/helpers/colors.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { fetch } from "bun"; -import { Vibrant } from "node-vibrant/node"; - -export async function getImageColors( - url: string, - hex?: boolean, -): Promise