commit 57562c5ba6753ac4d83f622870ea3f4783566148
Author: creations <creations@creations.works>
Date:   Thu Jan 16 11:34:53 2025 -0500

    first commit

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..b2d08f8
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,7 @@
+# NODE_ENV=development
+HOST=0.0.0.0
+PORT=8080
+
+DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN
+DISCORD_GUILD_ID=YOUR_DISCORD_GUILD_ID
+DISCORD_USER_ID=YOUR_DISCORD_USER_ID
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c308f77
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,179 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of "npm pack"
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+/.vscode
+bun.lockb
+pnpm-lock.yaml
+yarn.lock
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..94d3a0d
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Creation's // [creations@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.
\ No newline at end of file
diff --git a/config/environment.ts b/config/environment.ts
new file mode 100644
index 0000000..170acb1
--- /dev/null
+++ b/config/environment.ts
@@ -0,0 +1,43 @@
+import { dirname, join } from "path";
+import { fileURLToPath } from "url";
+
+import type{ IEnvironment } from "../src/interfaces/environment";
+
+const __dirname : string = join(dirname(fileURLToPath(import.meta.url)), "..");
+
+const environment : IEnvironment = {
+    development: process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
+
+    fastify: {
+        host: process.env.HOST || "0.0.0.0",
+        port: parseInt(process.env.PORT || "8080"),
+    },
+
+    discord: {
+        auth: {
+            token: process.env.DISCORD_TOKEN || "",
+            guildId: process.env.DISCORD_GUILD_ID || "",
+        },
+        watchIds: [
+            (() => {
+                const id = process.env.DISCORD_USER_ID;
+                if (id && /^\d+$/.test(id)) {
+                    return BigInt(id);
+                }
+                return BigInt(0);
+            })()
+        ],
+    },
+
+    paths: {
+        src: __dirname,
+        www: {
+            root: join(__dirname, "src", "www"),
+            views: join(__dirname, "src", "www", "views"),
+            public: join(__dirname, "src", "www", "public")
+        }
+    }
+};
+
+export default environment;
+export { environment };
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..43c558f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "ts_fastify_example",
+  "module": "src/index.ts",
+  "type": "module",
+  "scripts": {
+    "dev": "bun run --watch src/index.ts --development",
+    "prod": "bun run src/index.ts"
+  },
+  "devDependencies": {
+    "@types/bun": "latest",
+    "@types/ejs": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "latest"
+  },
+  "dependencies": {
+    "@fastify/static": "latest",
+    "@fastify/view": "latest",
+    "@fastify/websocket": "latest",
+    "ejs": "latest",
+    "fastify": "latest",
+    "oceanic.js": "latest",
+    "undici": "^6.20.1"
+  }
+}
diff --git a/src/api/status.ts b/src/api/status.ts
new file mode 100644
index 0000000..01e7e4c
--- /dev/null
+++ b/src/api/status.ts
@@ -0,0 +1,179 @@
+import { request as uRequest } from "undici";
+import { discordBot } from "../helpers/discord.bot";
+
+import type { FastifyRequest } from "fastify";
+import type { Presence } from "oceanic.js";
+import type { IRouteInfo } from "../interfaces/routes";
+
+const routeInfo: IRouteInfo = {
+    enabled: true,
+    path: "/status",
+    method: "GET",
+    websocket: true
+};
+
+async function route(socket: WebSocket, _request: FastifyRequest): Promise<void> {
+    discordBot.addWebSocket(socket);
+
+    socket.on("message", async (message: string) => {
+        try {
+            const data = JSON.parse(message);
+
+            if (data.type === "getStatus") {
+                let userPresences = Array.from(discordBot.getUserPresences()).reduce((acc, [key, value]) => {
+                    acc[key] = value;
+                    return acc;
+                }, {} as Record<string, Presence>);
+
+                for (const [userId, presence] of Object.entries(userPresences)) {
+                    if (presence.activities) {
+                        for (let index = 0; index < presence.activities.length; index++) {
+                            const activity = presence.activities[index];
+
+                            if (activity && activity.type === 0 && (!activity.assets || !activity.assets.largeImage)) {
+                                const gameName = activity.name;
+
+                                const iconUrl = await getSteamGameIcon(gameName);
+
+                                if (iconUrl) {
+                                    presence.activities[index].assets = {
+                                        largeImage: iconUrl,
+                                        largeText: gameName
+                                    };
+                                }
+                            }
+                        }
+                    }
+                }
+
+                socket.send(JSON.stringify({
+                    type: "statusUpdate",
+                    status: userPresences
+                }));
+            } else if (data.type === "getUsers") {
+                const usersInfo = Array.from(discordBot.getUsersInfo()).reduce((acc, [key, member]) => {
+                    if (member && member.user) {
+                        acc[key] = {
+                            id: member.user.id,
+                            username: member.user.username,
+                            discriminator: member.user.discriminator,
+                            nick: member.nick ?? member.user.username,
+                            avatar: member.user.avatar
+                                ? `https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}`
+                                : `https://cdn.discordapp.com/embed/avatars/${parseInt(member.user.discriminator) % 5}`,
+                            avatarDecoration: member.user.avatarDecoration
+                                ? `https://cdn.discordapp.com/avatar-decoration-presets/${member.user.avatarDecoration}`
+                                : null,
+                            banner: member.user.banner
+                                ? `https://cdn.discordapp.com/banners/${member.user.id}/${member.user.banner}`
+                                : null,
+                            roles: member.roles,
+                            joinedAt: member.joinedAt,
+                            premiumSince: member.premiumSince,
+                            user: member.user
+                        };
+                    } else {
+                        console.warn(`Invalid member data for key: ${key}`, member);
+                    }
+                    return acc;
+                }, {} as Record<string, any>);
+
+                socket.send(JSON.stringify({
+                    type: "usersUpdate",
+                    users: usersInfo
+                }));
+            } else if (data.type === "getBadges" && data.userId) {
+                let badgesFull: any[] = [];
+                const vencordLink = "https://badges.vencord.dev/badges.json";
+                const equicordLink = "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/badges.json";
+
+                try {
+                    const response = await uRequest(equicordLink);
+                    const text = await response.body.text();
+                    const body: any = JSON.parse(text);
+
+                    if (!body) return;
+
+                    const badges = body[data.userId];
+                    if (badges && Array.isArray(badges)) {
+                        badgesFull.push(...badges);
+                    }
+                } catch (error) {
+                    console.error("Error fetching Equicord badges:", error);
+                }
+
+                try {
+                    const response = await uRequest(vencordLink);
+                    const body: any = await response.body.json();
+
+                    if (!body) return;
+
+                    const badges = body[data.userId];
+                    if (badges && Array.isArray(badges)) {
+                        badgesFull.push(...badges);
+                    }
+                } catch (error) {
+                    console.error("Error fetching Vencord badges:", error);
+                }
+
+                socket.send(JSON.stringify({
+                    type: "badgesUpdate",
+                    badges: badgesFull
+                }));
+            }
+        } catch (error) {
+            console.error("Error parsing message", error);
+        }
+    });
+
+    socket.on("close", () => {
+        discordBot.removeWebSocket(socket);
+    });
+}
+
+let steamAppListCache: { data: any; timestamp: number } | null = null;
+const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
+
+async function getSteamAppList() {
+    if (steamAppListCache && (Date.now() - steamAppListCache.timestamp < CACHE_DURATION)) {
+        return steamAppListCache.data;
+    }
+
+    const response = await fetch(`https://api.steampowered.com/ISteamApps/GetAppList/v2/`);
+    const data = await response.json();
+
+    steamAppListCache = { data, timestamp: Date.now() };
+    return data;
+}
+
+async function getSteamAppID(gameName: string) {
+    const appList = await getSteamAppList();
+    const app = appList.applist.apps.find((app: any) => app.name.toLowerCase() === gameName.toLowerCase());
+    return app ? app.appid : null;
+}
+
+export async function getSteamGameIcon(gameName: string) {
+    const appID = await getSteamAppID(gameName);
+
+    if (!appID) {
+        console.error("Game not found on Steam.");
+        return;
+    }
+
+    const iconHashResponse = await fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}`);
+    const iconHashData = await iconHashResponse.json();
+    const gameData = iconHashData[appID].data;
+
+    const iconUrl = gameData.icon
+        ? `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appID}/${gameData.icon}.jpg`
+        : gameData.capsule_image || gameData.header_image;
+
+    if (iconUrl) {
+        return iconUrl;
+    } else {
+        console.error("Icon not available.");
+    }
+}
+
+
+export default { routeInfo, route };
\ No newline at end of file
diff --git a/src/fastify/manager.ts b/src/fastify/manager.ts
new file mode 100644
index 0000000..8124c62
--- /dev/null
+++ b/src/fastify/manager.ts
@@ -0,0 +1,176 @@
+import ejs from "ejs";
+import Fastify from "fastify";
+
+import { readdir, stat } from "fs/promises";
+import { IncomingMessage, Server, ServerResponse } from "http";
+import { join } from "path";
+
+import { environment } from "../../config/environment";
+
+// types
+import type { FastifyInstance, FastifyRequest } from "fastify";
+import type { Stats } from "fs";
+import type { AddressInfo } from "net";
+
+// official plugins
+import { fastifyStatic } from "@fastify/static";
+import { fastifyView } from "@fastify/view";
+import fastifyWebSocket from '@fastify/websocket';
+
+//custom plugins
+import faviconPlugin from "./plugins/favicon";
+
+class FastifyManager {
+    private readonly port: number = environment.fastify.port;
+    private readonly server: FastifyInstance<Server, IncomingMessage, ServerResponse>;
+
+    constructor() {
+        this.server = Fastify({
+            logger: false,
+            trustProxy: !environment.development,
+            ignoreTrailingSlash: true
+        });
+    }
+    private async registerPlugins(): Promise<void> {
+        // official plugins
+        this.server.register(fastifyView, {
+            engine: {
+                ejs
+            },
+            root: environment.paths.www.views,
+            includeViewExtension: true
+        });
+
+        this.server.register(fastifyStatic, {
+            root: environment.paths.www.public,
+            prefix: "/public/",
+            prefixAvoidTrailingSlash: true
+        });
+
+        await this.server.register(fastifyWebSocket, {
+            options: {
+                maxPayload: 1048576
+            }
+        });
+
+        // custom plugins
+        this.server.register(faviconPlugin, { path: join(environment.paths.www.public, "assets", "favicon.gif") });
+    }
+
+    // dynamic route loading
+    private async loadRoutes(): Promise<void> {
+        const routePaths: [string, string, boolean][] = [
+            ["routes", "/", false],
+            ["../api", "/api", true]
+        ];
+
+        for (const [routePath, prefix, recursive] of routePaths) {
+            const modifiedRoutePath: string = join(environment.paths.www.root, routePath);
+
+            let files: string[];
+            try {
+                files = await this.readDirRecursive(modifiedRoutePath, recursive);
+            } catch (err) {
+                console.error("Failed to read route directory", modifiedRoutePath, "error:", err);
+                return;
+            }
+
+            for (const file of files) {
+                try {
+                    const routeModule = await import(file);
+                    const { default: routeData } = routeModule;
+
+                    if (!routeData || !routeData.routeInfo || !routeData.route) {
+                        console.error(`Failed to load route from ${file}:`, "Route data is missing");
+                        continue;
+                    }
+
+                    if (routeData.routeInfo.enabled && routeData.route) {
+                        const { routeInfo, route } = routeData;
+
+                        let routePath = routeInfo.path || "/";
+
+                        if (prefix) {
+                            routePath = routePath === "/" ? prefix : join(prefix, routePath);
+                        }
+
+                        routePath = routePath.replace(/\\/g, '/');
+
+                        if (!routePath.startsWith('/')) {
+                            routePath = '/' + routePath;
+                        }
+
+                        routePath = routePath.replace(/\/+/g, "/");
+                        const methods = Array.isArray(routeInfo.method) ? routeInfo.method : [routeInfo.method];
+
+                        for (const method of methods) {
+                            if (routeInfo.websocket) {
+                                this.server.get(routePath, { websocket: true }, function wsHandler(socket: any, req: FastifyRequest) {
+                                    route(socket, req);
+                                });
+                                continue;
+                            } else {
+                                this.server.route({
+                                    method,
+                                    url: routePath,
+                                    handler: route
+                                });
+                            }
+                        }
+                    }
+                } catch (err) {
+                    console.error(`Failed to load route from ${file}:`, err);
+                }
+            }
+        }
+    }
+
+    private async readDirRecursive(dir: string, recursive: boolean): Promise<string[]> {
+        let results: string[] = [];
+        const list: string[] = await readdir(dir);
+
+        for (const file of list) {
+            const filePath: string = join(dir, file);
+            const statObj: Stats = await stat(filePath);
+            if (statObj && statObj.isDirectory()) {
+                if (recursive) {
+                    results = results.concat(await this.readDirRecursive(filePath, recursive));
+                }
+            } else {
+                results.push(filePath);
+            }
+        }
+
+        return results;
+    }
+
+    public async start(): Promise<void> {
+        try {
+            await this.registerPlugins();
+            await this.loadRoutes();
+
+            await this.server.listen({
+                port: environment.fastify.port,
+                host: environment.fastify.host
+            });
+
+            const [_address, _port, scheme]: [string, number, string] = ((): [string, number, string] => {
+                const address: string | AddressInfo | null = this.server.server.address();
+                const resolvedAddress: [string, number] = (address && typeof address === 'object') ? [(address.address.startsWith("::") || address.address === "0.0.0.0") ? "localhost" : address.address, address.port] : ["localhost", this.port];
+                const resolvedScheme: string = resolvedAddress[0] === "localhost" ? "http" : "https";
+
+                return [...resolvedAddress, resolvedScheme];
+            })();
+
+
+            console.info("Started Listening on", `${scheme}://${_address}:${_port}`);
+            console.info("Registered routes: ", "\n", this.server.printRoutes());
+        } catch (error) {
+            console.error("Failed to start Fastify server", error);
+        }
+    }
+}
+
+const fastifyManager = new FastifyManager();
+export default fastifyManager;
+export { fastifyManager };
diff --git a/src/fastify/plugins/favicon.ts b/src/fastify/plugins/favicon.ts
new file mode 100644
index 0000000..e7bfe86
--- /dev/null
+++ b/src/fastify/plugins/favicon.ts
@@ -0,0 +1,35 @@
+import type { FastifyInstance, FastifyPluginAsync } from "fastify";
+import type { FastifyReply } from "fastify/types/reply";
+import type { FastifyRequest } from "fastify/types/request";
+import { readFile } from "fs/promises";
+
+interface FaviconOptions {
+    path: string;
+}
+
+const faviconPlugin: FastifyPluginAsync<FaviconOptions> = async (fastify: FastifyInstance, options: FaviconOptions): Promise<void> => {
+    let faviconData: Buffer | null = null;
+
+    try {
+        faviconData = await readFile(options.path);
+    } catch (err) {
+        console.error("Error reading favicon:", err);
+    }
+
+    fastify.get("/favicon.ico", async (_request: FastifyRequest, reply: FastifyReply): Promise<void> => {
+        if (faviconData) {
+            const contentType = options.path.endsWith(".gif") ? "image/gif" : "image/x-icon";
+            reply.header("Content-Type", contentType)
+                .header("Cache-Control", "public, max-age=86400") // 1 day
+                .send(faviconData);
+        } else {
+            reply.status(404).send({
+                code: 404,
+                error: "FILE_NOT_FOUND",
+                message: "Favicon not found"
+            });
+        }
+    });
+};
+
+export default faviconPlugin as FastifyPluginAsync<FaviconOptions>;
diff --git a/src/helpers/discord.bot.ts b/src/helpers/discord.bot.ts
new file mode 100644
index 0000000..eee8e96
--- /dev/null
+++ b/src/helpers/discord.bot.ts
@@ -0,0 +1,212 @@
+import { Client, Guild, Member, type JSONMember, type Presence, type Uncached } from "oceanic.js";
+import { environment } from "../../config/environment";
+import { getSteamGameIcon } from "../api/status";
+
+class DiscordBot {
+    private client: Client;
+    private watchIds: string[] = environment.discord.watchIds.map((id) => id.toString());
+    private userPresences: Map<string, Presence>;
+    private connectedSockets: Set<WebSocket>;
+    private usersInfo: Map<string, any>;
+
+    constructor() {
+        this.client = new Client({
+            auth: `Bot ${environment.discord.auth.token}`,
+            gateway: {
+                intents: ["GUILDS", "GUILD_PRESENCES"]
+            }
+        });
+
+        this.userPresences = new Map();
+        this.usersInfo = new Map();
+        this.connectedSockets = new Set();
+        this.watchIds = environment.discord.watchIds.map((id) => id.toString());
+    }
+
+    public async start(): Promise<void> {
+        try {
+            await this.client.connect();
+            await this.setupListeners();
+        } catch (error) {
+            console.error("Failed to connect to Discord: ", error);
+        }
+    }
+
+    public async stop(): Promise<void> {
+        try {
+            this.client.disconnect();
+        } catch (error) {
+            console.error("Failed to disconnect from Discord: ", error);
+        }
+    }
+
+    public getClient(): Client {
+        return this.client;
+    }
+
+    public getUserPresences(): Map<string, Presence> {
+        return this.userPresences;
+    }
+
+    public getUsersInfo(): Map<string, any> {
+        return this.usersInfo;
+    }
+
+    public addWebSocket(socket: WebSocket): void {
+        this.connectedSockets.add(socket);
+    }
+
+    public removeWebSocket(socket: WebSocket): void {
+        this.connectedSockets.delete(socket);
+    }
+
+    private async broadcastPresenceUpdate(userId: string, presence: Presence): Promise<void> {
+        const updatedActivities = presence.activities
+            ? await Promise.all(
+                presence.activities.map(async (activity) => {
+                    if (activity && activity.type === 0 && (!activity.assets || !activity.assets.largeImage)) {
+                        const gameName = activity.name;
+                        // should use what discord uses but i dont care enough to switch it out
+                        const iconUrl = await getSteamGameIcon(gameName);
+
+                        if (iconUrl) {
+                            return {
+                                ...activity,
+                                assets: {
+                                    largeImage: iconUrl,
+                                    largeText: gameName
+                                }
+                            };
+                        }
+                    }
+                    return activity;
+                })
+            )
+            : [];
+
+        const updateMessage = JSON.stringify({
+            type: "presenceUpdate",
+            userId,
+            presence: {
+                status: presence.status,
+                activities: updatedActivities
+            }
+        });
+
+        this.broadcastToWebSockets(updateMessage);
+    }
+
+    private async fetchUsersInfo(): Promise<void> {
+        const guild = this.client.guilds.get(environment.discord.auth.guildId);
+
+        if (!guild) return;
+
+        for (const id of this.watchIds) {
+            try {
+                const member = await guild.getMember(id);
+                if (member) {
+                    const memberData = this.serializeMember(member);
+                    this.usersInfo.set(member.user.id, memberData);
+
+                    const updateMessage = JSON.stringify({
+                        type: "memberUpdate",
+                        user: memberData
+                    });
+
+                    this.broadcastToWebSockets(updateMessage);
+                }
+            } catch (error) {
+                console.error(`Failed to fetch member with ID ${id}:`, error);
+            }
+        }
+    }
+
+    private async fetchUserPresences(): Promise<void> {
+        const guild = this.client.guilds.get(environment.discord.auth.guildId);
+
+        if (!guild) return;
+
+        this.watchIds.forEach((id) => {
+            const member = guild.members.get(id);
+            if (member && member.presence) {
+                this.userPresences.set(member.user.id, member.presence);
+            }
+        });
+    }
+
+    private async setupListeners(): Promise<void> {
+        this.client.on("ready", async () => {
+            const user = this.client.user;
+            const guild = this.client.guilds.get(environment.discord.auth.guildId);
+
+            if (user && guild) {
+                await this.fetchUserPresences();
+                await this.fetchUsersInfo();
+            }
+        });
+
+        this.client.on("presenceUpdate", (guild: Guild | Uncached, member: Member | Uncached, presence: Presence, oldPresence: Presence | null) => {
+            if (!(member instanceof Member)) return;
+
+            const userId = member.user.id.toString();
+            if (!this.watchIds.includes(userId)) return;
+
+            const oldStatus = oldPresence;
+            const newStatus = presence;
+
+            if (oldStatus !== newStatus) {
+                this.userPresences.set(userId, presence);
+                this.broadcastPresenceUpdate(userId, presence);
+            }
+        });
+
+        this.client.on("guildMemberUpdate", (member: Member, oldMember: JSONMember | null) => {
+            const userId = member.user.id.toString();
+
+            if (!this.watchIds.includes(userId)) return;
+
+            const memberData = this.serializeMember(member);
+            this.usersInfo.set(userId, memberData);
+
+            const updateMessage = JSON.stringify({
+                type: "memberUpdate",
+                user: memberData
+            });
+
+            this.broadcastToWebSockets(updateMessage);
+        });
+    }
+
+    private serializeMember(member: Member): object {
+        const user = member.user;
+        return {
+            id: user.id,
+            username: user.username,
+            discriminator: user.discriminator,
+            nick: member.nick ?? user.username,
+            avatar: user.avatar
+                ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}`
+                : `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator) % 5}`,
+            avatarDecoration: user.avatarDecorationData?.asset
+                ? `https://cdn.discordapp.com/avatar-decoration-presets/${user.avatarDecorationData.asset}`
+                : null,
+            roles: member.roles,
+            joinedAt: member.joinedAt,
+            premiumSince: member.premiumSince,
+            user: user
+        };
+    }
+
+    private broadcastToWebSockets(message: string): void {
+        for (const socket of this.connectedSockets) {
+            if (socket.readyState === WebSocket.OPEN) {
+                socket.send(message);
+            }
+        }
+    }
+}
+
+const discordBot = new DiscordBot();
+
+export { discordBot };
+export default discordBot;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..cd5ab24
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,16 @@
+import { fastifyManager } from "./fastify/manager";
+import { discordBot } from "./helpers/discord.bot";
+
+class Index {
+    public static async main() : Promise<void> {
+        const main = new Index();
+        await main.start();
+    };
+
+    public async start() : Promise<void> {
+        await discordBot.start();
+        await fastifyManager.start();
+    }
+}
+
+const index : Promise<void> = Index.main();
diff --git a/src/interfaces/environment.ts b/src/interfaces/environment.ts
new file mode 100644
index 0000000..8fb82d6
--- /dev/null
+++ b/src/interfaces/environment.ts
@@ -0,0 +1,25 @@
+export interface IEnvironment {
+    development: boolean;
+
+    fastify: {
+        host: string;
+        port: number;
+    };
+
+    discord: {
+        auth: {
+            token: string;
+            guildId: string;
+        };
+        watchIds: bigint[];
+    };
+
+    paths: {
+        src: string;
+        www: {
+            root: string;
+            views: string;
+            public: string;
+        }
+    };
+}
\ No newline at end of file
diff --git a/src/interfaces/routes.ts b/src/interfaces/routes.ts
new file mode 100644
index 0000000..540ed57
--- /dev/null
+++ b/src/interfaces/routes.ts
@@ -0,0 +1,8 @@
+import type { TMethod } from "../types/routes";
+
+export interface IRouteInfo {
+    enabled: boolean;
+    path: string;
+    method: TMethod | TMethod[];
+    websocket?: boolean;
+}
\ No newline at end of file
diff --git a/src/types/routes.ts b/src/types/routes.ts
new file mode 100644
index 0000000..4c8e533
--- /dev/null
+++ b/src/types/routes.ts
@@ -0,0 +1 @@
+export type TMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD";
\ No newline at end of file
diff --git a/src/www/public/assets/favicon.gif b/src/www/public/assets/favicon.gif
new file mode 100644
index 0000000..59ac736
Binary files /dev/null and b/src/www/public/assets/favicon.gif differ
diff --git a/src/www/public/assets/fonts/FantasqueSansMNerdFont-Regular.ttf b/src/www/public/assets/fonts/FantasqueSansMNerdFont-Regular.ttf
new file mode 100644
index 0000000..eaca913
Binary files /dev/null and b/src/www/public/assets/fonts/FantasqueSansMNerdFont-Regular.ttf differ
diff --git a/src/www/public/assets/game_icon.svg b/src/www/public/assets/game_icon.svg
new file mode 100644
index 0000000..f9f068f
--- /dev/null
+++ b/src/www/public/assets/game_icon.svg
@@ -0,0 +1,4 @@
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <path fill="#43b581" fill-rule="evenodd" d="M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09 3.09 0 0 1-5.85 1.38l-1.76-3.51a1.09 1.09 0 0 0-1.23-.55c-.57.13-1.36.27-2.16.27s-1.6-.14-2.16-.27c-.49-.11-1 .1-1.23.55l-1.76 3.51A3.09 3.09 0 0 1 1 17.91V13c0-3.38.46-5.85.75-7.1.15-.6.49-1.13 1.04-1.4a.47.47 0 0 0 .24-.44c0-.7.48-1.32 1.2-1.47l2.93-.62c.5-.1 1 .06 1.36.4.35.34.78.71 1.28.68a42.4 42.4 0 0 1 4.4 0c.5.03.93-.34 1.28-.69.35-.33.86-.5 1.36-.39l2.94.62c.7.15 1.19.78 1.19 1.47ZM20 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM15.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 7a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2H7v1a1 1 0 1 1-2 0v-1H4a1 1 0 1 1 0-2h1V7Z" clip-rule="evenodd" class=""></path>
+</svg>
diff --git a/src/www/public/css/style.css b/src/www/public/css/style.css
new file mode 100644
index 0000000..84621a7
--- /dev/null
+++ b/src/www/public/css/style.css
@@ -0,0 +1,37 @@
+@font-face {
+    font-family: 'FantasqueSansMNerdFont';
+    src: url('/public/assets/fonts/FantasqueSansMNerdFont-Regular.ttf') format('truetype');
+    font-weight: normal;
+    font-style: normal;
+}
+
+[data-theme="light"] {
+    --background-color: rgb(255, 255, 255);
+    --text-color: #333;
+}
+
+[data-theme="dark"] {
+    --background-color: rgb(0, 0, 0);
+    --text-color: #f5f5f5;
+}
+
+html {
+    background-color: var(--background-color);
+    color: var(--text-color);
+}
+
+body {
+    margin: 0;
+    padding: 0;
+
+    font-family: 'FantasqueSansMNerdFont', sans-serif;
+    font-size: 16px;
+}
+
+.snowflake {
+    position: absolute;
+    background-color: white;
+    border-radius: 50%;
+    pointer-events: none;
+    z-index: 1;
+}
diff --git a/src/www/public/css/user.style.css b/src/www/public/css/user.style.css
new file mode 100644
index 0000000..21dd184
--- /dev/null
+++ b/src/www/public/css/user.style.css
@@ -0,0 +1,413 @@
+#user-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: -1;
+}
+
+#user-info {
+    text-align: center;
+    width: 100%;
+    max-width: 580px;
+    margin: 0 0 0 0;
+    padding: 20px;
+    box-sizing: border-box;
+    position: relative;
+    z-index: 1;
+    background-color: rgb(17, 17, 19);
+    margin: 5em 0 2em 0;
+    border: 3px solid rgb(52, 52, 57);
+    border-radius: 8px;
+}
+
+.avatar-container {
+    position: relative;
+    width: 128px;
+    height: 128px;
+    display: inline-block;
+    margin-top: 110px;
+    background-color: rgb(17, 17, 19);
+    border-radius: 50%;
+}
+
+#avatar {
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    object-fit: cover;
+    border: 6px solid rgb(17, 17, 19);
+}
+
+#avatar-decoration {
+    position: absolute;
+    top: -7px;
+    left: -7px;
+    width: 120%;
+    height: 120%;
+    object-fit: cover;
+    pointer-events: none;
+}
+
+#display-name {
+    margin: 0 0px -5px 0;
+    color: white;
+    font-size: 25px;
+    font-weight: 800;
+    font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+#name-pronouns-badges {
+    display: flex;
+    justify-content: left;
+    align-items: flex-start;
+    flex-direction: column;
+    margin: 0 0 0 0;
+}
+
+#pronouns-badges {
+    display: flex;
+    align-items: flex-end;
+    flex-direction: row;
+    gap: 10px;
+}
+
+#name-pronouns {
+    margin: 0 0 2px 0;
+    color: white;
+    font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+    font-size: 14px;
+    text-align: end;
+}
+
+.status-circle {
+    position: absolute;
+    bottom: -8px;
+    right: -8px;
+    width: 24px;
+    height: 24px;
+    border-radius: 50%;
+    border: 6px solid #202225;
+}
+
+.online {
+    background-color: #43b581;
+}
+
+.idle {
+    background-color: #faa61a;
+}
+
+.dnd {
+    background-color: #f04747;
+}
+
+.offline {
+    background-color: #747f8d;
+}
+
+#activity-list {
+    margin-top: 10px;
+    width: 100%;
+    box-sizing: border-box;
+    background-color: transparent;
+    box-sizing: border-box;
+}
+
+#activity-list.no-activity {
+    display: none;
+}
+
+.activity {
+    display: flex;
+    flex-direction: column;
+    background-color: rgb(6, 6, 6);
+    border-radius: 8px;
+    margin-bottom: 10px;
+    position: relative;
+    width: 100%;
+    padding: 10px;
+    box-sizing: border-box;
+}
+
+.activity:last-child {
+    margin-bottom: 0;
+}
+
+.activity-type-container {
+    width: 100%;
+    text-align: left;
+    margin-bottom: 5px;
+}
+
+.activity-content {
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+}
+
+.large-image {
+    position: relative;
+    display: inline-block;
+    margin-right: 15px;
+}
+
+.asset-large-image {
+    width: 60px;
+    height: 60px;
+    border-radius: 4px;
+}
+
+.asset-small-image {
+    position: absolute;
+    bottom: -5px;
+    right: -5px;
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    border: 2px solid rgb(17, 17, 19);
+}
+
+.activity-info {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    box-sizing: border-box;
+    align-items: flex-start;
+}
+
+.activity-type,
+.activity-info .activity-name {
+    font-size: 16px;
+    color: #fff;
+    margin: 0;
+    font-weight: 500;
+}
+
+.activity-type {
+    margin-bottom: 2px;
+    font-size: 12px;
+    color: #b9bbbe;
+}
+
+.activity-info .activity-state,
+.activity-info .activity-large-text,
+.activity-info .activity-details {
+    color: #b9bbbe;
+    font-size: 14px;
+    margin: 0 0 1px 0;
+}
+
+.activity-info .activity-timestamp {
+    color: #43b581;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    margin: 5px 0 0 0;
+}
+
+.activity-info .activity-timestamp img {
+    margin-right: 5px;
+    width: 14px;
+    height: 14px;
+}
+
+.avatar-custom-container {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    display: inline-block;
+    display: flex;
+    margin: 0 0 2em 0;
+}
+
+.custom-status-text {
+    background-color: rgb(7, 12, 23);
+    border-radius: 12px;
+    padding: 8px 0 5px 15px;
+    font-size: 14px;
+    color: #fff;
+    max-width: 200px;
+    line-height: 1.2;
+    position: absolute;
+    outline: 1px solid #2b2e35;
+    left: 160px;
+    top: 76%;
+    text-align: left;
+}
+
+.custom-status-text::before {
+    content: "";
+    position: absolute;
+    top: -11px;
+    left: 10px;
+    width: 20px;
+    height: 10px;
+    background-color: rgb(7, 12, 23);
+    border-radius: 10px 10px 0 0;
+    border: 1px solid #2b2e35;
+    border-bottom: none;
+}
+
+.custom-status-text::after {
+    content: "";
+    position: absolute;
+    top: -18px;
+    left: -2px;
+    width: 12px;
+    height: 12px;
+    background-color: rgb(7, 12, 23);
+    border-radius: 50%;
+    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
+    transform: translateY(-50%);
+    border: 1px solid #2b2e35;
+}
+
+#badge-container {
+    display: flex;
+    justify-content: space-between;
+    gap: 5px;
+
+    background-color: rgb(17, 17, 19);
+    border-radius: 4px;
+    border: 1px solid #2b2e35;
+    padding: 2px;
+    font-size: 14px;
+    align-items: center;
+}
+
+.badge {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+}
+
+#banner {
+    width: calc(100% + 40px);
+    height: 210px;
+    object-fit: fill;
+    position: absolute;
+    top: -20px;
+    left: -20px;
+    z-index: -1;
+    padding: 0;
+
+    border-radius: 5px 5px 0 0;
+}
+
+
+#user-tabs-container {
+    margin-top: 15px;
+    padding: 15px;
+    box-sizing: border-box;
+    border: 1px solid rgb(45, 45, 48);
+    border-radius: 8px;
+    background-color: rgb(10, 10, 10);
+}
+
+#user-tabs-buttons {
+    display: flex;
+    justify-content: flex-start;
+    gap: 20px;
+    margin: 0 0 10px 0;
+    padding: 0;
+}
+
+#user-tabs-buttons button {
+    background-color: transparent;
+    color: #fff;
+    border: none;
+    padding: 5px 10px;
+    font-size: 14px;
+    cursor: pointer;
+    padding: 0;
+}
+
+#user-tabs-buttons button.active {
+    text-decoration: underline;
+    text-underline-position: under;
+    text-underline-offset: 9px;
+    text-decoration-thickness: 1px;
+}
+
+.tab-content.hidden {
+    display: none;
+}
+
+.tab-content.active {
+    display: flex;
+}
+
+.tab-content {
+    border-top: .5px solid rgb(45, 45, 48);
+    padding-top: 10px;
+}
+
+#about-me-content {
+    flex-direction: column;
+    gap: 10px;
+    align-items: flex-start;
+}
+
+#about-me-content a {
+    text-decoration: none;
+    margin-top: 10px;
+}
+
+#about-me-content a:hover {
+    text-decoration: underline;
+    text-decoration-thickness: 2px;
+}
+
+#about-me-content a:visited {
+    color: rgb(59, 131, 194);
+}
+
+#about-me-content #socials {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+    margin-top: 20px;
+    align-items: flex-start;
+    border-radius: 10px;
+}
+
+#about-me-content #socials h3 {
+    margin: 0;
+    color: #ffffff;
+    font-size: 1.5em;
+    font-weight: bold;
+}
+
+#about-me-content #socials ul {
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    text-align: center;
+}
+
+#about-me-content #socials ul li {
+    margin: 0;
+    align-items: center;
+    text-align: left;
+    display: flex;
+    gap: 10px;
+    padding: 0;
+}
+
+#about-me-content #socials ul li img {
+    width: 30px;
+    height: 30px;
+}
+
+#about-me-content #socials ul li a {
+    padding: 0;
+    margin: 0;
+    align-self: center;
+}
+
+#about-me-content #socials ul li a.visited {
+    color: rgb(59, 131, 194) !important;
+}
\ No newline at end of file
diff --git a/src/www/public/js/snow.js b/src/www/public/js/snow.js
new file mode 100644
index 0000000..877b823
--- /dev/null
+++ b/src/www/public/js/snow.js
@@ -0,0 +1,80 @@
+document.addEventListener('DOMContentLoaded', function () {
+    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', function (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) => {
+            let rect = snowflake.getBoundingClientRect();
+
+            let dx = rect.left + rect.width / 2 - mouse.x;
+            let dy = rect.top + rect.height / 2 - mouse.y;
+            let 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/src/www/public/js/status.ws.js b/src/www/public/js/status.ws.js
new file mode 100644
index 0000000..6515fd5
--- /dev/null
+++ b/src/www/public/js/status.ws.js
@@ -0,0 +1,397 @@
+const ws = new WebSocket(`/api/status`);
+
+document.addEventListener("DOMContentLoaded", () => {
+    ws.addEventListener("open", () => {
+        ws.send(JSON.stringify({ type: "getStatus" }));
+        ws.send(JSON.stringify({ type: "getUsers" }));
+    });
+    
+    ws.addEventListener("message", (event) => {
+        handleMessage(event);
+    });
+    
+    ws.addEventListener("close", () => {
+        // console.log("WebSocket connection closed.");
+    });
+    
+    ws.addEventListener("error", (error) => {
+        console.error("WebSocket error:", error);
+    });
+
+    const bannerElem = document.getElementById("banner");
+    const avatar_custom_container = document.getElementById("avatar-custom-container",);
+    const avatarElem = document.getElementById("avatar");
+    const displayNameElem = document.getElementById("display-name");
+    const decorationElem = document.getElementById("avatar-decoration");
+    const usernameElem = document.getElementById("username");
+    const statusCircle = document.getElementById("status-circle");
+    const badgeContainer = document.getElementById("badge-container");
+
+    const about_me_tab = document.getElementById("about-me-content");
+    const about_me_tab_button = document.getElementById("about-me");
+    const activity_tab = document.getElementById("activity-content");
+    const activity_tab_button = document.getElementById("activity");
+
+    // const placeholderAvatar = "/public/images/avatar-placeholder.png";
+    const placeholderUsername = "Loading...";
+
+    // avatarElem.src = placeholderAvatar;
+    usernameElem.textContent = placeholderUsername;
+    decorationElem.style.display = "none";
+
+    function handleMessage(event) {
+        const data = JSON.parse(event.data);
+
+        if (data.type === "statusUpdate") {
+            handleStatusUpdate(data.status);
+        } else if (data.type === "presenceUpdate") {
+            handlePresenceUpdate(data.presence);
+        } else if (data.type === "usersUpdate") {
+            handleUsersUpdate(data.users);
+        } else if (data.type === "badgesUpdate") {
+            handleBadges(data.badges);
+        }
+    }
+
+    function updateStatusCircle(status) {
+        if (!statusCircle || !status) return;
+
+        statusCircle.classList.remove("online", "idle", "dnd", "offline");
+        statusCircle.classList.add(status);
+    }
+
+    function typeText(type) {
+        switch (type) {
+            case 0:
+                return "Playing";
+            case 1:
+                return "Streaming";
+            case 2:
+                return "Listening to";
+            case 3:
+                return "Watching";
+            case 4:
+                return "Custom Status";
+            default:
+                return "Unknown";
+        }
+    }
+
+    const tabButtons = document.querySelectorAll(".tab-button");
+
+    tabButtons.forEach((button) => {
+        const type = button.getAttribute("data-type");
+
+        button.addEventListener("click", () => {
+            tabSwitch(type);
+        });
+    });
+
+    function tabSwitch(tab) {
+        if (tab === "activity") {
+            about_me_tab.classList.remove("active");
+            about_me_tab_button.classList.remove("active");
+            about_me_tab.classList.add("hidden");
+
+            activity_tab.classList.add("active");
+            activity_tab_button.classList.add("active");
+            activity_tab.classList.remove("hidden");
+        } else {
+            activity_tab.classList.remove("active");
+            activity_tab_button.classList.remove("active");
+            activity_tab.classList.add("hidden");
+
+            about_me_tab.classList.add("active");
+            about_me_tab_button.classList.add("active");
+            about_me_tab.classList.remove("hidden");
+        }
+    }
+
+    function updateActivities(activities) {
+        const activityList = document.createElement("div");
+        const customStatusText = document.getElementById("custom-status-text");
+        activityList.id = "activity-list";
+
+        const shouldShowNoActivity = activities.length === 0 || (activities.length === 1 && activities[0]?.type === 4);
+        if (shouldShowNoActivity) {
+            const noActivity = document.createElement("p");
+            noActivity.classList.add("no-activity");
+            noActivity.textContent = "No activity(s) to display";
+            activityList.appendChild(noActivity);
+
+            tabSwitch("about-me");
+        }
+
+        const existingActivities = document.getElementById("activity-list");
+
+        if (existingActivities) existingActivities.remove();
+        if (customStatusText) customStatusText.remove();
+
+        activities.forEach((activity) => {
+            if (activity.type === 4) {
+                const customStatusDiv = document.createElement("div");
+                customStatusDiv.classList.add("custom-status");
+
+                const customStatus = document.createElement("p");
+                customStatus.classList.add("custom-status-text");
+                customStatus.id = "custom-status-text";
+                customStatus.textContent = activity.state;
+
+                customStatusDiv.appendChild(customStatus);
+                avatar_custom_container.appendChild(customStatusDiv);
+
+                return;
+            }
+
+            const activityDiv = document.createElement("div");
+            activityDiv.classList.add("activity");
+
+            let activityLargeText;
+
+            const activityTypeDiv = document.createElement("div");
+            activityTypeDiv.classList.add("activity-type-container");
+
+            const activityType = document.createElement("p");
+            activityType.classList.add("activity-type");
+
+            if (activity.type === 2) {
+                activityType.textContent = `${typeText(activity.type)} ${activity.name}`;
+            } else {
+                activityType.textContent = `${typeText(activity.type)}`;
+            }
+
+            activityTypeDiv.appendChild(activityType);
+            activityDiv.appendChild(activityTypeDiv);
+
+            const activityContentDiv = document.createElement("div");
+            activityContentDiv.classList.add("activity-content");
+
+            if (activity.assets?.largeImage) {
+                const largeImageDiv = document.createElement("div");
+                largeImageDiv.classList.add("large-image");
+
+                if (activity.assets.largeText && activity.type === 2)
+                    activityLargeText = activity.assets.largeText;
+
+                const largeImage = document.createElement("img");
+                largeImage.src = parseDiscordImage(
+                    activity.assets.largeImage,
+                    activity.applicationID,
+                );
+                largeImage.alt = "Large Image";
+                largeImage.classList.add("asset-large-image");
+                largeImageDiv.appendChild(largeImage);
+
+                if (activity.assets?.smallImage) {
+                    const smallImageDiv = document.createElement("div");
+                    smallImageDiv.classList.add("small-image");
+
+                    const smallImage = document.createElement("img");
+                    smallImage.src = parseDiscordImage(
+                        activity.assets.smallImage,
+                        activity.applicationID,
+                    );
+                    smallImage.alt = "Small Image";
+                    smallImage.classList.add("asset-small-image");
+                    smallImageDiv.appendChild(smallImage);
+
+                    largeImageDiv.appendChild(smallImageDiv);
+                }
+
+                activityContentDiv.appendChild(largeImageDiv);
+            }
+
+            const activityInfoDiv = document.createElement("div");
+            activityInfoDiv.classList.add("activity-info");
+
+            const name = document.createElement("p");
+            name.classList.add("activity-name");
+            if (activity.type === 2) {
+                name.textContent = `${activity.details}`;
+            } else {
+                name.textContent = `${activity.name}`;
+            }
+            activityInfoDiv.appendChild(name);
+
+            if (activity.details && activity.type !== 2) {
+                const details = document.createElement("p");
+                details.classList.add("activity-details");
+                details.textContent = `${activity.details}`;
+                activityInfoDiv.appendChild(details);
+            }
+
+            if (activity.state) {
+                const state = document.createElement("p");
+                state.classList.add("activity-state");
+                state.textContent = `${activity.state}`;
+                activityInfoDiv.appendChild(state);
+            }
+
+            if (activityLargeText) {
+                const largeText = document.createElement("p");
+                largeText.classList.add("activity-large-text");
+                largeText.textContent = `${activityLargeText}`;
+                activityInfoDiv.appendChild(largeText);
+            }
+
+            if (activity.timestamps?.start) {
+                const timestamp = document.createElement("p");
+                timestamp.classList.add("activity-timestamp");
+                const startTime = activity.timestamps.start;
+
+                function updateElapsedTime() {
+                    const elapsedTime = getElapsedTime(startTime);
+                    timestamp.innerHTML = `<img src="/public/assets/game_icon.svg" alt="Time icon"> ${elapsedTime}`;
+                }
+
+                updateElapsedTime();
+
+                setInterval(updateElapsedTime, 1000);
+
+                activityInfoDiv.appendChild(timestamp);
+            }
+
+            activityContentDiv.appendChild(activityInfoDiv);
+            activityDiv.appendChild(activityContentDiv);
+
+            activityList.appendChild(activityDiv);
+        });
+
+        activity_tab.appendChild(activityList);
+    }
+
+    function getElapsedTime(startTimestamp) {
+        const now = Date.now();
+        const elapsed = now - startTimestamp;
+
+        const seconds = Math.floor(elapsed / 1000) % 60;
+        const minutes = Math.floor(elapsed / (1000 * 60)) % 60;
+        const hours = Math.floor(elapsed / (1000 * 60 * 60)) % 24;
+        const days = Math.floor(elapsed / (1000 * 60 * 60 * 24));
+
+        let formattedTime = "";
+
+        if (days > 0) {
+            formattedTime += `${days}:`;
+        }
+        if (hours > 0 || days > 0) {
+            formattedTime += `${pad(hours)}:`;
+        }
+        if (minutes > 0 || hours > 0 || days > 0) {
+            formattedTime += `${pad(minutes)}:`;
+        }
+
+        formattedTime += `${pad(seconds)}`;
+
+        return formattedTime;
+    }
+
+    function pad(value) {
+        return String(value).padStart(2, "0");
+    }
+
+    function handleStatusUpdate(status) {
+        const firstUser = Object.values(status)[0];
+        const userStatus = firstUser?.status;
+        const activities = firstUser?.activities || [];
+
+        updateStatusCircle(userStatus);
+        updateActivities(activities);
+    }
+
+    function handlePresenceUpdate(presence) {
+        const userPresence = presence?.status;
+        const activities = presence?.activities || [];
+
+        updateStatusCircle(userPresence);
+        updateActivities(activities);
+    }
+
+    function parseDiscordImage(imageUrl, activityID) {
+        if (imageUrl.startsWith("mp:external/")) {
+            const httpsIndex = imageUrl.indexOf("https/");
+            if (httpsIndex !== -1) {
+                return "https://" + imageUrl.slice(httpsIndex + "https/".length);
+            }
+        } else if (/^\d+$/.test(imageUrl)) {
+            return `https://cdn.discordapp.com/app-assets/${activityID}/${imageUrl}`;
+        }
+        return imageUrl;
+    }
+
+    function handleUsersUpdate(users) {
+        const firstUser = Object.values(users)[0];
+
+        ws.send(JSON.stringify({ type: "getBadges", userId: firstUser.id }));
+        if (!firstUser || !firstUser.user) return;
+
+        const { avatar, avatarDecoration, username, banner } = firstUser;
+        const displayName = firstUser.user.globalName || firstUser.user.username;
+
+        updateUserInfo(avatar, avatarDecoration, username, displayName, banner);
+    }
+
+    function updateUserInfo(
+        avatarUrl,
+        decorationUrl,
+        username,
+        displayName,
+        banner,
+    ) {
+        if (bannerElem) {
+            bannerElem.src = banner + "?size=2048" || "";
+        }
+
+        if (avatarElem) {
+            avatarElem.src = avatarUrl; //|| placeholderAvatar;
+        }
+
+        if (decorationElem) {
+            if (decorationUrl) {
+                decorationElem.src = decorationUrl;
+                decorationElem.style.display = "block";
+            } else {
+                decorationElem.style.display = "none";
+            }
+        }
+
+        if (usernameElem) {
+            usernameElem.textContent = username || placeholderUsername;
+        }
+
+        if (displayNameElem) {
+            displayNameElem.textContent = displayName || placeholderUsername;
+        }
+    }
+
+    function handleBadges(badges) {
+
+        // known as 
+        badges.push({
+            badge: "https://cdn.discordapp.com/badge-icons/6de6d34650760ba5551a79732e98ed60.png",
+            tooltip: "Originally known as Creation's#0001",
+        });
+
+        //hypesquad
+        badges.push({
+            badge: "https://cdn.discordapp.com/badge-icons/3aa41de486fa12454c3761e8e223442e.png",
+            tooltip: "HypeSquad Balance",
+        });
+
+        badges.push({
+            badge: "https://cdn.discordapp.com/badge-icons/6bdc42827a38498929a4920da12695d9.png",
+            tooltip: "Active Developer",
+        });
+
+        for (const badge of badges) {
+            if ((badge && !badge.badge) || !badge.tooltip) continue;
+
+            const badgeImg = document.createElement("img");
+            badgeImg.src = badge.badge;
+            badgeImg.alt = badge.tooltip;
+            badgeImg.title = badge.tooltip;
+            badgeImg.classList.add("badge");
+            badgeContainer.appendChild(badgeImg);
+        }
+    }
+});
\ No newline at end of file
diff --git a/src/www/routes/index.ts b/src/www/routes/index.ts
new file mode 100644
index 0000000..03c02ae
--- /dev/null
+++ b/src/www/routes/index.ts
@@ -0,0 +1,20 @@
+import type { FastifyReply, FastifyRequest } from "fastify";
+import type { IRouteInfo } from "../../interfaces/routes";
+
+const routeInfo: IRouteInfo = {
+    enabled: true,
+    path: "/",
+    method: "GET"
+};
+
+async function route(request : FastifyRequest, reply : FastifyReply) : Promise<string> {
+    const siteData = {
+        title: "Home",
+    };
+
+    return reply.viewAsync("index", {
+        ...siteData,
+    });
+}
+
+export default { routeInfo, route }
\ No newline at end of file
diff --git a/src/www/views/index.ejs b/src/www/views/index.ejs
new file mode 100644
index 0000000..523336b
--- /dev/null
+++ b/src/www/views/index.ejs
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="dark">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>creations.works</title>
+
+  <link rel="stylesheet" href="/public/css/style.css" />
+  <link rel="stylesheet" href="/public/css/user.style.css" />
+
+  <script src="/public/js/status.ws.js" defer></script>
+  <script src="/public/js/snow.js" defer></script>
+</head>
+
+<body>
+  <div id="page-views"></div>
+  <div id="user-container">
+    <div id="user-info">
+      <div class="avatar-custom-container" id="avatar-custom-container">
+        <img id="banner" />
+        <div class="avatar-container">
+          <img id="avatar" />
+          <img id="avatar-decoration" src="" />
+          <div id="status-circle" class="status-circle offline"></div>
+        </div>
+      </div>
+      <div id="name-pronouns-badges">
+        <h2 id="display-name">User Name</h2>
+        <div id="pronouns-badges">
+          <div id="name-pronouns">
+            <span id="username">User Name</span>
+            <span id="dot">•</span>
+            <span id="pronouns">creations.works</span>
+          </div>
+          <div id="badge-container"></div>
+        </div>
+      </div>
+      <div id="user-tabs-container">
+        <div id="user-tabs-buttons">
+          <button id="about-me" class="tab-button" data-type='about-me'>About Me</button>
+          <button id="activity" class="tab-button active" data-type='activity'>Activity</button>
+        </div>
+        <div id="tab-content">
+          <div id="about-me-content" class="tab-content hidden">
+            <a href="https://atums.world" target="_blank">https://atums.world</a>
+            <div id="socials">
+              <h3>Find me here</h3>
+              <ul>
+                <li>
+                  <img src="https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fsudhop.com%2Fbilder%2Fmail.png&f=1&nofb=1&ipt=1ae00a3202725283cc556e3feefdb72750c50afa5a44f428ea3c15d7babf1d70&ipo=images" />
+                  <a href="mailto:creations@creations.works">Email, creations@creations.works</a>
+                </li>
+                <li>
+                  <img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" />
+                  <a href="https://discord.gg/DxFhhbr2pm" target="_blank">Discord</a>
+                </li>
+                <li>
+                  <img src="https://git.creations.works/assets/img/logo.svg" alt="Forgejo Icon">
+                  <a href="https://git.creations.works/creations" target="_blank">Forgejo</a>
+                </li>
+                <li>
+                  <img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fportalrepository.com%2Fwp-content%2Fuploads%2F2019%2F07%2F1024px-Steam_icon_logo.svg.png&f=1&nofb=1&ipt=42fde64bfc0c2aa3cab5944e37a1494d95b368514950c98317460883a1a94bf6&ipo=images" />
+                  <a href="https://steamcommunity.com/id/creations_works/" target="_blank">Steam</a>
+              </ul>
+            </div>
+          </div>
+          <div id="activity-content" class="tab-content active"></div>
+        </div>
+      </div>
+    </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    // Enable latest features
+    "lib": ["ESNext", "DOM"],
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}