Compare commits
105 Commits
9e17145e90
...
renovate/r
Author | SHA1 | Date | |
---|---|---|---|
d58a507331 | |||
96ab998031 | |||
7de2848f45 | |||
63fead44c3 | |||
f8aa41ed05 | |||
50e6506fc4 | |||
2a6488d4e5 | |||
7be126fe00 | |||
0ef741381c | |||
51ac278329 | |||
9996df2f92 | |||
c1c74ffef7 | |||
cf0e595cfc | |||
2efcb4a780 | |||
50037d9090 | |||
8a1263a1dd | |||
137de19133 | |||
c9c79c1f29 | |||
65d0cc7807 | |||
d44ace3f20 | |||
5b251e07ad | |||
636b0dde29 | |||
634cf8877a | |||
6ee34eaf44 | |||
d51215a781 | |||
22dba0bc90 | |||
78ff92eed3 | |||
7e73cc3e8c | |||
49cae6930b | |||
33ab92f579 | |||
db6af18475 | |||
a2cd3d0cf5 | |||
916ee81302 | |||
28aa81036f | |||
5fde0f46c5 | |||
5a4056f113 | |||
08e270da6f | |||
b2fbcb6ebf | |||
7b602d64f8 | |||
4026b08b42 | |||
d5b48e8bc9 | |||
d3886e4a39 | |||
94b81e0d69 | |||
35e786d9a5 | |||
9278a23f5a | |||
2954dd4955 | |||
0ce04fbc37 | |||
9930b8387f | |||
9be11a628b | |||
0afcd08f91 | |||
d1b3b7e1fe | |||
0ae2c2956a | |||
35ad8b458b | |||
c7365df0bd | |||
307551dc05 | |||
fc90fdd59b | |||
829a0afea0 | |||
266ac15bb1 | |||
78987e0c55 | |||
65a154a5dc | |||
179b5eb9d2 | |||
24f34ecd03 | |||
8e655681a7 | |||
91867bb718 | |||
2a17ac2d3d | |||
840711604b | |||
260dadda6d | |||
5370fd7cad | |||
725e0cd25d | |||
649141ece3 | |||
390f1ffd64 | |||
eb32d3786d | |||
bc27ed78b1 | |||
11e2efef2e | |||
68e0599159 | |||
bc04ac8e82 | |||
fe0a0d19f9 | |||
cfdc69a078 | |||
9abf21e890 | |||
2d3db00551 | |||
73441e0898 | |||
7bee4c1611 | |||
5fd3d2f822 | |||
3156916b4f | |||
a29f0c41ac | |||
dbcba6682b | |||
cf1f2bce26 | |||
795f02e0e0 | |||
239d2f2078 | |||
30e55d43c7 | |||
62919e1b93 | |||
0ccc90851a | |||
fb70dd6ab7 | |||
4198176b69 | |||
8001ad7a11 | |||
89805d0442 | |||
58f4a6edf8 | |||
a0b8777a94 | |||
1e96ed1b1c | |||
63e4eedc37 | |||
c2790054db | |||
1ab27f69be | |||
52fea3da68 | |||
4ffd10e9c3 | |||
c2d7f5f33c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -50,3 +50,6 @@ next-env.d.ts
|
|||||||
# Sitemap & Robots
|
# Sitemap & Robots
|
||||||
/public/sitemap*
|
/public/sitemap*
|
||||||
/public/robots.txt
|
/public/robots.txt
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {withSentryConfig} from "@sentry/nextjs";
|
import {withSentryConfig} from "@sentry/nextjs";
|
||||||
import nextBuildId from "next-build-id";
|
import nextBuildId from "next-build-id";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current git commit hash.
|
* The current git commit hash.
|
||||||
@ -37,29 +37,38 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSentryConfig(
|
export default withSentryConfig(nextConfig, {
|
||||||
nextConfig,
|
|
||||||
{
|
|
||||||
// For all available options, see:
|
// For all available options, see:
|
||||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||||
silent: true,
|
|
||||||
org: "minecraft-utilities",
|
org: "minecraft-utilities",
|
||||||
project: "frontend",
|
project: "frontend",
|
||||||
url: "https://glitchtip.fascinated.cc/",
|
sentryUrl: "https://glitchtip.fascinated.cc/",
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN || "",
|
|
||||||
release: `frontend@${buildId}`,
|
// Only print logs for uploading source maps in CI
|
||||||
},
|
silent: !process.env.CI,
|
||||||
{
|
|
||||||
// For all available options, see:
|
// For all available options, see:
|
||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
|
|
||||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
widenClientFileUpload: true,
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
|
// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
|
// This can increase your server load as well as your hosting bill.
|
||||||
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
|
// side errors will fail.
|
||||||
|
// tunnelRoute: "/monitoring",
|
||||||
|
|
||||||
// Hides source maps from generated client bundles
|
// Hides source maps from generated client bundles
|
||||||
hideSourceMaps: true,
|
hideSourceMaps: true,
|
||||||
|
|
||||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||||
disableLogger: true,
|
disableLogger: true,
|
||||||
},
|
|
||||||
);
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
// See the following for more information:
|
||||||
|
// https://docs.sentry.io/product/crons/
|
||||||
|
// https://vercel.com/docs/cron-jobs
|
||||||
|
automaticVercelMonitors: true,
|
||||||
|
});
|
20
package.json
20
package.json
@ -25,36 +25,36 @@
|
|||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@sentry/nextjs": "^7.105.0",
|
"@sentry/nextjs": "^8.20.0",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"lucide-react": "^0.372.0",
|
"lucide-react": "^0.451.0",
|
||||||
"mcutils-library": "^1.2.6",
|
"mcutils-library": "^1.3.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "14.2.2",
|
"next": "14.2.5",
|
||||||
"next-build-id": "^3.0.0",
|
"next-build-id": "^3.0.0",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-countup": "^6.5.3",
|
"react-countup": "^6.5.3",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.14.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-use-websocket": "4.8.1",
|
"react-use-websocket": "4.9.0",
|
||||||
"read-file": "^0.2.0",
|
"read-file": "^0.2.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remote-mdx": "^0.0.4",
|
"remote-mdx": "^0.0.8",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"remark-mdx": "3.0.0",
|
"remark-mdx": "3.0.1",
|
||||||
"unified": "11.0.4",
|
"unified": "11.0.5",
|
||||||
"remark-parse": "11.0.0",
|
"remark-parse": "11.0.0",
|
||||||
"mdast-util-frontmatter": "2.0.1"
|
"mdast-util-frontmatter": "2.0.1"
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"eslint-config-next": "14.2.2",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
11826
pnpm-lock.yaml
generated
11826
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/media/platform/bedrock.png
Normal file
BIN
public/media/platform/bedrock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
public/media/platform/java.png
Normal file
BIN
public/media/platform/java.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
@ -12,4 +12,19 @@ Sentry.init({
|
|||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
// in development and sample at a lower rate in production
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||||
|
integrations: [
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
// Additional Replay configuration goes in here, for example:
|
||||||
|
maskAllText: true,
|
||||||
|
blockAllMedia: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ Sentry.init({
|
|||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||||
// spotlight: process.env.NODE_ENV === 'development',
|
// spotlight: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Card } from "@/app/components/card";
|
import { Colors } from "@/app/common/colors";
|
||||||
import { generateEmbed } from "@/app/common/embed";
|
import { generateEmbed } from "@/app/common/embed";
|
||||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||||
import { cn } from "@/app/common/utils";
|
import { cn } from "@/app/common/utils";
|
||||||
|
import { Card } from "@/app/components/card";
|
||||||
|
import { Title } from "@/app/components/title";
|
||||||
import { CachedEndpointStatus, getMojangEndpointStatus, Status } from "mcutils-library";
|
import { CachedEndpointStatus, getMojangEndpointStatus, Status } from "mcutils-library";
|
||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { Colors } from "@/app/common/colors";
|
|
||||||
import { Title } from "@/app/components/title";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@ -89,20 +89,19 @@ export default async function Page(): Promise<ReactElement> {
|
|||||||
>
|
>
|
||||||
{endpoints.length == 0 && <p>Unable to fetch endpoint statuses</p>}
|
{endpoints.length == 0 && <p>Unable to fetch endpoint statuses</p>}
|
||||||
{endpoints.length > 0 &&
|
{endpoints.length > 0 &&
|
||||||
endpoints.map((endpoint: CachedEndpointStatus) => {
|
endpoints.map((server: CachedEndpointStatus) => {
|
||||||
const { name, hostname, status } = endpoint;
|
const { name, endpoint, status } = server;
|
||||||
|
|
||||||
const url = `https://${hostname}`;
|
|
||||||
return (
|
return (
|
||||||
<div key={name} className="flex flex-row justify-between pt-2">
|
<div key={name} className="flex flex-row justify-between pt-2">
|
||||||
<div className="flex flex-col leading-[1.5rem]">
|
<div className="flex flex-col leading-[1.5rem]">
|
||||||
<p className="font-semibold">{name}</p>
|
<p className="font-semibold">{name}</p>
|
||||||
<Link
|
<Link
|
||||||
href={url}
|
href={endpoint}
|
||||||
className="text-sm text-primary hover:opacity-75 transition-all transform-gpu"
|
className="text-sm text-primary hover:opacity-75 transition-all transform-gpu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<p>{url}</p>
|
<p>{endpoint}</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("flex items-center font-semibold", getColor(status))}>
|
<div className={cn("flex items-center font-semibold", getColor(status))}>
|
||||||
|
@ -5,36 +5,9 @@ import { Button } from "../components/ui/button";
|
|||||||
import { Separator } from "../components/ui/separator";
|
import { Separator } from "../components/ui/separator";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip";
|
||||||
import { Title } from "@/app/components/title";
|
import { Title } from "@/app/components/title";
|
||||||
|
import { LandingButton } from "@/app/types/landing/landing-button";
|
||||||
|
|
||||||
type Button = {
|
const buttons: LandingButton[] = [
|
||||||
/**
|
|
||||||
* The title of the button.
|
|
||||||
*/
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tooltip to display for this statistic.
|
|
||||||
*/
|
|
||||||
tooltip: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL to go to.
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether clicking the button will
|
|
||||||
* open the link in a new tab.
|
|
||||||
*/
|
|
||||||
openInNewTab?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The class name to apply to the button.
|
|
||||||
*/
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons: Button[] = [
|
|
||||||
{
|
{
|
||||||
title: "Get Started",
|
title: "Get Started",
|
||||||
tooltip: "Click to get started with the API",
|
tooltip: "Click to get started with the API",
|
||||||
|
@ -12,23 +12,19 @@ import { CachedPlayer, getPlayer, McUtilsAPIError } from "mcutils-library";
|
|||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { Title } from "@/app/components/title";
|
import { Title } from "@/app/components/title";
|
||||||
|
import { PlayerPageParams } from "@/app/types/player/page-params";
|
||||||
|
import { TryAPlayer } from "@/app/components/player/try-a-player";
|
||||||
|
|
||||||
export const revalidate = 60;
|
export const revalidate = 60;
|
||||||
|
|
||||||
type Params = {
|
export async function generateViewport({ params: { id } }: PlayerPageParams): Promise<Viewport> {
|
||||||
params: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateViewport({ params: { id } }: Params): Promise<Viewport> {
|
|
||||||
const validPlayer = await isValidPlayer(id);
|
const validPlayer = await isValidPlayer(id);
|
||||||
return {
|
return {
|
||||||
themeColor: validPlayer ? Colors.green : Colors.red,
|
themeColor: validPlayer ? Colors.green : Colors.red,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params: { id } }: Params): Promise<Metadata> {
|
export async function generateMetadata({ params: { id } }: PlayerPageParams): Promise<Metadata> {
|
||||||
// No id provided
|
// No id provided
|
||||||
if (!id || id.length === 0) {
|
if (!id || id.length === 0) {
|
||||||
return generateEmbed({
|
return generateEmbed({
|
||||||
@ -55,7 +51,7 @@ export async function generateMetadata({ params: { id } }: Params): Promise<Meta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params: { id } }: Params): Promise<ReactElement> {
|
export default async function Page({ params: { id } }: PlayerPageParams): 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
|
||||||
|
|
||||||
@ -98,6 +94,9 @@ export default async function Page({ params: { id } }: Params): Promise<ReactEle
|
|||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Try a Player */}
|
||||||
|
{player == null && !error && <TryAPlayer />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { ServerView } from "@/app/components/server/server-view";
|
|||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/app/components/ui/context-menu";
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/app/components/ui/context-menu";
|
||||||
import { Colors } from "@/app/common/colors";
|
import { Colors } from "@/app/common/colors";
|
||||||
import { generateEmbed } from "@/app/common/embed";
|
import { generateEmbed } from "@/app/common/embed";
|
||||||
import { formatNumber } from "@/app/common/number-utils";
|
|
||||||
import { isValidServer } from "@/app/common/server";
|
import { isValidServer } from "@/app/common/server";
|
||||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||||
import config from "@root/config.json";
|
import config from "@root/config.json";
|
||||||
@ -19,16 +18,11 @@ import {
|
|||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { Title } from "@/app/components/title";
|
import { Title } from "@/app/components/title";
|
||||||
|
import { ServerPageParams } from "@/app/types/server/page-params";
|
||||||
|
import { TryAServer } from "@/app/components/server/try-a-server";
|
||||||
|
|
||||||
export const revalidate = 60;
|
export const revalidate = 60;
|
||||||
|
|
||||||
type Params = {
|
|
||||||
params: {
|
|
||||||
platform: ServerPlatform;
|
|
||||||
hostname: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the favicon for a server
|
* Gets the favicon for a server
|
||||||
*
|
*
|
||||||
@ -57,14 +51,14 @@ function checkPlatform(platform: ServerPlatform): boolean {
|
|||||||
return platform === ServerPlatform.Java || platform === ServerPlatform.Bedrock;
|
return platform === ServerPlatform.Java || platform === ServerPlatform.Bedrock;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateViewport({ params: { platform, hostname } }: Params): Promise<Viewport> {
|
export async function generateViewport({ params: { platform, hostname } }: ServerPageParams): Promise<Viewport> {
|
||||||
const validPlayer = await isValidServer(platform, hostname);
|
const validPlayer = await isValidServer(platform, hostname);
|
||||||
return {
|
return {
|
||||||
themeColor: validPlayer || !platform ? Colors.green : Colors.red,
|
themeColor: validPlayer || !platform ? Colors.green : Colors.red,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params: { platform, hostname } }: Params): Promise<Metadata> {
|
export async function generateMetadata({ params: { platform, hostname } }: ServerPageParams): Promise<Metadata> {
|
||||||
if (!checkPlatform(platform)) {
|
if (!checkPlatform(platform)) {
|
||||||
// Invalid platform
|
// Invalid platform
|
||||||
return generateEmbed({
|
return generateEmbed({
|
||||||
@ -82,18 +76,14 @@ export async function generateMetadata({ params: { platform, hostname } }: Param
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const server = await getServer(platform, hostname);
|
const server = await getServer(platform, hostname);
|
||||||
const { hostname: serverHostname, players } = server;
|
const { hostname: serverHostname } = server;
|
||||||
|
|
||||||
const favicon = getFavicon(platform, server);
|
|
||||||
|
|
||||||
let description = `There is ${formatNumber(players.online)}/${formatNumber(players.max)} players connected!\n\n`;
|
|
||||||
description += "Click to view more information about the server.";
|
|
||||||
|
|
||||||
return generateEmbed({
|
return generateEmbed({
|
||||||
title: `${serverHostname} ${capitalizeFirstLetter(platform)} Server`,
|
title: `${serverHostname} ${capitalizeFirstLetter(platform)} Server`,
|
||||||
embedTitle: `${capitalizeFirstLetter(platform)} Server: ${serverHostname}`,
|
embedTitle: `${capitalizeFirstLetter(platform)} Server: ${serverHostname}`,
|
||||||
description: description,
|
description: "Click to view more information about the server.",
|
||||||
image: favicon,
|
image: `${config.apiEndpoint}/server/${platform}/preview/${serverHostname}`,
|
||||||
|
cardType: "summary_large_image",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// An error occurred
|
// An error occurred
|
||||||
@ -104,7 +94,7 @@ export async function generateMetadata({ params: { platform, hostname } }: Param
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params: { platform, hostname } }: Params): Promise<ReactElement> {
|
export default async function Page({ params: { platform, hostname } }: ServerPageParams): 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: boolean = !checkPlatform(platform); // Whether the platform is invalid
|
let invalidPlatform: boolean = !checkPlatform(platform); // Whether the platform is invalid
|
||||||
@ -133,7 +123,10 @@ export default async function Page({ params: { platform, hostname } }: Params):
|
|||||||
<LookupServer currentPlatform={platform.toLowerCase()} currentServer={hostname && hostname[0]} />
|
<LookupServer currentPlatform={platform.toLowerCase()} currentServer={hostname && hostname[0]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* An errored occurred when looking up the server */}
|
||||||
{error && <ErrorCard message={error} />}
|
{error && <ErrorCard message={error} />}
|
||||||
|
|
||||||
|
{/* The server */}
|
||||||
{server != null && (
|
{server != null && (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
@ -151,6 +144,9 @@ export default async function Page({ params: { platform, hostname } }: Params):
|
|||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Try a Server */}
|
||||||
|
{server == null && !error && <TryAServer />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,11 @@ type Embed = {
|
|||||||
* The image to show as the thumbmail.
|
* The image to show as the thumbmail.
|
||||||
*/
|
*/
|
||||||
image?: string;
|
image?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the card.
|
||||||
|
*/
|
||||||
|
cardType?: "summary" | "summary_large_image";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,13 +34,17 @@ type Embed = {
|
|||||||
* @param embedTitle the title of the embed
|
* @param embedTitle the title of the embed
|
||||||
* @param description the description of the embed
|
* @param description the description of the embed
|
||||||
* @param image the image to show as the thumbmail
|
* @param image the image to show as the thumbmail
|
||||||
|
* @param cardType the type of the card
|
||||||
* @returns the metadata for the embed
|
* @returns the metadata for the embed
|
||||||
*/
|
*/
|
||||||
export function generateEmbed({ title, embedTitle, description, image }: Embed): Metadata {
|
export function generateEmbed({ title, embedTitle, description, image, cardType }: Embed): Metadata {
|
||||||
// Fall back to the title
|
// Fall back to the title
|
||||||
if (!embedTitle) {
|
if (!embedTitle) {
|
||||||
embedTitle = title;
|
embedTitle = title;
|
||||||
}
|
}
|
||||||
|
if (!cardType) {
|
||||||
|
cardType = "summary";
|
||||||
|
}
|
||||||
|
|
||||||
const metadata: Metadata = {
|
const metadata: Metadata = {
|
||||||
title: `${title}`,
|
title: `${title}`,
|
||||||
@ -44,7 +53,7 @@ export function generateEmbed({ title, embedTitle, description, image }: Embed):
|
|||||||
description: description,
|
description: description,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary",
|
card: cardType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
17
src/app/common/hastebin.ts
Normal file
17
src/app/common/hastebin.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const PASTE_URL: string = "https://paste.fascinated.cc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new haste with the given content.
|
||||||
|
*
|
||||||
|
* @param content the content to create the haste with
|
||||||
|
* @returns the URL of the created haste
|
||||||
|
*/
|
||||||
|
export async function createHaste(content: string): Promise<string> {
|
||||||
|
const response = await fetch(`${PASTE_URL}/api/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
body: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await response.json();
|
||||||
|
return `${PASTE_URL}/${id}`;
|
||||||
|
}
|
@ -1,6 +1,15 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { CodeHighlighter } from "./code-highlighter";
|
import { CodeHighlighter } from "./code-highlighter";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "./ui/dialog";
|
||||||
|
import { CreateHasteButton } from "@/app/components/create-haste-button";
|
||||||
|
|
||||||
type CodeDialogProps = {
|
type CodeDialogProps = {
|
||||||
/**
|
/**
|
||||||
@ -28,12 +37,15 @@ export function CodeDialog({ title, description, code, children }: CodeDialogPro
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen md:max-h-[700px] text-sm">
|
<DialogContent className="text-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogDescription>{description}</DialogDescription>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<CodeHighlighter code={code} />
|
<CodeHighlighter code={code} />
|
||||||
|
<DialogFooter>
|
||||||
|
<CreateHasteButton content={code} />
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -77,6 +77,7 @@ export function CodeHighlighter({ code, language = "json", rounded = true }: Cod
|
|||||||
<span className="text-xs text-muted-foreground">{capitalizeFirstLetter(language)}</span>
|
<span className="text-xs text-muted-foreground">{capitalizeFirstLetter(language)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Code */}
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
className={cn("max-h-[600px] !bg-secondary break-all rounded-md", rounded && "rounded-md")}
|
className={cn("max-h-[600px] !bg-secondary break-all rounded-md", rounded && "rounded-md")}
|
||||||
language={language}
|
language={language}
|
||||||
|
21
src/app/components/create-haste-button.tsx
Normal file
21
src/app/components/create-haste-button.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Button } from "@/app/components/ui/button";
|
||||||
|
import { CreateHasteButtonProps } from "@/app/types/create-haste-button";
|
||||||
|
import { createHaste } from "@/app/common/hastebin";
|
||||||
|
|
||||||
|
export function CreateHasteButton({ content }: CreateHasteButtonProps): ReactElement {
|
||||||
|
/**
|
||||||
|
* Uploads the content to Haste and opens the URL in a new tab.
|
||||||
|
*/
|
||||||
|
async function upload(): Promise<void> {
|
||||||
|
const url = await createHaste(content);
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
// Open the URL in a new tab.
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Button onClick={() => upload()}>Create Haste</Button>;
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { Separator } from "../ui/separator";
|
|||||||
import { SkinPartImage } from "./skin-part-image";
|
import { SkinPartImage } from "./skin-part-image";
|
||||||
import { CacheInformation } from "@/app/components/cache-information";
|
import { CacheInformation } from "@/app/components/cache-information";
|
||||||
import { PlayerSkin } from "@/app/components/player/player-skin";
|
import { PlayerSkin } from "@/app/components/player/player-skin";
|
||||||
|
import { ReloadPageButton } from "@/app/components/reload-page-button";
|
||||||
|
|
||||||
type PlayerViewProps = {
|
type PlayerViewProps = {
|
||||||
/**
|
/**
|
||||||
@ -38,6 +39,7 @@ export function PlayerView({ player }: PlayerViewProps): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex gap-2 flex-wrap justify-center">
|
<div className="flex gap-2 flex-wrap justify-center">
|
||||||
|
<ReloadPageButton />
|
||||||
<CodeDialog
|
<CodeDialog
|
||||||
title="Player Data"
|
title="Player Data"
|
||||||
description="The player's data from the API"
|
description="The player's data from the API"
|
||||||
|
49
src/app/components/player/try-a-player.tsx
Normal file
49
src/app/components/player/try-a-player.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/app/components/ui/tooltip";
|
||||||
|
import Image from "next/image";
|
||||||
|
import config from "@root/config.json";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Card } from "@/app/components/card";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The players to try out.
|
||||||
|
*/
|
||||||
|
const tryMePlayers: string[] = ["Notch", "jeb_", "Dinnerbone", "Grumm", "deadmau5"];
|
||||||
|
|
||||||
|
export function TryAPlayer(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">Try a Player</h2>
|
||||||
|
<p className="text-muted-foreground">Try one of these players to see how the player view works.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-2 max-w-2xl mt-4">
|
||||||
|
{tryMePlayers.map(playerName => (
|
||||||
|
<Tooltip key={playerName}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center justify-center bg-background p-1.5 rounded-md gap-2">
|
||||||
|
<Image
|
||||||
|
src={`${config.apiEndpoint}/player/head/${playerName}`}
|
||||||
|
alt={"The player's head"}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<Link href={`/player/${playerName}`} className="hover:opacity-85 transform-gpu transition-all">
|
||||||
|
{playerName}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Click to try the player <b>{playerName}</b>.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
28
src/app/components/reload-page-button.tsx
Normal file
28
src/app/components/reload-page-button.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Button } from "@/app/components/ui/button";
|
||||||
|
import { ArrowPathIcon } from "@heroicons/react/16/solid";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/app/components/ui/tooltip";
|
||||||
|
|
||||||
|
export function ReloadPageButton(): ReactElement {
|
||||||
|
/**
|
||||||
|
* Reload the page.
|
||||||
|
*/
|
||||||
|
function reload(): void {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button onClick={() => reload()}>
|
||||||
|
<ArrowPathIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Reload the page</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { ReactElement } from "react";
|
|||||||
import { CodeDialog } from "../code-dialog";
|
import { CodeDialog } from "../code-dialog";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { CacheInformation } from "@/app/components/cache-information";
|
import { CacheInformation } from "@/app/components/cache-information";
|
||||||
|
import { ReloadPageButton } from "@/app/components/reload-page-button";
|
||||||
|
|
||||||
type ServerViewProps = {
|
type ServerViewProps = {
|
||||||
/**
|
/**
|
||||||
@ -30,6 +31,7 @@ export function ServerView({ server, favicon }: ServerViewProps): ReactElement {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 flex-wrap justify-center">
|
<div className="flex gap-2 flex-wrap justify-center">
|
||||||
|
<ReloadPageButton />
|
||||||
<CodeDialog
|
<CodeDialog
|
||||||
title="Server Data"
|
title="Server Data"
|
||||||
description="The servers's data from the API"
|
description="The servers's data from the API"
|
||||||
|
54
src/app/components/server/try-a-server.tsx
Normal file
54
src/app/components/server/try-a-server.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/app/components/ui/tooltip";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||||
|
import { Card } from "@/app/components/card";
|
||||||
|
import { ServerPlatform } from "mcutils-library";
|
||||||
|
import { TryMeServer } from "@/app/types/server/try-me-server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The servers to try out.
|
||||||
|
*/
|
||||||
|
const tryMeServers: TryMeServer[] = [
|
||||||
|
{ platform: ServerPlatform.Java, hostname: "mc.hypixel.net" },
|
||||||
|
{ platform: ServerPlatform.Java, hostname: "wildprison.net" },
|
||||||
|
{ platform: ServerPlatform.Java, hostname: "cubecraft.net" },
|
||||||
|
{ platform: ServerPlatform.Bedrock, hostname: "geo.hivebedrock.network" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TryAServer(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">Try a Server</h2>
|
||||||
|
<p className="text-muted-foreground">Try one of these servers to see how the server view works.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-2 max-w-2xl mt-4">
|
||||||
|
{tryMeServers.map(({ platform, hostname }) => (
|
||||||
|
<Tooltip key={hostname}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center justify-center bg-background p-1.5 rounded-md gap-2">
|
||||||
|
<Image src={`/media/platform/${platform}.png`} alt={"The server's platform"} width={28} height={28} />
|
||||||
|
<Link
|
||||||
|
href={`/server/${platform}/${hostname}`}
|
||||||
|
className="hover:opacity-85 transform-gpu transition-all"
|
||||||
|
>
|
||||||
|
{hostname}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Click to try the <b>{capitalizeFirstLetter(platform)}</b> server: <b>{hostname}</b>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@ -5,10 +5,10 @@ import Container from "./components/container";
|
|||||||
import ThemeProvider from "./components/theme-provider";
|
import ThemeProvider from "./components/theme-provider";
|
||||||
import { Toaster } from "./components/ui/toaster";
|
import { Toaster } from "./components/ui/toaster";
|
||||||
import { TooltipProvider } from "./components/ui/tooltip";
|
import { TooltipProvider } from "./components/ui/tooltip";
|
||||||
|
import { inter } from "@/app/font/fonts";
|
||||||
|
import config from "@root/config.json";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import config from "@root/config.json";
|
|
||||||
import { inter } from "@/app/font/fonts";
|
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: "#3498DB",
|
themeColor: "#3498DB",
|
||||||
|
6
src/app/types/create-haste-button.ts
Normal file
6
src/app/types/create-haste-button.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type CreateHasteButtonProps = {
|
||||||
|
/**
|
||||||
|
* The content to create the haste with.
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
};
|
27
src/app/types/landing/landing-button.tsx
Normal file
27
src/app/types/landing/landing-button.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export type LandingButton = {
|
||||||
|
/**
|
||||||
|
* The title of the button.
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tooltip to display for this statistic.
|
||||||
|
*/
|
||||||
|
tooltip: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to go to.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether clicking the button will
|
||||||
|
* open the link in a new tab.
|
||||||
|
*/
|
||||||
|
openInNewTab?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class name to apply to the button.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
};
|
5
src/app/types/player/page-params.ts
Normal file
5
src/app/types/player/page-params.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type PlayerPageParams = {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
8
src/app/types/server/page-params.ts
Normal file
8
src/app/types/server/page-params.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ServerPlatform } from "mcutils-library";
|
||||||
|
|
||||||
|
export type ServerPageParams = {
|
||||||
|
params: {
|
||||||
|
platform: ServerPlatform;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
};
|
13
src/app/types/server/try-me-server.ts
Normal file
13
src/app/types/server/try-me-server.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ServerPlatform } from "mcutils-library";
|
||||||
|
|
||||||
|
export type TryMeServer = {
|
||||||
|
/**
|
||||||
|
* The platform of the server.
|
||||||
|
*/
|
||||||
|
platform: ServerPlatform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hostname of the server.
|
||||||
|
*/
|
||||||
|
hostname: string;
|
||||||
|
};
|
9
src/instrumentation.ts
Normal file
9
src/instrumentation.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
await import('../sentry.server.config');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||||
|
await import('../sentry.edge.config');
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user