diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6596881..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.md b/README.md index 81bfda2..48d4c50 100644 --- a/README.md +++ b/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. - -## 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) +E diff --git a/biome.json b/biome.json index fa06b32..921a7a5 100644 --- a/biome.json +++ b/biome.json @@ -17,12 +17,6 @@ "organizeImports": { "enabled": true }, - "css": { - "formatter": { - "indentStyle": "tab", - "lineEnding": "lf" - } - }, "linter": { "enabled": true, "rules": { diff --git a/package.json b/package.json index 4627118..793fe71 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "bun run src/index.ts", "dev": "bun run --hot src/index.ts --dev", - "lint": "bunx biome ci . --verbose", + "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", "cleanup": "rm -rf logs node_modules bun.lockdb" }, diff --git a/public/css/index.css b/public/css/index.css index ac5f70f..1d8ad00 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -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 { font-family: system-ui, sans-serif; background-color: var(--background); 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 { - 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("")}`; -} diff --git a/src/routes/api/colors.ts b/src/routes/api/colors.ts index ef97012..9e05bd3 100644 --- a/src/routes/api/colors.ts +++ b/src/routes/api/colors.ts @@ -1,4 +1,7 @@ -import { getImageColors } from "@helpers/colors"; +import { fetch } from "bun"; +import { Vibrant } from "node-vibrant/node"; + +type Palette = Awaited>; const routeDef: RouteDef = { method: "GET", @@ -9,21 +12,46 @@ const routeDef: RouteDef = { async function handler(request: ExtendedRequest): Promise { const { url } = request.query; - const result: ImageColorResult | null = await getImageColors(url, true); - await getImageColors(url); - - if (!result) { - return new Response("Invalid URL", { - status: 400, - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "Access-Control-Allow-Origin": "*", - }, - }); + if (!url) { + return Response.json({ error: "URL is required" }, { status: 400 }); } - 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, { headers: { diff --git a/src/routes/index.ts b/src/routes/index.ts index b0d6a61..a15fa81 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,4 +1,3 @@ -import { getImageColors } from "@/helpers/colors"; import { lanyardConfig } from "@config/environment"; import { renderEjsTemplate } from "@helpers/ejs"; import { getLanyardData, handleReadMe } from "@helpers/lanyard"; @@ -38,14 +37,6 @@ async function handler(): Promise { 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 = { title: presence.discord_user.global_name || presence.discord_user.username, username: @@ -62,7 +53,6 @@ async function handler(): Promise { readme, allowSnow: presence.kv.snow === "true", allowRain: presence.kv.rain === "true", - colors: colors?.colors ?? {}, }; return await renderEjsTemplate("index", ejsTemplateData); diff --git a/src/views/index.ejs b/src/views/index.ejs index 0db4f4b..77fadd4 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -24,8 +24,6 @@ -<%- include('partial/style.ejs') %> -
diff --git a/src/views/partial/style.ejs b/src/views/partial/style.ejs deleted file mode 100644 index cccf3a2..0000000 --- a/src/views/partial/style.ejs +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/types/routes.d.ts b/types/routes.d.ts index 6af6a4b..9d9d809 100644 --- a/types/routes.d.ts +++ b/types/routes.d.ts @@ -13,10 +13,3 @@ type RouteModule = { ) => Promise | Response; routeDef: RouteDef; }; - -type Palette = Awaited>; -type Swatch = Awaited>; -type ImageColorResult = { - img: string; - colors: Palette | Record; -};