feat: new auto-link redirect (release)

This commit is contained in:
dvelo 2025-01-19 17:46:39 -06:00 committed by GitHub
commit c19f56b685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2081 additions and 1574 deletions

@ -40,7 +40,7 @@ export default function RootLayout({
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<span className="pt-[48px]"> <span className="pt-[48px] ">
<Sidebar curPage={pathname as string}>{children}</Sidebar> <Sidebar curPage={pathname as string}>{children}</Sidebar>
</span> </span>
); );

@ -85,7 +85,7 @@ export default function Settings() {
<br /> <br />
<strong className="font-bold">Unlink Account</strong> <strong className="font-bold">Unlink Account</strong>
<div className="flex items-center"> <div className="flex items-center">
<p>Unlink your Minecraft acconut if you have already linked one.</p> <p>Unlink your Minecraft account if you have already linked one.</p>
{!linked && ( {!linked && (
<Button className="h-[30px] ml-2" disabled> <Button className="h-[30px] ml-2" disabled>

@ -43,6 +43,7 @@ import type { Metadata, Viewport } from "next";
import { Inter as interFont } from "next/font/google"; import { Inter as interFont } from "next/font/google";
import LayoutPart from "@/components/feat/LayoutPart"; import LayoutPart from "@/components/feat/LayoutPart";
import AllBanners from "@/components/feat/AllBanners"; import AllBanners from "@/components/feat/AllBanners";
import Footer from "@/components/misc/Footer";
export const extraMetadata = { export const extraMetadata = {
twitter: { twitter: {
@ -84,6 +85,7 @@ export default async function RootLayout({
<Analytics /> <Analytics />
<NewDomainDialog /> <NewDomainDialog />
<UnofficalDialog /> <UnofficalDialog />
<Footer />
</TooltipProvider> </TooltipProvider>
</ThemeProvider> </ThemeProvider>
</ClerkThemeProvider> </ClerkThemeProvider>

@ -132,15 +132,17 @@ export default function ServerPage({ params }: { params: { server: string } }) {
<ColorProvider server={params.server}> <ColorProvider server={params.server}>
<div className={"pt-[300px] xl:px-[100px]"}> <div className={"pt-[300px] xl:px-[100px]"}>
<Banner server={params.server} /> <Banner server={params.server} />
<div className="pt-8 z-10 relative"> <div className="pt-8 z-8 relative">
<ServerView server={params.server} /> <ServerView server={params.server} />
</div> </div>
<StickyTopbar scrollElevation={100} className="pt-4"> <StickyTopbar scrollElevation={100} className="pt-4 z-10">
<TabServer server={params.server} tabDef="general" /> <TabServer server={params.server} tabDef="general" />
</StickyTopbar> </StickyTopbar>
<br /> <br />
<AfterServerView server={params.server} /> <div className="z-8 relative">
<AfterServerView server={params.server} />
</div>
</div> </div>
</ColorProvider> </ColorProvider>
</main> </main>

@ -118,7 +118,7 @@ export default function AfterServerView({ server }: { server: string }) {
<QRCodeGenerator server={server} /> <QRCodeGenerator server={server} />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
<FadeIn className="relative z-10"> <FadeIn className="relative z-8">
<div className="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 overflow-auto w-[calc(100vw-5rem)]"> <div className="ml-5 mb-2 flex items-center sm:hidden overflow-auto w-[calc(100vw-5rem)]">
{(description != "" || discord != "") && ( {(description != "" || discord != "") && (

@ -29,28 +29,48 @@
*/ */
"use client"; "use client";
import { getCustomization } from "@/lib/api"; import { getCustomization } from "@/lib/api";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import "@/themes.css"; import "@/themes.css";
import { toast } from "sonner";
import { useRouter } from "@/lib/useRouter";
import { useEffectOnce } from "@/lib/useEffectOnce";
export default function ColorProvider({ export default function ColorProvider({
server, server,
children, children,
fetch, fetchV,
}: { }: {
server: string; server: string;
children: any; children: any;
fetch?: any; fetchV?: any;
}) { }) {
const [color, setColor] = useState("zinc"); const [color, setColor] = useState("zinc");
const nav = useRouter();
useEffectOnce(() => {
fetch("https://api.minehut.com/server/" + server + "?byName=true")
.then((c) => c.json())
.then((c: any) => {
console.log(c.server.name, server);
if (c.server.name !== server) {
toast.warning(
"The capitalization of this server was incorrect. If your using a permanent link resource, please change it to account for a new name. (" +
c.server.name +
") Redirecting now.",
{ duration: 15000 }
);
nav.replace("/server/" + c.server.name);
}
});
});
useEffect(() => { useEffect(() => {
if (!fetch) if (!fetchV)
getCustomization(server).then((v) => getCustomization(server).then((v) =>
setColor(v != null ? v.colorScheme : "zinc") setColor(v != null ? v.colorScheme : "zinc")
); );
else setColor(fetch.colorScheme); else setColor(fetchV.colorScheme);
}, []); }, []);
return <div className={`theme-${color}`}>{children}</div>; return <div className={`theme-${color}`}>{children}</div>;

@ -33,103 +33,103 @@ import { Cog, ExternalLink, KeyRound, Link, UserPen } from "lucide-react";
import NextLink from "next/link"; import NextLink from "next/link";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "./ui/resizable"; } from "./ui/resizable";
export function Sidebar({ export function Sidebar({
children, children,
curPage, curPage,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
curPage: string; curPage: string;
}) { }) {
const clerk = useClerk(); const clerk = useClerk();
return ( return (
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
className="min-h-[calc(100vh-70px)] pt-[70px]" className="min-h-[calc(100vh-70px)] pt-[70px] xl:px-[100px] "
> >
<ResizablePanel className="max-md:hidden 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] ml-[10px]"> <div className="w-[300px] ml-[10px]">
<NextLink href="/account/settings" className="text-inherit"> <NextLink href="/account/settings" className="text-inherit">
<Button <Button
className="mb-[2px] w-[250px]" className="mb-[2px] w-[250px]"
variant={curPage !== "/account/settings" ? "ghost" : "default"} variant={curPage !== "/account/settings" ? "ghost" : "default"}
> >
<Link size={16} className="mr-2" /> Linking <Link size={16} className="mr-2" /> Linking
</Button> </Button>
</NextLink> </NextLink>
<NextLink href="/account/settings/options" className="text-inherit"> <NextLink href="/account/settings/options" className="text-inherit">
<Button <Button
className="mb-[2px] w-[250px] " className="mb-[2px] w-[250px] "
variant={ variant={
curPage !== "/account/settings/options" ? "ghost" : "default" curPage !== "/account/settings/options" ? "ghost" : "default"
} }
> >
<Cog size={16} className="mr-2" /> Options <Cog size={16} className="mr-2" /> Options
</Button> </Button>
</NextLink> </NextLink>
<Button <Button
className="mb-[2px] w-[250px]" className="mb-[2px] w-[250px]"
variant="ghost" variant="ghost"
onClick={() => clerk.openUserProfile({})} onClick={() => clerk.openUserProfile({})}
> >
<UserPen size={16} className="mr-2" /> Profile{" "} <UserPen size={16} className="mr-2" /> Profile{" "}
<ExternalLink size={16} className="ml-2" /> <ExternalLink size={16} className="ml-2" />
</Button> </Button>
<Button <Button
className="mb-[2px] w-[250px]" className="mb-[2px] w-[250px]"
variant="ghost" variant="ghost"
onClick={() => clerk.openUserProfile({})} onClick={() => clerk.openUserProfile({})}
> >
<KeyRound size={16} className="mr-2" /> Security{" "} <KeyRound size={16} className="mr-2" /> Security{" "}
<ExternalLink size={16} className="ml-2" /> <ExternalLink size={16} className="ml-2" />
</Button> </Button>
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle className="max-md:hidden" /> <ResizableHandle className="max-md:hidden" />
<ResizablePanel> <ResizablePanel>
<div className="md:hidden ml-2"> <div className="md:hidden ml-2">
<NextLink href="/account/settings" className="text-inherit"> <NextLink href="/account/settings" className="text-inherit">
<Button <Button
className="mr-[2px]" className="mr-[2px]"
variant={curPage !== "/account/settings" ? "ghost" : "default"} variant={curPage !== "/account/settings" ? "ghost" : "default"}
> >
<Link size={16} className="mr-2" /> Linking <Link size={16} className="mr-2" /> Linking
</Button> </Button>
</NextLink> </NextLink>
<NextLink href="/account/settings/options" className="text-inherit"> <NextLink href="/account/settings/options" className="text-inherit">
<Button <Button
className="mr-[2px]" className="mr-[2px]"
variant={ variant={
curPage !== "/account/settings/options" ? "ghost" : "default" curPage !== "/account/settings/options" ? "ghost" : "default"
} }
> >
<Cog size={16} className="mr-2" /> Options <Cog size={16} className="mr-2" /> Options
</Button> </Button>
</NextLink> </NextLink>
<Button <Button
className="mr-[2px]" className="mr-[2px]"
variant="ghost" variant="ghost"
onClick={() => clerk.openUserProfile({})} onClick={() => clerk.openUserProfile({})}
> >
<UserPen size={16} className="mr-2" /> Profile{" "} <UserPen size={16} className="mr-2" /> Profile{" "}
<ExternalLink size={16} className="ml-2" /> <ExternalLink size={16} className="ml-2" />
</Button> </Button>
<Button <Button
className="mr-[2px] mb-[30px]" className="mr-[2px] mb-[30px]"
variant="ghost" variant="ghost"
onClick={() => clerk.openUserProfile({})} onClick={() => clerk.openUserProfile({})}
> >
<KeyRound size={16} className="mr-2" /> Security{" "} <KeyRound size={16} className="mr-2" /> Security{" "}
<ExternalLink size={16} className="ml-2" /> <ExternalLink size={16} className="ml-2" />
</Button> </Button>
</div> </div>
{children}{" "} {children}{" "}
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
); );
} }

@ -49,7 +49,13 @@ import {
} from "./ui/card"; } from "./ui/card";
import { TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { TooltipContent, TooltipTrigger } from "./ui/tooltip";
export default function ServerCard({ b, motd, mini, favs }: any) { export default function ServerCard({
b,
motd,
mini,
favs,
selectedProperties,
}: any) {
const router = useRouter(); const router = useRouter();
const clipboard = useClipboard(); const clipboard = useClipboard();
const [favoriteStar, setFavoriteStar] = useState(false); const [favoriteStar, setFavoriteStar] = useState(false);
@ -165,100 +171,110 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
{b.author != undefined ? (
{selectedProperties.includes("Author") &&
b.author != undefined ? (
<div className="text-sm text-muted-foreground font-normal tracking-normal"> <div className="text-sm text-muted-foreground font-normal tracking-normal">
by {b.author} by {b.author}
</div> </div>
) : ( ) : (
<br /> <br />
)} )}
<TagShower server={b} /> {selectedProperties.includes("Tags") && <TagShower server={b} />}
</CardTitle> </CardTitle>
<CardDescription className="float-left inline "> <CardDescription className="float-left inline ">
<span className="flex items-center"> <span className="flex items-center">
{b.playerData.playerCount == 0 ? ( {selectedProperties.includes("Players Online") && (
<div <>
className="items-center border" {b.playerData.playerCount == 0 ? (
style={{ <div
width: ".5rem", className="items-center border"
height: ".5rem", style={{
borderRadius: "9999px", width: ".5rem",
}} height: ".5rem",
/> borderRadius: "9999px",
) : ( }}
<div />
className="items-center" ) : (
style={{ <div
backgroundColor: "#0cce6b", className="items-center"
width: ".5rem", style={{
height: ".5rem", backgroundColor: "#0cce6b",
borderRadius: "9999px", width: ".5rem",
}} height: ".5rem",
/> borderRadius: "9999px",
}}
/>
)}
</>
)} )}
<span className="pl-1"> {selectedProperties.includes("Players Online") && (
{b.playerData.playerCount}{" "} <span className="pl-1">
{b.playerData.playerCount == 1 ? "player" : "players"}{" "} {b.playerData.playerCount}{" "}
currently online {favs && <> {favs} favorited</>} {b.playerData.playerCount == 1 ? "player" : "players"}{" "}
</span> currently online {favs && <> {favs} favorited</>}
</span>
)}
</span> </span>
<ContextMenu> {selectedProperties.includes("Actions") && (
<ContextMenuTrigger> <ContextMenu>
<> <ContextMenuTrigger>
<Button <>
size="icon" <Button
variant="secondary" size="icon"
className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden" variant="secondary"
className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => {
clipboard.writeText(b.name + ".mshf.minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
<Copy size={18} />
<code className="ml-2">{b.name}</code>
</Button>
<Tooltip>
<TooltipTrigger>
<Link href={"/server/" + b.name}>
<Button
size="icon"
variant="secondary"
className="w-[32px] h-[32px] mt-2 ml-2 max-md:hidden"
>
<Layers size={18} />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
Open up the server page to see more information about
the server
</TooltipContent>
</Tooltip>
</>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => { onClick={() => {
clipboard.writeText(b.name + ".mshf.minehut.gg"); clipboard.writeText(b.name + ".mshf.minehut.gg");
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >
<Copy size={18} /> Copy server IP
<code className="ml-2">{b.name}</code> <div className="RightSlot">
</Button> <Copy size={18} />
<Tooltip> </div>
<TooltipTrigger> </ContextMenuItem>
<Link href={"/server/" + b.name}> <ContextMenuSeparator />
<Button <Link href={"/src/app/(main)/server/" + b.name}>
size="icon" <ContextMenuItem>Open server page</ContextMenuItem>
variant="secondary" </Link>
className="w-[32px] h-[32px] mt-2 ml-2 max-md:hidden" </ContextMenuContent>
> </ContextMenu>
<Layers size={18} /> )}
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
Open up the server page to see more information about
the server
</TooltipContent>
</Tooltip>
</>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
clipboard.writeText(b.name + ".mshf.minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
Copy server IP
<div className="RightSlot">
<Copy size={18} />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
<Link href={"/src/app/(main)/server/" + b.name}>
<ContextMenuItem>Open server page</ContextMenuItem>
</Link>
</ContextMenuContent>
</ContextMenu>
</CardDescription> </CardDescription>
<CardContent> <CardContent className="p-0">
{motd && ( {motd && selectedProperties.includes("MOTD") && (
<span <span
dangerouslySetInnerHTML={{ __html: motd }} dangerouslySetInnerHTML={{ __html: motd }}
className="w-[30px] text-center break-all overflow-hidden" className="w-[30px] text-center break-all overflow-hidden"

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

@ -1,16 +1,16 @@
"use client";; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { setCustomization } from "@/lib/api"; import { setCustomization } from "@/lib/api";
@ -18,73 +18,73 @@ import { toast } from "sonner";
import ColorProvider from "../ColorProvider"; import ColorProvider from "../ColorProvider";
const FormSchema = z.object({ const FormSchema = z.object({
website: z website: z
.string() .string()
.min(2, { .min(2, {
message: "ID must be at least 2 characters.", message: "ID must be at least 2 characters.",
}) })
.url({ message: "Image must be in URL form." }), .url({ message: "Image must be in URL form." }),
}); });
export function BannerPopover({ server, get }: { server: string; get: any }) { export function BannerPopover({ server, get }: { server: string; get: any }) {
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { defaultValues: {
website: "", website: "",
}, },
}); });
async function onSubmit(data: z.infer<typeof FormSchema>) { async function onSubmit(data: z.infer<typeof FormSchema>) {
toast.promise(setCustomization(server, { banner: data.website }), { toast.promise(setCustomization(server, { banner: data.website }), {
loading: "Setting banner..", loading: "Setting banner..",
success: "Set banner!", success: "Set banner!",
error: "Error while setting banner", error: "Error while setting banner",
}); });
} }
return ( return (
<ColorProvider server={server} fetch={get}> <ColorProvider server={server} fetchV={get}>
<div> <div>
<span className="text-sm"> <span className="text-sm">
All images that are in a web supported format can be used as the All images that are in a web supported format can be used as the
banner for a server. banner for a server.
</span> </span>
<br /> <br />
<br /> <br />
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="w-2/3 space-y-6" className="w-2/3 space-y-6"
defaultValue={get?.banner} defaultValue={get?.banner}
> >
<FormField <FormField
control={form.control} control={form.control}
name="website" name="website"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Image URL</FormLabel> <FormLabel>Image URL</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" className="h-[30px]"> <Button type="submit" className="h-[30px]">
Submit Submit
</Button> </Button>
<Button <Button
className="ml-2 h-[30px]" className="ml-2 h-[30px]"
type="button" type="button"
onClick={() => { onClick={() => {
console.log("hi"); console.log("hi");
}} }}
> >
Clear Clear
</Button> </Button>
</form> </form>
</Form> </Form>
</div> </div>
</ColorProvider> </ColorProvider>
); );
} }

@ -78,7 +78,7 @@ export function DiscordPopover({ server, get }: { server: string; get: any }) {
}, [get]); }, [get]);
return ( return (
<ColorProvider server={server} fetch={get}> <ColorProvider server={server} fetchV={get}>
<div> <div>
<span className="text-sm"> <span className="text-sm">
To embed a Discord server into your server page, first enable the To embed a Discord server into your server page, first enable the

@ -0,0 +1,200 @@
"use client";
import { CircleAlert, LayoutGrid, List, Phone } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Select } from "@radix-ui/react-select";
import {
SelectContent,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { SignedIn } from "@clerk/nextjs";
export function DisplaySettings({
presentationMode,
setPresentationMode,
selectedProperties,
setSelectedProperties,
hero,
setHero,
ipr,
am,
iprChangerCallback,
padding,
paddingChangerCallback,
}: any) {
const toggleProperty = (property: string) => {
setSelectedProperties((prev: any) =>
prev.includes(property)
? prev.filter((p: any) => p !== property)
: [...prev, property]
);
};
return (
<div className="w-full space-y-6 bg-background">
<Tabs
defaultValue="cards"
className="w-full"
onValueChange={setPresentationMode}
value={presentationMode}
>
<div className="border-b">
<TabsList className="grid w-full grid-cols-2 bg-background p-0">
<TabsTrigger
value="grid"
className="flex items-center gap-2 py-2.5 px-4 data-[state=active]:bg-background data-[state=active]:shadow-none rounded-none border-r"
>
<LayoutGrid className="h-4 w-4" />
Grid
</TabsTrigger>
<TabsTrigger
value="table"
className="flex items-center gap-2 py-2.5 px-4 data-[state=active]:bg-background data-[state=active]:shadow-none rounded-none"
>
<List className="h-4 w-4" />
Table
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="grid" className="space-y-6 mt-0 ">
<SignedIn>
<div className="flex items-center justify-between pt-5 pb-1 p-4">
<Label htmlFor="set-hero" className="font-normal">
Show hero at the top of the page
</Label>
<Switch id="set-hero" value={hero} onCheckedChange={setHero} />
</div>
</SignedIn>
<Separator />
<div
className={
"flex items-center justify-between py-1 " +
(am ? "border border-orange-500 rounded px-2 mx-2" : "mx-4")
}
>
<Label
htmlFor="grid-columns"
className="font-normal flex items-center"
>
{am && (
<Tooltip>
<TooltipTrigger>
<CircleAlert size={16} className="mr-2 text-orange-500" />
</TooltipTrigger>
<TooltipContent>
If you change this setting, it will take priority over your{" "}
<br />
account settings. These settings will not save over reloads.
</TooltipContent>
</Tooltip>
)}
Grid items p/ row
</Label>
<Select value={ipr} onValueChange={iprChangerCallback}>
<SelectTrigger className="w-[125px]">
<SelectValue placeholder="" id="grid-columns" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="4">4 items</SelectItem>
<SelectItem value="5">5 items</SelectItem>
<SelectItem value="6">6 items</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div
className={
"flex items-center justify-between py-1 " +
(am ? "border border-orange-500 rounded px-2 mx-2" : "mx-4")
}
>
<Label htmlFor="padding" className="font-normal flex items-center">
{am && (
<Tooltip>
<TooltipTrigger>
<CircleAlert size={16} className="mr-2 text-orange-500" />
</TooltipTrigger>
<TooltipContent>
If you change this setting, it will take priority over your{" "}
<br />
account settings. These settings will not save over reloads.
</TooltipContent>
</Tooltip>
)}
Padding
</Label>
<Select
value={padding.toString()}
onValueChange={paddingChangerCallback}
>
<SelectTrigger className="w-[125px]">
<SelectValue placeholder="" id="padding" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="0">Default</SelectItem>
<SelectSeparator />
<SelectItem value="15">15px</SelectItem>
<SelectItem value="30">30px</SelectItem>
<SelectItem value="40">40px</SelectItem>
<SelectItem value="60">60px</SelectItem>
<SelectItem value="100">100px</SelectItem>
<SelectItem value="200">200px</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<Separator />
<div className="space-y-4 pt-1 p-4">
<h3 className="text-xs uppercase text-gray-500">
Display Properties
</h3>
<div className="flex flex-wrap gap-2">
{["Author", "MOTD", "Tags", "Players Online", "Actions"].map(
(property) => (
<button
key={property}
onClick={() => toggleProperty(property)}
className={cn(
"px-3 py-1.5 text-sm rounded-md transition-colors",
selectedProperties.includes(property)
? "bg-secondary text-secondary-foreground border"
: "hover:bg-muted/80"
)}
>
{property}
</button>
)
)}
</div>
</div>
</TabsContent>
<TabsContent value="table" className="mt-0 px-4 my-4">
<Alert className="md:hidden">
<Phone className="h-4 w-4" />
<AlertTitle>Table mode isn't optimized for mobile</AlertTitle>
<AlertDescription>
At this time, we do not recommend using table mode on mobile.
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
</div>
);
}

@ -0,0 +1,134 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "../ui/badge";
import { allCategories, allTags } from "@/config/tags";
export function FilterMenu({
serverSizeChangerCallback,
serverSizeChangerValueCallback,
templateFilter,
tagChangerValueCallback,
tagChangerCallback,
categoryChangerCallback,
categoryChangerValueCallback,
children,
}: {
children: React.ReactNode;
serverSizeChangerCallback: any;
serverSizeChangerValueCallback: any;
templateFilter: any;
tagChangerValueCallback: any;
tagChangerCallback: any;
categoryChangerCallback: any;
categoryChangerValueCallback: any;
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[400px] overflow-auto">
<DropdownMenuRadioGroup
onValueChange={serverSizeChangerCallback}
value={serverSizeChangerValueCallback()}
>
<DropdownMenuRadioItem value="smaller">
<div className="block">
Only allow smaller servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers that have the player range 7-15, and cannot{" "}
<br />
be Always Online.
</span>
</div>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bigger">
<div className="block">
Only allow bigger servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers with more than 15 players.
</span>
</div>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="none">
No/custom requirements
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<span className="text-sm text-muted-foreground ml-2">Tags</span>
</DropdownMenuSub>
{allTags.map((tag) => (
<div key={tag.docsName}>
{tag.docsName && tag.__filter == undefined && (
<DropdownMenuCheckboxItem
disabled={templateFilter && tag.__disab != undefined}
id={tag.docsName}
checked={tagChangerValueCallback(tag)}
onCheckedChange={tagChangerCallback(tag)}
>
<Badge variant={tag.role} className="mr-1">
{tag.docsName}
</Badge>
</DropdownMenuCheckboxItem>
)}
</div>
))}
<DropdownMenuSeparator />
<DropdownMenuSub>
<span className="text-sm text-muted-foreground ml-2">Categories</span>
</DropdownMenuSub>
{allCategories.map((categorie) => (
<DropdownMenuCheckboxItem
id={categorie.name}
key={categorie.name}
onCheckedChange={categoryChangerCallback(categorie)}
checked={categoryChangerValueCallback(categorie)}
>
<Badge variant={categorie.role} className="mr-1">
{categorie.name}
</Badge>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

@ -0,0 +1,74 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { Book } from "lucide-react";
import { BrandingColorfulIcon } from "../Icon";
import { Button } from "../ui/button";
import Github from "../ui/github";
import { Separator } from "../ui/separator";
import Link from "next/link";
import { useTheme } from "next-themes";
export default function Footer() {
const { resolvedTheme } = useTheme();
return (
<footer>
<Separator />
<p className="px-4 pt-8 pb-2">
<span className="text-xl font-bold text-muted-foreground pb-12 flex items-center">
<BrandingColorfulIcon className="w-12 h-12 mr-2" />
MHSF
</span>
<p>© {new Date().getFullYear()} dvelo</p>
<strong className="text-sm">
MHSF is built on open-source technologies and is not endorsed by or
affiliated with GamerSafer or its subsidiaries.{" "}
</strong>
<br />
<span className="flex items-center">
<Link href="https://github.com/DeveloLongScript/MHSF">
<Button variant="ghost" size="icon">
<Github fill={resolvedTheme === "dark" ? "white" : "black"} />
</Button>
</Link>
<Link href="/docs">
<Button variant="ghost" size="icon">
<Book size={14} />
</Button>
</Link>
</span>
</p>
</footer>
);
}

@ -31,7 +31,7 @@
import { DatabaseZap } from "lucide-react"; import { DatabaseZap } from "lucide-react";
export default function NoItems() { export default function NoItems({ title }: { title?: string }) {
return ( return (
<> <>
<div className="flex flex-col items-center justify-center p-4 pt-10"> <div className="flex flex-col items-center justify-center p-4 pt-10">
@ -40,7 +40,9 @@ export default function NoItems() {
size={32} size={32}
/> />
<p className="text-xl text-gray-600 mt-2"> <p className="text-xl text-gray-600 mt-2">
Huh, we tried to find something, but nothing was found. {title
? title
: "Huh, we tried to find something, but nothing was found."}
</p> </p>
</div> </div>
</> </>

@ -0,0 +1,158 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
ChevronDown,
Dices,
ListRestart,
MoreVertical,
Search,
SquareTerminal,
} from "lucide-react";
import { FilterMenu } from "./FilterMenu";
import { DisplaySettings } from "./DisplaySettings";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import events from "@/lib/commandEvent";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
export default function ServerListInterface({
linksProps,
viewProps,
refreshCallback,
pickRandomServerCallback,
}: {
linksProps: {
serverSizeChangerCallback: any;
serverSizeChangerValueCallback: any;
templateFilter: any;
tagChangerValueCallback: any;
tagChangerCallback: any;
categoryChangerCallback: any;
categoryChangerValueCallback: any;
};
viewProps: {
setPresentationMode: any;
presentationMode: any;
selectedProperties: any;
setSelectedProperties: any;
hero: any;
setHero: any;
iprChangerCallback: any;
ipr: any;
am: any;
padding: any;
paddingChangerCallback: any;
};
refreshCallback: any;
pickRandomServerCallback: any;
}) {
return (
<div className="w-full mt-6">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-between gap-2 sm:gap-8">
<div className="grid grid-cols-2 sm:flex sm:flex-row items-stretch sm:items-center gap-2">
<FilterMenu {...linksProps}>
<Button variant="outline" className="w-full gap-2">
Filter
<ChevronDown className="h-4 w-4" />
</Button>
</FilterMenu>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full gap-2">
Display
<ChevronDown className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0">
<DisplaySettings {...viewProps} />
</PopoverContent>
</Popover>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search..."
className="pl-8"
value=""
onClick={(c) => {
c.preventDefault();
events.emit("search-request-event");
}}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="shrink-0">
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="dark:text-white py-2"
onSelect={refreshCallback}
>
<ListRestart />
Reload Servers
</DropdownMenuItem>
<DropdownMenuItem
className="dark:text-white py-2"
onSelect={pickRandomServerCallback}
>
<Dices />
Pick Random Server
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="dark:text-white py-2"
onSelect={() => events.emit("cmd-event")}
>
<SquareTerminal />
Show Command Bar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
);
}

@ -1,15 +1,20 @@
import { useTheme } from "next-themes";
import type { SVGProps } from "react"; import type { SVGProps } from "react";
const Github = (props: SVGProps<SVGSVGElement>) => ( const Github = (props: SVGProps<SVGSVGElement>) => {
<svg const { resolvedTheme } = useTheme();
viewBox="0 0 256 250"
width="1em" return (
height="1em" <svg
fill="#fff" viewBox="0 0 256 250"
xmlns="http://www.w3.org/2000/svg" width="1em"
preserveAspectRatio="xMidYMid" height="1em"
{...props} fill={resolvedTheme === "dark" ? "#fff" : "#24292f"}
> xmlns="http://www.w3.org/2000/svg"
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" /> preserveAspectRatio="xMidYMid"
</svg> {...props}
); >
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
};
export default Github; export default Github;

@ -294,7 +294,6 @@ async function requestServer(s: OnlineServer): Promise<ServerResponse> {
const json = await re.json(); const json = await re.json();
serverCache[s.name] = json.server; serverCache[s.name] = json.server;
return json.server; return json.server;
} else {
return serverCache[s.name];
} }
return serverCache[s.name];
} }

@ -30,6 +30,9 @@
"use client"; "use client";
import A from "@/components/misc/Link"; import A from "@/components/misc/Link";
import { Button } from "@/components/ui/button";
import Github from "@/components/ui/github";
import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
const User = ({ user }: { user: string }) => ( const User = ({ user }: { user: string }) => (
@ -41,13 +44,22 @@ const User = ({ user }: { user: string }) => (
const FeatureList = ({ const FeatureList = ({
features, features,
title, title,
github,
}: { }: {
features: (string | ReactNode)[]; features: (string | ReactNode)[];
github?: string;
title: ReactNode; title: ReactNode;
}) => { }) => {
return ( return (
<ul> <ul>
{title} {title}
{github && (
<Link href={github}>
<Button variant="ghost" size="sm">
<Github className="mr-1" /> Release
</Button>
</Link>
)}
{features.map((feature, i) => ( {features.map((feature, i) => (
<li key={i}> {feature}</li> <li key={i}> {feature}</li>
))} ))}
@ -55,13 +67,42 @@ const FeatureList = ({
); );
}; };
export const version = "1.6.50"; export const version = "1.7.0";
export const changelog: { name: string; id: string; changelog: ReactNode }[] = [ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
{
id: "38ufajf8efajwj3njdaisef",
name: "v1.7",
changelog: (
<FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.7"
features={[
"Partnered with CoreBoxx!",
"You can now link your Minecraft account on CoreBoxx! (check out CB 3.0.15)",
"Revamped the server finding controls",
"Fixed various bugs",
"Made banners a different style",
"Made Discord embed not inside a card",
"Added incorrect server capitalization detection",
"Made the MOTD area slightly bigger",
"New footer",
"Added padding for settings page",
"Added new table mode",
"Added new button for GitHub release on changelog",
]}
title={
<strong className="flex items-center">
Version 1.7 (January 18th 2025)
</strong>
}
/>
),
},
{ {
id: "dut6hx3f2paswzjve4yg9r", id: "dut6hx3f2paswzjve4yg9r",
name: "v1.6.5", name: "v1.6.5",
changelog: ( changelog: (
<FeatureList <FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.6.5"
features={[ features={[
"New MOTD engine that is over 3,000% faster, runs client-side, and doesn't need any requests to run.", "New MOTD engine that is over 3,000% faster, runs client-side, and doesn't need any requests to run.",
"Fixed issue where GitHub link was broken if you were signed-out", "Fixed issue where GitHub link was broken if you were signed-out",
@ -80,6 +121,7 @@ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
name: "v1.6.0", name: "v1.6.0",
changelog: ( changelog: (
<FeatureList <FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.6"
features={[ features={[
"Completely redid top of server view", "Completely redid top of server view",
"Favorite counts are now prominent on the server view", "Favorite counts are now prominent on the server view",
@ -102,6 +144,7 @@ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
name: "v1.5.0", name: "v1.5.0",
changelog: ( changelog: (
<FeatureList <FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.5"
features={[ features={[
"New embeds", "New embeds",
"More mobile friendly elements", "More mobile friendly elements",
@ -121,6 +164,7 @@ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
name: "v1.4.5", name: "v1.4.5",
changelog: ( changelog: (
<FeatureList <FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.4.5"
features={["Add server icons"]} features={["Add server icons"]}
title={ title={
<strong className="flex items-center"> <strong className="flex items-center">
@ -135,6 +179,7 @@ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
name: "v1.4.0", name: "v1.4.0",
changelog: ( changelog: (
<FeatureList <FeatureList
github="https://github.com/DeveloLongScript/MHSF/releases/tag/1.4"
features={[ features={[
"Revamped documentation", "Revamped documentation",
"Revamped changelog UI", "Revamped changelog UI",