Compare commits
No commits in common. "7f9f166f8af0f4ce4c35e065e11ead0b7a2d4d08" and "30e9057ba87f950e356037cbce66f466892843af" have entirely different histories.
7f9f166f8a
...
30e9057ba8
11 changed files with 76 additions and 227 deletions
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 [creations.works]
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
88
README.md
88
README.md
|
@ -1,87 +1,3 @@
|
||||||
# Discord Profile Page
|
# Cool little discord profile page
|
||||||
|
|
||||||
A cool little web app that shows your Discord profile, current activity, and more. Built with Bun and EJS.
|
E
|
||||||
|
|
||||||
## Prerequisite: Lanyard Backend
|
|
||||||
|
|
||||||
This project depends on a self-hosted or public [Lanyard](https://github.com/Phineas/lanyard) instance for Discord presence data.
|
|
||||||
|
|
||||||
Make sure Lanyard is running and accessible before using this profile page.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### 1. Clone & Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.creations.works/creations/profilePage.git
|
|
||||||
cd profilePage
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Environment
|
|
||||||
|
|
||||||
Copy the example environment file and update it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Required `.env` Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|--------------------|--------------------------------------------------|
|
|
||||||
| `HOST` | Host to bind the Bun server (default: `0.0.0.0`) |
|
|
||||||
| `PORT` | Port to run the server on (default: `8080`) |
|
|
||||||
| `LANYARD_USER_ID` | Your Discord user ID |
|
|
||||||
| `LANYARD_INSTANCE` | Lanyard WebSocket endpoint URL |
|
|
||||||
|
|
||||||
#### Optional Lanyard KV Vars (per-user customization)
|
|
||||||
|
|
||||||
These are expected to be defined in Lanyard's KV store:
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|-----------|-------------------------------------------------------------|
|
|
||||||
| `snow` | Enables snow background effect (`true`) |
|
|
||||||
| `rain` | Enables rain background effect (`true`) |
|
|
||||||
| `readme` | URL to a README file displayed on your profile |
|
|
||||||
| `colors` | Enables avatar-based color theme (uses `node-vibrant`) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Start the App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open `http://localhost:8080` in your browser.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker Support
|
|
||||||
|
|
||||||
### Build & Start with Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure your `.env` file is correctly configured before starting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- Bun – Runtime
|
|
||||||
- EJS – Templating
|
|
||||||
- CSS – Styling
|
|
||||||
- node-vibrant – Avatar color extraction
|
|
||||||
- Biome.js – Linting and formatting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](/LICENSE)
|
|
||||||
|
|
|
@ -17,12 +17,6 @@
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"css": {
|
|
||||||
"formatter": {
|
|
||||||
"indentStyle": "tab",
|
|
||||||
"lineEnding": "lf"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
"dev": "bun run --hot src/index.ts --dev",
|
"dev": "bun run --hot src/index.ts --dev",
|
||||||
"lint": "bunx biome ci . --verbose",
|
"lint": "bunx biome check",
|
||||||
"lint:fix": "bunx biome check --fix",
|
"lint:fix": "bunx biome check --fix",
|
||||||
"cleanup": "rm -rf logs node_modules bun.lockdb"
|
"cleanup": "rm -rf logs node_modules bun.lockdb"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,34 @@
|
||||||
|
:root {
|
||||||
|
--background: #0e0e10;
|
||||||
|
--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;
|
||||||
|
|
||||||
|
--status-online: #23a55a;
|
||||||
|
--status-idle: #f0b232;
|
||||||
|
--status-dnd: #e03e3e;
|
||||||
|
--status-offline: #747f8d;
|
||||||
|
--status-streaming: #b700ff;
|
||||||
|
|
||||||
|
--progress-bg: #f23f43;
|
||||||
|
--progress-fill: #5865f2;
|
||||||
|
|
||||||
|
--button-bg: #5865f2;
|
||||||
|
--button-hover-bg: #4752c4;
|
||||||
|
--button-disabled-bg: #2d2e31;
|
||||||
|
|
||||||
|
--blockquote-color: #aaa;
|
||||||
|
--code-bg: #2e2e30;
|
||||||
|
|
||||||
|
--readme-bg: #1a1a1d;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { fetch } from "bun";
|
|
||||||
import { Vibrant } from "node-vibrant/node";
|
|
||||||
|
|
||||||
export async function getImageColors(
|
|
||||||
url: string,
|
|
||||||
hex?: boolean,
|
|
||||||
): Promise<ImageColorResult | null> {
|
|
||||||
if (!url) return null;
|
|
||||||
|
|
||||||
if (typeof url !== "string" || !url.startsWith("http")) return null;
|
|
||||||
|
|
||||||
let res: Response;
|
|
||||||
try {
|
|
||||||
res = await fetch(url);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
const type: string | null = res.headers.get("content-type");
|
|
||||||
if (!type?.startsWith("image/")) return null;
|
|
||||||
|
|
||||||
const buffer: Buffer = Buffer.from(await res.arrayBuffer());
|
|
||||||
const base64: string = buffer.toString("base64");
|
|
||||||
const colors: Palette = await Vibrant.from(buffer).getPalette();
|
|
||||||
|
|
||||||
return {
|
|
||||||
img: `data:${type};base64,${base64}`,
|
|
||||||
colors: hex
|
|
||||||
? {
|
|
||||||
Muted: rgbToHex(safeRgb(colors.Muted)),
|
|
||||||
LightVibrant: rgbToHex(safeRgb(colors.LightVibrant)),
|
|
||||||
Vibrant: rgbToHex(safeRgb(colors.Vibrant)),
|
|
||||||
LightMuted: rgbToHex(safeRgb(colors.LightMuted)),
|
|
||||||
DarkVibrant: rgbToHex(safeRgb(colors.DarkVibrant)),
|
|
||||||
DarkMuted: rgbToHex(safeRgb(colors.DarkMuted)),
|
|
||||||
}
|
|
||||||
: colors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeRgb(swatch: Swatch | null | undefined): number[] {
|
|
||||||
return Array.isArray(swatch?.rgb) ? (swatch.rgb ?? [0, 0, 0]) : [0, 0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rgbToHex(rgb: number[]): string {
|
|
||||||
return `#${rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`;
|
|
||||||
}
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { getImageColors } from "@helpers/colors";
|
import { fetch } from "bun";
|
||||||
|
import { Vibrant } from "node-vibrant/node";
|
||||||
|
|
||||||
|
type Palette = Awaited<ReturnType<typeof Vibrant.prototype.getPalette>>;
|
||||||
|
|
||||||
const routeDef: RouteDef = {
|
const routeDef: RouteDef = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -9,21 +12,46 @@ const routeDef: RouteDef = {
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
const { url } = request.query;
|
const { url } = request.query;
|
||||||
|
|
||||||
const result: ImageColorResult | null = await getImageColors(url, true);
|
if (!url) {
|
||||||
await getImageColors(url);
|
return Response.json({ error: "URL is required" }, { status: 400 });
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return new Response("Invalid URL", {
|
|
||||||
status: 400,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Cache-Control": "no-store",
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const compressed: Uint8Array = Bun.gzipSync(JSON.stringify(result));
|
if (typeof url !== "string" || !url.startsWith("http")) {
|
||||||
|
return Response.json({ error: "Invalid URL" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await fetch(url);
|
||||||
|
} catch {
|
||||||
|
return Response.json({ error: "Failed to fetch image" }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Image fetch returned error" },
|
||||||
|
{ status: res.status },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type: string | null = res.headers.get("content-type");
|
||||||
|
if (!type?.startsWith("image/")) {
|
||||||
|
return Response.json({ error: "Not an image" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: Buffer = Buffer.from(await res.arrayBuffer());
|
||||||
|
const base64: string = buffer.toString("base64");
|
||||||
|
const colors: Palette = await Vibrant.from(buffer).getPalette();
|
||||||
|
|
||||||
|
const payload: {
|
||||||
|
img: string;
|
||||||
|
colors: Palette;
|
||||||
|
} = {
|
||||||
|
img: `data:${type};base64,${base64}`,
|
||||||
|
colors,
|
||||||
|
};
|
||||||
|
|
||||||
|
const compressed: Uint8Array = Bun.gzipSync(JSON.stringify(payload));
|
||||||
|
|
||||||
return new Response(compressed, {
|
return new Response(compressed, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getImageColors } from "@/helpers/colors";
|
|
||||||
import { lanyardConfig } from "@config/environment";
|
import { lanyardConfig } from "@config/environment";
|
||||||
import { renderEjsTemplate } from "@helpers/ejs";
|
import { renderEjsTemplate } from "@helpers/ejs";
|
||||||
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
|
import { getLanyardData, handleReadMe } from "@helpers/lanyard";
|
||||||
|
@ -38,14 +37,6 @@ async function handler(): Promise<Response> {
|
||||||
status = presence.discord_status;
|
status = presence.discord_status;
|
||||||
}
|
}
|
||||||
|
|
||||||
let colors: ImageColorResult | null = null;
|
|
||||||
if (presence.kv.colors === "true") {
|
|
||||||
const avatar: string = presence.discord_user.avatar
|
|
||||||
? `https://cdn.discordapp.com/avatars/${presence.discord_user.id}/${presence.discord_user.avatar}`
|
|
||||||
: `https://cdn.discordapp.com/embed/avatars/${presence.discord_user.discriminator || 1 % 5}`;
|
|
||||||
colors = await getImageColors(avatar, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ejsTemplateData: EjsTemplateData = {
|
const ejsTemplateData: EjsTemplateData = {
|
||||||
title: presence.discord_user.global_name || presence.discord_user.username,
|
title: presence.discord_user.global_name || presence.discord_user.username,
|
||||||
username:
|
username:
|
||||||
|
@ -62,7 +53,6 @@ async function handler(): Promise<Response> {
|
||||||
readme,
|
readme,
|
||||||
allowSnow: presence.kv.snow === "true",
|
allowSnow: presence.kv.snow === "true",
|
||||||
allowRain: presence.kv.rain === "true",
|
allowRain: presence.kv.rain === "true",
|
||||||
colors: colors?.colors ?? {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return await renderEjsTemplate("index", ejsTemplateData);
|
return await renderEjsTemplate("index", ejsTemplateData);
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<%- include('partial/style.ejs') %>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
<div class="avatar-status-wrapper">
|
<div class="avatar-status-wrapper">
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--background: <%= colors.DarkVibrant || '#0e0e10' %>;
|
|
||||||
--readme-bg: <%= colors.DarkMuted || '#1a1a1d' %>;
|
|
||||||
--card-bg: <%= colors.DarkMuted || '#1e1f22' %>;
|
|
||||||
--card-hover-bg: <%= colors.Muted || '#2a2a2d' %>;
|
|
||||||
--border-color: <%= colors.Muted || '#2e2e30' %>;
|
|
||||||
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--text-subtle: #bbb;
|
|
||||||
--text-secondary: #b5bac1;
|
|
||||||
--text-muted: #888;
|
|
||||||
--link-color: <%= colors.Vibrant || '#00b0f4' %>;
|
|
||||||
|
|
||||||
--button-bg: <%= colors.Vibrant || '#5865f2' %>;
|
|
||||||
--button-hover-bg: <%= colors.LightVibrant || '#4752c4' %>;
|
|
||||||
--button-disabled-bg: #2d2e31;
|
|
||||||
|
|
||||||
--progress-bg: <%= colors.DarkVibrant || '#f23f43' %>;
|
|
||||||
--progress-fill: <%= colors.Vibrant || '#5865f2' %>;
|
|
||||||
|
|
||||||
--status-online: #23a55a;
|
|
||||||
--status-idle: #f0b232;
|
|
||||||
--status-dnd: #e03e3e;
|
|
||||||
--status-offline: #747f8d;
|
|
||||||
--status-streaming: #b700ff;
|
|
||||||
|
|
||||||
--blockquote-color: #aaa;
|
|
||||||
--code-bg: <%= colors.Muted || '#2e2e30' %>;
|
|
||||||
}
|
|
||||||
</style>
|
|
7
types/routes.d.ts
vendored
7
types/routes.d.ts
vendored
|
@ -13,10 +13,3 @@ type RouteModule = {
|
||||||
) => Promise<Response> | Response;
|
) => Promise<Response> | Response;
|
||||||
routeDef: RouteDef;
|
routeDef: RouteDef;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Palette = Awaited<ReturnType<typeof Vibrant.prototype.getPalette>>;
|
|
||||||
type Swatch = Awaited<ReturnType<typeof Vibrant.prototype.getSwatches>>;
|
|
||||||
type ImageColorResult = {
|
|
||||||
img: string;
|
|
||||||
colors: Palette | Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue