├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── interfaces │ └── aether.ts └── ws │ ├── event-emitter.ts │ ├── event-handler.ts │ ├── message-util.ts │ └── message.ts ├── next-env.d.ts ├── package.json ├── public ├── favicon.ico ├── logo-gr.svg ├── logo.svg └── logo_white.svg ├── src ├── components │ ├── AppNavbar.tsx │ ├── CategoryFlagsTable.tsx │ ├── ClusterModal.tsx │ ├── CommandsTable.tsx │ ├── Footer.tsx │ ├── Head.tsx │ ├── Pagination.tsx │ └── ServersList.tsx ├── hooks │ └── use-websocket.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── commands.tsx │ ├── discover.tsx │ ├── index.tsx │ └── stats.tsx ├── style.css └── types.ts ├── tsconfig.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: GamingGeek 2 | patreon: FireBot 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | /.idea 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | package-lock.json 27 | 28 | .vscode 29 | .vercel/README.txt 30 | .vercel/project.json 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fire-Website 2 | Source Code of [Fire Website](https://fire.gaminggeek.dev) 3 | -------------------------------------------------------------------------------- /lib/interfaces/aether.ts: -------------------------------------------------------------------------------- 1 | export type ShardStats = { 2 | id: number; 3 | wsPing: number; 4 | guilds: number; 5 | unavailableGuilds: number; 6 | status: number; 7 | }; 8 | 9 | export type ClusterStats = { 10 | id: number; 11 | name: string; 12 | env: string; 13 | user: string; 14 | userId: string; 15 | uptime: string; 16 | started: string; 17 | cpu: number; 18 | ram: string; 19 | ramBytes: number; 20 | totalRam: string; 21 | totalRamBytes: number; 22 | pid: number; 23 | version: string; 24 | versions: string; 25 | guilds: number; 26 | unavailableGuilds: number; 27 | users: number; 28 | userStatuses?: { online: number; dnd: number; idle: number; offline: number }; 29 | commands: number; 30 | events: number; 31 | restPing: number; 32 | shards: ShardStats[]; 33 | error?: string; 34 | reason?: string; 35 | code?: number; 36 | }; 37 | 38 | export type FireStats = { 39 | cpu: number; 40 | ram: string; 41 | ramBytes: number; 42 | totalRamBytes: number; 43 | aetherStats?: { 44 | ramBytes: number; 45 | restLatency: number; 46 | }; 47 | clusterCount: number; 48 | shardCount: number; 49 | guilds: number; 50 | users: number; 51 | events: number; 52 | clusters: ClusterStats[]; 53 | }; 54 | 55 | export type Command = { 56 | name: string; 57 | description: string; 58 | usage: string; 59 | aliases: string; 60 | category?: string; 61 | }; 62 | 63 | export type CategoryFlag = { 64 | name: string; 65 | description: string; 66 | usage: string; 67 | }; 68 | 69 | export type Category = { 70 | id: number; 71 | name: string; 72 | commands: Command[]; 73 | flags?: CategoryFlag[]; 74 | Note?: string; 75 | }; 76 | 77 | export type DiscoverableGuild = { 78 | name: string; 79 | id: string; 80 | icon: string; 81 | splash: string; 82 | vanity: string; 83 | members: number; 84 | key?: number; 85 | }; 86 | 87 | export interface Payload { 88 | op: number; 89 | d?: unknown; 90 | s?: number; 91 | t?: string; 92 | } 93 | 94 | export enum WebsiteEvents { 95 | IDENTIFY_CLIENT, 96 | RESUME_CLIENT, 97 | HELLO, 98 | HEARTBEAT, 99 | HEARTBEAT_ACK, 100 | SUBSCRIBE, 101 | GUILD_CREATE, 102 | GUILD_DELETE, 103 | GUILD_SYNC, 104 | REALTIME_STATS, 105 | COMMANDS_UPDATE, 106 | DISCOVERY_UPDATE, 107 | NOTIFICATION, 108 | REMINDERS_UPDATE, 109 | CONFIG_UPDATE, 110 | GUILD_JOIN_REQUEST, 111 | DATA_REQUEST, 112 | } 113 | 114 | export interface Notification { 115 | text: string; 116 | severity: "success" | "info" | "warning" | "error"; 117 | horizontal: "left" | "right" | "center"; 118 | vertical: "top" | "bottom"; 119 | autoHideDuration: number; 120 | } 121 | -------------------------------------------------------------------------------- /lib/ws/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { FireStats } from "../interfaces/aether"; 2 | import { EventEmitter } from "events"; 3 | 4 | interface EmitterEvents { 5 | REALTIME_STATS: (stats: FireStats) => void; 6 | SUBSCRIBE: (route: string, extra?: unknown) => void; 7 | HELLO: (hello: { interval: number }) => void; 8 | } 9 | 10 | export declare interface Emitter { 11 | on(event: T, listener: EmitterEvents[T]): this; 12 | emit( 13 | event: T, 14 | ...args: Parameters 15 | ): boolean; 16 | } 17 | 18 | export class Emitter extends EventEmitter {} 19 | -------------------------------------------------------------------------------- /lib/ws/event-handler.ts: -------------------------------------------------------------------------------- 1 | import { WebsiteEvents } from "../interfaces/aether"; 2 | import { MessageUtil } from "./message-util"; 3 | import { EventEmitter } from "events"; 4 | import { Message } from "./message"; 5 | 6 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 7 | 8 | export class EventHandler { 9 | identified: "identifying" | boolean; 10 | heartbeat?: NodeJS.Timeout; 11 | websocket?: WebSocket; 12 | emitter: EventEmitter; 13 | subscribed: string; 14 | session?: string; 15 | queue: Message[]; 16 | acked?: boolean; 17 | seq?: number; 18 | 19 | constructor(emitter: EventEmitter) { 20 | this.identified = false; 21 | this.emitter = emitter; 22 | this.subscribed = 23 | typeof window != "undefined" ? window.location.pathname : "/"; 24 | this.queue = []; 25 | } 26 | 27 | setWebsocket(websocket: WebSocket, reconnect?: boolean) { 28 | if (this.websocket) { 29 | this.websocket.close(1000, "Reconnecting"); 30 | delete this.websocket; 31 | } 32 | this.websocket = websocket; 33 | this.websocket.onmessage = (message) => { 34 | const decoded = MessageUtil.decode(message.data); 35 | if (!decoded) 36 | return console.error( 37 | "%c WS %c Failed to decode message! ", 38 | "background: #C95D63; color: white; border-radius: 3px 0 0 3px;", 39 | "background: #353A47; color: white; border-radius: 0 3px 3px 0", 40 | { data: message.data } 41 | ); 42 | if (typeof decoded.s == "number") this.seq = decoded.s; 43 | // heartbeats acks can be spammy and have a null body anyways 44 | if (decoded.op != WebsiteEvents.HEARTBEAT_ACK) 45 | console.debug( 46 | `%c WS %c Incoming %c ${WebsiteEvents[decoded.op]} `, 47 | "background: #279AF1; color: white; border-radius: 3px 0 0 3px;", 48 | "background: #9CFC97; color: black; border-radius: 0 3px 3px 0", 49 | "background: #353A47; color: white; border-radius: 0 3px 3px 0", 50 | decoded 51 | ); 52 | this.emitter.emit(WebsiteEvents[decoded.op], decoded.d); 53 | if (WebsiteEvents[decoded.op] in this) 54 | this[WebsiteEvents[decoded.op]](decoded.d); 55 | }; 56 | this.websocket.onopen = () => { 57 | reconnect && 58 | this.emitter.emit("NOTIFICATION", { 59 | text: "Websocket connected", 60 | severity: "success", 61 | horizontal: "right", 62 | vertical: "top", 63 | autoHideDuration: 3000, 64 | }); 65 | console.info( 66 | `%c WS %c Websocket connected! `, 67 | "background: #9CFC97; color: black; border-radius: 3px 0 0 3px;", 68 | "background: #353A47; color: white; border-radius: 0 3px 3px 0", 69 | this.websocket 70 | ); 71 | this.identify(); 72 | while (this.queue?.length) this.send(this.queue.pop()); 73 | }; 74 | this.websocket.onclose = (event) => { 75 | console.error( 76 | `%c WS %c Websocket closed! `, 77 | "background: #C95D63; color: white; border-radius: 3px 0 0 3px;", 78 | "background: #353A47; color: white; border-radius: 0 3px 3px 0", 79 | event 80 | ); 81 | if (event.code == 4015) 82 | this.emitter.emit("NOTIFICATION", { 83 | text: "Connected to websocket in another session", 84 | severity: "error", 85 | horizontal: "right", 86 | vertical: "top", 87 | autoHideDuration: 15000, 88 | }); 89 | else if (event.code == 4016) 90 | this.emitter.emit("NOTIFICATION", { 91 | text: "Connected to websocket from another location", 92 | severity: "error", 93 | horizontal: "right", 94 | vertical: "top", 95 | autoHideDuration: 15000, 96 | }); 97 | else if (event.code == 4005) { 98 | this.emitter.emit("NOTIFICATION", { 99 | text: "Invalid Session", 100 | severity: "error", 101 | horizontal: "right", 102 | vertical: "top", 103 | autoHideDuration: 5000, 104 | }); 105 | delete this.seq; 106 | } else 107 | this.emitter.emit("NOTIFICATION", { 108 | text: "Websocket error occurred", 109 | severity: "error", 110 | horizontal: "right", 111 | vertical: "top", 112 | autoHideDuration: 5000, 113 | }); 114 | this.identified = false; 115 | // cannot recover from codes below 116 | if ( 117 | event.code == 1013 || 118 | event.code == 1008 || 119 | event.code == 4001 || 120 | event.code == 4015 || 121 | event.code == 4016 122 | ) 123 | return; 124 | try { 125 | sleep(2500).then(() => { 126 | console.info( 127 | "%c WS %c Reconnecting... ", 128 | "background: #9CFC97; color: black; border-radius: 3px 0 0 3px;", 129 | "background: #353A47; color: white; border-radius: 0 3px 3px 0" 130 | ); 131 | const ws = new WebSocket( 132 | "wss://aether-ws.gaminggeek.dev/website?encoding=zlib" 133 | ); 134 | return this.setWebsocket(ws, true); 135 | }); 136 | } catch { 137 | console.error( 138 | "%c WS %c Websocket failed to reconnect! ", 139 | "background: #C95D63; color: white; border-radius: 3px 0 0 3px;", 140 | "background: #353A47; color: white; border-radius: 0 3px 3px 0" 141 | ); 142 | } 143 | }; 144 | if (this.websocket.readyState == this.websocket.CLOSED) 145 | this.websocket.onclose( 146 | new CloseEvent("unknown", { 147 | code: 0, 148 | reason: "unknown", 149 | wasClean: false, 150 | }) 151 | ); 152 | return this; 153 | } 154 | 155 | handleSubscribe(route: string, extra?: unknown) { 156 | if (route == this.subscribed && !extra) return; 157 | this.send(new Message(WebsiteEvents.SUBSCRIBE, { route, extra })); 158 | this.subscribed = route; 159 | } 160 | 161 | HELLO(data: { sessionId: string; interval: number }) { 162 | if (this.heartbeat) clearInterval(this.heartbeat); 163 | this.heartbeat = setInterval(() => { 164 | if (this.acked == false) 165 | return this.websocket?.close(4004, "Did not receive heartbeat ack"); 166 | this.acked = false; 167 | this.send(new Message(WebsiteEvents.HEARTBEAT, this.seq || null)); 168 | }, data.interval); 169 | this.session = data.sessionId; 170 | } 171 | 172 | HEARTBEAT_ACK() { 173 | this.acked = true; 174 | } 175 | 176 | identify() { 177 | if (this.identified) return; 178 | this.identified = "identifying"; 179 | if (this.heartbeat) { 180 | clearInterval(this.heartbeat); 181 | delete this.heartbeat; 182 | } 183 | this.send( 184 | new Message(WebsiteEvents.IDENTIFY_CLIENT, { 185 | config: { subscribed: this.subscribed ?? window.location.pathname }, 186 | env: process.env.NODE_ENV, 187 | sessionId: this.session, 188 | seq: this.seq, 189 | }) 190 | ); 191 | this.identified = true; 192 | setTimeout(() => { 193 | if ( 194 | !this.heartbeat && 195 | this.websocket && 196 | this.websocket.readyState == this.websocket.OPEN 197 | ) 198 | this.websocket.close(4004, "Did not receive HELLO"); 199 | }, 2000); 200 | } 201 | 202 | devToolsWarning() { 203 | console.log( 204 | `%c STOP! 205 | 206 | %cUNLESS YOU KNOW WHAT YOU'RE DOING, DO NOT COPY/PASTE ANYTHING IN HERE! 207 | DOING SO COULD REVEAL SENSITIVE INFORMATION SUCH AS YOUR EMAIL OR ACCESS TOKEN 208 | 209 | IT'S BEST TO JUST CLOSE THIS WINDOW AND PRETEND IT DOES NOT EXIST.`, 210 | "background: #C95D63; color: white; font-size: xxx-large; border-radius: 8px 8px 8px 8px;", 211 | "background: #353A47; color: white; font-size: medium; border-radius: 0 0 0 0" 212 | ); 213 | } 214 | 215 | private send(message?: Message) { 216 | if ( 217 | !message || 218 | !this.websocket || 219 | this.websocket.readyState != this.websocket.OPEN 220 | ) 221 | return message && this.queue.push(message); 222 | if (process.env.NODE_ENV == "development") 223 | (globalThis as { [key: string]: unknown }).eventHandler = this; 224 | // heartbeats can be spammy and just have the sequence anyways 225 | if (message.type != WebsiteEvents.HEARTBEAT) 226 | console.debug( 227 | `%c WS %c Outgoing %c ${WebsiteEvents[message.type]} `, 228 | "background: #279AF1; color: white; border-radius: 3px 0 0 3px;", 229 | "background: #9CFC97; color: black; border-radius: 0 3px 3px 0", 230 | "background: #353A47; color: white; border-radius: 0 3px 3px 0", 231 | message.data 232 | ); 233 | if (this.identified == false) this.identify(); 234 | this.websocket.send(MessageUtil.encode(message)); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /lib/ws/message-util.ts: -------------------------------------------------------------------------------- 1 | import { deflateSync, inflateSync } from "zlib" 2 | import { Payload } from "../interfaces/aether" 3 | import { Message } from "./message" 4 | 5 | export class MessageUtil { 6 | static encode(message: Message) { 7 | const deflated = deflateSync(JSON.stringify(message), { level: 5 }) 8 | return deflated.toString("base64") 9 | } 10 | 11 | static decode(message: string) { 12 | const inflated = inflateSync(Buffer.from(message, "base64"), { 13 | level: 5, 14 | })?.toString() 15 | if (!inflated) return null 16 | else return JSON.parse(inflated) as Payload 17 | } 18 | } -------------------------------------------------------------------------------- /lib/ws/message.ts: -------------------------------------------------------------------------------- 1 | import { WebsiteEvents } from "../interfaces/aether" 2 | 3 | export class Message { 4 | type: WebsiteEvents 5 | data: unknown 6 | 7 | constructor(type: WebsiteEvents, data: unknown) { 8 | this.type = type 9 | this.data = data 10 | } 11 | 12 | toJSON() { 13 | return { 14 | op: this.type, 15 | d: this.data, 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fire", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3002", 7 | "build": "next build", 8 | "start": "next start -p 3001", 9 | "export": "next export" 10 | }, 11 | "dependencies": { 12 | "@appbaseio/reactivesearch": "^3.12.7", 13 | "bootstrap": "^4.5.3", 14 | "cookie-parser": "^1.4.5", 15 | "cross-env": "^7.0.2", 16 | "express": "^4.17.1", 17 | "next": "^11.1.1", 18 | "next-cookies": "^2.0.3", 19 | "react": "17.0.1", 20 | "react-bootstrap": "^1.4.0", 21 | "react-dom": "17.0.1" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^14.14.5", 25 | "@types/react": "^16.9.53", 26 | "prop-types": "^15.7.2", 27 | "typescript": "^4.0.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FireDiscordBot/website/102cda0630679f47331b669b8ab82d80c8016d9e/public/favicon.ico -------------------------------------------------------------------------------- /public/logo-gr.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /public/logo_white.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /src/components/AppNavbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "next/link"; 3 | import Container from "react-bootstrap/Container"; 4 | import Navbar from "react-bootstrap/Navbar"; 5 | import { useRouter } from "next/router"; 6 | import Nav from "react-bootstrap/Nav"; 7 | 8 | // import { emitter } from "../pages/_app"; 9 | 10 | interface NavLinkProps { 11 | href: string; 12 | } 13 | 14 | const NavLink = ({ href, children }: React.PropsWithChildren) => { 15 | const isExternal = href.startsWith("http"); 16 | const navLinks = ( 17 | 18 | {children} 19 | 20 | ); 21 | 22 | return !isExternal ? ( 23 | 24 | {navLinks} 25 | 26 | ) : ( 27 | navLinks 28 | ); 29 | }; 30 | 31 | const AppNavbar = () => { 32 | const router = useRouter(); 33 | // emitter.emit("SUBSCRIBE", router.route); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default AppNavbar; 60 | -------------------------------------------------------------------------------- /src/components/CategoryFlagsTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Table from "react-bootstrap/Table"; 3 | 4 | import type { CategoryFlag } from "../types"; 5 | 6 | interface FlagsTableProps { 7 | prefix: string; 8 | flags: CategoryFlag[]; 9 | } 10 | 11 | const CategoryFlagsTable = ({ prefix, flags }: FlagsTableProps) => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {flags.map((flag) => ( 20 | 21 | 22 | 23 | 24 | 25 | ))} 26 | 27 |
NameDescriptionUsage
{flag.name}{flag.description}{flag.usage.replace("{prefix}", prefix)}
28 | ); 29 | 30 | export default CategoryFlagsTable; 31 | -------------------------------------------------------------------------------- /src/components/ClusterModal.tsx: -------------------------------------------------------------------------------- 1 | import { ClusterStats } from "../../lib/interfaces/aether"; 2 | import Modal from "react-bootstrap/Modal"; 3 | import React from "react"; 4 | 5 | interface Props { 6 | show: boolean; 7 | onHide: () => void; 8 | cluster: ClusterStats; 9 | } 10 | 11 | const getCommitURL = (version: string) => 12 | `https://github.com/FireDiscordBot/bot/commit/${version}`; 13 | 14 | const OnlineCluster = (cluster: ClusterStats) => { 15 | return ( 16 | 17 |
18 | Uptime: 19 | {cluster.uptime} 20 |
21 |
22 | CPU: 23 | {cluster.cpu}% 24 |
25 |
26 | RAM: 27 | {cluster.ram} 28 |
29 |
30 | Version: 31 | {cluster.version != "dev" ? ( 32 | 39 | {cluster.version} 40 | 41 | ) : ( 42 | cluster.version 43 | )} 44 |
45 |
46 | Guilds: 47 | {cluster.guilds?.toLocaleString()} 48 |
49 |
50 | Unavailable Guilds: 51 | {cluster.unavailableGuilds?.toLocaleString()} 52 |
53 |
54 | Users: 55 | {cluster.users?.toLocaleString()} 56 |
57 |
58 | Commands: 59 | {cluster.commands} 60 |
61 |
62 | ); 63 | }; 64 | 65 | const OfflineCluster = (cluster: ClusterStats) => { 66 | return ( 67 | 68 |
69 | Error: 70 | {cluster.error} 71 |
72 |
73 | Reason: 74 | {cluster.reason} 75 |
76 |
77 | Code: 78 | {cluster.code} 79 |
80 |
81 | ); 82 | }; 83 | 84 | const ClusterModal = ({ show, onHide, cluster }: Props) => ( 85 | 86 | 87 | 88 | {cluster.name ? ( 89 | {cluster.name} 90 | ) : ( 91 | Cluster {cluster.id} 92 | )} 93 | 94 | 95 | {cluster.error ? OfflineCluster(cluster) : OnlineCluster(cluster)} 96 | 97 | ); 98 | 99 | export default ClusterModal; 100 | -------------------------------------------------------------------------------- /src/components/CommandsTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Table from "react-bootstrap/Table"; 3 | 4 | import type { Command } from "../types"; 5 | 6 | interface CommandsTableProps { 7 | prefix: string; 8 | commands: Command[]; 9 | } 10 | 11 | const CommandsTable = ({ prefix, commands }: CommandsTableProps) => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {commands.map((command) => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | ))} 28 | 29 |
NameDescriptionUsageAliases
{command.name}{command.description}{command.usage.replace("{prefix}", prefix)}{command.aliases}
30 | ); 31 | 32 | export default CommandsTable; 33 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => ( 4 | 29 | ); 30 | 31 | export default Footer; 32 | -------------------------------------------------------------------------------- /src/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextHead from "next/head"; 3 | 4 | interface Props { 5 | title: string; 6 | } 7 | 8 | const Head = ({ title }: Props) => ( 9 | 10 | {title} 11 | 12 | ); 13 | 14 | export default Head; 15 | -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BootstrapPagination from "react-bootstrap/Pagination"; 3 | 4 | interface Props { 5 | paginate: (page: number) => void; 6 | rowsPerPage: number; 7 | totalRowCount: number; 8 | currentPage: number; 9 | className?: string; 10 | } 11 | 12 | const Pagination = ({ paginate, rowsPerPage, totalRowCount, currentPage, className }: Props) => { 13 | const pages: number[] = []; 14 | 15 | for (let i = 1; i <= Math.ceil(totalRowCount / rowsPerPage); i++) { 16 | pages.push(i); 17 | } 18 | 19 | const goToFirst = () => paginate(1); 20 | const goToPrevious = () => paginate(currentPage - 1); 21 | const goToNext = () => paginate(currentPage + 1); 22 | const goToLast = () => paginate(pages.length); 23 | 24 | return ( 25 | 26 | 27 | 28 | {pages.slice(currentPage - 1, currentPage + 5).map((number) => { 29 | const clickHandler = () => paginate(number); 30 | return ( 31 | 32 | {number} 33 | 34 | ); 35 | })} 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Pagination; 43 | -------------------------------------------------------------------------------- /src/components/ServersList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Col from "react-bootstrap/Col"; 3 | import Card from "react-bootstrap/Card"; 4 | import Media from "react-bootstrap/Media"; 5 | import type { Server } from "../types"; 6 | 7 | interface ServerDisplayProps { 8 | server: Server; 9 | } 10 | 11 | const ServerDisplay = ({ server }: ServerDisplayProps) => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | {server.name} 19 |
20 |
21 | {server.members.toLocaleString()} Members 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 | ); 30 | 31 | interface ServersListProps { 32 | servers: Server[]; 33 | } 34 | 35 | const ServersList = ({ servers }: ServersListProps) => { 36 | return ( 37 | <> 38 | {servers.map((server) => ( 39 | 40 | 41 | 42 | ))} 43 | 44 | ); 45 | }; 46 | 47 | export default ServersList; 48 | -------------------------------------------------------------------------------- /src/hooks/use-websocket.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | import * as React from "react"; 4 | 5 | import { EventHandler } from "../../lib/ws/event-handler"; 6 | 7 | const useWebsocket = (url: string, emitter: EventEmitter) => { 8 | const [handler, setHandler] = React.useState(null); 9 | React.useEffect(() => { 10 | const ws = new WebSocket(url); 11 | const handler = new EventHandler(emitter).setWebsocket(ws); 12 | setHandler(handler); 13 | 14 | return () => ws.close(); 15 | }, [emitter, url]); 16 | 17 | return [handler]; 18 | }; 19 | 20 | export default useWebsocket; 21 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { AppProps } from "next/app"; 3 | 4 | import Footer from "../components/Footer"; 5 | import AppNavbar from "../components/AppNavbar"; 6 | 7 | import "bootstrap/dist/css/bootstrap.css"; 8 | import "../style.css"; 9 | 10 | import { EventHandler } from "../../lib/ws/event-handler"; 11 | import { Emitter } from "../../lib/ws/event-emitter"; 12 | import useWebsocket from "../hooks/use-websocket"; 13 | 14 | // export const emitter = new Emitter(); 15 | 16 | const MyApp = ({ Component, pageProps }: AppProps) => { 17 | // const [handler] = useWebsocket( 18 | // "wss://aether-ws.gaminggeek.dev/website?encoding=zlib", 19 | // emitter 20 | // ); 21 | // if (handler) { 22 | // initHandler(handler); 23 | // } 24 | 25 | return ( 26 | <> 27 | 28 | 29 |