diff --git a/.env.example b/.env.example
index a10f7a4..d2b3d15 100644
--- a/.env.example
+++ b/.env.example
@@ -2,24 +2,6 @@
HOST=0.0.0.0
PORT=8080
-REDIS_URL=redis://dragonfly:6379
-REDIS_TTL=3600 # seconds
-
# this is only the default value if non is give in /id
LANYARD_USER_ID=id-here
LANYARD_INSTANCE=https://lanyard.rest
-
-# Required if you want to enable badges
-BADGE_API_URL=http://localhost:8081
-
-# Required if you want to enable reviews from reviewdb
-REVIEW_DB=true
-
-#Timezone api url, aka: https://git.creations.works/creations/timezoneDB
-TIMEZONE_API_URL=
-
-# https://www.steamgriddb.com/api/v2, if you want games to have images
-STEAMGRIDDB_API_KEY=steamgrid_api_key
-
-# https://plausible.io
-PLAUSIBLE_SCRIPT_HTML=''
diff --git a/.forgejo/workflows/biomejs.yml b/.forgejo/workflows/biomejs.yml
deleted file mode 100644
index 15c990c..0000000
--- a/.forgejo/workflows/biomejs.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: Code quality checks
-
-on:
- push:
- pull_request:
-
-jobs:
- biome:
- runs-on: docker
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install Bun
- run: |
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun"
- echo "$BUN_INSTALL/bin" >> $GITHUB_PATH
-
- - name: Install Dependencies
- run: bun install
-
- - name: Run Biome with verbose output
- run: bunx biome ci . --verbose
diff --git a/.gitignore b/.gitignore
index 16498de..d0ef245 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,4 @@
/node_modules
bun.lock
.env
-logs/
-.vscode/
-robots.txt
-public/custom
+logs/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..25117cc
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "github-enterprise.uri": "https://git.creations.works"
+}
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 2438f47..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,38 +0,0 @@
-# use the official Bun image
-# see all versions at https://hub.docker.com/r/oven/bun/tags
-FROM oven/bun:latest AS base
-WORKDIR /usr/src/app
-
-FROM base AS install
-RUN mkdir -p /temp/dev
-COPY package.json bun.lock /temp/dev/
-RUN cd /temp/dev && bun install --frozen-lockfile
-
-# install with --production (exclude devDependencies)
-RUN mkdir -p /temp/prod
-COPY package.json bun.lock /temp/prod/
-RUN cd /temp/prod && bun install --frozen-lockfile --production
-
-# copy node_modules from temp directory
-# then copy all (non-ignored) project files into the image
-FROM base AS prerelease
-COPY --from=install /temp/dev/node_modules node_modules
-COPY . .
-
-# [optional] tests & build
-ENV NODE_ENV=production
-
-# copy production dependencies and source code into final image
-FROM base AS release
-COPY --from=install /temp/prod/node_modules node_modules
-COPY --from=prerelease /usr/src/app/src ./src
-COPY --from=prerelease /usr/src/app/public ./public
-COPY --from=prerelease /usr/src/app/package.json .
-COPY --from=prerelease /usr/src/app/tsconfig.json .
-COPY --from=prerelease /usr/src/app/config ./config
-COPY --from=prerelease /usr/src/app/types ./types
-
-RUN mkdir -p /usr/src/app/logs && chown bun:bun /usr/src/app/logs
-
-USER bun
-ENTRYPOINT [ "bun", "run", "start" ]
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index d93a942..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,28 +0,0 @@
-BSD 3-Clause License
-
-Copyright (c) 2025, creations.works
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
-3. Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index f4b9394..48d4c50 100644
--- a/README.md
+++ b/README.md
@@ -1,145 +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.
-
-# Preview
-https://creations.works
-
----
-
-## Requirements
-
-This project depends on the following services to function properly:
-
-### 1. Lanyard Backend
-
-This project depends on a self-hosted or public [Lanyard](https://github.com/Phineas/lanyard) instance to fetch real-time Discord presence data.
-Make sure the Lanyard instance is running and accessible before using this.
-
-### 2. Redis Instance
-
-A Redis-compatible key-value store is required to cache third-party data (e.g., SteamGridDB icons).
-I recommend [Dragonfly](https://www.dragonflydb.io/), a high-performance drop-in replacement for Redis.
-
-### 3. Badge API
-
-A lightweight API to render Discord-style badges.
->Only needed if you want to show badges on profiles:
-https://git.creations.works/creations/badgeAPI
-
-### 4. SteamGridDB
-
->Only needed if you want to fetch game icons that Discord doesn’t provide:
-https://www.steamgriddb.com/api/v2
-
----
-
-## 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
-```
-
-#### `.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`) |
-| `REDIS_URL` | Redis connection string |
-| `LANYARD_USER_ID` | Your Discord user ID, for the default page |
-| `LANYARD_INSTANCE` | Endpoint of the Lanyard instance |
-| `BADGE_API_URL` | Badge API URL ([badgeAPI](https://git.creations.works/creations/badgeAPI)) |
-| `REVIEW_DB` | Enables showing reviews from reviewdb on user pages |
-| `TIMEZONE_API_URL` | Enables showing times from [timezoneDB](https://git.creations.works/creations/timezoneDB-rs) |
-| `STEAMGRIDDB_API_KEY` | SteamGridDB API key for fetching game icons |
-
-#### Optional Lanyard KV Variables (per-user customization)
-
-These can be defined in Lanyard's KV store to customize the page:
-
-| Variable | Description |
-|-----------|--------------------------------------------------------------------|
-| `snow` | Enables snow background (`true` / `false`) |
-| `rain` | Enables rain background (`true` / `false`) |
-| `stars` | Enables starfield background (`true` / `false`) |
-| `badges` | Enables badge fetching (`true` / `false`) |
-| `readme` | URL to a README displayed on the profile (`.md` or `.html`) |
-| `css` | URL to a css to change styles on the page, no import or require allowed |
-| `optout` | Allows users to stop sharing there profile on the website (`true` / `false`) |
-| `reviews` | Enables reviews from reviewdb (`true` / `false`) |
-| `timezone`| Enables the showing of the current time from the timezone db API (`true` / `false`) |
-| `timezone_12` | Sets the timezone to default show 12h format, default is 24h |
-
----
-
-### 3. Start the Instance
-
-```bash
-bun run start
-```
-
----
-
-## Optional: Analytics with Plausible
-
-You can enable [Plausible Analytics](https://plausible.io) tracking by setting a script snippet in your environment.
-
-### `.env` Variable
-
-| Variable | Description |
-|-------------------------|------------------------------------------------------------------------|
-| `PLAUSIBLE_SCRIPT_HTML` | Full `'
-```
-
-- The script will only be injected if this variable is set.
-- Plausible provides the correct script when you add a domain.
-- Be sure to wrap it in single quotes (`'`) so it works in `.env`.
-
----
-
-## Docker Support
-
-### Build & Start with Docker Compose
-
-```bash
-docker compose up -d --build
-```
-
-Make sure the `.env` file is configured correctly before starting the container.
-
----
-
-## Routes
-
-These are the main public routes exposed by the server:
-
-| Route | Description |
-|---------|-----------------------------------------------------------------------------|
-| `/` | Loads the profile page for the default Discord user defined in `.env` (`LANYARD_USER_ID`) |
-| `/[id]` | Loads the profile page for a specific Discord user ID passed in the URL |
-
-> Example: `https://creations.works/209830981060788225` shows the profile of that specific user.
-
----
-
-## License
-
-[BSD 3](LICENSE)
+E
diff --git a/biome.json b/biome.json
deleted file mode 100644
index 46ee8c9..0000000
--- a/biome.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
- "vcs": {
- "enabled": true,
- "clientKind": "git",
- "useIgnoreFile": false
- },
- "files": {
- "ignoreUnknown": true,
- "ignore": []
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "tab",
- "lineEnding": "lf"
- },
- "organizeImports": {
- "enabled": true
- },
- "css": {
- "formatter": {
- "indentStyle": "tab",
- "lineEnding": "lf"
- }
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true,
- "correctness": {
- "noUnusedImports": "error"
- }
- }
- },
- "javascript": {
- "formatter": {
- "quoteStyle": "double",
- "indentStyle": "tab",
- "lineEnding": "lf",
- "jsxQuoteStyle": "double",
- "semicolons": "always"
- }
- }
-}
diff --git a/compose.yml b/compose.yml
deleted file mode 100644
index f2c01ff..0000000
--- a/compose.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-services:
- profile-page:
- container_name: profilePage
- build:
- context: .
- restart: unless-stopped
- ports:
- - "${PORT:-6600}:${PORT:-6600}"
- env_file:
- - .env
- networks:
- - profilePage-network
-
- dragonfly:
- image: 'docker.dragonflydb.io/dragonflydb/dragonfly'
- restart: unless-stopped
- ulimits:
- memlock: -1
- volumes:
- - dragonflydata:/data
- networks:
- - profilePage-network
-
-volumes:
- dragonflydata:
-
-networks:
- profilePage-network:
- driver: bridge
diff --git a/config/environment.ts b/config/environment.ts
index 2c89b09..c584b45 100644
--- a/config/environment.ts
+++ b/config/environment.ts
@@ -1,66 +1,12 @@
-import { echo } from "@atums/echo";
-
-const environment: Environment = {
- port: Number.parseInt(process.env.PORT || "8080", 10),
+export const environment: Environment = {
+ port: parseInt(process.env.PORT || "8080", 10),
host: process.env.HOST || "0.0.0.0",
development:
- process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
+ process.env.NODE_ENV === "development" ||
+ process.argv.includes("--dev"),
};
-const redisTtl: number = process.env.REDIS_TTL
- ? Number.parseInt(process.env.REDIS_TTL, 10)
- : 60 * 60 * 1; // 1 hour
-
-const lanyardConfig: LanyardConfig = {
+export const lanyardConfig: LanyardConfig = {
userId: process.env.LANYARD_USER_ID || "",
- instance: process.env.LANYARD_INSTANCE || "",
-};
-
-const reviewDb = {
- enabled: process.env.REVIEW_DB === "true" || process.env.REVIEW_DB === "1",
- url: "https://manti.vendicated.dev/api/reviewdb",
-};
-
-const timezoneAPIUrl: string | null = process.env.TIMEZONE_API_URL || null;
-
-const badgeApi: string | null = process.env.BADGE_API_URL || null;
-const steamGridDbKey: string | undefined = process.env.STEAMGRIDDB_API_KEY;
-
-const plausibleScript: string | null =
- process.env.PLAUSIBLE_SCRIPT_HTML?.trim() || null;
-
-function verifyRequiredVariables(): void {
- const requiredVariables = [
- "HOST",
- "PORT",
-
- "LANYARD_USER_ID",
- "LANYARD_INSTANCE",
- ];
-
- let hasError = false;
-
- for (const key of requiredVariables) {
- const value = process.env[key];
- if (value === undefined || value.trim() === "") {
- echo.error(`Missing or empty environment variable: ${key}`);
- hasError = true;
- }
- }
-
- if (hasError) {
- process.exit(1);
- }
-}
-
-export {
- environment,
- lanyardConfig,
- redisTtl,
- reviewDb,
- timezoneAPIUrl,
- badgeApi,
- steamGridDbKey,
- plausibleScript,
- verifyRequiredVariables,
+ instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest",
};
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..d43df76
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,132 @@
+import pluginJs from "@eslint/js";
+import tseslintPlugin from "@typescript-eslint/eslint-plugin";
+import tsParser from "@typescript-eslint/parser";
+import prettier from "eslint-plugin-prettier";
+import promisePlugin from "eslint-plugin-promise";
+import simpleImportSort from "eslint-plugin-simple-import-sort";
+import unicorn from "eslint-plugin-unicorn";
+import unusedImports from "eslint-plugin-unused-imports";
+import globals from "globals";
+
+/** @type {import('eslint').Linter.FlatConfig[]} */
+export default [
+ {
+ files: ["**/*.{js,mjs,cjs}"],
+ languageOptions: {
+ globals: globals.node,
+ },
+ ...pluginJs.configs.recommended,
+ plugins: {
+ "simple-import-sort": simpleImportSort,
+ "unused-imports": unusedImports,
+ promise: promisePlugin,
+ prettier: prettier,
+ unicorn: unicorn,
+ },
+ rules: {
+ "eol-last": ["error", "always"],
+ "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error",
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": [
+ "warn",
+ {
+ vars: "all",
+ varsIgnorePattern: "^_",
+ args: "after-used",
+ argsIgnorePattern: "^_",
+ },
+ ],
+ "promise/always-return": "error",
+ "promise/no-return-wrap": "error",
+ "promise/param-names": "error",
+ "promise/catch-or-return": "error",
+ "promise/no-nesting": "warn",
+ "promise/no-promise-in-callback": "warn",
+ "promise/no-callback-in-promise": "warn",
+ "prettier/prettier": [
+ "error",
+ {
+ useTabs: true,
+ tabWidth: 4,
+ },
+ ],
+ indent: ["error", "tab", { SwitchCase: 1 }],
+ "unicorn/filename-case": [
+ "error",
+ {
+ case: "camelCase",
+ },
+ ],
+ },
+ },
+ {
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ parser: tsParser,
+ globals: globals.node,
+ },
+ plugins: {
+ "@typescript-eslint": tseslintPlugin,
+ "simple-import-sort": simpleImportSort,
+ "unused-imports": unusedImports,
+ promise: promisePlugin,
+ prettier: prettier,
+ unicorn: unicorn,
+ },
+ rules: {
+ ...tseslintPlugin.configs.recommended.rules,
+ quotes: ["error", "double"],
+ "eol-last": ["error", "always"],
+ "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error",
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": [
+ "warn",
+ {
+ vars: "all",
+ varsIgnorePattern: "^_",
+ args: "after-used",
+ argsIgnorePattern: "^_",
+ },
+ ],
+ "promise/always-return": "error",
+ "promise/no-return-wrap": "error",
+ "promise/param-names": "error",
+ "promise/catch-or-return": "error",
+ "promise/no-nesting": "warn",
+ "promise/no-promise-in-callback": "warn",
+ "promise/no-callback-in-promise": "warn",
+ "prettier/prettier": [
+ "error",
+ {
+ useTabs: true,
+ tabWidth: 4,
+ },
+ ],
+ indent: ["error", "tab", { SwitchCase: 1 }],
+ "unicorn/filename-case": [
+ "error",
+ {
+ case: "camelCase",
+ },
+ ],
+ "@typescript-eslint/explicit-function-return-type": ["error"],
+ "@typescript-eslint/explicit-module-boundary-types": ["error"],
+ "@typescript-eslint/typedef": [
+ "error",
+ {
+ arrowParameter: true,
+ variableDeclaration: true,
+ propertyDeclaration: true,
+ memberVariableDeclaration: true,
+ parameter: true,
+ },
+ ],
+ },
+ },
+];
diff --git a/logger.json b/logger.json
deleted file mode 100644
index 521b3bc..0000000
--- a/logger.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "directory": "logs",
- "level": "debug",
- "disableFile": false,
-
- "rotate": true,
- "maxFiles": 3,
-
- "console": true,
- "consoleColor": true,
-
- "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
- "timezone": "local",
-
- "silent": false,
-
- "pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}",
- "levelColor": {
- "debug": "blue",
- "info": "green",
- "warn": "yellow",
- "error": "red",
- "fatal": "red"
- },
-
- "customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
- "customColors": {
- "GET": "green",
- "POST": "blue",
- "PUT": "yellow",
- "DELETE": "red",
- "PATCH": "cyan",
- "HEAD": "magenta",
- "OPTIONS": "white",
- "TRACE": "gray"
- },
-
- "prettyPrint": true
-}
diff --git a/package.json b/package.json
index 5654ef1..7d18b41 100644
--- a/package.json
+++ b/package.json
@@ -5,20 +5,31 @@
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --hot src/index.ts --dev",
- "lint": "bunx biome ci . --verbose",
- "lint:fix": "bunx biome check --fix",
+ "lint": "eslint",
+ "lint:fix": "bun lint --fix",
"cleanup": "rm -rf logs node_modules bun.lockdb"
},
"devDependencies": {
- "@biomejs/biome": "^1.9.4",
+ "@eslint/js": "^9.24.0",
"@types/bun": "^1.2.8",
- "globals": "^16.0.0"
+ "@types/ejs": "^3.1.5",
+ "@typescript-eslint/eslint-plugin": "^8.29.0",
+ "@typescript-eslint/parser": "^8.29.0",
+ "eslint": "^9.24.0",
+ "eslint-plugin-prettier": "^5.2.6",
+ "eslint-plugin-promise": "^7.2.1",
+ "eslint-plugin-simple-import-sort": "^12.1.1",
+ "eslint-plugin-unicorn": "^56.0.1",
+ "eslint-plugin-unused-imports": "^4.1.4",
+ "globals": "^15.15.0",
+ "prettier": "^3.5.3"
},
"peerDependencies": {
"typescript": "^5.8.3"
},
"dependencies": {
- "@atums/echo": "^1.0.3",
+ "ejs": "^3.1.10",
+ "node-vibrant": "^4.0.3",
"marked": "^15.0.7"
}
}
diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico
index 72ecc34..69ec50d 100644
Binary files a/public/assets/favicon.ico and b/public/assets/favicon.ico differ
diff --git a/public/assets/favicon.png b/public/assets/favicon.png
deleted file mode 100644
index d65b7ec..0000000
Binary files a/public/assets/favicon.png and /dev/null differ
diff --git a/public/assets/forgejo_logo.svg b/public/assets/forgejo_logo.svg
deleted file mode 100644
index be0b3ce..0000000
--- a/public/assets/forgejo_logo.svg
+++ /dev/null
@@ -1,36 +0,0 @@
-
\ No newline at end of file
diff --git a/public/css/error.css b/public/css/error.css
new file mode 100644
index 0000000..a8e591d
--- /dev/null
+++ b/public/css/error.css
@@ -0,0 +1,25 @@
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 90vh;
+ background: #0e0e10;
+ color: #fff;
+ font-family: system-ui, sans-serif;
+}
+.error-container {
+ text-align: center;
+ padding: 2rem;
+ background: #1a1a1d;
+ border-radius: 12px;
+ box-shadow: 0 0 20px rgba(0,0,0,0.3);
+}
+.error-title {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ color: #ff4e4e;
+}
+.error-message {
+ font-size: 1.2rem;
+ opacity: 0.8;
+}
diff --git a/public/css/index.css b/public/css/index.css
index 8960831..da470b9 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -1,83 +1,7 @@
-.raindrop {
- position: absolute;
- background-color: white;
- border-radius: 50%;
- pointer-events: none;
- z-index: 1;
-}
-
-.star,
-.snowflake {
- position: absolute;
- background-color: white;
- border-radius: 50%;
- pointer-events: none;
- z-index: 1;
-}
-
-.star {
- animation: twinkle ease-in-out infinite alternate;
-}
-
-.shooting-star {
- position: absolute;
- background: linear-gradient(90deg, white, transparent);
- width: 100px;
- height: 2px;
- opacity: 0.8;
- border-radius: 2px;
- transform-origin: left center;
-}
-
-@keyframes twinkle {
- from {
- opacity: 0.3;
- transform: scale(1);
- }
- to {
- opacity: 1;
- transform: scale(1.2);
- }
-}
-
-#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;
- background-color: var(--background);
- color: var(--text-color);
+ background-color: #0e0e10;
+ color: #ffffff;
margin: 0;
padding: 2rem;
display: flex;
@@ -85,59 +9,19 @@ body {
align-items: center;
}
-main {
- width: 100%;
- margin: 0;
- padding: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-.open-source-logo {
- width: 2rem;
- height: 2rem;
- margin: 0;
- padding: 0;
- cursor: pointer;
-
- position: fixed;
- bottom: 1rem;
- right: 0.5rem;
- z-index: 1000;
-
- opacity: 0.5;
- transition: opacity 0.3s ease;
-
- &:hover {
- opacity: 1 !important;
- }
-}
-
-.hidden {
- display: none !important;
-}
-
-.activity-header.hidden {
- display: none;
-}
-
.user-card {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2rem;
- max-width: 700px;
+ max-width: 600px;
width: 100%;
}
.avatar-status-wrapper {
display: flex;
align-items: center;
- gap: 2rem;
-
- width: fit-content;
- max-width: 700px;
+ gap: 1.5rem;
}
.avatar-wrapper {
@@ -152,35 +36,12 @@ main {
border-radius: 50%;
}
-.badges {
- max-width: 700px;
- box-sizing: border-box;
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- gap: 0.3rem;
- flex-wrap: wrap;
- margin-top: 0.5rem;
- padding: 0.5rem;
- background-color: var(--card-bg);
- border-radius: 10px;
- border: 1px solid var(--border-color);
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
-}
-
-.badge {
- width: 26px;
- height: 26px;
- border-radius: 50%;
-}
-
.decoration {
position: absolute;
- top: -13px;
- left: -16px;
- width: 160px;
- height: 160px;
+ top: -18px;
+ left: -18px;
+ width: 164px;
+ height: 164px;
pointer-events: none;
}
@@ -191,58 +52,35 @@ main {
width: 24px;
height: 24px;
border-radius: 50%;
- border: 4px solid var(--background);
+ border: 4px solid #0e0e10;
display: flex;
align-items: center;
justify-content: center;
}
.status-indicator.online {
- background-color: var(--status-online);
+ background-color: #23a55a;
}
.status-indicator.idle {
- background-color: var(--status-idle);
+ background-color: #f0b232;
}
.status-indicator.dnd {
- background-color: var(--status-dnd);
+ background-color: #f23f43;
}
.status-indicator.offline {
- background-color: var(--status-offline);
-}
-
-.status-indicator.streaming {
- background-color: var(--status-streaming);
+ background-color: #747f8d;
}
.platform-icon.mobile-only {
position: absolute;
- bottom: 0;
+ bottom: 4px;
right: 4px;
width: 30px;
height: 30px;
pointer-events: none;
- background-color: var(--background);
- padding: 0.3rem 0.1rem;
- border-radius: 8px;
-}
-
-.platform-icon.mobile-only.dnd {
- fill: var(--status-dnd);
-}
-.platform-icon.mobile-only.idle {
- fill: var(--status-idle);
-}
-.platform-icon.mobile-only.online {
- fill: var(--status-online);
-}
-.platform-icon.mobile-only.offline {
- fill: var(--status-offline);
-}
-.platform-icon.mobile-only.streaming {
- fill: var(--status-streaming);
}
.user-info {
@@ -250,65 +88,15 @@ main {
flex-direction: column;
}
-.user-info-inner {
- display: flex;
- flex-direction: row;
- align-items: center;
- text-align: center;
- gap: 0.5rem;
-}
-.user-info-inner a {
- text-decoration: none;
- color: var(--link-color);
-}
-
-.user-info-inner h1 {
- font-size: 2rem;
- margin: 0;
-}
-
-.clan-badge {
- width: fit-content;
- height: fit-content;
- border-radius: 8px;
- background-color: var(--card-bg);
-
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- gap: 0.3rem;
- padding: 0.3rem 0.5rem;
-
- text-align: center;
- align-items: center;
- justify-content: center;
-}
-
-.clan-badge img {
- width: 20px;
- height: 20px;
- margin: 0;
- padding: 0;
-}
-
-.clan-badge span {
- font-size: 0.9rem;
- color: var(--text-color);
- margin: 0;
-
- font-weight: 600;
-}
-
h1 {
font-size: 2.5rem;
margin: 0;
- color: var(--link-color);
+ color: #00b0f4;
}
.custom-status {
font-size: 1.2rem;
- color: var(--text-subtle);
+ color: #bbb;
margin-top: 0.25rem;
word-break: break-word;
overflow-wrap: anywhere;
@@ -319,6 +107,7 @@ h1 {
flex-wrap: wrap;
}
+
.custom-status .custom-emoji {
width: 20px;
height: 20px;
@@ -336,26 +125,7 @@ ul {
list-style: none;
padding: 0;
width: 100%;
- max-width: 700px;
-}
-
-.activities-section {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- width: 100%;
- max-width: 700px;
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-.activities-section .activity-block-header {
- margin: 1rem 0 .5rem;
- font-size: 2rem;
- font-weight: 600;
-
- text-align: center;
+ max-width: 600px;
}
.activities {
@@ -363,8 +133,7 @@ ul {
flex-direction: column;
gap: 1rem;
width: 100%;
- max-width: 700px;
- box-sizing: border-box;
+ max-width: 600px;
padding: 0;
margin: 0;
}
@@ -373,57 +142,19 @@ ul {
display: flex;
flex-direction: row;
gap: 1rem;
- background-color: var(--card-bg);
- padding: 0.75rem 1rem;
- border-radius: 10px;
- border: 1px solid var(--border-color);
-
- transition: background-color 0.3s ease;
-
- &:hover {
- background: var(--card-hover-bg);
- }
+ background: #1a1a1d;
+ padding: 1rem;
+ border-radius: 6px;
+ box-shadow: 0 0 0 1px #2e2e30;
+ transition: background 0.2s ease;
+ align-items: flex-start;
}
-.activity-wrapper {
- display: flex;
- flex-direction: column;
- width: 100%;
+.activity:hover {
+ background: #2a2a2d;
}
-.activity-wrapper-inner {
- display: flex;
- flex-direction: row;
- gap: 1rem;
-}
-
-.activity-image-wrapper {
- position: relative;
- width: 80px;
- height: 80px;
-}
-
-.no-asset {
- display: none !important;
-}
-
-.activity-image-small {
- width: 25px;
- height: 25px;
- border-radius: 50%;
- object-fit: cover;
- flex-shrink: 0;
- border-color: var(--card-bg);
- background-color: var(--card-bg);
- border-width: 2px;
- border-style: solid;
-
- position: absolute;
- bottom: -6px;
- right: -10px;
-}
-
-.activity-image {
+.activity-art {
width: 80px;
height: 80px;
border-radius: 6px;
@@ -434,15 +165,7 @@ ul {
.activity-content {
display: flex;
flex-direction: column;
- justify-content: space-between;
flex: 1;
- gap: 0.5rem;
- position: relative;
-}
-
-.activity-top {
- display: flex;
- flex-direction: column;
gap: 0.25rem;
}
@@ -452,91 +175,64 @@ ul {
align-items: flex-start;
}
-.activity-bottom {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
.activity-name {
- font-weight: 600;
- font-size: 1rem;
- color: var(--text-color);
+ font-weight: bold;
+ font-size: 1.1rem;
+ color: #ffffff;
}
.activity-detail {
- font-size: 0.875rem;
- color: var(--text-secondary);
+ font-size: 0.95rem;
+ color: #ccc;
}
.activity-timestamp {
- font-size: 0.75rem;
- color: var(--text-secondary);
+ font-size: 0.8rem;
+ color: #777;
text-align: right;
- margin-left: auto;
- white-space: nowrap;
}
.progress-bar {
- height: 4px;
- background-color: var(--border-color);
- border-radius: 2px;
- margin-top: 1rem;
+ height: 6px;
+ background-color: #333;
+ border-radius: 3px;
overflow: hidden;
+ width: 100%;
+ margin-top: 0.5rem;
}
.progress-fill {
- background-color: var(--progress-fill);
- transition: width 0.4s ease;
height: 100%;
-}
-
-.progress-bar,
-.progress-time-labels {
- width: 100%;
+ background-color: #00b0f4;
+ transition: width 0.5s ease;
}
.progress-time-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
- color: var(--text-muted);
+ color: #888;
margin-top: 0.25rem;
}
-.activity-type-wrapper {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.activity-type-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- font-weight: 600;
- color: var(--blockquote-color);
- margin-bottom: 0.5rem;
- display: block;
-}
-
.activity-header.no-timestamp {
justify-content: flex-start;
}
.progress-time-labels.paused .progress-current::after {
content: " ⏸";
- color: var(--status-idle);
+ color: #f0b232;
}
.activity-buttons {
display: flex;
+ flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
- justify-content: flex-end;
}
.activity-button {
- background-color: var(--progress-fill);
+ background-color: #5865f2;
color: white;
border: none;
border-radius: 3px;
@@ -546,17 +242,18 @@ ul {
text-decoration: none;
transition: background-color 0.2s ease;
display: inline-block;
+}
- &:hover {
- background-color: var(--button-hover-bg);
- text-decoration: none;
- }
+.activity-button:hover {
+ background-color: #4752c4;
+ text-decoration: none;
+}
- &:disabled {
- background-color: var(--button-disabled-bg);
- cursor: not-allowed;
- opacity: 0.8;
- }
+.activity-button.disabled {
+ background-color: #4e5058;
+ cursor: default;
+ pointer-events: none;
+ opacity: 0.8;
}
@media (max-width: 600px) {
@@ -573,16 +270,6 @@ ul {
.user-card {
width: 100%;
align-items: center;
- margin-top: 2rem;
- }
-
- .badges {
- max-width: 100%;
- border-radius: 0;
- border: none;
- background-color: transparent;
- margin-top: 0;
- box-shadow: none;
}
.avatar-status-wrapper {
@@ -593,13 +280,6 @@ ul {
width: 100%;
}
- .activity-image-wrapper {
- max-width: 100%;
- max-height: 100%;
- width: 100%;
- height: 100%;
- }
-
.avatar-wrapper {
width: 96px;
height: 96px;
@@ -654,31 +334,21 @@ ul {
align-items: center;
text-align: center;
padding: 1rem;
- border-radius: 0;
+ border-radius:0;
}
- .activity-image {
+ .activity-art {
width: 100%;
- max-width: 100%;
+ max-width: 300px;
height: auto;
border-radius: 8px;
}
- .activity-image-small {
- width: 40px;
- height: 40px;
- }
-
.activity-content {
width: 100%;
align-items: center;
}
- .activity-wrapper-inner {
- flex-direction: column;
- align-items: center;
- }
-
.activity-header {
flex-direction: column;
align-items: center;
@@ -715,16 +385,12 @@ ul {
/* readme :p */
.readme {
- max-width: fit-content;
- min-width: 700px;
- overflow: hidden;
+ max-width: 600px;
width: 100%;
- background: var(--readme-bg);
+ background: #1a1a1d;
padding: 1.5rem;
border-radius: 8px;
- border: 1px solid var(--border-color);
-
- margin-top: 1rem;
+ box-shadow: 0 0 0 1px #2e2e30;
box-sizing: border-box;
overflow: hidden;
@@ -733,13 +399,13 @@ ul {
.readme h2 {
margin-top: 0;
- color: var(--link-color);
+ color: #00b0f4;
}
.markdown-body {
font-size: 1rem;
line-height: 1.6;
- color: var(--text-color);
+ color: #ddd;
}
.markdown-body h1,
@@ -748,7 +414,7 @@ ul {
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
- color: var(--text-color);
+ color: #ffffff;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
@@ -758,7 +424,7 @@ ul {
}
.markdown-body a {
- color: var(--link-color);
+ color: #00b0f4;
text-decoration: none;
}
@@ -767,7 +433,7 @@ ul {
}
.markdown-body code {
- background: var(--border-color);
+ background: #2e2e30;
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: monospace;
@@ -775,7 +441,7 @@ ul {
}
.markdown-body pre {
- background: var(--border-color);
+ background: #2e2e30;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
@@ -790,9 +456,9 @@ ul {
}
.markdown-body blockquote {
- border-left: 4px solid var(--link-color);
+ border-left: 4px solid #00b0f4;
padding-left: 1rem;
- color: var(--blockquote-color);
+ color: #aaa;
margin: 1rem 0;
}
@@ -802,8 +468,7 @@ ul {
@media (max-width: 600px) {
.readme {
- max-width: 100%;
- min-width: 100%;
+ width: 100%;
padding: 1rem;
margin-top: 1rem;
@@ -814,218 +479,3 @@ ul {
font-size: 0.95rem;
}
}
-
-/* reviews */
-.reviews {
- width: 100%;
- max-width: 700px;
- margin-top: 2rem;
- display: flex;
- flex-direction: column;
- gap: 1rem;
- background-color: var(--card-bg);
- padding: 1rem;
- border-radius: 10px;
- border: 1px solid var(--border-color);
- box-sizing: border-box;
-}
-
-.reviews h2 {
- margin: 0 0 1rem;
- font-size: 2rem;
- font-weight: 600;
- text-align: center;
-}
-
-.reviews-list {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.review {
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- gap: 1rem;
- padding: 0.75rem 1rem;
- border-radius: 8px;
- border: 1px solid var(--border-color);
- transition: background-color 0.3s ease;
-}
-
-.review:hover {
- background-color: var(--card-hover-bg);
-}
-
-.review-avatar {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- object-fit: cover;
- flex-shrink: 0;
-}
-
-.review-body {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
-.review-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.review-header-inner {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 0.5rem;
-}
-
-.review-username {
- font-weight: 600;
- color: var(--text-color);
-}
-
-.review-timestamp {
- font-size: 0.8rem;
- color: var(--text-muted);
-}
-
-.review-content {
- color: var(--text-secondary);
- font-size: 0.95rem;
- word-break: break-word;
- white-space: pre-wrap;
-}
-
-.review-badges {
- display: flex;
- gap: 0.3rem;
- flex-wrap: wrap;
-}
-
-.emoji {
- width: 20px;
- height: 20px;
- vertical-align: middle;
- margin: 0 2px;
- display: inline-block;
- transition: transform 0.3s ease;
-}
-
-.emoji:hover {
- transform: scale(1.2);
-}
-
-.review-content img.emoji {
- vertical-align: middle;
-}
-
-@media (max-width: 600px) {
- .reviews {
- max-width: 100%;
- padding: 1rem;
- border-radius: 0;
- border: none;
- background-color: transparent;
- }
-
- .reviews h2 {
- font-size: 1.4rem;
- text-align: center;
- margin-bottom: 1rem;
- }
-
- .reviews-list {
- gap: 0.75rem;
- }
-
- .review {
- flex-direction: column;
- align-items: center;
- text-align: center;
- padding: 1rem;
- border-radius: 0;
- }
-
- .review-avatar {
- width: 64px;
- height: 64px;
- }
-
- .review-body {
- width: 100%;
- align-items: center;
- }
-
- .review-header {
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
- }
-
- .review-username {
- font-size: 1rem;
- }
-
- .review-timestamp {
- font-size: 0.75rem;
- }
-
- .review-content {
- font-size: 0.9rem;
- }
-
- .review-badges {
- justify-content: center;
- }
-
- .emoji {
- width: 16px;
- height: 16px;
- }
-}
-
-/* timezone display */
-
-.timezone-wrapper {
- position: fixed;
- top: 1rem;
- right: 1rem;
- background-color: var(--card-bg);
- color: var(--text-color);
- font-size: 0.9rem;
- padding: 0.4rem 0.8rem;
- border-radius: 6px;
- border: 1px solid var(--border-color);
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
- z-index: 100;
- user-select: none;
- opacity: 0.85;
- transition: opacity 0.2s ease;
-}
-
-.timezone-wrapper:hover {
- opacity: 1;
-}
-
-.timezone-label {
- color: var(--text-muted);
- margin-right: 0.4rem;
-}
-
-@media (max-width: 600px) {
- .timezone-label {
- display: none;
- }
-}
diff --git a/public/css/root.css b/public/css/root.css
deleted file mode 100644
index dec65f3..0000000
--- a/public/css/root.css
+++ /dev/null
@@ -1,29 +0,0 @@
-: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 68e05b6..6feec74 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1,60 +1,36 @@
-const head = document.querySelector("head");
-const userId = head?.dataset.userId;
+/* eslint-disable indent */
+
const activityProgressMap = new Map();
-const reviewURL = head?.dataset.reviewDb;
-const timezoneApiUrl = head?.dataset.timezoneApi;
-let instanceUri = head?.dataset.instanceUri;
-let badgeURL = head?.dataset.badgeUrl;
-let socket;
-
-let badgesLoaded = false;
-let readmeLoaded = false;
-let cssLoaded = false;
-let timezoneLoaded = false;
-
-const reviewsPerPage = 50;
-let currentReviewOffset = 0;
-let hasMoreReviews = true;
-let isLoadingReviews = false;
-
function formatTime(ms) {
const totalSecs = Math.floor(ms / 1000);
- const hours = Math.floor(totalSecs / 3600);
- const mins = Math.floor((totalSecs % 3600) / 60);
+ const mins = Math.floor(totalSecs / 60);
const secs = totalSecs % 60;
-
- return `${String(hours).padStart(1, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
-}
-
-function formatVerbose(ms) {
- const totalSecs = Math.floor(ms / 1000);
- const hours = Math.floor(totalSecs / 3600);
- const mins = Math.floor((totalSecs % 3600) / 60);
- const secs = totalSecs % 60;
-
- return `${hours}h ${mins}m ${secs}s`;
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
}
function updateElapsedAndProgress() {
const now = Date.now();
- for (const el of document.querySelectorAll(".activity-timestamp")) {
+ document.querySelectorAll(".activity-timestamp").forEach((el) => {
const start = Number(el.dataset.start);
- if (!start) continue;
+ if (!start) return;
const elapsed = now - start;
+ const mins = Math.floor(elapsed / 60000);
+ const secs = Math.floor((elapsed % 60000) / 1000);
const display = el.querySelector(".elapsed");
- if (display) display.textContent = `(${formatVerbose(elapsed)} ago)`;
- }
+ if (display)
+ display.textContent = `(${mins}m ${secs.toString().padStart(2, "0")}s ago)`;
+ });
- for (const bar of document.querySelectorAll(".progress-bar")) {
+ document.querySelectorAll(".progress-bar").forEach((bar) => {
const start = Number(bar.dataset.start);
const end = Number(bar.dataset.end);
- if (!start || !end || end <= start) continue;
+ if (!start || !end || end <= start) return;
const duration = end - start;
- const elapsed = Math.min(now - start, duration);
+ const elapsed = now - start;
const progress = Math.min(
100,
Math.max(0, Math.floor((elapsed / duration) * 100)),
@@ -62,15 +38,14 @@ function updateElapsedAndProgress() {
const fill = bar.querySelector(".progress-fill");
if (fill) fill.style.width = `${progress}%`;
- }
+ });
- for (const label of document.querySelectorAll(".progress-time-labels")) {
+ document.querySelectorAll(".progress-time-labels").forEach((label) => {
const start = Number(label.dataset.start);
const end = Number(label.dataset.end);
- if (!start || !end || end <= start) continue;
+ if (!start || !end || end <= start) return;
- const isPaused = now > end;
- const current = isPaused ? end - start : Math.max(0, now - start);
+ const current = Math.max(0, now - start);
const total = end - start;
const currentEl = label.querySelector(".progress-current");
@@ -79,7 +54,7 @@ function updateElapsedAndProgress() {
const id = `${start}-${end}`;
const last = activityProgressMap.get(id);
- if (isPaused || (last !== undefined && last === current)) {
+ if (last !== undefined && last === current) {
label.classList.add("paused");
} else {
label.classList.remove("paused");
@@ -87,209 +62,49 @@ function updateElapsedAndProgress() {
activityProgressMap.set(id, current);
- if (currentEl) {
- currentEl.textContent = isPaused
- ? `Paused at ${formatTime(current)}`
- : formatTime(current);
- }
+ if (currentEl) currentEl.textContent = formatTime(current);
if (totalEl) totalEl.textContent = formatTime(total);
- }
+ });
}
-function loadEffectScript(effect) {
- const existing = document.querySelector(`script[data-effect="${effect}"]`);
- if (existing) return;
+updateElapsedAndProgress();
+setInterval(updateElapsedAndProgress, 1000);
- const script = document.createElement("script");
- script.src = `/public/js/${effect}.js`;
- script.dataset.effect = effect;
- document.head.appendChild(script);
-}
+const head = document.querySelector("head");
+let userId = head?.dataset.userId;
+let instanceUri = head?.dataset.instanceUri;
-function resolveActivityImage(img, applicationId) {
- if (!img) return null;
-
- if (img.startsWith("mp:external/")) {
- return `https://media.discordapp.net/external/${img.slice("mp:external/".length)}`;
+if (userId && instanceUri) {
+ if (!instanceUri.startsWith("http")) {
+ instanceUri = `https://${instanceUri}`;
}
- if (img.includes("/https/")) {
- const clean = img.split("/https/")[1];
- return clean ? `https://${clean}` : null;
- }
+ const wsUri = instanceUri
+ .replace(/^http:/, "ws:")
+ .replace(/^https:/, "wss:")
+ .replace(/\/$/, "");
- if (img.startsWith("spotify:")) {
- return `https://i.scdn.co/image/${img.split(":")[1]}`;
- }
+ const socket = new WebSocket(`${wsUri}/socket`);
- if (img.startsWith("twitch:")) {
- const username = img.split(":")[1];
- return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${username}-440x248.jpg`;
- }
+ socket.addEventListener("open", () => {
+ socket.send(
+ JSON.stringify({
+ op: 2,
+ d: {
+ subscribe_to_id: userId,
+ },
+ }),
+ );
+ });
- return `https://cdn.discordapp.com/app-assets/${applicationId}/${img}.png`;
-}
+ socket.addEventListener("message", (event) => {
+ const payload = JSON.parse(event.data);
-async function populateReviews(userId) {
- if (!reviewURL || !userId || isLoadingReviews || !hasMoreReviews) return;
- const reviewSection = document.getElementById("reviews-section");
- const reviewList = reviewSection?.querySelector(".reviews-list");
- if (!reviewList) return;
-
- isLoadingReviews = true;
-
- try {
- const url = `${reviewURL}/users/${userId}/reviews?flags=2&offset=${currentReviewOffset}`;
- const res = await fetch(url);
- const data = await res.json();
-
- if (!data.success || !Array.isArray(data.reviews)) {
- if (currentReviewOffset === 0) reviewSection.classList.add("hidden");
- isLoadingReviews = false;
- return;
+ if (payload.t === "INIT_STATE" || payload.t === "PRESENCE_UPDATE") {
+ updatePresence(payload.d);
+ updateElapsedAndProgress();
}
-
- const reviewsHTML = data.reviews
- .map((review) => {
- const sender = review.sender;
- const username = sender.username;
- const avatar = sender.profilePhoto;
- let comment = review.comment;
-
- comment = comment.replace(
- /<(a?):\w+:(\d+)>/g,
- (_, animated, id) =>
- ``,
- );
-
- const timestamp = review.timestamp
- ? new Date(review.timestamp * 1000).toLocaleString(undefined, {
- hour12: false,
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- })
- : "N/A";
-
- const badges = (sender.badges || [])
- .map(
- (b) =>
- `
`,
- )
- .join("");
-
- return `
-
- ${emojiHTML}${custom.state ? `${custom.state}` : ""} -
- `; - - if (customStatus) { - customStatus.outerHTML = html; - } else { - userInfoInner.insertAdjacentHTML("beforeend", html); - } - } else if (customStatus) { - customStatus.remove(); - } -} - -async function getAllNoAsset() { - const noAssetImages = document.querySelectorAll( - "img.activity-image.no-asset", - ); - - for (const img of noAssetImages) { - const name = img.dataset.name; - if (!name) continue; - - try { - const res = await fetch(`/api/art/${encodeURIComponent(name)}`); - if (!res.ok) continue; - - const { icon } = await res.json(); - if (icon) { - img.src = icon; - img.classList.remove("no-asset"); - img.parentElement.classList.remove("no-asset"); - } - } catch (err) { - console.warn(`Failed to fetch fallback icon for "${name}"`, err); - } - } -} - -function updateClanBadge(data) { - const userInfoInner = document.querySelector(".user-info-inner"); - if (!userInfoInner) return; - - const clan = data?.discord_user?.primary_guild; - if (!clan || !clan.tag || !clan.identity_guild_id || !clan.badge) return; - - const existing = userInfoInner.querySelector(".clan-badge"); - if (existing) existing.remove(); - - const wrapper = document.createElement("div"); - wrapper.className = "clan-badge"; - - const img = document.createElement("img"); - img.src = `https://cdn.discordapp.com/clan-badges/${clan.identity_guild_id}/${clan.badge}`; - img.alt = "Clan Badge"; - - const span = document.createElement("span"); - span.className = "clan-name"; - span.textContent = clan.tag; - - wrapper.appendChild(img); - wrapper.appendChild(span); - - const usernameEl = userInfoInner.querySelector(".username"); - if (usernameEl) { - usernameEl.insertAdjacentElement("afterend", wrapper); - } else { - userInfoInner.appendChild(wrapper); - } -} - -if (instanceUri) { - if (!instanceUri.startsWith("http")) { - instanceUri = `https://${instanceUri}`; - } - - const wsUri = instanceUri - .replace(/^http:/, "ws:") - .replace(/^https:/, "wss:") - .replace(/\/$/, ""); - - socket = new WebSocket(`${wsUri}/socket`); -} - -if (badgeURL && badgeURL !== "null" && userId) { - if (!badgeURL.startsWith("http")) { - badgeURL = `https://${badgeURL}`; - } - - if (!badgeURL.endsWith("/")) { - badgeURL += "/"; - } -} - -if (userId && instanceUri) { - let heartbeatInterval = null; - - socket.addEventListener("message", (event) => { - const payload = JSON.parse(event.data); - - if (payload.error || payload.success === false) { - const loadingOverlay = document.getElementById("loading-overlay"); - if (loadingOverlay) { - loadingOverlay.innerHTML = ` - - `; - loadingOverlay.style.opacity = "1"; - } - return; - } - - if (payload.op === 1 && payload.d?.heartbeat_interval) { - heartbeatInterval = setInterval(() => { - socket.send(JSON.stringify({ op: 3 })); - }, payload.d.heartbeat_interval); - - socket.send( - JSON.stringify({ - op: 2, - d: { - subscribe_to_id: userId, - }, - }), - ); - } - - if (payload.t === "INIT_STATE" || payload.t === "PRESENCE_UPDATE") { - updatePresence(payload); - requestAnimationFrame(updateElapsedAndProgress); - } - }); - - socket.addEventListener("close", () => { - if (heartbeatInterval) clearInterval(heartbeatInterval); - }); -} - -updateElapsedAndProgress(); -setInterval(updateElapsedAndProgress, 1000); diff --git a/public/js/rain.js b/public/js/rain.js deleted file mode 100644 index 1e51d6e..0000000 --- a/public/js/rain.js +++ /dev/null @@ -1,88 +0,0 @@ -const rainContainer = document.createElement("div"); -rainContainer.style.position = "fixed"; -rainContainer.style.top = "0"; -rainContainer.style.left = "0"; -rainContainer.style.width = "100vw"; -rainContainer.style.height = "100vh"; -rainContainer.style.pointerEvents = "none"; -document.body.appendChild(rainContainer); - -const maxRaindrops = 100; -const raindrops = []; -const mouse = { x: -100, y: -100 }; - -document.addEventListener("mousemove", (e) => { - mouse.x = e.clientX; - mouse.y = e.clientY; -}); - -const getRaindropColor = () => { - const htmlTag = document.documentElement; - return htmlTag.getAttribute("data-theme") === "dark" - ? "rgba(173, 216, 230, 0.8)" - : "rgba(70, 130, 180, 0.8)"; -}; - -const createRaindrop = () => { - if (raindrops.length >= maxRaindrops) { - const oldest = raindrops.shift(); - rainContainer.removeChild(oldest); - } - - const raindrop = document.createElement("div"); - raindrop.classList.add("raindrop"); - raindrop.style.position = "absolute"; - const height = Math.random() * 10 + 10; - raindrop.style.width = "2px"; - raindrop.style.height = `${height}px`; - raindrop.style.background = getRaindropColor(); - raindrop.style.borderRadius = "1px"; - raindrop.style.opacity = Math.random() * 0.5 + 0.3; - - raindrop.x = Math.random() * window.innerWidth; - raindrop.y = -height; - raindrop.speed = Math.random() * 6 + 4; - raindrop.directionX = (Math.random() - 0.5) * 0.2; - raindrop.directionY = Math.random() * 0.5 + 0.8; - - raindrop.style.left = `${raindrop.x}px`; - raindrop.style.top = `${raindrop.y}px`; - - raindrops.push(raindrop); - rainContainer.appendChild(raindrop); -}; - -setInterval(createRaindrop, 50); - -function updateRaindrops() { - raindrops.forEach((raindrop, index) => { - const height = Number.parseFloat(raindrop.style.height); - - raindrop.x += raindrop.directionX * raindrop.speed; - raindrop.y += raindrop.directionY * raindrop.speed; - - raindrop.style.left = `${raindrop.x}px`; - raindrop.style.top = `${raindrop.y}px`; - - if (raindrop.y > window.innerHeight) { - rainContainer.removeChild(raindrop); - raindrops.splice(index, 1); - return; - } - - if ( - raindrop.x > window.innerWidth || - raindrop.y > window.innerHeight || - raindrop.x < 0 - ) { - raindrop.x = Math.random() * window.innerWidth; - raindrop.y = -height; - raindrop.style.left = `${raindrop.x}px`; - raindrop.style.top = `${raindrop.y}px`; - } - }); - - requestAnimationFrame(updateRaindrops); -} - -updateRaindrops(); diff --git a/public/js/snow.js b/public/js/snow.js deleted file mode 100644 index 4d3e755..0000000 --- a/public/js/snow.js +++ /dev/null @@ -1,95 +0,0 @@ -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 }; - -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"; - const size = Math.random() * 3 + 2; - snowflake.style.width = `${size}px`; - snowflake.style.height = `${size}px`; - snowflake.style.background = "white"; - snowflake.style.borderRadius = "50%"; - snowflake.style.opacity = Math.random(); - - snowflake.x = Math.random() * window.innerWidth; - snowflake.y = -size; - snowflake.speed = Math.random() * 3 + 2; - snowflake.directionX = (Math.random() - 0.5) * 0.5; - snowflake.directionY = Math.random() * 0.5 + 0.5; - - snowflake.style.left = `${snowflake.x}px`; - snowflake.style.top = `${snowflake.y}px`; - - snowflakes.push(snowflake); - snowContainer.appendChild(snowflake); -}; - -setInterval(createSnowflake, 80); - -function updateSnowflakes() { - snowflakes.forEach((snowflake, index) => { - const size = Number.parseFloat(snowflake.style.width); - const centerX = snowflake.x + size / 2; - const centerY = snowflake.y + size / 2; - - const dx = centerX - mouse.x; - const dy = centerY - 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.x += snowflake.directionX * snowflake.speed; - snowflake.y += snowflake.directionY * snowflake.speed; - - snowflake.style.left = `${snowflake.x}px`; - snowflake.style.top = `${snowflake.y}px`; - - if (snowflake.y > window.innerHeight) { - snowContainer.removeChild(snowflake); - snowflakes.splice(index, 1); - return; - } - - if ( - snowflake.x > window.innerWidth || - snowflake.y > window.innerHeight || - snowflake.x < 0 - ) { - snowflake.x = Math.random() * window.innerWidth; - snowflake.y = -size; - snowflake.style.left = `${snowflake.x}px`; - snowflake.style.top = `${snowflake.y}px`; - } - }); - - requestAnimationFrame(updateSnowflakes); -} - -updateSnowflakes(); diff --git a/public/js/stars.js b/public/js/stars.js deleted file mode 100644 index 6fff4eb..0000000 --- a/public/js/stars.js +++ /dev/null @@ -1,63 +0,0 @@ -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"); - star.className = "star"; - const size = Math.random() * 2 + 1; - star.style.width = `${size}px`; - star.style.height = `${size}px`; - star.style.opacity = Math.random(); - star.style.top = `${Math.random() * 100}vh`; - star.style.left = `${Math.random() * 100}vw`; - star.style.animationDuration = `${Math.random() * 3 + 2}s`; - container.appendChild(star); -} - -function createShootingStar() { - const star = document.createElement("div"); - star.className = "shooting-star"; - - star.x = Math.random() * window.innerWidth * 0.8; - star.y = Math.random() * window.innerHeight * 0.3; - const angle = (Math.random() * Math.PI) / 6 + Math.PI / 8; - const speed = 10; - const totalFrames = 60; - let frame = 0; - - const deg = angle * (180 / Math.PI); - star.style.left = `${star.x}px`; - star.style.top = `${star.y}px`; - star.style.transform = `rotate(${deg}deg)`; - - container.appendChild(star); - - function animate() { - star.x += Math.cos(angle) * speed; - star.y += Math.sin(angle) * speed; - star.style.left = `${star.x}px`; - star.style.top = `${star.y}px`; - star.style.opacity = `${1 - frame / totalFrames}`; - - frame++; - if (frame < totalFrames) { - requestAnimationFrame(animate); - } else if (star.parentNode === container) { - container.removeChild(star); - } - } - - animate(); -} - -setInterval(() => { - if (Math.random() < 0.3) createShootingStar(); -}, 1000); diff --git a/src/helpers/char.ts b/src/helpers/char.ts new file mode 100644 index 0000000..6ecab40 --- /dev/null +++ b/src/helpers/char.ts @@ -0,0 +1,6 @@ +export function timestampToReadable(timestamp?: number): string { + const date: Date = + timestamp && !isNaN(timestamp) ? new Date(timestamp) : new Date(); + if (isNaN(date.getTime())) return "Invalid Date"; + return date.toISOString().replace("T", " ").replace("Z", ""); +} diff --git a/src/helpers/ejs.ts b/src/helpers/ejs.ts new file mode 100644 index 0000000..6b03dd0 --- /dev/null +++ b/src/helpers/ejs.ts @@ -0,0 +1,26 @@ +import { renderFile } from "ejs"; +import { resolve } from "path"; + +export async function renderEjsTemplate( + viewName: string | string[], + data: EjsTemplateData, + headers?: Record