add realtime statistics!!!
Some checks failed
Deploy App / docker (ubuntu-latest) (push) Has been cancelled
Some checks failed
Deploy App / docker (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
parent
f5203f9742
commit
defdcccdb6
4
.fleet/settings.json
Normal file
4
.fleet/settings.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"toolchains": [],
|
||||||
|
"nodejs.editor.formatOnSave.prettier.mode": "Enabled"
|
||||||
|
}
|
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
@ -22,7 +22,9 @@
|
|||||||
"next": "14.2.1",
|
"next": "14.2.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"react-countup": "^6.5.3",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-use-websocket": "3.0.0",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@ -44,9 +44,15 @@ dependencies:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18
|
specifier: ^18
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-countup:
|
||||||
|
specifier: ^6.5.3
|
||||||
|
version: 6.5.3(react@18.2.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18
|
specifier: ^18
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-use-websocket:
|
||||||
|
specifier: 3.0.0
|
||||||
|
version: 3.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.2
|
specifier: ^2.2.2
|
||||||
version: 2.2.2
|
version: 2.2.2
|
||||||
@ -2064,6 +2070,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/countup.js@2.8.0:
|
||||||
|
resolution: {integrity: sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/create-jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2):
|
/create-jest@29.7.0(@types/node@20.12.7)(ts-node@10.9.2):
|
||||||
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@ -4446,6 +4456,15 @@ packages:
|
|||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
/react-countup@6.5.3(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.3.0'
|
||||||
|
dependencies:
|
||||||
|
countup.js: 2.8.0
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-dom@18.2.0(react@18.2.0):
|
/react-dom@18.2.0(react@18.2.0):
|
||||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4464,6 +4483,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-use-websocket@3.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
react-dom: '>= 16.8.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react@18.2.0:
|
/react@18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -7,6 +7,7 @@ import { cn } from "@/common/utils";
|
|||||||
import { getMojangEndpointStatus } from "mcutils-library";
|
import { getMojangEndpointStatus } from "mcutils-library";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the page to be dynamic, so it will be regenerated on every request
|
* Force the page to be dynamic, so it will be regenerated on every request
|
||||||
@ -58,7 +59,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page(): Promise<JSX.Element> {
|
export default async function Page(): Promise<ReactElement> {
|
||||||
const { endpoints } = await getMojangEndpointStatus();
|
const { endpoints } = await getMojangEndpointStatus();
|
||||||
const endpointsSize = Object.entries(endpoints).length;
|
const endpointsSize = Object.entries(endpoints).length;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
type Button = {
|
type Button = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -24,7 +25,7 @@ const buttons: Button[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="text-center flex flex-col justify-center">
|
<div className="text-center flex flex-col justify-center">
|
||||||
<h1 className="text-4xl mb-2">Minecraft Utilities</h1>
|
<h1 className="text-4xl mb-2">Minecraft Utilities</h1>
|
||||||
|
@ -9,6 +9,7 @@ import { CachedPlayer, McUtilsAPIError, SkinPart, getPlayer } from "mcutils-libr
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
params: {
|
params: {
|
||||||
@ -46,7 +47,7 @@ export async function generateMetadata({ params: { id } }: Params): Promise<Meta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params: { id } }: Params): Promise<JSX.Element> {
|
export default async function Page({ params: { id } }: Params): Promise<ReactElement> {
|
||||||
let error: string | undefined = undefined; // The error to display
|
let error: string | undefined = undefined; // The error to display
|
||||||
let player: CachedPlayer | undefined = undefined; // The player to display
|
let player: CachedPlayer | undefined = undefined; // The player to display
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
} from "mcutils-library";
|
} from "mcutils-library";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
params: {
|
params: {
|
||||||
@ -30,7 +31,7 @@ type Params = {
|
|||||||
*/
|
*/
|
||||||
function getFavicon(
|
function getFavicon(
|
||||||
platform: ServerPlatform,
|
platform: ServerPlatform,
|
||||||
server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer
|
server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (platform === ServerPlatform.Bedrock) {
|
if (platform === ServerPlatform.Bedrock) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -88,14 +89,13 @@ export async function generateMetadata({ params: { platform, hostname } }: Param
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params: { platform, hostname } }: Params): Promise<JSX.Element> {
|
export default async function Page({ params: { platform, hostname } }: Params): Promise<ReactElement> {
|
||||||
let error: string | undefined = undefined; // The error to display
|
let error: string | undefined = undefined; // The error to display
|
||||||
let server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer | undefined = undefined; // The server to display
|
let server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer | undefined = undefined; // The server to display
|
||||||
let invalidPlatform = checkPlatform(platform) === false; // Whether the platform is invalid
|
let invalidPlatform = !checkPlatform(platform); // Whether the platform is invalid
|
||||||
|
|
||||||
// Try and get the player to display
|
// Try and get the player to display
|
||||||
try {
|
try {
|
||||||
console.log(platform);
|
|
||||||
if (invalidPlatform) {
|
if (invalidPlatform) {
|
||||||
error = "Invalid platform"; // Set the error message
|
error = "Invalid platform"; // Set the error message
|
||||||
} else {
|
} else {
|
||||||
|
14
src/app/(pages)/stats/page.tsx
Normal file
14
src/app/(pages)/stats/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Stats } from "@/app/components/stats";
|
||||||
|
|
||||||
|
export default async function Page(): Promise<ReactElement> {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 items-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="font-semibold text-xl">API Statistics</p>
|
||||||
|
<p>View the statistics for the API in real-time!</p>
|
||||||
|
</div>
|
||||||
|
<Stats />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -8,7 +8,7 @@ export default function Container({
|
|||||||
return (
|
return (
|
||||||
<div className="z-[9999] m-auto flex h-screen min-h-full flex-col items-center opacity-90 w-full xs:max-w-[1200px]">
|
<div className="z-[9999] m-auto flex h-screen min-h-full flex-col items-center opacity-90 w-full xs:max-w-[1200px]">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="w-full flex m-4 justify-center">{children}</div>
|
<div className="w-full h-full flex m-4 justify-center">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ import { IconProps } from "./icon-props";
|
|||||||
export function MoonIcon({ size = 24, color = "#fff" }: IconProps): JSX.Element {
|
export function MoonIcon({ size = 24, color = "#fff" }: IconProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill={color} viewBox="0 -960 960 960">
|
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill={color} viewBox="0 -960 960 960">
|
||||||
<path d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z" />
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480-120Zm0-60q109 0 190-67.5T771-406q-25 11-53.667 16.5Q688.667-384 660-384q-114.689 0-195.345-80.655Q384-545.311 384-660q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180-480q0 125 87.5 212.5T480-180Zm-4-297Z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
12
src/app/components/icon/storage-icon.tsx
Normal file
12
src/app/components/icon/storage-icon.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { IconProps } from "./icon-props";
|
||||||
|
|
||||||
|
export function StorageIcon({ size = 24, color = "#fff" }: IconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill={color} viewBox="0 -960 960 960">
|
||||||
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d="M120-160v-148h720v148H120Zm60-38h72v-72h-72v72Zm-60-454v-148h720v148H120Zm60-38h72v-72h-72v72Zm-60 284v-148h720v148H120Zm60-38h72v-72h-72v72Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
20
src/app/components/stat.tsx
Normal file
20
src/app/components/stat.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import CountUp from "react-countup";
|
||||||
|
|
||||||
|
type StatProps = {
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
icon: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Stat({ title, value, icon }: StatProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="bg-secondary p-2 rounded-lg flex divide-x justify-center items-center text-center w-fit">
|
||||||
|
<div className="pr-2">{icon}</div>
|
||||||
|
<div className="pl-2">
|
||||||
|
<p>{title}</p>
|
||||||
|
<CountUp start={0} end={value} duration={0.5} preserveValue />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
src/app/components/stats.tsx
Normal file
35
src/app/components/stats.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { StorageIcon } from "@/app/components/icon/storage-icon";
|
||||||
|
import { Stat } from "@/app/components/stat";
|
||||||
|
import useWebSocket from "react-use-websocket";
|
||||||
|
|
||||||
|
type Stat = {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
icon: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats: Stat[] = [
|
||||||
|
{
|
||||||
|
id: "totalRequests",
|
||||||
|
displayName: "Total Requests",
|
||||||
|
icon: <StorageIcon />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Stats(): ReactElement {
|
||||||
|
const { lastMessage, readyState } = useWebSocket("https://api.mcutils.xyz/websocket/metrics");
|
||||||
|
const metrics = lastMessage !== null ? JSON.parse(lastMessage.data).metrics : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{stats.map((stat, index) => {
|
||||||
|
const value = metrics ? metrics[stat.id] : "???";
|
||||||
|
|
||||||
|
return <Stat key={index} title={stat.displayName} value={value} icon={stat.icon} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user