feat: new documentation!

This commit is contained in:
dvelo 2024-09-03 23:56:15 -05:00
parent c7a52700fe
commit 60e0c5863a
34 changed files with 3670 additions and 970 deletions

5
.gitignore vendored

@ -7,6 +7,9 @@
.yarn/install-state.gz .yarn/install-state.gz
.turbo .turbo
# contentlayer
.contentlayer
# cron # cron
/cron/dist /cron/dist
/cron/node_modules /cron/node_modules
@ -41,3 +44,5 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
css-obfuscator css-obfuscator
*.sync-conflict*

42
contentlayer.config.js Executable file

@ -0,0 +1,42 @@
import { defineDocumentType, makeSource } from "contentlayer/source-files";
import rehypeSlug from "rehype-slug";
import GithubSlugger from "github-slugger"
export const Docs = defineDocumentType(() => ({
name: "Docs",
filePathPattern: `**/*.mdx`,
contentType: "mdx",
fields: {
title: {
type: "string",
required: true,
},
},
computedFields: {
url: {
type: "string",
resolve: (docs) => `/docs/${docs._raw.flattenedPath}`,
},
toc: {
type: "json",
resolve: async (doc) => {
const headingsRegex = /\n(?<flag>#{1,6})\s+(?<content>.+)/g;
const slugger = new GithubSlugger()
const headings = Array.from(doc.body.raw.matchAll(headingsRegex)).map(
({ groups }) => {
const flag = groups?.flag;
const content = groups?.content;
return {
level: flag.length,
text: content,
slug: content ? slugger.slug(content) : undefined
};
}
);
return headings;
},
},
},
}));
export default makeSource({ contentDirPath: "docs", documentTypes: [Docs], mdx: {rehypePlugins: [rehypeSlug]} });

@ -0,0 +1,93 @@
---
title: "Troubleshooting: Making external servers on Minehut"
---
# External Servers on Minehut
I think creating external servers on Minehut is a advanced subject, and it is not documented well enough for the circumstances that
might occur with server owners. This is a [extension/rephrasing of the offical wiki guide](https://minehut.wiki.gg/wiki/External). All points in **bold** are things you shouldn't miss, and are commonly misread.
<br/>
<Separator/>
_Note: This is an unoffical guide, but the offical way of connecting! This is safe!_
## Getting started
You must pick a host that allows the following when making external servers:
- **Editing server flags** Editing flags for the server to run is essential. There are many cases where you were not able to continue because your provider was resetting your flags back
- **A supported server software** For standalone servers, you must run on [Paper](https://papermc.io/software/paper). For proxy networks, you can use [Velocity](https://velocitypowered.com/), [Waterfall](https://papermc.io/software/waterfall) & [Lilypad](https://www.lilypadmc.org/). **BungeeCord is not supported by any means!**
Minehut offically recommends Velocity, which you can find instructions [here](https://docs.papermc.io/velocity/getting-started) to get going! If you do not wish to use a proxy, using Paper is recommended, which you can find a guide [here](https://docs.papermc.io/paper/getting-started).
Before doing below, **make sure your proxy _actually works!_**
## Changing flags
To ensure that Minehut can properly connect your players to your server, you need to add flags when booting up your server. These are commonly in `start.bat` or `start.sh` for Linux-based hosts. **Players cannot join your server from Minehut without adding these flags!**
### Velocity
Add the following `sessionserver` flag to your start script:
```
-Dmojang.sessionserver=https://api.minehut.com/mitm/proxy/session/minecraft/hasJoined
```
All flags put together should look like the following:
```
java -Dmojang.sessionserver=https://api.minehut.com/mitm/proxy/session/minecraft/hasJoined -jar velocity.jar
```
### Paper - standalone
Like said above, if you run a proxy, add the flags for Velocity. **Adding both the Velocity (or any other proxy server) & Paper flags will cause your server to be unauthenticatable!** <br/>
Add the following `auth.host`, `account.host`, `services.host` & `session.host` flags:
```
-Dminecraft.api.auth.host=https://authserver.mojang.com/
-Dminecraft.api.account.host=https://api.mojang.com/
-Dminecraft.api.services.host=https://api.minecraftservices.com/
-Dminecraft.api.session.host=https://api.minehut.com/mitm/proxy
```
All the script together
```
java -Dminecraft.api.auth.host=https://authserver.mojang.com/ -Dminecraft.api.account.host=https://api.mojang.com/ -Dminecraft.api.services.host=https://api.minecraftservices.com/ -Dminecraft.api.session.host=https://api.minehut.com/mitm/proxy -jar paper.jar
```
**Along with this,** make sure to set the `enforce-secure-profile` flag in the `server.properties` file to `false`.
```
enable-status=true
# Set this to false!
enforce-secure-profile=false
enforce-whitelist=false
```
### Lilypad
Set the following environment variable `LILYPAD_MOJANG_SESSIONSERVER_URL` to `https://api.minehut.com/mitm/proxy/session/minecraft/hasJoined`. Environment variables are set as a seperate command in the start script:
```
LILYPAD_MOJANG_SESSIONSERVER_URL="https://api.minehut.com/mitm/proxy/session/minecraft/hasJoined"
```
If above doesn't work, try this:
```
export LILYPAD_MOJANG_SESSIONSERVER_URL="https://api.minehut.com/mitm/proxy/session/minecraft/hasJoined"
```
## Enable Proxy Protocol
_Note: Skip this step if you're using [TCPShield](https://tcpshield.com/) for DDoS protection._
Enable proxy protocol in your proxy's configuration file:
### Velocity
In velocity.toml under advanced, set
```
haproxy-protocol = true
```
### Waterfall
In config.yml under listeners, set
```
proxy_protocol: true
```
### Paper
In config/paper-global.yml under proxies, set
```
proxy-protocol: true
```
## Thats it!
After this, there are mostly no more common issues. Continue on [the wiki](https://minehut.wiki.gg/wiki/External#Connect_Your_External_Server_Plan_on_Minehut)!
## Have any issues?
Go to the offical Minehut Discord server and go into the [#ask-for-help](https://discord.com/channels/239599059415859200/1014801630295760897) channel and create a thread.

@ -3,3 +3,23 @@ title: "Customization"
--- ---
# Customize your server # Customize your server
Customizing your server is very easy after you have [linked your account](/docs/guides/linking). Make sure you have done that before then.
## Getting started
Make sure you've also [owned your server](/docs/guides/owning-a-server).
## Customization Types
### Discord Server
Enable the server widget in your Discord server settings, and copy and paste in the Discord server ID, and your Discord server will appear!
### Banner
Your server must have an image from [Imgur](https://imgur.com), and can be any image type that can be rendered on the web. Copy and paste the link (not the link after uploading the image, but by right clicking and hitting "Copy Image Address") into the input box!
### Color Scheme
You can pick any color in the box and choosing a color scheme to show on your server specificly.
### Description
You can use Markdown formatting to add a description to describe what your server is.
## Thats it!
If you have additional customization types or other things you'd like us to add, [feel free to hit us with an issue on GitHub!](https://github.com/DeveloLongScript/MHSF)

@ -8,9 +8,9 @@ Owning a server is quite simple and allows you to [customize your server](/docs/
## Linking ## Linking
Find the server you would like to own, and make sure your account has [already been linked with your Minecraft account](/docs/guides/linking). Go to the server, and hit the Customization tab. If the owner of the server, and the user your linked to match, you will gain access to the server. Find the server you would like to own (either by looking for it, or using the keyboard shortcut `Ctrl`+`Shift`+`K` and searching for it), and make sure your account has [already been linked with your Minecraft account](/docs/guides/linking). Go to the server, and hit the Customization tab. If the owner of the server, and the user your linked to match, you will gain access to the server.
If they match, you should see a button named Click to own. Press that button, and you should automagically own the server. Congratulations! If they match, you should see a button named Click to own. Press that button, and you should automagically own the server. Congratulations!
## I can't link my server, because my server doesn't have a author ## I can't link my server, because my server doesn't have a author
Your server must have an author in-order to be automagically linked, and if it doesn't have an author, that means you will have to manually link your server. To do that, make an issue on GitHub, showing that your server has no author, but needs to be linked. Show proof that you own the server, and your account will own the server you need. Your server must have an author in-order to be automagically linked, and if it doesn't have an author, that means you will have to manually link your server. To do that, make an issue on GitHub, showing that your server has no author, but needs to be linked. Show proof that you own the server, along with your account username, and your account will own the server you need.

@ -1,3 +1,5 @@
import { withContentlayer } from "next-contentlayer";
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
images: { images: {
@ -8,6 +10,16 @@ const nextConfig = {
}, },
], ],
}, },
async redirects() {
return [
// Basic redirect
{
source: '/docs',
destination: '/docs/getting-started',
permanent: true,
},
]
},
}; };
export default nextConfig; export default withContentlayer(nextConfig);

@ -4,7 +4,7 @@
"private": true, "private": true,
"packageManager": "yarn@1.22.22", "packageManager": "yarn@1.22.22",
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@ -23,7 +23,9 @@
"@unocss/postcss": "^0.61.5", "@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5", "@unocss/transformer-directives": "^0.61.5",
"@unocss/webpack": "^0.61.5", "@unocss/webpack": "^0.61.5",
"contentlayer": "^0.3.4",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"github-slugger": "^2.0.0",
"inngest": "^3.21.2", "inngest": "^3.21.2",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"json-beautify": "^1.1.1", "json-beautify": "^1.1.1",
@ -31,6 +33,7 @@
"minimessage-2-html": "1.6.0", "minimessage-2-html": "1.6.0",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"next": "14.2.3", "next": "14.2.3",
"next-contentlayer": "^0.3.4",
"next-css-obfuscator": "^2.2.16", "next-css-obfuscator": "^2.2.16",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"nextjs-toploader": "^1.6.12", "nextjs-toploader": "^1.6.12",
@ -40,6 +43,7 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-fade-in": "^2.0.1", "react-fade-in": "^2.0.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

@ -0,0 +1,367 @@
"use client";
import { Button } from "@/components/ui/button";
import { useClerk, useUser } from "@clerk/nextjs";
import { ExternalLink, KeyRound, UserPen, Link, Cog } from "lucide-react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { default as NextLink } from "next/link";
import { useEffect, useState } from "react";
export default function Settings() {
const clerk = useClerk();
const { user, isSignedIn } = useUser();
const [linked, setLinked] = useState(false);
useEffect(() => {
setLinked(user?.publicMetadata.player != undefined);
}, [user, isSignedIn]);
return (
<main className="pt-[48px] p-4">
<ResizablePanelGroup
direction="horizontal"
className="min-h-[calc(100vh-70px)] "
>
<ResizablePanel className="min-w-[285px] max-w-[285px] w-[285px] max-md:hidden">
<div className="w-[300px] mt-[20px] ml-[10px]">
<NextLink href="/account/settings" className="text-inherit">
<Button className="mb-[2px] w-[250px]" variant="ghost">
<Link size={16} className="mr-2" /> Linking
</Button>
</NextLink>
<Button className="mb-[2px] w-[250px]">
<Cog size={16} className="mr-2" /> Options
</Button>
<Button
className="mb-[2px] w-[250px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<UserPen size={16} className="mr-2" /> Profile{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
<Button
className="mb-[2px] w-[250px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<KeyRound size={16} className="mr-2" /> Security{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
</div>
</ResizablePanel>
<ResizableHandle className="max-md:hidden" />
<ResizablePanel>
<div className="p-4">
<div className="md:hidden">
<NextLink href="/account/settings" className="text-inherit">
<Button className="mr-[2px]" variant="ghost">
<Link size={16} className="mr-2" /> Linking
</Button>
</NextLink>
<Button className="mr-[2px] ">
<Cog size={16} className="mr-2" /> Options
</Button>
<Button
className="mr-[2px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<UserPen size={16} className="mr-2" /> Profile{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
<Button
className="mr-[2px] mb-[30px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<KeyRound size={16} className="mr-2" /> Security{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
</div>
<strong className="text-3xl">Profile Preferences</strong>
<br />
<br />
<div className="md:grid md:grid-cols-3 gap-1.5">
<Card>
<CardContent>
<br />
<PaddingRadioForm />
</CardContent>
</Card>
<br className="md:hidden" />
<Card>
<CardContent>
<br />
<RowRadioForm />
</CardContent>
</Card>
<br className="md:hidden" />
<Card>
<CardContent>
<br />
<ServerPaddingForm />
</CardContent>
</Card>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</main>
);
}
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Card, CardContent } from "@/components/ui/card";
import { setAccountSL } from "@/lib/api";
import toast from "react-hot-toast";
const FormSchema = z.object({
type: z.enum(["15", "30", "40", "60", "100", "200"], {
required_error: "You need to select a valid padding.",
}),
});
const FormSchemaGrid = z.object({
grid: z.enum(["4", "5", "6"], {
required_error: "You need to select a valid grid type.",
}),
});
export function RowRadioForm() {
const { user } = useUser();
const form = useForm<z.infer<typeof FormSchemaGrid>>({
resolver: zodResolver(FormSchemaGrid),
defaultValues: {
grid: user?.publicMetadata.ipr as "4" | "5" | "6" | undefined,
},
});
function onSubmit(data: z.infer<typeof FormSchemaGrid>) {
toast.promise(setAccountSL(Number.parseInt(data.grid), "ipr"), {
loading: "Saving...",
success: "Saved!",
error: "Failed to save",
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<FormField
control={form.control}
name="grid"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Items per row</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="4" />
</FormControl>
<FormLabel className="font-normal">
4 items per row
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="5" />
</FormControl>
<FormLabel className="font-normal">
5 items per row
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="6" />
</FormControl>
<FormLabel className="font-normal">
6 items per row
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
export function PaddingRadioForm() {
const { user } = useUser();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
type: user?.publicMetadata.pad as
| "15"
| "30"
| "40"
| "60"
| "100"
| "200"
| undefined,
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
toast.promise(setAccountSL(Number.parseInt(data.type), "pad"), {
loading: "Saving...",
success: "Saved!",
error: "Failed to save",
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Padding of servers</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="15" />
</FormControl>
<FormLabel className="font-normal">15px</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="30" />
</FormControl>
<FormLabel className="font-normal">30px</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="40" />
</FormControl>
<FormLabel className="font-normal">40px</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="60" />
</FormControl>
<FormLabel className="font-normal">60px</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="100" />
</FormControl>
<FormLabel className="font-normal">100px</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="200" />
</FormControl>
<FormLabel className="font-normal">200px</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
<Button
onClick={() => setAccountSL(null, "pad")}
variant="outline"
type="button"
className="ml-2"
>
Clear
</Button>
</form>
</Form>
);
}
const FormSchemaCB = z.object({
padding: z.boolean().default(false).optional(),
});
export function ServerPaddingForm() {
const { user } = useUser();
const srv = user?.publicMetadata.srv as boolean | undefined;
const form = useForm<z.infer<typeof FormSchemaCB>>({
resolver: zodResolver(FormSchemaCB),
defaultValues: {
padding: srv === undefined ? false : srv,
},
});
function onSubmit(data: z.infer<typeof FormSchemaCB>) {
toast.promise(
setAccountSL(data.padding === undefined ? false : data.padding, "srv"),
{
loading: "Saving...",
success: "Saved!",
error: "Failed to save",
}
);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="padding"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Use padding on the sides of only the servers, not the whole
server list
</FormLabel>
<FormDescription>
Only show the padding settings on the servers themselves, not
the whole entire page of the server list.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}

@ -1,12 +1,13 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useClerk, useUser } from "@clerk/nextjs"; import { useClerk, useUser } from "@clerk/nextjs";
import { ExternalLink, KeyRound, Link, UserPen } from "lucide-react"; import { ExternalLink, KeyRound, UserPen, Link, Cog } from "lucide-react";
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable"; } from "@/components/ui/resizable";
import { default as NextLink } from "next/link";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { unlinkMCAccount } from "@/lib/api"; import { unlinkMCAccount } from "@/lib/api";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -27,13 +28,18 @@ export default function Settings() {
<main className="pt-[48px] p-4"> <main className="pt-[48px] p-4">
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
className="min-h-[calc(100vh-70px)] " className="min-h-[calc(100vh-70px)]"
> >
<ResizablePanel className="min-w-[285px] max-w-[285px] w-[285px]"> <ResizablePanel className="max-md:hidden min-w-[285px] max-w-[285px] w-[285px]">
<div className="w-[300px] mt-[20px] ml-[10px]"> <div className="w-[300px] mt-[20px] ml-[10px]">
<Button className="mb-[2px] w-[250px]"> <Button className="mb-[2px] w-[250px]">
<Link size={16} className="mr-2" /> Linking <Link size={16} className="mr-2" /> Linking
</Button> </Button>
<NextLink href="/account/settings/options" className="text-inherit">
<Button className="mb-[2px] w-[250px] " variant="ghost">
<Cog size={16} className="mr-2" /> Options
</Button>
</NextLink>
<Button <Button
className="mb-[2px] w-[250px]" className="mb-[2px] w-[250px]"
variant="ghost" variant="ghost"
@ -52,9 +58,38 @@ export default function Settings() {
</Button> </Button>
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle className="max-md:hidden" />
<ResizablePanel> <ResizablePanel>
<div className="p-4"> <div className="p-4">
<div className="md:hidden">
<Button className="mr-[2px]">
<Link size={16} className="mr-2" /> Linking
</Button>
<NextLink
href="/account/settings/options"
className="text-inherit"
>
<Button className="mr-[2px] " variant="ghost">
<Cog size={16} className="mr-2" /> Options
</Button>
</NextLink>
<Button
className="mr-[2px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<UserPen size={16} className="mr-2" /> Profile{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
<Button
className="mr-[2px] mb-[30px]"
variant="ghost"
onClick={() => clerk.openUserProfile({})}
>
<KeyRound size={16} className="mr-2" /> Security{" "}
<ExternalLink size={16} className="ml-2" />
</Button>
</div>
<strong className="text-3xl">Linking</strong> <strong className="text-3xl">Linking</strong>
<br /> <br />
<br /> <br />

@ -0,0 +1,89 @@
import TableOfContent from "@/components/docs/TOC";
import { ScrollArea } from "@/components/ui/scroll-area";
import { allDocs } from "contentlayer/generated";
import { useMDXComponent } from "next-contentlayer/hooks";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Separator } from "@/components/ui/separator";
export const generateStaticParams = async () =>
allDocs.map((post) => ({ slug: [post._raw.flattenedPath] }));
export const generateMetadata = ({
params,
}: {
params: { slug: string[] };
}) => {
const post = allDocs.find(
(post) => post._raw.flattenedPath === params.slug.join("/")
);
if (!post) notFound();
return { title: post.title + " | MHSF Docs" };
};
const PostLayout = ({ params }: { params: { slug: string[] } }) => {
const doc = allDocs.find(
(post) => post._raw.flattenedPath === params.slug.join("/")
);
if (!doc) notFound();
console.log(doc);
const MDXContent = useMDXComponent(doc.body.code);
return (
<main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
<div className="mx-auto w-full min-w-0">
<div className="pb-12 pt-8 prose dark:prose-invert">
<MDXContent components={{ Separator }} />
</div>
</div>
{doc.toc && (
<div className="hidden text-sm xl:block">
<div className="sticky top-16 -mt-10 pt-4">
<ScrollArea className="pb-10">
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] py-12 space-y-2">
<p className="font-medium">On This Page</p>
{doc.toc.map(
(c: { level: number; text: string; slug: string }) => (
<TableOfContent toc={c} doc={doc} />
)
)}
<br />
<div className="space-y-2">
<p className="font-medium">Contribute</p>
<ul className="m-0 list-none">
<li className="mt-0 pt-2">
<Link
href={
"https://github.com/DeveloLongScript/MHSF/edit/main/docs/" +
doc._raw.flattenedPath +
".mdx"
}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors no-underline"
>
<svg
viewBox="0 0 438.549 438.549"
fontSize={16}
className="mr-2 size-4"
>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
Edit page on GitHub
</Link>
</li>
</ul>
</div>
</div>
</ScrollArea>
</div>
</div>
)}
</main>
);
};
export default PostLayout;

44
src/app/docs/layout.tsx Normal file

@ -0,0 +1,44 @@
import { Sidebar } from "@/components/docs/Sidebar";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { ScrollArea } from "@/components/ui/scroll-area";
import { version } from "@/config/version";
import { HamburgerMenuIcon } from "@radix-ui/react-icons";
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="border-b pt-[40px]">
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
<aside className="fixed top-14 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
<ScrollArea className="h-full py-6 pr-6 lg:py-8">
<div className="bg-muted w-full rounded justify-center p-4 flex items-center">
MHSF Docs <small className="ml-2">Version {version}</small>
</div>
<br />
<Sidebar />
</ScrollArea>
</aside>
<br className="md:hidden" />
<div className="bg-muted w-full rounded justify-center p-4 flex items-center md:hidden">
MHSF Docs <small className="ml-2">Version {version}</small>
<Drawer>
<DrawerTrigger>
<Button className="ml-2">
<HamburgerMenuIcon />
</Button>
</DrawerTrigger>
<DrawerContent className="p-4">
<Sidebar />
</DrawerContent>
</Drawer>
</div>
{children}
</div>
</div>
);
}

@ -1,37 +0,0 @@
import Link from "next/link";
import { ReactNode } from "react";
export default function HelpPage() {
return (
<main className="sm:grid sm:grid-cols-6 pt-20">
<div className="pl-3 border rounded ml-4">
<strong className="pt-2">MHSF Help Guides</strong>
<br />
<br />
<L H="/">Go back to server list</L>
</div>
<div className="prose dark:prose-invert p-4 pl-[50px] max-w-[100%] col-span-5">
<h1>How to customize your server</h1>
<p>
Customizing a part of your server is easy, as long as you have access
to the account the server is owned under. To own a server, first go to
the server from the <L H="/">server list</L>. Make sure you own an
account by linking an account or creating a new one,{" "}
<L H="/help/how-to-link">
and make sure you linked your Minecraft account.
</L>{" "}
</p>
<h2>Guide</h2>
<p>
After going to the server page, click the Customization tab, and if
you own the server, click the button "Click to own this server". You
have successfully owned your Minehut server!
</p>
</div>
</main>
);
}
function L({ H, children }: { H: string; children: ReactNode }) {
return <Link href={H}>{children}</Link>;
}

@ -1,43 +0,0 @@
import Link from "next/link";
import { ReactNode } from "react";
export default function HelpPage() {
return (
<main className="sm:grid sm:grid-cols-6 pt-20">
<div className="pl-3 border rounded ml-4">
<strong className="pt-2">MHSF Help Guides</strong>
<br />
<br />
<L H="/">Go back to server list</L>
</div>
<div className="prose dark:prose-invert p-4 pl-[50px] max-w-[100%] col-span-5">
<h1>How to link your Minecraft account</h1>
<p>
To link your Minecraft account, make sure you have a MHSF account
created, and go into the settings in the top right, and press
"Security/Profile settings". Click "Link Account".
</p>
<h2>Joining the server</h2>
<p>
After launching Minecraft, join the server{" "}
<code>MHSFPV.minehut.gg</code> and take note of the code being said in
chat. <i>(You may need to go into the lobby to start up MHSFPV)</i>{" "}
Put this code the number selector, click "Submit", and you have linked
your account!
<br /> Congratulations!
</p>
<br />
<b>Related Articles:</b>
<p>
{" "}
- <L H="/help/how-to-customize">How to customize your server</L>
</p>
</div>
</main>
);
}
function L({ H, children }: { H: string; children: ReactNode }) {
return <Link href={H}>{children}</Link>;
}

@ -6,7 +6,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/components/ThemeProvider"; import { ThemeProvider } from "@/components/ThemeProvider";
import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider"; import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider";
import NextTopLoader from "@/lib/top-loader"; import NextTopLoader from "@/lib/top-loader";
import { banner } from "@/banner"; import { banner } from "@/config/banner";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbList, BreadcrumbList,
@ -48,7 +48,7 @@ export default async function RootLayout({
(banner.isBanner == true ? "mt-8" : "") (banner.isBanner == true ? "mt-8" : "")
} }
> >
<div className="items-center me-auto mt-2 pl-7"> <div className="items-center me-auto mt-2 pl-7 max-sm:mt-3">
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<Link href="/"> <Link href="/">

@ -1,114 +0,0 @@
import ClientFadeIn from "@/components/ClientFadeIn";
export default function ECA() {
return (
<main>
<div className="pt-[100px] p-[300px]">
<strong className="text-2xl">External Content Agreement (ECA)</strong>
<br />
By making external content available for anyone to see, there needs to
be an agreement to keep MHSF ("Minehut Server List") a friendly place
for anyone to look at.{" "}
<i>
As such, this agreement outlines what you can't and can do, when
making content on the platform.
</i>{" "}
The goal by making an agreement like this, is not to make you worried
what you can upload, its just showing what the limits of content
uploaded onto the platform are.
<br />
<br />
<ClientFadeIn>
<div>
<strong className="text-xl">Source Code</strong>
<br />
The source code for MHSF is defined by the{" "}
<a href="https://github.com/DeveloLongScript/MHSF/blob/main/LICENSE">
MIT License
</a>
. You are free to use MHSF for commercial use, and you may modify
the software however you'd like. Taking copies of the software (aka
<i>"forking"</i>) is also freely allowed.
<br />
</div>
</ClientFadeIn>
<br />
<ClientFadeIn delay={200}>
<div>
<strong className="text-xl">What your limits are</strong>
<br />
When creating content, if its a matter of making a profile picture,
or editing the description for a server, (and more), you must follow
the underlying agreements below.
<br />
For making banners & descriptions, you must follow{" "}
<a href="https://minehut.wiki.gg/wiki/Rules">
Minehuts Terms of Service
</a>{" "}
<i>
as all content made is associated to Minehut (as the server is
mostly on a community for Minehut).
</i>{" "}
<br />
For making Discord server embeds, you must follow{" "}
<a href="https://discord.com/terms/">
Discords Terms of Service
</a>{" "}
<i>as all content made is associated to Discord.</i> <br />
<strong>
For all other content, they must follow the following: <br />
</strong>
- No inappropriate/adult images <br />
- No swear words of any kind or slurs <br />- Endorsing unethical
client modifications (aka cheating or hacking)
<br />
</div>
</ClientFadeIn>
<br />
<ClientFadeIn delay={400}>
<div>
<strong className="text-xl">When you agree to the ECA</strong>
<br />
When you add customization to your server, or add a profile picture
(linking an account is included), you must follow the ECA.
<br />
</div>
</ClientFadeIn>
<br />
<ClientFadeIn delay={600}>
<div>
<strong className="text-xl">When linking an account</strong>
<br />
When linking an account, you must follow the privacy policy and
terms of service to the associated service that you linked your MHSF
account to. Additionally, if you link an external account{" "}
<i>after</i> account creation, everything said before is still true.
<br />
</div>
</ClientFadeIn>
<br />
<ClientFadeIn delay={800}>
<div>
<strong className="text-xl">Violations</strong>
<br />
Violations from above have 1 warning. Your first interaction is a
warning by removing the content from MHSF, and your 2nd is
banning/deleting your account. (some violations are an instant
delete)
</div>
</ClientFadeIn>
<br />
<ClientFadeIn delay={1000}>
<div>
<strong className="text-xl">Reporting</strong>
<br />
If you personally see a violation of the ECA, you can report it by
clicking the customization tab on a server, and hitting the Report
button (it doesn't appear when the server was never owned). If you
misuse this feature, you may get your account deleted.
</div>
</ClientFadeIn>
</div>
</main>
);
}

@ -1,28 +1,16 @@
import { import Link from "next/link";
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Server } from "lucide-react";
export default function NotFound() { export default function NotFound() {
return ( return (
<div className="w-screen h-12 border-b fixed backdrop-blur flex"> <main>
<div className="me-auto mt-3 pl-7"> <div className="pt-[60px] p-4">
<Breadcrumb> <strong>404 - Page not found</strong>
<BreadcrumbList> <br />
<BreadcrumbItem className="max-sm:hidden"> <p>
<Server /> We couldn't find the page you were looking for.{" "}
</BreadcrumbItem> <Link href="/">Go home</Link>
<BreadcrumbSeparator className="max-sm:hidden" /> </p>
<BreadcrumbItem>
<BreadcrumbPage>Not Found</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</div> </div>
</main>
); );
} }

@ -4,6 +4,9 @@ import TabServer from "@/components/misc/TabServer";
import ColorProvider from "@/components/ColorProvider"; import ColorProvider from "@/components/ColorProvider";
import AfterServerView from "@/components/AfterServerView"; import AfterServerView from "@/components/AfterServerView";
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
import { Button } from "@/components/ui/button";
import { CornerDownLeft } from "lucide-react";
import Link from "next/link";
type Props = { type Props = {
params: { server: string }; params: { server: string };
@ -69,6 +72,12 @@ export default function ServerPage({ params }: { params: { server: string } }) {
<ColorProvider server={params.server}> <ColorProvider server={params.server}>
<div className={"pt-16"}> <div className={"pt-16"}>
<Banner server={params.server} /> <Banner server={params.server} />
<Link href="/">
<Button variant="link" className="text-muted-foreground text-sm">
<CornerDownLeft size={16} className="mr-2" /> Go back to the
server list
</Button>
</Link>
<TabServer server={params.server} tabDef="general" /> <TabServer server={params.server} tabDef="general" />
<div className="pt-8"> <div className="pt-8">
<ServerView server={params.server} /> <ServerView server={params.server} />

@ -1,5 +1,5 @@
import FavoriteSortView from "@/components/FavoritesSortView"; import FavoriteSortView from "@/components/FavoritesSortView";
import { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Favorites Sort | MHSF", title: "Favorites Sort | MHSF",

@ -70,7 +70,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
key={b.name} key={b.name}
className={ className={
(!mini ? "min-h-[450px] max-h-[450px]" : "") + (!mini ? "min-h-[450px] max-h-[450px]" : "") +
" mb-4 flex items-start" " mb-4 flex items-start shadow-md"
} }
> >
<CardHeader> <CardHeader>

@ -150,7 +150,7 @@ export default function ServerCustomize({
Is this server in violation of the ECA? Is this server in violation of the ECA?
</div> </div>
Is this server in violation of the{" "} Is this server in violation of the{" "}
<Link href="/legal/external-content-agreement"> <Link href="/docs/legal/external-content-agreement">
External Content Agreement (aka ECA) External Content Agreement (aka ECA)
</Link> </Link>
? You can report the server to remove the customizations from the ? You can report the server to remove the customizations from the
@ -165,9 +165,11 @@ export default function ServerCustomize({
<DialogDescription> <DialogDescription>
This will send a notification to MHSF maintainers. This This will send a notification to MHSF maintainers. This
server must be in violation of the{" "} server must be in violation of the{" "}
<Link href="/legal/external-content-agreement">ECA</Link> to <Link href="/docs/legal/external-content-agreement">
be a valid report. Typical response times include 1 hour to ECA
1 day, and you will not be notified if your report is </Link>{" "}
to be a valid report. Typical response times include 1 hour
to 1 day, and you will not be notified if your report is
successful or not.{" "} successful or not.{" "}
<b> <b>
Please do not spam this form with mindless reports. If you Please do not spam this form with mindless reports. If you
@ -293,7 +295,7 @@ export default function ServerCustomize({
Minehuts Terms of Service Minehuts Terms of Service
</Link>{" "} </Link>{" "}
& the{" "} & the{" "}
<Link href="/legal/external-content-agreement"> <Link href="/docs/legal/external-content-agreement">
External Content Agreement External Content Agreement
</Link> </Link>
. .
@ -400,7 +402,7 @@ export default function ServerCustomize({
Imgurs Terms of Service Imgurs Terms of Service
</Link>{" "} </Link>{" "}
& the{" "} & the{" "}
<Link href="/legal/external-content-agreement"> <Link href="/docs/legal/external-content-agreement">
External Content Agreement External Content Agreement
</Link> </Link>
. .
@ -432,7 +434,7 @@ export default function ServerCustomize({
Discords Terms of Service Discords Terms of Service
</Link>{" "} </Link>{" "}
& the{" "} & the{" "}
<Link href="/legal/external-content-agreement"> <Link href="/docs/legal/external-content-agreement">
External Content Agreement External Content Agreement
</Link> </Link>
. .

@ -30,7 +30,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { allTags, allCategories } from "@/allTags"; import { allTags, allCategories } from "@/config/tags";
import IconDisplay from "./IconDisplay"; import IconDisplay from "./IconDisplay";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { Spinner } from "./ui/spinner"; import { Spinner } from "./ui/spinner";
@ -77,7 +77,7 @@ const features = [
name: "Add a Discord widget", name: "Add a Discord widget",
description: description:
"Show where your players talk to each-other, including an online users count.", "Show where your players talk to each-other, including an online users count.",
href: "/help/how-to-customize", href: "/docs/guides/customization",
cta: "Learn more", cta: "Learn more",
background: <span />, background: <span />,
className: "lg:row-start-1 lg:row-end-2 lg:col-start-2 lg:col-end-3", className: "lg:row-start-1 lg:row-end-2 lg:col-start-2 lg:col-end-3",
@ -85,7 +85,7 @@ const features = [
{ {
Icon: InputIcon, Icon: InputIcon,
name: "Descriptions", name: "Descriptions",
href: "/help/how-to-customize", href: "/docs/guides/customization",
cta: "Learn more", cta: "Learn more",
description: description:
"Format your descriptions using Markdown to show what your server has to offer.", "Format your descriptions using Markdown to show what your server has to offer.",
@ -95,7 +95,7 @@ const features = [
{ {
Icon: ImageIcon, Icon: ImageIcon,
name: "Banners", name: "Banners",
href: "/help/how-to-customize", href: "/docs/guides/customization",
cta: "Learn more", cta: "Learn more",
description: description:
"Show a banner with can contain images that show on your server page.", "Show a banner with can contain images that show on your server page.",
@ -116,7 +116,7 @@ export default function ServerList() {
const [random, setRandom] = useState(false); const [random, setRandom] = useState(false);
const [serverList, setServerList] = useState(new ServersList([])); const [serverList, setServerList] = useState(new ServersList([]));
const [textCopied, setTextCopied] = useState(false); const [textCopied, setTextCopied] = useState(false);
const [padding, setPadding] = useState(0); const [padding, setPadding] = useState<string>("0");
const bigger = async (server: OnlineServer) => const bigger = async (server: OnlineServer) =>
server.playerData.playerCount > 15; server.playerData.playerCount > 15;
const smaller = async (server: OnlineServer) => const smaller = async (server: OnlineServer) =>
@ -128,8 +128,10 @@ export default function ServerList() {
const [servers, setServers] = useState<Array<OnlineServer>>([]); const [servers, setServers] = useState<Array<OnlineServer>>([]);
const clipboard = useClipboard(); const clipboard = useClipboard();
const router = useRouter(); const router = useRouter();
const { user, isSignedIn } = useUser();
const [pOS, setpOS] = useState(false); const [pOS, setpOS] = useState(false);
const [ipr, setIPR] = useState("4"); const [ipr, setIPR] = useState<string>("4");
const [am, setAM] = useState<boolean>(false);
const [filters, setFilters] = useState< const [filters, setFilters] = useState<
Array<(server: OnlineServer) => Promise<boolean>> Array<(server: OnlineServer) => Promise<boolean>>
>([]); >([]);
@ -143,6 +145,16 @@ export default function ServerList() {
setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000"); setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000");
}, [resolvedTheme]); }, [resolvedTheme]);
useEffect(() => {
if (isSignedIn) {
console.log(user.publicMetadata);
setIPR((user.publicMetadata.ipr as string | undefined) || "4");
setPadding((user.publicMetadata.pad as string | undefined) || "0");
setpOS((user.publicMetadata.srv as boolean | undefined) || false);
setAM(pOS != false || padding != "0" || ipr != "4");
}
}, [isSignedIn, user]);
useEffectOnce(() => { useEffectOnce(() => {
setRandomText(getRandomText()); setRandomText(getRandomText());
serverList serverList
@ -172,7 +184,6 @@ export default function ServerList() {
const ref = useRef(null); const ref = useRef(null);
const [clickedPage, setClickedPage] = useState("banners"); const [clickedPage, setClickedPage] = useState("banners");
const [hero, setHero] = useState(false); const [hero, setHero] = useState(false);
const { isSignedIn } = useUser();
if (inErrState) { if (inErrState) {
return ( return (
<> <>
@ -211,11 +222,11 @@ export default function ServerList() {
} }
return ( return (
<div style={!pOS ? { padding } : undefined}> <div style={!pOS ? { padding: `${padding}px` } : undefined}>
<div className="p-0 branding-hero"> <div className="p-0 branding-hero">
<> <>
{(!isSignedIn || hero) && ( {(!isSignedIn || hero) && (
<div className=" py-2 h-[300vh] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 "> <div className=" py-2 max-lg:h-[370vh] lg:h-[300vh] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 ">
<Particles <Particles
className="absolute inset-0 -z-10 block" className="absolute inset-0 -z-10 block"
quantity={100} quantity={100}
@ -474,7 +485,7 @@ export default function ServerList() {
<br id="serverlist" className="pb-14" /> <br id="serverlist" className="pb-14" />
<Separator /> <Separator />
<ClientFadeIn delay={100}> <ClientFadeIn delay={100}>
<Menubar className="mt-3 ml-2 border rounded p-2"> <Menubar className="mt-3 ml-2 border rounded p-2 shadow">
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>Servers</MenubarTrigger> <MenubarTrigger>Servers</MenubarTrigger>
<MenubarContent> <MenubarContent>
@ -856,7 +867,7 @@ export default function ServerList() {
<MenubarSubContent> <MenubarSubContent>
<MenubarRadioGroup <MenubarRadioGroup
value={padding.toString()} value={padding.toString()}
onValueChange={(v) => setPadding(Number(v))} onValueChange={(v) => setPadding(v)}
> >
<MenubarRadioItem value="0">Default</MenubarRadioItem> <MenubarRadioItem value="0">Default</MenubarRadioItem>
<MenubarSeparator /> <MenubarSeparator />
@ -891,12 +902,20 @@ export default function ServerList() {
</MenubarRadioGroup> </MenubarRadioGroup>
</MenubarSubContent> </MenubarSubContent>
</MenubarSub> </MenubarSub>
<SignedIn>
<MenubarSeparator /> <MenubarSeparator />
<SignedIn>
<MenubarCheckboxItem checked={hero} onCheckedChange={setHero}> <MenubarCheckboxItem checked={hero} onCheckedChange={setHero}>
Show Hero Show Hero
</MenubarCheckboxItem> </MenubarCheckboxItem>
</SignedIn> </SignedIn>
<MenubarItem onClick={() => router.push("/docs")}>
View the docs
</MenubarItem>
{am && (
<MenubarItem onClick={() => router.push("/account/settings")}>
Currently using settings in your preferences
</MenubarItem>
)}
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
</Menubar> </Menubar>
@ -1013,8 +1032,8 @@ export default function ServerList() {
} }
style={{ style={{
overflow: "hidden !important", overflow: "hidden !important",
paddingLeft: pOS ? padding : 6, paddingLeft: pOS ? `${padding}px` : 6,
paddingRight: pOS ? padding : 6, paddingRight: pOS ? `${padding}px` : 6,
}} }}
> >
<ClientFadeIn delay={200}> <ClientFadeIn delay={200}>

@ -1,11 +1,11 @@
"use client"; "use client";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "./ui/breadcrumb"; } from "./ui/breadcrumb";
import { allDocs } from "contentlayer/generated";
export default function TextFromPathname() { export default function TextFromPathname() {
const pathname = usePathname(); const pathname = usePathname();
@ -72,6 +72,18 @@ export default function TextFromPathname() {
</BreadcrumbItem> </BreadcrumbItem>
</> </>
)} )}
{pathname == "/account/settings/options" && (
<>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbPage>Settings</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Preferences</BreadcrumbPage>
</BreadcrumbItem>
</>
)}
{pathname == "/legal/external-content-agreement" && ( {pathname == "/legal/external-content-agreement" && (
<> <>
<BreadcrumbSeparator className="max-sm:hidden" /> <BreadcrumbSeparator className="max-sm:hidden" />
@ -94,6 +106,27 @@ export default function TextFromPathname() {
</BreadcrumbItem> </BreadcrumbItem>
</> </>
)} )}
{pathname?.startsWith("/docs/") && (
<>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>Docs</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{
allDocs.find(
(c) =>
c._raw.flattenedPath ===
pathname
?.split("/")
.splice(2, pathname?.split("/").length)
.join("/")
)?.title
}
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}
</> </>
); );
} }

@ -37,7 +37,7 @@ export default function TopBar({ inter }: { inter: string }) {
<path <path
fill="currentColor" fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path> />
</svg> </svg>
</Button> </Button>
<ThemeSwitcher /> <ThemeSwitcher />

@ -0,0 +1,107 @@
"use client";
import { allFolders, Docs, DocsFolder } from "@/config/docs";
import { usePathname } from "next/navigation";
import { Button } from "../ui/button";
import { useState } from "react";
import { ChevronRight } from "lucide-react";
import { useRouter } from "@/lib/useRouter";
import { AnimatePresence, motion } from "framer-motion";
export function Sidebar() {
const pathname = usePathname();
return (
<>
{allFolders.map((docs) => {
const [folderOpen, setOpen] = useState(false);
const router = useRouter();
return (
<div>
<Button
size="sm"
className="w-full font-normal tracking-normal mt-1"
noJustify
variant={
"url" in docs
? pathname == docs.url
? "default"
: "ghost"
: "ghost"
}
onClick={() => {
if ("docs" in docs) {
setOpen(!folderOpen);
} else {
router.push(docs.url);
}
}}
>
{"url" in docs ? docs.title : docs.name}
<div className="flex items-center ml-auto text-muted-foreground">
<AnimatePresence>
{"docs" in docs && folderOpen && (
<motion.div
initial={{ rotate: 90 }}
animate={{ rotate: 0 }}
>
<ChevronRight size={18} />
</motion.div>
)}
{"docs" in docs && !folderOpen && (
<motion.div
initial={{ rotate: 0 }}
animate={{ rotate: 90 }}
>
<ChevronRight size={18} />
</motion.div>
)}
</AnimatePresence>
</div>
</Button>
<div className="ml-2">
{folderOpen && <Subdocs docs={"docs" in docs ? docs.docs : []} />}
</div>
</div>
);
})}
</>
);
}
function Subdocs({ docs }: { docs: (Docs | DocsFolder)[] }) {
const pathname = usePathname();
const router = useRouter();
return (
<>
{docs.map((doc) => {
if ("docs" in doc) {
return <Subdocs docs={doc.docs} />;
} else {
return (
<>
<Button
size="sm"
className="w-full font-normal tracking-normal mt-1"
noJustify
onClick={() => {
router.push(doc.url);
}}
variant={
"url" in doc
? pathname == doc.url
? "default"
: "ghost"
: "ghost"
}
>
{doc.title}
</Button>
<br />
</>
);
}
})}
</>
);
}

@ -0,0 +1,85 @@
"use client";
import { cn } from "@/lib/utils";
import { Docs } from "contentlayer/generated";
import { useEffect, useMemo, useState } from "react";
export default function TableOfContent({
doc,
toc,
}: {
doc: Docs;
toc: { level: number; text: string; slug: string };
}) {
const itemIds = useMemo(
() =>
doc?.toc.flatMap(
(c: { level: number; text: string; slug: string }) => c.slug
),
[doc]
);
const activeHeading = useActiveItem(itemIds);
return <Tree item={toc} activeItem={activeHeading} />;
}
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = useState<any>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: `0% 0% -80% 0%` }
);
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.observe(element);
}
});
return () => {
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.unobserve(element);
}
});
};
}, [itemIds]);
return activeId;
}
interface TreeProps {
item: { level: number; text: string; slug: string };
activeItem?: string;
}
function Tree({ item, activeItem }: TreeProps) {
return (
<>
<ul className={cn("m-0 list-none", { "pl-4": item.level !== 1 })}>
<li key={item.text} className={cn("mt-0 pt-2")}>
<a
href={"#" + item.slug}
className={cn(
"inline-block no-underline transition-colors hover:text-foreground",
item.slug === `${activeItem}`
? "font-medium text-foreground"
: "text-muted-foreground"
)}
>
{item.text}
</a>
</li>
</ul>
</>
);
}

@ -1,13 +1,15 @@
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Calendar, Star, TerminalIcon } from "lucide-react"; import { Book, Calendar, Star, TerminalIcon } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
import { useState } from "react"; import { useState } from "react";
import { Changelog, version } from "@/version"; import { Changelog, version } from "@/config/version";
import events from "@/lib/commandEvent"; import events from "@/lib/commandEvent";
import { ScrollArea } from "../ui/scroll-area"; import { ScrollArea } from "../ui/scroll-area";
import { useRouter } from "@/lib/useRouter";
export default function InfoPopover() { export default function InfoPopover() {
const [changeLog, setChangelog] = useState(false); const [changeLog, setChangelog] = useState(false);
const router = useRouter();
return ( return (
<div className="grid w-full"> <div className="grid w-full">
@ -43,6 +45,9 @@ export default function InfoPopover() {
> >
<Star size={18} className="mr-2" /> Star on GitHub <Star size={18} className="mr-2" /> Star on GitHub
</Button> </Button>
<Button variant={"ghost"} onClick={() => router.push("/docs")}>
<Book size={18} className="mr-2" /> See the docs
</Button>
<Button variant="ghost" onClick={() => events.emit("cmd-event")}> <Button variant="ghost" onClick={() => events.emit("cmd-event")}>
<TerminalIcon size={18} className="mr-2" /> Open commands <TerminalIcon size={18} className="mr-2" /> Open commands
</Button> </Button>

48
src/config/docs.ts Normal file

@ -0,0 +1,48 @@
export const allFolders: (DocsFolder | Docs)[] = [
{
title: "Getting Started",
url: "/docs/getting-started",
},
{
name: "Guides",
docs: [
{
title: "Linking",
url: "/docs/guides/linking",
},
{
title: "Owning a Server",
url: "/docs/guides/owning-a-server",
},
{
title: "Server Customization",
url: "/docs/guides/customization",
},
{ title: "Reporting a server", url: "/docs/guides/reporting-server" },
],
},
{
name: "Advanced",
docs: [
{ title: "Tech Stack", url: "/docs/advanced/tech-stack" },
{ title: "Using the Command-bar", url: "/docs/advanced/command-bar" },
{ title: "Tips with external servers", url: "/docs/advanced/external"}
],
},
{
name: "Legal",
docs: [
{ title: "ECA Agreement", url: "/docs/legal/external-content-agreement" },
],
},
];
export type Docs = {
title: string;
url: string;
};
export type DocsFolder = {
name: string;
docs: Array<Docs>;
};

@ -1,4 +1,4 @@
import { OnlineServer, ServerResponse } from "./lib/types/mh-server"; import { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
const serverCache: any = {}; const serverCache: any = {};

@ -1,10 +1,10 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Separator } from "./components/ui/separator"; import { Separator } from "../components/ui/separator";
import { Button } from "./components/ui/button"; import { Button } from "../components/ui/button";
import confetti from "canvas-confetti"; import confetti from "canvas-confetti";
export const version = "1.1.0"; export const version = "1.2.0";
const User = ({ user }: { user: string }) => ( const User = ({ user }: { user: string }) => (
<span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]"> <span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]">
@ -77,6 +77,18 @@ export const Changelog = () => (
</code> </code>
</div> </div>
<br /> <br />
<div>
<strong className="flex items-center">
Version 1.2.0 (September 3rd 2024)
</strong>
<ul>
<li> Added documentation</li>
<li> Brand new linking of padding and server options</li>
<li> New system to ensure automatic Cron actions!</li>
<li> and alot more!</li>
</ul>
</div>
<br />
<div> <div>
<strong className="flex items-center"> <strong className="flex items-center">
Version 1.1.0 (August 24rd 2024) Version 1.1.0 (August 24rd 2024)

@ -7,12 +7,12 @@
const connector = ( const connector = (
endpoint: string, endpoint: string,
options: { version: number; starting?: string } options: { version: number; starting?: string },
) => ) =>
`${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`; `${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`;
export async function getMOTDFromServer( export async function getMOTDFromServer(
list: Array<{ server: string; motd: string }> list: Array<{ server: string; motd: string }>,
): Promise<Array<{ server: string; motd: string }>> { ): Promise<Array<{ server: string; motd: string }>> {
const result = await fetch(connector("/motd", { version: 1 }), { const result = await fetch(connector("/motd", { version: 1 }), {
body: JSON.stringify({ motd: list }), body: JSON.stringify({ motd: list }),
@ -27,7 +27,7 @@ export async function getMOTDFromServer(
} }
export async function getCommunityServerFavorites( export async function getCommunityServerFavorites(
server: string server: string,
): Promise<number> { ): Promise<number> {
const result = await fetch( const result = await fetch(
connector(`/favorites/${server}/community-favorites`, { version: 0 }), connector(`/favorites/${server}/community-favorites`, { version: 0 }),
@ -36,7 +36,7 @@ export async function getCommunityServerFavorites(
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
let json = await result.json(); let json = await result.json();
@ -53,7 +53,7 @@ export async function favoriteServer(server: string) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
} catch { } catch {
throw Error("Not authenticated with a user."); throw Error("Not authenticated with a user.");
@ -70,7 +70,7 @@ export async function isFavorited(server: string): Promise<boolean> {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return (await response.json()).result; return (await response.json()).result;
@ -89,7 +89,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return (await response.json()).result; return (await response.json()).result;
@ -103,7 +103,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
*/ */
export async function getHistoricalData( export async function getHistoricalData(
server: string, server: string,
scopes: Array<"player_count" | "favorites" | "server" | "time"> scopes: Array<"player_count" | "favorites" | "server" | "time">,
): Promise< ): Promise<
Array<{ Array<{
player_count?: number; player_count?: number;
@ -120,7 +120,7 @@ export async function getHistoricalData(
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return (await response.json()).data; return (await response.json()).data;
@ -128,7 +128,7 @@ export async function getHistoricalData(
export async function getShortTermData( export async function getShortTermData(
server: string, server: string,
scopes: Array<"player_count" | "favorites" | "server" | "date"> scopes: Array<"player_count" | "favorites" | "server" | "date">,
): Promise< ): Promise<
Array<{ Array<{
player_count?: number; player_count?: number;
@ -145,14 +145,14 @@ export async function getShortTermData(
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return (await response.json()).data; return (await response.json()).data;
} }
export async function getMetaShortTerm( export async function getMetaShortTerm(
scopes: Array<"total_players" | "total_servers" | "date"> scopes: Array<"total_players" | "total_servers" | "date">,
): Promise< ): Promise<
Array<{ Array<{
total_players?: number; total_players?: number;
@ -168,7 +168,7 @@ export async function getMetaShortTerm(
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return (await response.json()).data; return (await response.json()).data;
@ -185,7 +185,7 @@ export async function linkMCAccount(code: string): Promise<string | undefined> {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ code }), body: JSON.stringify({ code }),
} },
); );
if (response.status == 400) { if (response.status == 400) {
@ -208,13 +208,13 @@ export async function unlinkMCAccount(): Promise<boolean> {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
return true; return true;
} catch { } catch {
throw Error( throw Error(
"Not authenticated with a user or user already linked account." "Not authenticated with a user or user already linked account.",
); );
} }
} }
@ -229,7 +229,7 @@ export async function serverOwned(server: string): Promise<boolean> {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ server }), body: JSON.stringify({ server }),
} },
); );
return (await response.json()).owned; return (await response.json()).owned;
@ -249,7 +249,7 @@ export async function userOwnedServer(server: string): Promise<boolean> {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ server }), body: JSON.stringify({ server }),
} },
); );
return (await response.json()).result; return (await response.json()).result;
@ -269,7 +269,7 @@ export async function ownServer(server: string): Promise<boolean> {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ server }), body: JSON.stringify({ server }),
} },
); );
if (response.status >= 400) { if (response.status >= 400) {
@ -293,7 +293,7 @@ export async function unownServer(server: string): Promise<boolean> {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ server }), body: JSON.stringify({ server }),
} },
); );
if (response.status == 400) { if (response.status == 400) {
@ -309,7 +309,7 @@ export async function unownServer(server: string): Promise<boolean> {
/** requires authentication */ /** requires authentication */
export async function setCustomization( export async function setCustomization(
server: string, server: string,
customization: any customization: any,
): Promise<boolean | Error> { ): Promise<boolean | Error> {
try { try {
const response = await fetch( const response = await fetch(
@ -320,7 +320,7 @@ export async function setCustomization(
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ customization }), body: JSON.stringify({ customization }),
} },
); );
if (response.status == 400) { if (response.status == 400) {
@ -343,7 +343,7 @@ export async function getCustomization(server: string): Promise<any> {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
if (response.status == 400) { if (response.status == 400) {
@ -367,7 +367,7 @@ export async function sortedFavorites(): Promise<
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
if (response.status == 400) { if (response.status == 400) {
@ -382,7 +382,7 @@ export async function sortedFavorites(): Promise<
export async function reportServer( export async function reportServer(
server: string, server: string,
reason: string reason: string,
): Promise<boolean> { ): Promise<boolean> {
try { try {
const response = await fetch(connector(`/report-server`, { version: 1 }), { const response = await fetch(connector(`/report-server`, { version: 1 }), {
@ -402,3 +402,29 @@ export async function reportServer(
throw Error("Error while running API"); throw Error("Error while running API");
} }
} }
export async function setAccountSL(
data: number | boolean | null,
type: "srv" | "ipr" | "pad",
): Promise<boolean> {
try {
const response = await fetch(
connector(`/account-sl/change`, { version: 0 }),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data, type }),
},
);
if (response.status === 400) {
return false;
}
return true;
} catch {
throw Error("Error while running API");
}
}

@ -0,0 +1,47 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { clerkClient, getAuth } from "@clerk/nextjs/server";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const { data } = req.body;
if (data === undefined) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const { type } = req.body;
if (type === undefined) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (data === null) {
clerkClient.users.updateUserMetadata(userId, {
publicMetadata: { [type]: null },
});
res.status(200).send({ message: "Success" });
}
if (type !== "srv" && type !== "ipr" && type !== "pad")
return res.status(400).send({ message: "Couldn't find data" });
if (type === "srv" && typeof data !== "boolean")
return res.status(400).send({ message: "Couldn't find data" });
if (type === "ipr" && typeof data !== "number")
return res.status(400).send({ message: "Couldn't find data" });
if (type === "pad" && typeof data !== "number")
return res.status(400).send({ message: "Couldn't find data" });
clerkClient.users.updateUserMetadata(userId, {
publicMetadata: { [type]: typeof data === "number" ? data.toString() : data },
});
res.status(200).send({ message: "Success" });
}

@ -18,9 +18,16 @@
} }
], ],
"paths": { "paths": {
"contentlayer/generated": ["./.contentlayer/generated"],
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

1821
yarn.lock

File diff suppressed because it is too large Load Diff