mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 09:18:01 -05:00
feat: achievements
This commit is contained in:
parent
190024aef5
commit
5699da4dc4
10
biome.json
Normal file
10
biome.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useTemplate": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,24 +122,14 @@ async function periodicCronJob() {
|
||||
"[CRON] " +
|
||||
INFO +
|
||||
" Remaining servers: " +
|
||||
(y + "/" + mh.servers.length)
|
||||
(y + "/" + mh.servers.length),
|
||||
);
|
||||
y++;
|
||||
if (y == mh.servers.length) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
"[CRON] " + SUCCESS + " Finished! Closing MongoDB connection."
|
||||
);
|
||||
|
||||
// Close connection
|
||||
await mongo
|
||||
.close()
|
||||
.catch((e) =>
|
||||
console.log(
|
||||
"[CRON] " + WARN + " Error while closing MongoDB connection:",
|
||||
e
|
||||
)
|
||||
"[CRON] " + SUCCESS + " Finished! Closing MongoDB connection.",
|
||||
);
|
||||
|
||||
return;
|
||||
@ -200,7 +190,7 @@ async function achievementTask() {
|
||||
|
||||
const achievementsTsk = await achievementEngine(
|
||||
srvExt,
|
||||
prevAchievements.achievements
|
||||
prevAchievements.achievements,
|
||||
);
|
||||
|
||||
await achievements.insertOne({
|
||||
@ -217,7 +207,7 @@ async function achievementTask() {
|
||||
|
||||
async function achievementEngine(
|
||||
server: OnlineServerExtended,
|
||||
currentAchievements: Achievement[]
|
||||
currentAchievements: Achievement[],
|
||||
): Promise<Achievement[]> {
|
||||
const achievements: Array<Achievement> = [];
|
||||
|
||||
@ -247,7 +237,7 @@ async function achievementEngine(
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true"
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true",
|
||||
)
|
||||
).json();
|
||||
|
||||
@ -264,7 +254,7 @@ async function achievementEngine(
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true"
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true",
|
||||
)
|
||||
).json();
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ title: "Troubleshooting: Making external servers on Minehut"
|
||||
|
||||
# External Servers on Minehut
|
||||
I think creating external servers on Minehut is an 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.
|
||||
might occur with server owners. This is a [extension/rephrasing of the offical wiki guide](Wiki:External). All points in **bold** are things you shouldn't miss, and are commonly misread.
|
||||
|
||||
<br/>
|
||||
<Separator/>
|
||||
@ -87,7 +87,7 @@ 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)!
|
||||
After this, there are mostly no more common issues. Continue on [the wiki](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.
|
||||
Go to the offical <Discord/> Minehut Discord server and go into the [#ask-for-help](https://discord.com/channels/239599059415859200/1014801630295760897) channel and create a thread.
|
||||
@ -28,4 +28,4 @@ No. Your Minehut account is not associated with your MHSF one, and consequently,
|
||||
|
||||
## Conclusion
|
||||
|
||||
If you'd like to use MHSF, go to the server list [here](/) to try it out! You may also give MHSF a star on GitHub if you feel like this project deserves it.
|
||||
If you'd like to use MHSF, go to the server list [here](Special:Root) to try it out! You may also give MHSF a star on GitHub if you feel like this project deserves it.
|
||||
|
||||
@ -3,10 +3,10 @@ title: "Customization"
|
||||
---
|
||||
|
||||
# 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.
|
||||
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).
|
||||
Make sure you've also [owned your server](Docs:guides/owning-a-server).
|
||||
|
||||
## Customization Types
|
||||
### Discord Server
|
||||
@ -22,4 +22,4 @@ You can pick any color in the box and choosing a color scheme to show on your se
|
||||
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)
|
||||
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!](Special:GitHub)
|
||||
@ -22,7 +22,7 @@ Login to the server `MHSFPV.minehut.gg`. (its on a free plan, you may have to st
|
||||
|
||||
You can do many things with a linked account:
|
||||
|
||||
- [Own a server](/docs/guides/owning-a-server)
|
||||
- [Customize a server](/docs/guides/customization)
|
||||
- [Own a server](Docs:guides/owning-a-server)
|
||||
- [Customize a server](Docs:guides/customization)
|
||||
|
||||
More will be coming in future updates, however the only thing you can do with a linked account is server-based functions.
|
||||
|
||||
@ -4,11 +4,11 @@ title: "Own a server"
|
||||
|
||||
# Owning a server
|
||||
|
||||
Owning a server is quite simple and allows you to [customize your server](/docs/guides/customization) your server and make it stand out from other servers. Before owning your server, make sure you agree to the [ECA](/docs/legal/external-content-agreement).
|
||||
Owning a server is quite simple and allows you to [customize your server](/docs/guides/customization) your server and make it stand out from other servers. Before owning your server, make sure you agree to the [ECA](Docs:legal/external-content-agreement).
|
||||
|
||||
## Linking
|
||||
|
||||
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.
|
||||
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!
|
||||
|
||||
## I can't link my server, because my server doesn't have a author
|
||||
|
||||
@ -4,7 +4,7 @@ title: "Reporting a server"
|
||||
|
||||
# Reporting a server
|
||||
|
||||
If you believe a server that you've seen is under breach of the [ECA Agreement](/docs/legal/external-content-agreement), you may request the server in question to be taken down.
|
||||
If you believe a server that you've seen is under breach of the [ECA Agreement](Docs:legal/external-content-agreement), you may request the server in question to be taken down.
|
||||
Make sure you are logged into a account, and go to the server page and hit the customization tab. Hit the Report button, and add a reason to your report.
|
||||
Your report will be processed and the appropriate action will be taken.
|
||||
|
||||
|
||||
@ -12,14 +12,14 @@ uploaded onto the platform are.
|
||||
|
||||
## Source Code
|
||||
|
||||
The source code for MHSF is defined by the [MIT License](https://github.com/DeveloLongScript/MHSF/blob/main/LICENSE). 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 _"forking"_) is also freely allowed.
|
||||
The source code for MHSF is defined by the [MIT License](Special:GitHub/blob/main/LICENSE). 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 _"forking"_) is also freely allowed.
|
||||
|
||||
## What your limits are
|
||||
|
||||
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 [Minehuts Terms of Service](https://minehut.wiki.gg/wiki/Rules) _as all content made is associated to Minehut (as the server is mostly on a community for Minehut)._<br/>
|
||||
For making banners & descriptions, you must follow [Minehuts Terms of Service](Wiki:Rules) _as all content made is associated to Minehut (as the server is mostly on a community for Minehut)._<br/>
|
||||
For making Discord server embeds, you must follow [Discords Terms of Service](https://discord.com/terms/) _as all content made is associated to Discord._
|
||||
|
||||
### All other content
|
||||
|
||||
30
docs/reading.mdx
Normal file
30
docs/reading.mdx
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Reading the docs"
|
||||
---
|
||||
|
||||
# Reading the docs
|
||||
The documentation in MHSF has some special symbols used in the docs that might be useful to know.
|
||||
|
||||
<Separator/>
|
||||
|
||||
## Icons
|
||||
When looking at a link, there will be symbols used that indicate where the link is going: *(these apply to the whole site)*
|
||||
- <Notebook className="inline-block mb-1" size={20}/> indicates the link will link to the [official Minehut wiki](Wiki:/)
|
||||
- <Book className="inline-block mb-1" size={20} /> indicates the link will link to another page on the docs
|
||||
- <ExternalLink className="inline-block mb-1" size={20}/> indicates the link will go to an external site
|
||||
|
||||
When contributing, these links are as follows:
|
||||
```
|
||||
Wiki:<wiki url after https://minehut.wiki.gg/wiki/>
|
||||
Docs:<docs url after /docs/>
|
||||
Special:Root (links back to /)
|
||||
https://example.com
|
||||
|
||||
**Example:**
|
||||
[Ranks](Wiki:Ranks)
|
||||
[Getting Started](Docs:Getting-started)
|
||||
https://google.com
|
||||
```
|
||||
|
||||
The icons above will be automatically added when using the syntax above. <br/>
|
||||
<small>The source code for above is stored [here](Special:GitHub/edit/main/src/components/misc/Link.tsx)</small>
|
||||
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"@clerk/nextjs": "^5.1.3",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
@ -48,8 +49,6 @@
|
||||
"react-dom": "^18",
|
||||
"react-fade-in": "^2.0.1",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-marquee-text": "^1.0.4",
|
||||
"react-smart-ticker": "^1.2.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
|
||||
17
src/app/account/settings/layout.tsx
Normal file
17
src/app/account/settings/layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
import { Sidebar } from "@/components/PreferencesSidebar";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<span className="pt-[48px]">
|
||||
<Sidebar curPage={pathname as string}>{children}</Sidebar>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -1,14 +1,7 @@
|
||||
"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";
|
||||
import { SLCustomize } from "@/components/SLCustomizePage";
|
||||
|
||||
export default function Settings() {
|
||||
const clerk = useClerk();
|
||||
@ -20,348 +13,11 @@ export default function Settings() {
|
||||
}, [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>
|
||||
<main className="p-4">
|
||||
<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>
|
||||
<SLCustomize />
|
||||
</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.",
|
||||
}),
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
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,13 +1,6 @@
|
||||
"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 toast from "react-hot-toast";
|
||||
import { unlinkMCAccount } from "@/lib/api";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -25,71 +18,7 @@ export default function Settings() {
|
||||
}, [user, isSignedIn]);
|
||||
|
||||
return (
|
||||
<main className="pt-[48px] p-4">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="min-h-[calc(100vh-70px)]"
|
||||
>
|
||||
<ResizablePanel className="max-md:hidden min-w-[285px] max-w-[285px] w-[285px]">
|
||||
<div className="w-[300px] mt-[20px] ml-[10px]">
|
||||
<Button className="mb-[2px] w-[250px]">
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</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
|
||||
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">
|
||||
<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>
|
||||
<main className="p-4">
|
||||
<strong className="text-3xl">Linking</strong>
|
||||
<br />
|
||||
<br />
|
||||
@ -99,17 +28,13 @@ export default function Settings() {
|
||||
Link a Minecraft account to customize a server you own.
|
||||
<br />{" "}
|
||||
{user?.publicMetadata.player != undefined && linked && (
|
||||
<>
|
||||
Currently linked to {user?.publicMetadata.player as string}
|
||||
</>
|
||||
<>Currently linked to {user?.publicMetadata.player as string}</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
{!linked && (
|
||||
<Button className="h-[30px] ml-2">Link Account</Button>
|
||||
)}
|
||||
{!linked && <Button className="h-[30px] ml-2">Link Account</Button>}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<CodeDialog
|
||||
@ -130,9 +55,7 @@ export default function Settings() {
|
||||
<br />
|
||||
<strong className="font-bold">Unlink Account</strong>
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Unlink your Minecraft acconut if you have already linked one.
|
||||
</p>
|
||||
<p>Unlink your Minecraft acconut if you have already linked one.</p>
|
||||
|
||||
{!linked && (
|
||||
<Button className="h-[30px] ml-2" disabled>
|
||||
@ -158,12 +81,9 @@ export default function Settings() {
|
||||
)}
|
||||
</div>
|
||||
<small className="mt-0">
|
||||
All of your customizations stay the same, and can be changed if{" "}
|
||||
another account links your Minecraft account.
|
||||
All of your customizations stay the same, and can be changed if another
|
||||
account links your Minecraft account.
|
||||
</small>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ 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 NextLink from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ALegacy } from "@/components/misc/Link";
|
||||
import { MDXElements } from "@/components/misc/MDXElements";
|
||||
|
||||
export const generateStaticParams = async () =>
|
||||
allDocs.map((post) => ({ slug: [post._raw.flattenedPath] }));
|
||||
@ -34,7 +36,13 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<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 }} />
|
||||
<MDXContent
|
||||
components={{
|
||||
Separator,
|
||||
a: (props) => <ALegacy {...props} />,
|
||||
...MDXElements,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{doc.toc && (
|
||||
@ -53,7 +61,7 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<p className="font-medium">Contribute</p>
|
||||
<ul className="m-0 list-none">
|
||||
<li className="mt-0 pt-2">
|
||||
<Link
|
||||
<NextLink
|
||||
href={
|
||||
"https://github.com/DeveloLongScript/MHSF/edit/main/docs/" +
|
||||
doc._raw.flattenedPath +
|
||||
@ -71,10 +79,10 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<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>
|
||||
</NextLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -24,10 +24,17 @@ import { BrandingGenericIcon } from "@/components/Icon";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata = {
|
||||
twitter: {
|
||||
images: [
|
||||
{
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: "/branding/meta-banner.png",
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -48,7 +48,36 @@ export async function generateMetadata(
|
||||
: "https://minehut-server-icons-live.s3.us-west-2.amazonaws.com/" +
|
||||
(json.server.icon == undefined ? "OAK_SIGN" : json.server.icon) +
|
||||
".png",
|
||||
twitter: {},
|
||||
twitter: {
|
||||
title:
|
||||
json.server == null
|
||||
? "Server doesn't exist | MHSF"
|
||||
: json.server.name +
|
||||
", " +
|
||||
(json.server.online
|
||||
? json.server.playerCount +
|
||||
(json.server.maxPlayers != 10
|
||||
? "/" + json.server.maxPlayers
|
||||
: "") +
|
||||
" online"
|
||||
: "Offline") +
|
||||
" | MHSF",
|
||||
description:
|
||||
json.server == null
|
||||
? `The server ${server} doesn't exist.`
|
||||
: `View ${server} on Minehut Server Finder!`,
|
||||
images: [
|
||||
{
|
||||
url:
|
||||
"https://minehut-server-icons-live.s3.us-west-2.amazonaws.com/" +
|
||||
json.server.icon +
|
||||
".png",
|
||||
},
|
||||
{
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
openGraph: {
|
||||
type: "profile",
|
||||
siteName: "MHSF (Minehut Server Finder)",
|
||||
@ -61,7 +90,7 @@ export async function generateMetadata(
|
||||
".png",
|
||||
},
|
||||
{
|
||||
url: "/favicon.ico",
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@ import { Copy, Info } from "lucide-react";
|
||||
import toast, { CheckmarkIcon } from "react-hot-toast";
|
||||
import { MHSF } from "@/lib/mhsf";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
import AchievementList from "./feat/AchievementList";
|
||||
|
||||
export default function AfterServerView({ server }: { server: string }) {
|
||||
const [description, setDescription] = useState("");
|
||||
@ -24,7 +25,9 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
const [mhsf, setMHSF] = useState(new MHSF());
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState("desc");
|
||||
const [view, setView] = useState(
|
||||
description !== "" || discord !== "" ? "desc" : "extra"
|
||||
);
|
||||
const [serverObject, setServerObject] = useState<ServerResponse | undefined>(
|
||||
undefined
|
||||
);
|
||||
@ -51,20 +54,16 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
return (
|
||||
<>
|
||||
<FadeIn>
|
||||
<div
|
||||
className={
|
||||
description != "" || discord != ""
|
||||
? "grid sm:grid-cols-6 h-full pl-4 pr-4"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div className="grid sm:grid-cols-6 h-full pl-4 pr-4">
|
||||
<div className="ml-5 mb-2 flex items-center sm:hidden">
|
||||
{(description != "" || discord != "") && (
|
||||
<Button
|
||||
variant={view == "desc" ? undefined : "ghost"}
|
||||
onClick={() => setView("desc")}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={view == "extra" ? undefined : "ghost"}
|
||||
onClick={() => setView("extra")}
|
||||
@ -72,25 +71,38 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
>
|
||||
Server Information
|
||||
</Button>
|
||||
<Button
|
||||
variant={view == "achievements" ? undefined : "ghost"}
|
||||
onClick={() => setView("achievements")}
|
||||
className="ml-2"
|
||||
>
|
||||
Achievements
|
||||
</Button>
|
||||
</div>
|
||||
{(description != "" || discord != "") && (
|
||||
<div className="max-sm:hidden">
|
||||
<div className="grid">
|
||||
{(description != "" || discord != "") && (
|
||||
<Button
|
||||
variant={view == "desc" ? undefined : "ghost"}
|
||||
onClick={() => setView("desc")}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={view == "extra" ? undefined : "ghost"}
|
||||
onClick={() => setView("extra")}
|
||||
>
|
||||
Server Information
|
||||
</Button>
|
||||
<Button
|
||||
variant={view == "achievements" ? undefined : "ghost"}
|
||||
onClick={() => setView("achievements")}
|
||||
>
|
||||
Achievements
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid lg:grid-cols-4 pl-4 pr-4 gap-3.5 col-span-5">
|
||||
{description != "" && view == "desc" && (
|
||||
@ -118,7 +130,12 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}{" "}
|
||||
{((description == "" && discord == "") || view == "extra") && (
|
||||
{view == "achievements" && (
|
||||
<div className="col-span-4">
|
||||
<AchievementList server={server} />
|
||||
</div>
|
||||
)}
|
||||
{view == "extra" && (
|
||||
<div className="sm:grid sm:grid-cols-3 col-span-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
105
src/components/PreferencesSidebar.tsx
Normal file
105
src/components/PreferencesSidebar.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { Cog, ExternalLink, KeyRound, Link, UserPen } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "./ui/resizable";
|
||||
import NextLink from "next/link";
|
||||
import { useClerk } from "@clerk/nextjs";
|
||||
|
||||
export function Sidebar({
|
||||
children,
|
||||
curPage,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
curPage: string;
|
||||
}) {
|
||||
const clerk = useClerk();
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="min-h-[calc(100vh-70px)] pt-[70px]"
|
||||
>
|
||||
<ResizablePanel className="max-md:hidden min-w-[285px] max-w-[285px] w-[285px]">
|
||||
<div className="w-[300px] ml-[10px]">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant={curPage !== "/account/settings" ? "ghost" : "default"}
|
||||
>
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
<NextLink href="/account/settings/options" className="text-inherit">
|
||||
<Button
|
||||
className="mb-[2px] w-[250px] "
|
||||
variant={
|
||||
curPage !== "/account/settings/options" ? "ghost" : "default"
|
||||
}
|
||||
>
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
</NextLink>
|
||||
<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="md:hidden ml-2">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant={curPage !== "/account/settings" ? "ghost" : "default"}
|
||||
>
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
<NextLink href="/account/settings/options" className="text-inherit">
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant={
|
||||
curPage !== "/account/settings/options" ? "ghost" : "default"
|
||||
}
|
||||
>
|
||||
<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>
|
||||
{children}{" "}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
189
src/components/SLCustomizePage.tsx
Normal file
189
src/components/SLCustomizePage.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { setAccountSL } from "@/lib/api";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
export function SLCustomize() {
|
||||
const [padding, setPadding] = useState("0");
|
||||
const [itemsPerRow, setItemsPerRow] = useState("4");
|
||||
const [usePaddingOnSides, setUsePaddingOnSides] = useState(false);
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const { user } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
setItemsPerRow((user?.publicMetadata.ipr as string | undefined) || "4");
|
||||
setPadding((user?.publicMetadata.pad as string | undefined) || "0");
|
||||
setUsePaddingOnSides(
|
||||
(user?.publicMetadata.srv as boolean | undefined) || false
|
||||
);
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (padding) setAccountSL(Number(padding), "pad");
|
||||
if (usePaddingOnSides) setAccountSL(usePaddingOnSides, "srv");
|
||||
if (itemsPerRow) setAccountSL(Number(itemsPerRow), "ipr");
|
||||
toast.success("Set account preferences");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>Server Display Settings</CardTitle>
|
||||
<CardDescription className="pt-2">
|
||||
Customize how servers are displayed in the list
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="advanced-mode"
|
||||
checked={advanced}
|
||||
onCheckedChange={setAdvanced}
|
||||
/>
|
||||
<Label htmlFor="advanced-mode">Advanced Mode</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Padding of servers</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adjust the spacing between server items in the list.
|
||||
</p>
|
||||
<RadioGroup value={padding} onValueChange={setPadding}>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="0" id="padding-normal" />
|
||||
<Label htmlFor="padding-normal">
|
||||
Normal{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(0px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Standard spacing, balanced appearance.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="15" id="padding-relaxed" />
|
||||
<Label htmlFor="padding-normal">
|
||||
Relaxed{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(15px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
More spacious, easier to distinguish between servers.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="40" id="padding-comfortable" />
|
||||
<Label htmlFor="padding-comfortable">
|
||||
Comfortable{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(40px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Better for larger screens.
|
||||
</p>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Items per row</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Set how many server items appear in each row of the list.
|
||||
</p>
|
||||
<RadioGroup value={itemsPerRow} onValueChange={setItemsPerRow}>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="4" id="items-4" />
|
||||
<Label htmlFor="items-4">
|
||||
4 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Balanced size and quantity, suitable for most screens.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="5" id="items-5" />
|
||||
<Label htmlFor="items-5">
|
||||
5 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
More compact view, fit more servers on screen.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="6" id="items-6" />
|
||||
<Label htmlFor="items-6">
|
||||
6 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Great for monitors/screens with more space.
|
||||
</p>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="use-padding-sides"
|
||||
checked={usePaddingOnSides}
|
||||
onCheckedChange={(checked) =>
|
||||
setUsePaddingOnSides(checked as boolean)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="use-padding-sides"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Use padding on the sides of only the servers, not the whole
|
||||
server list
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
When enabled, applies padding to individual server items instead
|
||||
of the entire list container. This can create a more distinct
|
||||
separation between servers.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit">Save Settings</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -70,6 +70,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { SignInPopover } from "./clerk/SignInPopoverButton";
|
||||
import { BentoCard, BentoGrid } from "./effects/bento-grid";
|
||||
import { pageFind } from "./misc/Link";
|
||||
|
||||
const features = [
|
||||
{
|
||||
@ -227,7 +228,7 @@ export default function ServerList() {
|
||||
<div className="p-0 branding-hero">
|
||||
<>
|
||||
{(!isSignedIn || hero) && (
|
||||
<div className=" py-[300px] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 ">
|
||||
<div className=" pb-[300px] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 ">
|
||||
<Particles
|
||||
className="absolute inset-0 -z-10 block"
|
||||
quantity={100}
|
||||
@ -320,7 +321,9 @@ export default function ServerList() {
|
||||
"border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05] " +
|
||||
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
|
||||
)}
|
||||
onClick={() => router.push(`/server/${server.name}`)}
|
||||
onClick={() =>
|
||||
router.push(pageFind(`Server:${server.name}`))
|
||||
}
|
||||
>
|
||||
<div className="items-center gap-2 p-4">
|
||||
<div>
|
||||
|
||||
131
src/components/feat/AchievementList.tsx
Normal file
131
src/components/feat/AchievementList.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { getAchievements } from "@/lib/api";
|
||||
import type { Achievement } from "@/lib/types/achievement";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { Medal, Sparkle, Sparkles, Users } from "lucide-react";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import A from "../misc/Link";
|
||||
|
||||
export default function AchievementList({ server }: { server: string }) {
|
||||
const [achievements, setAchievements] = useState<
|
||||
Array<WithInterval<Achievement>>
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffectOnce(() => {
|
||||
setAchievements(() => []);
|
||||
getAchievements(server).then((v) => {
|
||||
for (const a of v)
|
||||
a.achievements.forEach((item, interval) =>
|
||||
setAchievements((prev) => [...prev, { interval, ...item }])
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div>
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
Achievements are earned automatically when the server is online. See{" "}
|
||||
<A alt="Achievement collection">Special:Root</A>
|
||||
</span>
|
||||
{achievements
|
||||
.filter(
|
||||
(value, index) => listify(achievements).indexOf(value.type) === index
|
||||
)
|
||||
.map((a) => {
|
||||
const Icon = formalNames[a.type].icon;
|
||||
return (
|
||||
<div key={`${a.date}--${a.interval}`}>
|
||||
<Card className="my-4">
|
||||
<CardContent className="pt-4">
|
||||
<span
|
||||
className="flex items-center"
|
||||
style={{ color: formalNames[a.type].color }}
|
||||
>
|
||||
<Icon size={16} className="mr-2" />
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formalNames[a.type].title,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<p>{formalNames[a.type].description}</p>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Achieved on {new Date(a.date).getMonth()}/
|
||||
{new Date(a.date).getDate()}/
|
||||
{new Date(a.date).getFullYear()}{" "}
|
||||
<span className="text-muted-foreground/70">
|
||||
{new Date(a.date).toLocaleTimeString()}
|
||||
</span>
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formalNames = {
|
||||
mostJoined: {
|
||||
title:
|
||||
"At one time, <b>this server had the most players on the platform!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had the number 1 permission at the time of the achievements getting resolved.",
|
||||
color: "#9aedff",
|
||||
icon: Medal,
|
||||
},
|
||||
has1kFavorites: {
|
||||
title: "This server has more than <b>1,000 favorites on MHSF!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 1,000 favorites at the time of the achievements getting resolved.",
|
||||
color: "#d064ff",
|
||||
icon: Sparkle,
|
||||
},
|
||||
has1kTotalJoins: {
|
||||
title: "This server has more than <b>1,000 total joins on Minehut!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 1,000 total joins at the time of the achievements getting resolved.",
|
||||
color: "#aefa1f",
|
||||
icon: Users,
|
||||
},
|
||||
has100kFavorites: {
|
||||
title: "This server has more than <b>100,000 favorites on MHSF!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 100,000 favorites at the time of the achievements getting resolved.",
|
||||
color: "#fa5b07",
|
||||
icon: Sparkles,
|
||||
},
|
||||
has100kTotalJoins: {
|
||||
title: "This server has more than <b>100,000 total joins on Minehut!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 100,000 total joins at the time of the achievements getting resolved.",
|
||||
color: "#bdcffa",
|
||||
icon: Users,
|
||||
},
|
||||
};
|
||||
|
||||
type WithInterval<K> = K & {
|
||||
interval: number;
|
||||
};
|
||||
|
||||
const listify = (list: WithInterval<Achievement>[]) => {
|
||||
const newL: Array<string> = [];
|
||||
|
||||
list.forEach((c) => newL.push(c.type));
|
||||
|
||||
return newL;
|
||||
};
|
||||
@ -43,8 +43,8 @@ export function DiscordPopover({ server, get }: { server: string; get: any }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(get.discord == null ? "" : get.discord);
|
||||
form.reset({ id: get.discord == null ? "" : get.discord });
|
||||
setValue(get.discord === undefined ? "" : get.discord);
|
||||
form.reset({ id: get.discord === undefined ? "" : get.discord });
|
||||
}, [get]);
|
||||
|
||||
return (
|
||||
|
||||
65
src/components/misc/Link.tsx
Normal file
65
src/components/misc/Link.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { default as NextLink } from "next/link";
|
||||
import { Book, ExternalLink, NotebookText } from "lucide-react";
|
||||
|
||||
export default function A({
|
||||
children,
|
||||
alt,
|
||||
}: {
|
||||
children: string;
|
||||
alt: string | ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<NextLink href={pageFind(children)}>
|
||||
{children.startsWith("Docs:") && <Book />}
|
||||
{alt}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function ALegacy({
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
children?: string | ReactNode;
|
||||
href?: string;
|
||||
}) {
|
||||
return (
|
||||
<NextLink
|
||||
href={pageFind(href || "")}
|
||||
className="no-underline transition duration-300 hover:underline "
|
||||
>
|
||||
{(href || "").startsWith("Docs:") && (
|
||||
<Book size={16} className="mr-[2px] inline-flex" />
|
||||
)}
|
||||
{(href || "").startsWith("Wiki:") && (
|
||||
<NotebookText size={14} className="mr-[2px] mb-[3px] inline-flex" />
|
||||
)}
|
||||
{children}
|
||||
{(href || "").startsWith("https") && (
|
||||
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
|
||||
)}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export const pageFind = (text: string) => {
|
||||
if (text.startsWith("Docs:")) {
|
||||
return "/docs/" + text.substring(5);
|
||||
}
|
||||
if (text === "Special:Root") return "/";
|
||||
if (text === "Special:Preferences") return "/account/settings";
|
||||
if (text === "Special:AccountOptions") return "/account/settings/options";
|
||||
if (text.startsWith("Server:") && text.endsWith("/Customization"))
|
||||
return "/server/" + text.substring(7, text.length - 14) + "/customization";
|
||||
if (text.startsWith("Server:")) return "/server/" + text.substring(7);
|
||||
if (text.startsWith("Wiki:"))
|
||||
return "https://minehut.wiki.gg/wiki/" + text.substring(5);
|
||||
if (text.startsWith("GitHub:"))
|
||||
return "https://github.com/" + text.substring(7);
|
||||
if (text === "Special:GitHub")
|
||||
return "https://github.com/DeveloLongScript/MHSF";
|
||||
if (text.startsWith("Special:GitHub/"))
|
||||
return "https://github.com/DeveloLongScript/MHSF/" + text.substring(15);
|
||||
return text;
|
||||
};
|
||||
30
src/components/misc/MDXElements.tsx
Normal file
30
src/components/misc/MDXElements.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Book, ExternalLink, NotebookText } from "lucide-react";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
type MDXElementType = {
|
||||
[key: string]: (props: any) => JSX.Element;
|
||||
};
|
||||
|
||||
const Discord = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 256 199"
|
||||
width="1em"
|
||||
height="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className="inline-block mb-1"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MDXElements: MDXElementType = {
|
||||
Discord,
|
||||
Book: (props) => <Book {...props} />,
|
||||
Notebook: (props) => <NotebookText {...props} />,
|
||||
ExternalLink: (props) => <ExternalLink {...props} />,
|
||||
};
|
||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@ -3,6 +3,10 @@ export const allFolders: (DocsFolder | Docs)[] = [
|
||||
title: "Getting Started",
|
||||
url: "/docs/getting-started",
|
||||
},
|
||||
{
|
||||
title: "Reading",
|
||||
url: "/docs/reading",
|
||||
},
|
||||
{
|
||||
name: "Guides",
|
||||
docs: [
|
||||
@ -26,7 +30,7 @@ export const allFolders: (DocsFolder | Docs)[] = [
|
||||
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"}
|
||||
{ title: "Tips with external servers", url: "/docs/advanced/external" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ const serverCache: any = {};
|
||||
// __filter: if your name isn't static, set this to true
|
||||
//
|
||||
// You may also use `requestServer()` to grab the offline version of the server from the API, which may get you more information about the server (ServerResponse)
|
||||
export var allTags: Array<{
|
||||
export const allTags: Array<{
|
||||
name: (server: OnlineServer) => Promise<string>;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
listCondition?: (server: ServerResponse) => Promise<boolean>;
|
||||
@ -86,7 +86,7 @@ export var allTags: Array<{
|
||||
}, */
|
||||
];
|
||||
|
||||
export var allCategories: Array<{
|
||||
export const allCategories: Array<{
|
||||
name: string;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
primary: boolean;
|
||||
@ -250,7 +250,7 @@ export var allCategories: Array<{
|
||||
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
||||
if (serverCache[s.name] == undefined) {
|
||||
const re = await fetch(
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true"
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true",
|
||||
);
|
||||
const json = await re.json();
|
||||
serverCache[s.name] = json.server;
|
||||
|
||||
@ -5,12 +5,30 @@
|
||||
*/
|
||||
//
|
||||
|
||||
import { Achievement } from "./types/achievement";
|
||||
|
||||
const connector = (
|
||||
endpoint: string,
|
||||
options: { version: number; starting?: string },
|
||||
) =>
|
||||
`${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`;
|
||||
|
||||
async function apiConstructor<K>(
|
||||
connector: string,
|
||||
requestInit: RequestInit,
|
||||
modifier: (data: any) => K,
|
||||
): Promise<K> {
|
||||
try {
|
||||
const response = await fetch(connector, requestInit);
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
return modifier(await response.json());
|
||||
} catch {
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
}
|
||||
export async function getMOTDFromServer(
|
||||
list: Array<{ server: string; motd: string }>,
|
||||
): Promise<Array<{ server: string; motd: string }>> {
|
||||
@ -428,3 +446,10 @@ export async function setAccountSL(
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
}
|
||||
|
||||
export const getAchievements = async (server: string) =>
|
||||
apiConstructor<{ _id: string; name: string; achievements: Achievement[] }[]>(
|
||||
connector(`/achievements/${server}`, { version: 0 }),
|
||||
{},
|
||||
(data) => data.result,
|
||||
);
|
||||
|
||||
18
src/lib/types/achievement.ts
Normal file
18
src/lib/types/achievement.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type Achievement = {
|
||||
type:
|
||||
| "mostJoined"
|
||||
| "has1kFavorites"
|
||||
| "has1kTotalJoins"
|
||||
| "has100kFavorites"
|
||||
| "has100kTotalJoins";
|
||||
/** The ISO time of the gaining of the achievement. */
|
||||
date: string;
|
||||
};
|
||||
|
||||
export const orderOfAchievements = [
|
||||
"mostJoined",
|
||||
"has100kFavorites",
|
||||
"has100kTotalJoins",
|
||||
"has1kFavorites",
|
||||
"has1kTotalJoins",
|
||||
];
|
||||
18
src/pages/api/v0/achievements/[server].ts
Normal file
18
src/pages/api/v0/achievements/[server].ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { server } = req.query;
|
||||
if (!server) return res.status(400).send({ error: "No server was provided" });
|
||||
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db("mhsf");
|
||||
const collection = db.collection("achievements");
|
||||
|
||||
res.send({ result: await collection.find({ name: server }).toArray() });
|
||||
}
|
||||
1
src/pages/api/v0/achievements/multiple.ts
Normal file
1
src/pages/api/v0/achievements/multiple.ts
Normal file
@ -0,0 +1 @@
|
||||
// TODO: make multiple endpoint to allow achievements to be shown on the server-list
|
||||
151
yarn.lock
151
yarn.lock
@ -150,6 +150,60 @@
|
||||
"@babel/helper-validator-identifier" "^7.24.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@biomejs/biome@^1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.3.tgz#3b5eecea90d973f71618aae3e6e8be4d2ca23e42"
|
||||
integrity sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==
|
||||
optionalDependencies:
|
||||
"@biomejs/cli-darwin-arm64" "1.8.3"
|
||||
"@biomejs/cli-darwin-x64" "1.8.3"
|
||||
"@biomejs/cli-linux-arm64" "1.8.3"
|
||||
"@biomejs/cli-linux-arm64-musl" "1.8.3"
|
||||
"@biomejs/cli-linux-x64" "1.8.3"
|
||||
"@biomejs/cli-linux-x64-musl" "1.8.3"
|
||||
"@biomejs/cli-win32-arm64" "1.8.3"
|
||||
"@biomejs/cli-win32-x64" "1.8.3"
|
||||
|
||||
"@biomejs/cli-darwin-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz#be2bfdd445cd2d3cb0ff41a96a72ec761753997c"
|
||||
integrity sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==
|
||||
|
||||
"@biomejs/cli-darwin-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz#47d408edd9f5c04069fbcf8610bacf1db8c6c0d9"
|
||||
integrity sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz#44df284383d57cf4f28daeedd080dad7be05df78"
|
||||
integrity sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==
|
||||
|
||||
"@biomejs/cli-linux-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz#6a6b1da1dfce0294a028cbb5d6c40d73691dd713"
|
||||
integrity sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==
|
||||
|
||||
"@biomejs/cli-linux-x64-musl@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz#ceef30a8ee1a00d4ad31e32dd31ba2a661f2719d"
|
||||
integrity sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==
|
||||
|
||||
"@biomejs/cli-linux-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz#665df74d19fb8f83001a9d80824d3a1723e2123f"
|
||||
integrity sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==
|
||||
|
||||
"@biomejs/cli-win32-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz#0fb6f58990f4de0331a6ed22c47c66f5a89133cc"
|
||||
integrity sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==
|
||||
|
||||
"@biomejs/cli-win32-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz#6a9dc5a4e13357277da43c015cd5cdc374035448"
|
||||
integrity sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==
|
||||
|
||||
"@clerk/backend@1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/@clerk/backend/-/backend-1.2.2.tgz"
|
||||
@ -1089,20 +1143,19 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz"
|
||||
integrity sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==
|
||||
"@radix-ui/react-checkbox@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz#a559c4303957d797acee99914480b755aa1f27d6"
|
||||
integrity sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.0.3":
|
||||
version "1.0.3"
|
||||
@ -1506,29 +1559,28 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-primitive@2.0.0":
|
||||
"@radix-ui/react-primitive@2.0.0", "@radix-ui/react-primitive@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
|
||||
integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.1.0"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz"
|
||||
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
|
||||
"@radix-ui/react-radio-group@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz#f937dd6b9436ded80c4bebdf3901c20cb8bcbb5a"
|
||||
integrity sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-direction" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-roving-focus" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.4":
|
||||
version "1.0.4"
|
||||
@ -1599,6 +1651,19 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
|
||||
"@radix-ui/react-switch@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3"
|
||||
integrity sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-tabs@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz#0a6db1caed56776a1176aae68532060e301cc1c0"
|
||||
@ -1693,6 +1758,11 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
|
||||
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz"
|
||||
@ -1930,6 +2000,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/luxon@~3.4.0":
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7"
|
||||
integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
|
||||
@ -2846,6 +2921,14 @@ core-util-is@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cron@^3.1.7:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.7.tgz#3423d618ba625e78458fff8cb67001672d49ba0d"
|
||||
integrity sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==
|
||||
dependencies:
|
||||
"@types/luxon" "~3.4.0"
|
||||
luxon "~3.4.0"
|
||||
|
||||
cross-fetch@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
|
||||
@ -4905,6 +4988,11 @@ lucide-react@^0.416.0:
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.416.0.tgz#657da248f9b862703d7d80aafb912e79ad886313"
|
||||
integrity sha512-wPWxTzdss1CTz2aqcNWNlbh4YSnH9neJWP3RaeXepxpLCTW+pmu7WcT/wxJe+Q7Y7DqGOxAqakJv0pIK3431Ag==
|
||||
|
||||
luxon@~3.4.0:
|
||||
version "3.4.4"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af"
|
||||
integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==
|
||||
|
||||
magic-bytes.js@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92"
|
||||
@ -6552,6 +6640,11 @@ react-fade-in@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fade-in/-/react-fade-in-2.0.1.tgz#b4bcd7dac63d6857ebcd68facbff2f5f9616278f"
|
||||
integrity sha512-oqS/WT4znaXEHmL+yo0IDUDY7uC9K4RP35j1SdRUEBspR09B2iIC0i8oJ28tPOr6Ez/L2aktF9p89j+DbsTVNw==
|
||||
|
||||
react-fast-marquee@^1.6.5:
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz#98929ae93eef087a607a71e9d45ab76bba97dc16"
|
||||
integrity sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==
|
||||
|
||||
react-hook-form@^7.52.2:
|
||||
version "7.52.2"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.2.tgz#ff40f4776250b86ddfcde6be68d34aa82b1c60fe"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user