/* * 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) 2025 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 { BorderBeam } from "@/components/effects/border-beam"; import { Button } from "@/components/ui/button"; import { AgGridReact, CustomCellRendererProps } from "ag-grid-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { AllCommunityModule, ModuleRegistry, colorSchemeDarkBlue, colorSchemeLightWarm, themeQuartz, } from "ag-grid-community"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { allTags } from "@/config/tags"; import ServersList from "@/lib/list"; import { OnlineServer } from "@/lib/types/mh-server"; import useClipboard from "@/lib/useClipboard"; import { useEffectOnce } from "@/lib/useEffectOnce"; import { useRouter } from "@/lib/useRouter"; import { cn } from "@/lib/utils"; import { SignedIn, SignedOut, useUser } from "@clerk/nextjs"; import { ChatBubbleIcon, InputIcon } from "@radix-ui/react-icons"; import { ArrowDown, ArrowDownZA, Check, CircleUser, Copy, ImageIcon, Info, Layers, LogIn, Network, Sun, XIcon, } from "lucide-react"; import { useTheme } from "next-themes"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import InfiniteScroll from "react-infinite-scroll-component"; import ClientFadeIn from "./ClientFadeIn"; import IconDisplay from "./IconDisplay"; import ServerCard from "./ServerCard"; import Stat from "./Stat"; import { SignInPopover } from "./clerk/SignInPopoverButton"; import { BentoCard, BentoGrid } from "./effects/bento-grid"; import Marquee from "./effects/marquee"; import Particles from "./effects/particles"; import SparklesText from "./effects/sparkles-text"; import { pageFind } from "./misc/Link"; import { Badge } from "./ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Skeleton } from "./ui/skeleton"; import { LoadingSpinner } from "./ui/loading-spinner"; import StickyTopbar from "./misc/StickyTopbar"; import { HoverCard } from "@radix-ui/react-hover-card"; import { HoverCardTrigger } from "./ui/hover-card"; import { ExampleChart } from "./charts/ExampleChart"; import ServerListInterface from "./misc/ServerListInterface"; import NoItems from "./misc/NoItems"; // ag-grid ModuleRegistry.registerModules([AllCommunityModule]); const themeLightWarm = themeQuartz.withPart(colorSchemeLightWarm); const themeDarkWarm = themeQuartz.withPart(colorSchemeDarkBlue); const features = [ { Icon: ChatBubbleIcon, name: "Add a Discord widget", description: "Show where your players talk to each-other, including an online users count.", href: "/docs/guides/customization", cta: "Learn more", background: , className: "lg:row-start-1 lg:row-end-2 lg:col-start-2 lg:col-end-3", }, { Icon: InputIcon, name: "Descriptions", href: "/docs/guides/customization", cta: "Learn more", description: "Format your descriptions using Markdown to show what your server has to offer.", background: , className: "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-2", }, { Icon: ImageIcon, name: "Banners", href: "/docs/guides/customization", cta: "Learn more", description: "Show a banner with can contain images that show on your server page.", background: , className: "lg:col-start-3 lg:col-end-4 lg:row-start-1 lg:row-end-2", }, ]; export default function ServerList() { const [loading, setLoading]: any = useState(true); const [randomText, setRandomText] = useState(""); const [motdList, setMotdList] = useState({}); const allText = [""]; const getRandomText = () => { return allText[Math.floor(Math.random() * allText.length)]; }; const [templateFilter, setTemplateFilter] = useState(false); const [random, setRandom] = useState(false); const [serverList, setServerList] = useState(new ServersList([])); const [textCopied, setTextCopied] = useState(false); const [padding, setPadding] = useState("0"); const bigger = async (server: OnlineServer) => server.playerData.playerCount > 15; const smaller = async (server: OnlineServer) => !server.staticInfo.alwaysOnline && server.playerData.playerCount < 15 && server.playerData.playerCount > 7; const [nameFilters, setNameFilters] = useState({}); const [inErrState, setErrState] = useState(false); const [servers, setServers] = useState>([]); const clipboard = useClipboard(); const router = useRouter(); const { user, isSignedIn } = useUser(); const [pOS, setpOS] = useState(false); const [selectedProperties, setSelectedProperties] = useState([ "Author", "MOTD", "Tags", "Players Online", "Actions", ]); const [ipr, setIPR] = useState("4"); const [presentationMode, setPresentationMode] = useState<"table" | "grid">( "grid" ); const [am, setAM] = useState(false); const [filters, setFilters] = useState< Array<(server: OnlineServer) => Promise> >([]); const [randomData, setRandomData] = useState( undefined ); const { resolvedTheme } = useTheme(); const [color, setColor] = useState("#ffffff"); useEffect(() => { setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000"); }, [resolvedTheme]); useEffect(() => { if (isSignedIn) { setAM(true); console.log(user.publicMetadata); setIPR((user.publicMetadata.ipr as string | undefined) || "4"); setPadding((user.publicMetadata.pad as string | undefined) || "0"); setpOS((user.publicMetadata.srv as boolean | undefined) || false); } }, [isSignedIn, user]); useEffectOnce(() => { setRandomText(getRandomText()); serverList .fetchDataAndFilter() .then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); setLoading(false); }); }) .catch(() => setErrState(true)); }); const ref = useRef(null); const [clickedPage, setClickedPage] = useState("banners"); const [hero, setHero] = useState(false); if (inErrState) { return ( <>

Hmm. Something is wrong. Reload the page.
); } if (loading) { return (


); } return (
<> {(!isSignedIn || hero) && (
<> Meet MHSF,
the modern server finder

MHSF is the next generation server list for Minehut, with interactive filters,
{" "} intuitive keyboard shortcuts, and everything between.

Hero Image Hero Image


For players

Find what you want now, not later

Use interactive filters and customization modes to find the server of your choice
in less than 10 minutes.

{serverList.currentServers.slice(0, 20).map((server) => (
router.push(pageFind(`Server:${server.name}`) as string) } >
{server.name}
{server.author && (

by {server.author}

)}
))}
{serverList.currentServers.slice(0, 20).map((server) => (
router.push(`/server/${server.name}`)} >
{server.name}
{server.author && (

by {server.author}

)}
))}

For server owners

Make your server stand out

Servers can have custom banners, Discord widgets, color schemes, and descriptions, making your server stand out with information that can be shown to players.

{features.map((feature, idx) => ( ))}

Monitor your success

Ever wondered how a server was doing? MHSF constantly monitors servers and shows you statistics about how a server is doing at any point of time.


Check it out below!
)}
= 3200 ? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500" : "" } > Servers online{" "}
} className="relative z-0" desc={
= 3200 ? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500 " : "" } > {serverList.getExtraData().total_servers.toString()}
{serverList.getExtraData().total_servers >= 3200 && ( The server amount is over 3.2k, meaning that new servers have to go into a queue before being able to be online.{" "}
(the server count isn't entirely accurate, so sometimes you might not go into a queue even when the server count is at 3.2k)
)}
} icon={Network} > {serverList.getExtraData().total_servers >= 3200 && ( )} {serverList.currentServers[0] != undefined ? serverList.currentServers[0].name : "None"}{" "} {serverList.currentServers[0] != undefined && ( )} } icon={Sun} />

{ if (am) toast.warning( "These settings will not change over reloads because you have account specific options enabled", { action: { label: "Check settings", onClick: () => router.push("/account/settings/options"), }, } ); setPadding(v); }, am, iprChangerCallback: (v: any) => { if (am) toast.warning( "These settings will not change over reloads because you have account specific options enabled", { action: { label: "Check settings", onClick: () => router.push("/account/settings/options"), }, } ); setIPR(v); }, ipr, }} pickRandomServerCallback={() => { setRandomData(serverList.getRandomServer()); setRandom(true); }} refreshCallback={() => { toast.promise( new Promise((s, e) => { setLoading(true); serverList .fetchDataAndFilter() .then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name, }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); setLoading(false); s(false); }); }) .catch(() => { e(); }); }), { success: "Succesfully reloaded servers", loading: "Reloading...", error: "Error while refreshing", } ); }} linksProps={{ templateFilter, tagChangerValueCallback: (tag: any) => { return nameFilters["t-" + tag.docsName]; }, categoryChangerValueCallback: (categorie: any) => { return nameFilters["c-" + categorie.name]; }, categoryChangerCallback: (categorie: any) => async (b: any) => { var filt = nameFilters; filt["c-" + categorie.name] = b; setNameFilters(filt); if (b) { var filt2 = filters; filt2.push(categorie.condition); setFilters(filt2); } else { var filt2 = filters; filt2.splice(filt2.indexOf(categorie.condition), 1); setFilters(filt2); } serverList.editFilters(filters); serverList.fetchDataAndFilter().then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); }); }); }, tagChangerCallback: (tag: any) => async (b: any) => { var filt = nameFilters; filt["t-" + tag.docsName] = b; setNameFilters(filt); if (b) { var filt2 = filters; filt2.push(tag.condition); setFilters(filt2); } else { var filt2 = filters; filt2.splice(filt2.indexOf(tag.condition), 1); setFilters(filt2); } serverList.editFilters(filters); serverList.fetchDataAndFilter().then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name, }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); }); }); }, serverSizeChangerValueCallback: () => { if (nameFilters["smaller-tf"]) { return "smaller"; } if (nameFilters["bigger-tf"]) { return "bigger"; } return "none"; }, serverSizeChangerCallback: (v: any) => { toast.promise( new Promise((g, b) => { if (v == "smaller") { setTemplateFilter(true); var filt = nameFilters; filt["smaller-tf"] = true; filt["bigger-tf"] = false; setNameFilters(filt); var filt2 = filters; filt2.push(smaller); if (filt2.includes(bigger)) { filt2.splice(filt2.indexOf(bigger), 1); } setFilters(filt2); serverList.editFilters(filters); serverList.fetchDataAndFilter().then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name, }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); g(undefined); }); }); } else if (v == "bigger") { setTemplateFilter(true); var filt = nameFilters; filt["smaller-tf"] = false; filt["bigger-tf"] = true; setNameFilters(filt); var filt2 = filters; filt2.push(bigger); filt2.splice(filt2.indexOf(smaller), 1); setFilters(filt2); serverList.editFilters(filters); serverList.fetchDataAndFilter().then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name, }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); g(undefined); }); }); } else { var filt = nameFilters; filt["smaller-tf"] = false; filt["bigger-tf"] = false; setNameFilters(filt); setTemplateFilter(false); var filt2 = filters; filt2.splice(filt2.indexOf(smaller), 1); filt2.splice(filt2.indexOf(bigger), 1); setFilters(filt2); console.log(filters, filters.includes(smaller)); serverList.editFilters(filters); serverList.fetchDataAndFilter().then(() => { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string; }> = []; let obj: any = {}; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name, }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); g(undefined); }); }); } }), { error: "Error while changing filters", loading: "Changing filters...", success: "Changed filters!", } ); }, }} /> {randomData == undefined && <>No data to randomize} {randomData != undefined && ( {randomData.name} {randomData.author != undefined ? (
by {randomData.author}
) : (
)}
{randomData.playerData.playerCount == 0 ? (
) : (
)} {randomData.playerData.playerCount}{" "} {randomData.playerData.playerCount == 1 ? "player" : "players"}{" "} currently online
Server IP

{randomData.name}.minehut.gg{" "} )}

{presentationMode === "grid" && ( { serverList.moveListDown(); let stringList: Array<{ server: string; motd: string }> = []; serverList.currentServers.forEach((b) => { stringList.push({ motd: b.motd, server: b.name }); }); serverList.getMOTDs(stringList).then((c) => { var updatedSL = motdList; c.forEach((b: { server: string; motd: string }) => { updatedSL[b.server] = b.motd; }); setMotdList(updatedSL); setServers(serverList.currentServers); setLoading(false); }); }} loader={} endMessage={

} style={{ overflow: "hidden !important", paddingLeft: pOS ? `${padding}px` : 6, paddingRight: pOS ? `${padding}px` : 6, }} > {/** This looks stupid, but its the only way that works */}
{servers.map((b: any, i: number) => ( <> {/* <> {i === Number(ipr) && affiliates.length != 0 && (

Affiliates

We have been able to partner with some servers that we think are high-effort servers that need to be recognized.

Please take some interest in a server you find interesting and give them some much needed support.

These servers have absolutely no financial affiliation with MHSF.
{affiliates .filter((a) => a.mode.includes("server-list")) .map((a) => (
{a.name}
))}
)} */} ))}
)} {presentationMode === "table" && (
{ return ( <> {c.data?.name} {c.data?.name} ); }, }, { field: "playerData.playerCount", headerName: "Players", cellRenderer: (params: CustomCellRendererProps) => { return (
{params.value == 0 ? (
) : (
)}{" "} {params.value}
); }, }, { headerName: "Owner", valueGetter: (p) => p.data?.author ?? "--", cellRenderer: (c: CustomCellRendererProps) => { if (c.data.author === "--") { return <>--; } return ( <> {c.data?.author} {c.data?.author} ); }, }, { headerName: "Tags", valueGetter: (p) => ( ), cellRenderer: TagCR, minWidth: 249, }, { headerName: "Actions", minWidth: 107, cellRenderer: (c: CustomCellRendererProps) => { return (
Open up the server page to see more information about the server
); }, }, ]} pagination={true} paginationPageSize={50} paginationPageSizeSelector={[10, 25, 50, 100]} theme={resolvedTheme === "dark" ? themeDarkWarm : themeLightWarm} defaultColDef={{ flex: 1, }} />
)}
); } export function TagCR(params: CustomCellRendererProps) { return {params.value}; } export function TagShower(props: { server: OnlineServer; className?: string; unclickable?: boolean; }) { const [loading, setLoading] = useState(true); const [compatiableTags, setCompatiableTags] = useState< Array<{ name: string; docsName?: string; tooltip: string; htmlDocs: string; role: | "default" | "destructive" | "outline" | "secondary" | "red" | "orange" | "yellow" | "green" | "lime" | "blue" | "teal" | "cyan" | "violet" | "indigo" | "purple" | "fuchsia" | "pink"; }> >([]); useEffectOnce(() => { if (loading) { allTags.forEach((tag) => { tag.condition(props.server).then((b) => { if (b && tag.primary) { tag.name(props.server).then((n) => { compatiableTags.push({ name: n, docsName: tag.docsName, tooltip: tag.tooltipDesc, htmlDocs: tag.htmlDocs, role: tag.role == undefined ? "secondary" : tag.role, }); setLoading(false); }); } }); }); } }); if (loading) { return <>; } return (
{compatiableTags.map((t, i) => ( {props.unclickable && ( {t.name} )} {!props.unclickable && ( {t.name}
{t.tooltip}
Click the tag to learn more about it.
{'"'} {t.docsName == undefined ? t.name : t.docsName} {'"'} documentation
)}
))}
); }