Compare commits
127 Commits
81e6580eed
...
renovate/r
Author | SHA1 | Date | |
---|---|---|---|
dfeedcc7de | |||
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 | |||
9e17145e90 | |||
782b5efd4b | |||
e277645333 | |||
9ba13d1160 | |||
40029b9839 | |||
ffa2db120f | |||
525b4981fe | |||
ce55389cba | |||
656eefa2e5 | |||
230e5d6864 | |||
69fd6483de | |||
a6650af640 | |||
f0eaaaad5e | |||
96dc8de92f | |||
00a5febf66 | |||
49daf6f1a4 | |||
c2d7f5f33c | |||
a200fa045c | |||
d26c70f507 | |||
0b9a112c5b | |||
23c4284d8a | |||
5c05493ed1 | |||
32364ab58f |
1
.env
1
.env
@ -1 +0,0 @@
|
||||
SENTRY_AUTH_TOKEN=afb368d9e6c8a4042883798bba964afad3896261ac123f717801338a3fd2e2b1
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -46,3 +46,10 @@ next-env.d.ts
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Sitemap & Robots
|
||||
/public/sitemap*
|
||||
/public/robots.txt
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
16
Dockerfile
16
Dockerfile
@ -22,11 +22,18 @@ WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# Get the git commit hash
|
||||
ARG GIT_REV
|
||||
ENV GIT_REV ${GIT_REV}
|
||||
|
||||
# Sentry Auth Token
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN ${SENTRY_AUTH_TOKEN}
|
||||
|
||||
# Disable telemetry during build
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Build the frontend
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn run build; \
|
||||
elif [ -f package-lock.json ]; then npm run build; \
|
||||
@ -39,7 +46,8 @@ FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
|
||||
# Disable telemetry during runtime
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Mojang - Endpoint Status
|
||||
title: Mojang Endpoint Status
|
||||
summary: Get the status of the Mojang APIs.
|
||||
---
|
||||
|
||||
# Mojang - Endpoint Status
|
||||
# Overview
|
||||
|
||||
The Mojang endpoint status endpoint allows you to get the status of the Mojang APIs.
|
||||
|
||||
|
@ -3,7 +3,7 @@ title: Player Lookup
|
||||
summary: Get information about a player.
|
||||
---
|
||||
|
||||
# Player Lookup
|
||||
# Overview
|
||||
|
||||
The player lookup endpoint allows you to get information about a player. This includes their UUID, username, and any other information that is available.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Player - Skin Parts
|
||||
title: Player Skin Parts
|
||||
summary: Get a specific part of a player's skin.
|
||||
---
|
||||
|
||||
# Player - Skin Parts
|
||||
# Overview
|
||||
|
||||
The player skin parts endpoint allows you to get a specific part of a player's skin.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Player - Username to UUID
|
||||
title: Player Username to UUID
|
||||
summary: Get a player's UUID from their username.
|
||||
---
|
||||
|
||||
# Player - Username to UUID
|
||||
# Overview
|
||||
|
||||
The player username to UUID endpoint allows you to get a player's UUID from their username.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Server - Blocked Status
|
||||
title: Server Blocked Status
|
||||
summary: Get the Mojang blocked status of a Minecraft server.
|
||||
---
|
||||
|
||||
# Server - Blocked Status
|
||||
# Overview
|
||||
|
||||
The server blocked status endpoint allows you to get the Mojang blocked status of a Minecraft server.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Server - Favicon
|
||||
title: Server Favicon
|
||||
summary: Get the favicon of a Minecraft server.
|
||||
---
|
||||
|
||||
# Server - Favicon
|
||||
# Overview
|
||||
|
||||
The server favicon endpoint allows you to get the favicon of a Minecraft server.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Server - Preview
|
||||
title: Server Preview
|
||||
summary: Get the server list preview of a Minecraft server.
|
||||
---
|
||||
|
||||
# Server - Preview
|
||||
# Overview
|
||||
|
||||
The server preview endpoint allows you to get the server list preview of a Minecraft server.
|
||||
|
||||
|
@ -3,7 +3,7 @@ title: Server Lookup
|
||||
summary: Get information about a server.
|
||||
---
|
||||
|
||||
# Server Lookup
|
||||
# Overview
|
||||
|
||||
The server lookup endpoint allows you to get information about a server. This includes the server's IP address, port, and any other information that is available.
|
||||
|
||||
@ -38,17 +38,17 @@ curl -X GET "https://api.mcutils.xyz/server/java/play.hypixel.net" -H "accept: a
|
||||
```json
|
||||
{
|
||||
"cache": {
|
||||
"cached": false,
|
||||
"cachedTime": -1
|
||||
"cached": true,
|
||||
"cachedTime": 1713747259031
|
||||
},
|
||||
"hostname": "play.hypixel.net",
|
||||
"ip": "209.222.115.48",
|
||||
"ip": "209.222.115.34",
|
||||
"port": 25565,
|
||||
"records": [
|
||||
{
|
||||
"type": "A",
|
||||
"ttl": 60,
|
||||
"address": "209.222.115.48"
|
||||
"address": "209.222.115.34"
|
||||
}
|
||||
],
|
||||
"motd": {
|
||||
@ -66,9 +66,13 @@ curl -X GET "https://api.mcutils.xyz/server/java/play.hypixel.net" -H "accept: a
|
||||
]
|
||||
},
|
||||
"players": {
|
||||
"online": 33892,
|
||||
"max": 200000,
|
||||
"sample": []
|
||||
"online": 34943,
|
||||
"max": 200000
|
||||
},
|
||||
"location": {
|
||||
"country": "Canada",
|
||||
"latitude": 43.6319,
|
||||
"longitude": -79.3716
|
||||
},
|
||||
"version": {
|
||||
"name": "Requires MC 1.8 / 1.20",
|
||||
|
5
next-sitemap.config.js
Normal file
5
next-sitemap.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
/** @type {import('next-sitemap').IConfig} */
|
||||
module.exports = {
|
||||
siteUrl: process.env.SITE_URL || "https://mcutils.xyz",
|
||||
generateRobotsTxt: true,
|
||||
};
|
@ -1,14 +1,27 @@
|
||||
import {withSentryConfig} from "@sentry/nextjs";
|
||||
import nextBuildId from "next-build-id";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
/**
|
||||
* The current git commit hash.
|
||||
*
|
||||
* @type {string|string} The current git commit hash.
|
||||
*/
|
||||
const buildId = (
|
||||
process.env.GIT_REV || nextBuildId.sync({ dir: path.dirname(fileURLToPath(import.meta.url)) })
|
||||
).substring(0, 7);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
remotePatterns: [{
|
||||
protocol: "https",
|
||||
hostname: "git.fascinated.cc",
|
||||
pathname: "/**",
|
||||
},
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "git.fascinated.cc",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "api.mcutils.xyz",
|
||||
@ -16,45 +29,46 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_BUILD_ID: buildId,
|
||||
},
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
// Suppresses source map uploading logs during build
|
||||
silent: true,
|
||||
org: "minecraft-utilities",
|
||||
project: "frontend",
|
||||
url: "https://glitchtip.fascinated.cc/"
|
||||
}, {
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
org: "minecraft-utilities",
|
||||
project: "frontend",
|
||||
sentryUrl: "https://glitchtip.fascinated.cc/",
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: !process.env.CI,
|
||||
|
||||
// Transpiles SDK to be compatible with IE11 (increases bundle size)
|
||||
transpileClientSDK: true,
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// 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",
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: 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",
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors.
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
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,
|
||||
});
|
24
package.json
24
package.json
@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"postbuild": "next-sitemap",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
@ -24,33 +25,36 @@
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.105.0",
|
||||
"@sentry/nextjs": "^8.20.0",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^1.0.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lucide-react": "^0.372.0",
|
||||
"mcutils-library": "^1.2.6",
|
||||
"lucide-react": "^0.451.0",
|
||||
"mcutils-library": "^1.3.1",
|
||||
"moment": "^2.30.1",
|
||||
"next": "14.2.2",
|
||||
"next": "14.2.5",
|
||||
"next-build-id": "^3.0.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^18",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-spinners": "^0.14.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use-websocket": "4.8.1",
|
||||
"react-use-websocket": "4.9.0",
|
||||
"read-file": "^0.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remote-mdx": "^0.0.4",
|
||||
"remote-mdx": "^0.0.8",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"remark-mdx": "3.0.0",
|
||||
"unified": "11.0.4",
|
||||
"remark-mdx": "3.0.1",
|
||||
"unified": "11.0.5",
|
||||
"remark-parse": "11.0.0",
|
||||
"mdast-util-frontmatter": "2.0.1"
|
||||
}
|
||||
@ -60,7 +64,7 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"eslint-config-next": "14.2.2",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
|
11982
pnpm-lock.yaml
generated
11982
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.
|
||||
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,7 +13,7 @@ Sentry.init({
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
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',
|
||||
|
||||
});
|
||||
|
@ -3,16 +3,9 @@ import { Metadata } from "next";
|
||||
import { generateEmbed } from "@/app/common/embed";
|
||||
import { Title } from "@/app/components/title";
|
||||
import { DocsContentMetadata, getDocContent, getDocsContent } from "@/app/common/documentation";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/app/components/ui/breadcrumb";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { GithubLink } from "@/app/components/docs/github-link";
|
||||
import { DocsBreadcrumb } from "@/app/components/docs/breadcrumb";
|
||||
|
||||
type DocumentationPageParams = {
|
||||
params: {
|
||||
@ -56,38 +49,16 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const slugParts: string[] = page.slug.split("/");
|
||||
const isHome: boolean = slugParts.length == 1;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full px-4 flex flex-col gap-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="w-full h-full px-4 flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
{/* The breadcrumb for the documentation page */}
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/docs`}>Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{!isHome &&
|
||||
slugParts.map((slug, index, array) => {
|
||||
const path: string = array.slice(0, index + 1).join("/");
|
||||
const name: string = slug.includes("-")
|
||||
? slug.split("-").map(capitalizeFirstLetter).join(" ")
|
||||
: capitalizeFirstLetter(slug);
|
||||
<DocsBreadcrumb page={page} />
|
||||
|
||||
return (
|
||||
<div key={slug} className="flex items-center ">
|
||||
<BreadcrumbSeparator className="pr-1.5" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/docs/${path}`}>{capitalizeFirstLetter(name)}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<GithubLink page={page} />
|
||||
{/* The Git link for the documentation page */}
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<GithubLink page={page} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The documentation page title and description */}
|
||||
@ -96,7 +67,7 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
|
||||
</div>
|
||||
|
||||
{/* The content of the documentation page */}
|
||||
<div className="text-left w-full pb-[5rem]">
|
||||
<div className="text-left w-full pb-[2rem]">
|
||||
<CustomMDX source={page.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,15 +0,0 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import { Sidebar } from "@/app/components/docs/sidebar";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>): ReactElement {
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center gap-2 h-full md:flex-row md:items-start">
|
||||
<Sidebar />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import { Card } from "@/app/components/card";
|
||||
import { Colors } from "@/app/common/colors";
|
||||
import { generateEmbed } from "@/app/common/embed";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-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 { Metadata, Viewport } from "next";
|
||||
import Link from "next/link";
|
||||
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
|
||||
@ -89,20 +89,19 @@ export default async function Page(): Promise<ReactElement> {
|
||||
>
|
||||
{endpoints.length == 0 && <p>Unable to fetch endpoint statuses</p>}
|
||||
{endpoints.length > 0 &&
|
||||
endpoints.map((endpoint: CachedEndpointStatus) => {
|
||||
const { name, hostname, status } = endpoint;
|
||||
endpoints.map((server: CachedEndpointStatus) => {
|
||||
const { name, endpoint, status } = server;
|
||||
|
||||
const url = `https://${hostname}`;
|
||||
return (
|
||||
<div key={name} className="flex flex-row justify-between pt-2">
|
||||
<div className="flex flex-col leading-[1.5rem]">
|
||||
<p className="font-semibold">{name}</p>
|
||||
<Link
|
||||
href={url}
|
||||
href={endpoint}
|
||||
className="text-sm text-primary hover:opacity-75 transition-all transform-gpu"
|
||||
target="_blank"
|
||||
>
|
||||
<p>{url}</p>
|
||||
<p>{endpoint}</p>
|
||||
</Link>
|
||||
</div>
|
||||
<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 { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip";
|
||||
import { Title } from "@/app/components/title";
|
||||
import { LandingButton } from "@/app/types/landing/landing-button";
|
||||
|
||||
type Button = {
|
||||
/**
|
||||
* 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[] = [
|
||||
const buttons: LandingButton[] = [
|
||||
{
|
||||
title: "Get Started",
|
||||
tooltip: "Click to get started with the API",
|
||||
|
@ -12,26 +12,19 @@ import { CachedPlayer, getPlayer, McUtilsAPIError } from "mcutils-library";
|
||||
import { Metadata, Viewport } from "next";
|
||||
import { ReactElement } from "react";
|
||||
import { Title } from "@/app/components/title";
|
||||
import { PlayerPageParams } from "@/app/types/player/page-params";
|
||||
import { TryAPlayer } from "@/app/components/player/try-a-player";
|
||||
|
||||
/**
|
||||
* Force the page to be dynamic, so it will be regenerated on every request
|
||||
*/
|
||||
export const revalidate = 0;
|
||||
export const revalidate = 60;
|
||||
|
||||
type Params = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function generateViewport({ params: { id } }: Params): Promise<Viewport> {
|
||||
export async function generateViewport({ params: { id } }: PlayerPageParams): Promise<Viewport> {
|
||||
const validPlayer = await isValidPlayer(id);
|
||||
return {
|
||||
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
|
||||
if (!id || id.length === 0) {
|
||||
return generateEmbed({
|
||||
@ -58,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 player: CachedPlayer | undefined = undefined; // The player to display
|
||||
|
||||
@ -101,6 +94,9 @@ export default async function Page({ params: { id } }: Params): Promise<ReactEle
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)}
|
||||
|
||||
{/* Try a Player */}
|
||||
{player == null && !error && <TryAPlayer />}
|
||||
</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 { Colors } from "@/app/common/colors";
|
||||
import { generateEmbed } from "@/app/common/embed";
|
||||
import { formatNumber } from "@/app/common/number-utils";
|
||||
import { isValidServer } from "@/app/common/server";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||
import config from "@root/config.json";
|
||||
@ -19,18 +18,10 @@ import {
|
||||
import { Metadata, Viewport } from "next";
|
||||
import { ReactElement } from "react";
|
||||
import { Title } from "@/app/components/title";
|
||||
import { ServerPageParams } from "@/app/types/server/page-params";
|
||||
import { TryAServer } from "@/app/components/server/try-a-server";
|
||||
|
||||
/**
|
||||
* Force the page to be dynamic, so it will be regenerated on every request
|
||||
*/
|
||||
export const revalidate = 0;
|
||||
|
||||
type Params = {
|
||||
params: {
|
||||
platform: ServerPlatform;
|
||||
hostname: string;
|
||||
};
|
||||
};
|
||||
export const revalidate = 60;
|
||||
|
||||
/**
|
||||
* Gets the favicon for a server
|
||||
@ -60,14 +51,14 @@ function checkPlatform(platform: ServerPlatform): boolean {
|
||||
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);
|
||||
return {
|
||||
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)) {
|
||||
// Invalid platform
|
||||
return generateEmbed({
|
||||
@ -85,18 +76,14 @@ export async function generateMetadata({ params: { platform, hostname } }: Param
|
||||
|
||||
try {
|
||||
const server = await getServer(platform, hostname);
|
||||
const { hostname: serverHostname, players } = 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.";
|
||||
const { hostname: serverHostname } = server;
|
||||
|
||||
return generateEmbed({
|
||||
title: `${serverHostname} ${capitalizeFirstLetter(platform)} Server`,
|
||||
embedTitle: `${capitalizeFirstLetter(platform)} Server: ${serverHostname}`,
|
||||
description: description,
|
||||
image: favicon,
|
||||
description: "Click to view more information about the server.",
|
||||
image: `${config.apiEndpoint}/server/${platform}/preview/${serverHostname}`,
|
||||
cardType: "summary_large_image",
|
||||
});
|
||||
} catch (err) {
|
||||
// An error occurred
|
||||
@ -107,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 server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer | undefined = undefined; // The server to display
|
||||
let invalidPlatform: boolean = !checkPlatform(platform); // Whether the platform is invalid
|
||||
@ -136,7 +123,10 @@ export default async function Page({ params: { platform, hostname } }: Params):
|
||||
<LookupServer currentPlatform={platform.toLowerCase()} currentServer={hostname && hostname[0]} />
|
||||
</div>
|
||||
|
||||
{/* An errored occurred when looking up the server */}
|
||||
{error && <ErrorCard message={error} />}
|
||||
|
||||
{/* The server */}
|
||||
{server != null && (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
@ -154,6 +144,9 @@ export default async function Page({ params: { platform, hostname } }: Params):
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)}
|
||||
|
||||
{/* Try a Server */}
|
||||
{server == null && !error && <TryAServer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ export function getDocContent(path?: string[]): DocsContentMetadata | undefined
|
||||
* @param limit the maximum number of results
|
||||
*/
|
||||
export function searchDocs(
|
||||
query: string,
|
||||
query?: string,
|
||||
limit?: number,
|
||||
): {
|
||||
title: string;
|
||||
@ -129,7 +129,7 @@ export function searchDocs(
|
||||
if (!limit) {
|
||||
limit = 5; // Default to 5 results
|
||||
}
|
||||
return fuseIndex.search(query, { limit }).map(result => {
|
||||
return fuseIndex.search(query || "", { limit }).map(result => {
|
||||
return {
|
||||
title: result.item.title,
|
||||
summary: result.item.summary,
|
||||
|
@ -20,6 +20,11 @@ type Embed = {
|
||||
* The image to show as the thumbmail.
|
||||
*/
|
||||
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 description the description of the embed
|
||||
* @param image the image to show as the thumbmail
|
||||
* @param cardType the type of the card
|
||||
* @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
|
||||
if (!embedTitle) {
|
||||
embedTitle = title;
|
||||
}
|
||||
if (!cardType) {
|
||||
cardType = "summary";
|
||||
}
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: `${title}`,
|
||||
@ -44,7 +53,7 @@ export function generateEmbed({ title, embedTitle, description, image }: Embed):
|
||||
description: description,
|
||||
},
|
||||
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 { 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 = {
|
||||
/**
|
||||
@ -28,12 +37,15 @@ export function CodeDialog({ title, description, code, children }: CodeDialogPro
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen md:max-h-[700px] text-sm">
|
||||
<DialogContent className="text-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CodeHighlighter code={code} />
|
||||
<DialogFooter>
|
||||
<CreateHasteButton content={code} />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -2,6 +2,8 @@ import { ReactElement } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import createElement from "react-syntax-highlighter/dist/esm/create-element";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { cn } from "@/app/common/utils";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||
|
||||
type CodeHighlighterProps = {
|
||||
/**
|
||||
@ -69,18 +71,19 @@ function rowRenderer({
|
||||
|
||||
export function CodeHighlighter({ code, language = "json", rounded = true }: CodeHighlighterProps): ReactElement {
|
||||
return (
|
||||
<div className="text-xs md:text-md">
|
||||
<div className="text-xs md:text-md relative">
|
||||
{/* Language */}
|
||||
<div className="absolute top-0 right-0 p-1 bg-muted rounded-bl-md rounded-tr-md">
|
||||
<span className="text-xs text-muted-foreground">{capitalizeFirstLetter(language)}</span>
|
||||
</div>
|
||||
|
||||
{/* Code */}
|
||||
<SyntaxHighlighter
|
||||
className={cn("max-h-[600px] !bg-secondary break-all rounded-md", rounded && "rounded-md")}
|
||||
language={language}
|
||||
style={atomOneDark}
|
||||
wrapLongLines
|
||||
renderer={rowRenderer}
|
||||
customStyle={{
|
||||
maxHeight: "600px",
|
||||
backgroundColor: "hsl(var(--background-accent))",
|
||||
wordBreak: "break-all",
|
||||
borderRadius: rounded ? "0.75rem" : undefined,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
|
117
src/app/components/command-menu.tsx
Normal file
117
src/app/components/command-menu.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import { DocsContentMetadata } from "@/app/common/documentation";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/app/components/ui/command";
|
||||
import { Button, ButtonProps } from "@/app/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cn } from "@/app/common/utils";
|
||||
|
||||
export function CommandMenu({ ...props }: ButtonProps): ReactElement {
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Whether to show the search
|
||||
*/
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* The pages that were found
|
||||
*/
|
||||
const [pages, setPages] = useState<DocsContentMetadata[] | undefined>(undefined);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
setOpen(open => !open);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Search the documentation
|
||||
* for the given query.
|
||||
*
|
||||
* @param query the query to search for
|
||||
*/
|
||||
async function searchDocs(query: string): Promise<void> {
|
||||
// Don't bother searching if the query is less than 3 characters
|
||||
if (query.length < 3) {
|
||||
setPages(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to search for the query
|
||||
const response = await fetch(`/api/docs/search?query=${query}`);
|
||||
const pages: DocsContentMetadata[] = await response.json();
|
||||
setPages(pages);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"relative h-8 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput
|
||||
placeholder="Query..."
|
||||
onValueChange={async search => {
|
||||
await searchDocs(search);
|
||||
}}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{pages && pages.length > 1 && (
|
||||
<CommandGroup heading="Suggestions">
|
||||
{pages.map(page => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={page.slug}
|
||||
onSelect={() => {
|
||||
router.push(`/docs/${page.slug}`); // Go to the page
|
||||
setOpen(false); // Close the dialog
|
||||
setPages(undefined); // Clear the pages
|
||||
}}
|
||||
className="flex flex-col items-start gap-1"
|
||||
>
|
||||
<p className="font-semibold text-primary">{page.title}</p>
|
||||
<p>{page.summary}</p>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
}
|
@ -10,6 +10,11 @@ type CopyButtonProps = {
|
||||
*/
|
||||
content: string;
|
||||
|
||||
/**
|
||||
* The message to display when the content is copied.
|
||||
*/
|
||||
message?: string | boolean;
|
||||
|
||||
/**
|
||||
* The children for this element.
|
||||
*/
|
||||
@ -22,7 +27,7 @@ type CopyButtonProps = {
|
||||
* @param props the properties for the button
|
||||
* @returns the copy button
|
||||
*/
|
||||
export function CopyButton({ content, children }: CopyButtonProps): ReactElement {
|
||||
export function CopyButton({ content, message, children }: CopyButtonProps): ReactElement {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
@ -33,7 +38,7 @@ export function CopyButton({ content, children }: CopyButtonProps): ReactElement
|
||||
title: "Copied!",
|
||||
description: (
|
||||
<p>
|
||||
Copied <b>{content}</b> to your clipboard.
|
||||
Copied <b>{!message ? content : message}</b> to your clipboard.
|
||||
</p>
|
||||
),
|
||||
duration: 5000,
|
||||
|
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>;
|
||||
}
|
48
src/app/components/docs/breadcrumb.tsx
Normal file
48
src/app/components/docs/breadcrumb.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { ReactElement } from "react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/app/components/ui/breadcrumb";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||
import { DocsContentMetadata } from "@/app/common/documentation";
|
||||
|
||||
type DocsBreadcrumbProps = {
|
||||
/**
|
||||
* The page to render the breadcrumb for.
|
||||
*/
|
||||
page: DocsContentMetadata;
|
||||
};
|
||||
|
||||
export function DocsBreadcrumb({ page }: DocsBreadcrumbProps): ReactElement {
|
||||
const slugParts: string[] = page.slug.split("/");
|
||||
const isHome: boolean = slugParts.length == 1;
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/docs`}>Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{!isHome &&
|
||||
slugParts.map((slug, index, array) => {
|
||||
const path: string = array.slice(0, index + 1).join("/");
|
||||
const name: string = slug.includes("-")
|
||||
? slug.split("-").map(capitalizeFirstLetter).join(" ")
|
||||
: capitalizeFirstLetter(slug);
|
||||
|
||||
return (
|
||||
<div key={slug} className="flex items-center ">
|
||||
<BreadcrumbSeparator className="pr-1.5" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/docs/${path}`}>{capitalizeFirstLetter(name)}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { DocsContentMetadata } from "@/app/common/documentation";
|
||||
import React, { ReactElement } from "react";
|
||||
import { DialogClose } from "../ui/dialog";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type PagesProps = {
|
||||
/**
|
||||
* The documentation pages to display.
|
||||
*/
|
||||
pages: DocsContentMetadata[] | undefined;
|
||||
};
|
||||
|
||||
export function DocumentationPages({ pages }: PagesProps): ReactElement {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{pages && pages.length === 0 && <p>No results found</p>}
|
||||
|
||||
{pages &&
|
||||
pages.length > 1 &&
|
||||
pages.map(page => {
|
||||
return (
|
||||
<DialogClose
|
||||
key={page.slug}
|
||||
className="text-left bg-card p-2 rounded-lg"
|
||||
onClick={() => {
|
||||
router.replace(`/docs/${page.slug}`);
|
||||
}}
|
||||
>
|
||||
<h2 className="font-semibold">{page.title}</h2>
|
||||
<p className="text-accent">{page.summary}</p>
|
||||
</DialogClose>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ export function GithubLink({ page }: GithubLink): ReactElement {
|
||||
href={`https://git.fascinated.cc/MinecraftUtilities/Frontend/src/branch/master/documentation/${page.slug}.md`}
|
||||
target="_blank"
|
||||
>
|
||||
<Image src="/media/github.png" alt="The GitHub logo" width={24} height={24} className="filter dark:invert" />
|
||||
<Image src="/media/github.png" alt="The GitHub logo" width={32} height={32} className="filter dark:invert" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import { Button } from "@/app/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/app/components/ui/dialog";
|
||||
import { DocsContentMetadata } from "@/app/common/documentation";
|
||||
import { DocumentationPages } from "@/app/components/docs/documentation-pages";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
export function Search(): ReactElement {
|
||||
/**
|
||||
* The pages that were found
|
||||
*/
|
||||
const [pages, setPages] = useState<DocsContentMetadata[] | undefined>(undefined);
|
||||
const [continueTyping, setContinueTyping] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Search the documentation
|
||||
* for the given query.
|
||||
*
|
||||
* @param query the query to search for
|
||||
*/
|
||||
async function searchDocs(query: string): Promise<void> {
|
||||
// Don't bother searching if the query is less than 3 characters
|
||||
if (query.length < 3) {
|
||||
if (query.length > 0) {
|
||||
setContinueTyping(true);
|
||||
} else {
|
||||
setContinueTyping(false);
|
||||
}
|
||||
return setPages(undefined);
|
||||
}
|
||||
|
||||
// Attempt to search for the query
|
||||
const response = await fetch(`/api/docs/search?query=${query}`);
|
||||
const pages: DocsContentMetadata[] = await response.json();
|
||||
setPages(pages);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full md:w-[250px] min-h-fit h-fit bg-card rounded-lg p-3">
|
||||
<div className="flex flex-col w-full">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-1">
|
||||
<SearchIcon width={16} height={16} />
|
||||
<p>Search</p>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-screen md:w-[600px]">
|
||||
<DialogTitle>Search Documentation</DialogTitle>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Query..."
|
||||
className="w-full p-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:border-ring"
|
||||
onChange={event => searchDocs(event.target.value)}
|
||||
/>
|
||||
|
||||
{!pages && continueTyping && <p>Continue typing...</p>}
|
||||
|
||||
<DocumentationPages pages={pages} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { Search } from "@/app/components/docs/search";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export function Sidebar(): ReactElement {
|
||||
return (
|
||||
<div className="flex flex-row gap-2 h-full md:flex-col">
|
||||
<Search />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,8 +3,16 @@
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/app/common/utils";
|
||||
|
||||
export function GithubStar(): ReactElement {
|
||||
type GithubStarProps = {
|
||||
/**
|
||||
* The class name for this component.
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function GithubStar({ className }: GithubStarProps): ReactElement {
|
||||
const [starCount, setStarCount] = useState(0);
|
||||
|
||||
const getStarCount = async () => {
|
||||
@ -19,7 +27,10 @@ export function GithubStar(): ReactElement {
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="bg-github-green px-2 py-1 rounded-lg items-center gap-1 hover:opacity-85 transform-gpu transition-all hidden md:flex"
|
||||
className={cn(
|
||||
"bg-github-green px-2 py-1 rounded-lg items-center gap-1 hover:opacity-85 transform-gpu transition-all hidden md:flex",
|
||||
className,
|
||||
)}
|
||||
href="https://github.com/RealFascinated/MinecraftUtilities"
|
||||
target="_blank"
|
||||
>
|
||||
|
@ -9,6 +9,7 @@ import { ToggleThemeButton } from "./theme-toggle-button";
|
||||
import { GithubStar } from "@/app/components/github-star";
|
||||
import { Card } from "@/app/components/card";
|
||||
import { cn } from "@/app/common/utils";
|
||||
import { CommandMenu } from "@/app/components/command-menu";
|
||||
|
||||
type Page = {
|
||||
/**
|
||||
@ -42,7 +43,8 @@ const pages: Page[] = [
|
||||
];
|
||||
|
||||
export default function NavBar(): ReactElement {
|
||||
const path = usePathname();
|
||||
const path: string = usePathname();
|
||||
const isDocs: boolean = path ? path.includes("/docs") : false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@ -50,15 +52,17 @@ export default function NavBar(): ReactElement {
|
||||
classNameContent="p-0 relative rounded-lg flex justify-between items-center gap-3 px-3 bg-opacity-85 h-12"
|
||||
>
|
||||
{/* Left */}
|
||||
<div className="z-50">
|
||||
<div className={cn("flex flex-row items-center gap-2 z-50", isDocs ? "w-full md:w-fit" : "w-fit")}>
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<Logo />
|
||||
<p className="hidden md:block text-lg font-semibold">Minecraft Utilities</p>
|
||||
</Link>
|
||||
|
||||
{/* Command Menu */}
|
||||
<CommandMenu className={cn(isDocs ? "" : "hidden md:inline-flex")} />
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="absolute inset-x-0 flex justify-center">
|
||||
<div className={cn("absolute inset-x-0 justify-center", isDocs ? "hidden md:flex" : "flex")}>
|
||||
<div className="flex gap-4">
|
||||
{pages.map((page, index) => {
|
||||
const isActive: boolean = path ? path.includes(page.url) : false;
|
||||
@ -79,7 +83,7 @@ export default function NavBar(): ReactElement {
|
||||
{/* Right */}
|
||||
<div className="flex gap-4 items-center z-50">
|
||||
<ToggleThemeButton />
|
||||
<GithubStar />
|
||||
<GithubStar className={isDocs ? "hidden md:flex" : "hidden"} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import { Separator } from "../ui/separator";
|
||||
import { SkinPartImage } from "./skin-part-image";
|
||||
import { CacheInformation } from "@/app/components/cache-information";
|
||||
import { PlayerSkin } from "@/app/components/player/player-skin";
|
||||
import { ReloadPageButton } from "@/app/components/reload-page-button";
|
||||
|
||||
type PlayerViewProps = {
|
||||
/**
|
||||
@ -38,6 +39,7 @@ export function PlayerView({ player }: PlayerViewProps): ReactElement {
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex gap-2 flex-wrap justify-center">
|
||||
<ReloadPageButton />
|
||||
<CodeDialog
|
||||
title="Player Data"
|
||||
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 { Button } from "../ui/button";
|
||||
import { CacheInformation } from "@/app/components/cache-information";
|
||||
import { ReloadPageButton } from "@/app/components/reload-page-button";
|
||||
|
||||
type ServerViewProps = {
|
||||
/**
|
||||
@ -30,6 +31,7 @@ export function ServerView({ server, favicon }: ServerViewProps): ReactElement {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap justify-center">
|
||||
<ReloadPageButton />
|
||||
<CodeDialog
|
||||
title="Server Data"
|
||||
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>
|
||||
);
|
||||
}
|
135
src/app/components/ui/command.tsx
Normal file
135
src/app/components/ui/command.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/app/common/utils";
|
||||
import { Dialog, DialogContent } from "@/app/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
shouldFilter={false}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-md md:max-w-2xl xl:max-w-6xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid max-w-md md:max-w-2xl xl:max-w-6xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import Error from "next/error";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/app/components/ui/button";
|
||||
|
||||
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
useEffect(() => {
|
||||
@ -10,10 +11,12 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<h1>Something shit the fan</h1>
|
||||
</body>
|
||||
</html>
|
||||
<div className="flex text-center flex-col gap-4">
|
||||
<div>
|
||||
<h2 className="text-red-400 font-2xl font-semibold">Error</h2>
|
||||
<p>An error occurred while rendering this page.</p>
|
||||
</div>
|
||||
<Button onClick={reset}>Reload</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,51 +2,51 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 95%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 355.7 100% 97.3%;
|
||||
--secondary: 5 5% 95%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.3rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 6.5%;
|
||||
--background-accent: 20 14.3% 8.5%;
|
||||
--foreground: 0 0% 95%;
|
||||
--card: 24 9.8% 10%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
--popover: 0 0% 9%;
|
||||
--popover-foreground: 0 0% 95%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 8%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 144.9 80.4% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 15%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 12 6.5% 75%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--border: 240 5.9% 30%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,10 @@ import Container from "./components/container";
|
||||
import ThemeProvider from "./components/theme-provider";
|
||||
import { Toaster } from "./components/ui/toaster";
|
||||
import { TooltipProvider } from "./components/ui/tooltip";
|
||||
import { inter } from "@/app/font/fonts";
|
||||
import config from "@root/config.json";
|
||||
|
||||
import "./globals.css";
|
||||
import config from "@root/config.json";
|
||||
import { inter } from "@/app/font/fonts";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
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