Compare commits

...

8 Commits

Author SHA1 Message Date
dependabot[bot]
5773e1da62
build(deps-dev): bump postcss from 8.4.39 to 8.4.49
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.39 to 8.4.49.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.39...8.4.49)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 00:03:23 +00:00
dvelo
cbdcc74ce8
chore: move dev branch commits -> main
chore: move dev branch commits -> main
2024-11-23 18:01:43 -06:00
dvelo
e45d8f0a70 fix: react-related issues 2024-11-23 17:59:09 -06:00
dvelo
b073f719be feat: cosmetic changes for toasts and tabs 2024-11-23 17:54:51 -06:00
dvelo
2bd8d17b75 feat: revamp loading spinner 2024-11-23 17:54:15 -06:00
dvelo
9026c69ca7 fix: change types 2024-11-17 21:17:12 -06:00
dvelo
346f9d210e feat: redid server view 2024-11-17 21:10:02 -06:00
dvelo
fd40a8e143 fix: move from pixels to rem in embed selector 2024-11-16 15:10:45 -06:00
42 changed files with 3216 additions and 2650 deletions

@ -44,7 +44,7 @@
"next": "14.2.10",
"next-contentlayer": "^0.3.4",
"next-css-obfuscator": "^2.2.16",
"next-themes": "^0.3.0",
"next-themes": "^0.4.3",
"nextjs-toploader": "^1.6.12",
"nprogress": "^0.2.0",
"postcss-obfuscator": "^1.6.1",
@ -53,8 +53,11 @@
"react-dom": "^18",
"react-fade-in": "^2.0.1",
"react-fast-marquee": "^1.6.5",
"react-hot-toast": "^2.4.1",
"react-qr-code": "^2.0.15",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"sonner": "^1.7.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-patch": "^4.0.0",
@ -100,7 +103,6 @@
"mangle-css-class-webpack-plugin": "^5.1.0",
"postcss": "^8",
"react-hook-form": "^7.52.2",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",

@ -31,7 +31,7 @@
"use client";
import { Button } from "@/components/ui/button";
import { useClerk, useUser } from "@clerk/nextjs";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { unlinkMCAccount } from "@/lib/api";
import { useEffect, useState } from "react";
import { Dialog } from "@/components/ui/dialog";

@ -84,12 +84,7 @@ export default async function RootLayout({
}>) {
return (
<ClerkThemeProvider className={GeistSans.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
{banner.isBanner && (
<div className="bg-orange-600 z-10 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">

@ -32,6 +32,7 @@ import AfterServerView from "@/components/AfterServerView";
import Banner from "@/components/Banner";
import ColorProvider from "@/components/ColorProvider";
import ServerView from "@/components/ServerView";
import StickyTopbar from "@/components/misc/StickyTopbar";
import TabServer from "@/components/misc/TabServer";
import { Separator } from "@/components/ui/separator";
import type { Metadata, ResolvingMetadata } from "next";
@ -130,9 +131,11 @@ export default function ServerPage({ params }: { params: { server: string } }) {
return (
<main>
<ColorProvider server={params.server}>
<div className={"pt-16"}>
<div className={"pt-16 xl:px-[100px]"}>
<Banner server={params.server} />
<StickyTopbar scrollElevation={100} className="pt-4">
<TabServer server={params.server} tabDef="general" />
</StickyTopbar>
<div className="pt-8">
<ServerView server={params.server} />
</div>

@ -42,7 +42,7 @@ type Props = {
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
parent: ResolvingMetadata
): Promise<Metadata> {
// read route params
const { server } = params;
@ -98,7 +98,7 @@ export default function ServerPage({ params }: { params: { server: string } }) {
return (
<main>
<ColorProvider server={params.server}>
<div className={"pt-16"}>
<div className={"pt-16 xl:px-[100px]"}>
<Banner server={params.server} />
<TabServer server={params.server} tabDef="statistics" />
<div className="pt-8">

@ -147,6 +147,37 @@
/* }*/
/*}*/
@layer base {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 2;
}
@keyframes clip-down {
from {
clip-path: inset(0 0 100% 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
.dark::view-transition-new(root),
.light::view-transition-new(root) {
animation: 0.7s clip-down;
}
}
.backdrop-blur {
-webkit-backdrop-filter: blur(8px) !important;
backdrop-filter: blur(8px) !important;

@ -37,11 +37,12 @@ import {
getIndexFromRarity,
getMinehutIcons,
} from "@/lib/types/server-icon";
import { Copy, ExternalLink, Info } from "lucide-react";
import { Copy, Info, QrCode, Share2 } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import FadeIn from "react-fade-in/lib/FadeIn";
import toast, { CheckmarkIcon } from "react-hot-toast";
import { CheckmarkIcon } from "react-hot-toast";
import { toast } from "sonner";
import Markdown from "react-markdown";
import IconDisplay from "./IconDisplay";
import AchievementList from "./feat/AchievementList";
@ -56,6 +57,9 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "./ui/drawer";
import EmbedSelector from "./feat/EmbedSelector";
import { Separator } from "./ui/separator";
import QRCodeGenerator from "./feat/QRCodeGen";
import NoItems from "./misc/NoItems";
export default function AfterServerView({ server }: { server: string }) {
const [description, setDescription] = useState("");
@ -64,6 +68,7 @@ export default function AfterServerView({ server }: { server: string }) {
const [icons, setIcons] = useState<MinehutIcon[]>();
const { resolvedTheme } = useTheme();
const [loading, setLoading] = useState(true);
const [qrCodeOpen, setQrCodeOpen] = useState(false);
const [view, setView] = useState(
description !== "" || discord !== "" ? "desc" : "extra"
);
@ -104,6 +109,14 @@ export default function AfterServerView({ server }: { server: string }) {
<EmbedSelector server={server} />
</DrawerContent>
</Drawer>
<Drawer open={qrCodeOpen} onOpenChange={setQrCodeOpen}>
<DrawerContent className="max-w-md w-full mx-auto rounded-t-[10px]">
<DrawerHeader>
<DrawerTitle>QR Code generator</DrawerTitle>
</DrawerHeader>
<QRCodeGenerator server={server} />
</DrawerContent>
</Drawer>
<FadeIn>
<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)]">
@ -135,9 +148,10 @@ export default function AfterServerView({ server }: { server: string }) {
>
Purchased Icons
</Button>
<Separator orientation="vertical" />
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
Embed Creator
<ExternalLink className="h-[1.2rem] w-[1.2rem] ml-1" />
<Share2 className="h-[1rem] w-[1rem] mr-2" />
Embeds
</Button>
</div>
<div className="max-sm:hidden">
@ -168,9 +182,16 @@ export default function AfterServerView({ server }: { server: string }) {
>
Purchased Icons
</Button>
<br />
<Separator />
<br />
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
Embed Creator
<ExternalLink className="h-[1.2rem] w-[1.2rem] ml-1" />
<Share2 className="h-[1rem] w-[1rem] mr-2" />
Embeds
</Button>
<Button variant="ghost" onClick={() => setQrCodeOpen(true)}>
<QrCode className="h-[1rem] w-[1rem] mr-2" />
QR Code
</Button>
</div>
</div>
@ -450,6 +471,7 @@ export default function AfterServerView({ server }: { server: string }) {
ownership, they may or may not available at that certain
moment either.
</p>
{serverObject?.purchased_icons.length == 0 && <NoItems />}
{serverObject?.purchased_icons.map((icon) => (
<Card key={icon} className="my-4">
<CardContent

@ -68,7 +68,7 @@ import {
} from "@/lib/api";
import IconDisplay from "./IconDisplay";
import ServerSingle from "@/lib/single";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { ServerResponse, OnlineServer } from "@/lib/types/mh-server";
import {
Dialog,
@ -80,6 +80,7 @@ import {
import { TagShower } from "./ServerList";
import { Button } from "./ui/button";
import { useTheme } from "next-themes";
import { CheckmarkIcon } from "react-hot-toast";
export function SearchCommandBar() {
const [serverList, setServerList] = useState<OnlineServer[]>([]);
@ -278,7 +279,7 @@ export function OfflineServerCB() {
{customized && (
<h2 className="flex items-center text-muted-foreground">
<CheckIcon />
<CheckmarkIcon />
<span className="pl-1.5 text-[16px]">
Is customized by a MHSF User
</span>

@ -29,7 +29,6 @@
*/
"use client";
import { Separator } from "@/components/ui/separator";
import { useState } from "react";
import Banner from "./Banner";
import ServerCustomize from "./ServerCustomize";
@ -42,10 +41,9 @@ export default function CustomizeRoot({
}) {
const [color, setColor] = useState("");
return (
<div className={"pt-16 theme-" + color}>
<div className={"pt-16 xl:px-[100px] theme-" + color}>
<Banner server={params.server} />
<TabServer server={params.server} tabDef="customize" />
<Separator />
<br />
<div className="pl-[40px] pr-[40px]">
<ServerCustomize server={params.server} cs={color} setCS={setColor} />

@ -30,13 +30,12 @@
"use client";
import { useState } from "react";
import { Spinner } from "./ui/spinner";
import { Card, CardHeader, CardTitle } from "./ui/card";
import type { ServerResponse } from "@/lib/types/mh-server";
import { useEffectOnce } from "@/lib/useEffectOnce";
import { Button } from "./ui/button";
import { Copy, Layers, XIcon } from "lucide-react";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { getAccountFavorites } from "@/lib/api";
import { useRouter } from "@/lib/useRouter";

@ -304,3 +304,19 @@ export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
);
}
}
export const Discord = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 199"
width="1em"
height="1em"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...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>
);

@ -196,7 +196,7 @@ export function NewChart({ server }: { server: string }) {
);
}
function convert(value: number) {
export function convert(value: number) {
var result: string = value.toString();
if (value >= 1000000) {
result = Math.floor(value / 1000000) + "m";

@ -45,7 +45,7 @@ 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 { toast } from "sonner";
import { useUser } from "@clerk/nextjs";
export function SLCustomize() {

@ -1,34 +1,3 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://list.mlnehut.com/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 { NewChart } from "@/components/NewChart";
import { MiniJoinsChart } from "@/components/misc/MiniJoinsChart";
import {
ContextMenu,
@ -66,7 +35,8 @@ import {
import { useTheme } from "next-themes";
import Link from "next/link";
import { useState } from "react";
import toast, { LoaderIcon } from "react-hot-toast";
import { LoaderIcon } from "react-hot-toast";
import { toast } from "sonner";
import IconDisplay from "./IconDisplay";
import { TagShower } from "./ServerList";
import { Button } from "./ui/button";

@ -45,7 +45,7 @@ import { CheckIcon, X } from "lucide-react";
import Link from "next/link";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "./ui/button";
import {
@ -82,7 +82,7 @@ import {
DialogTrigger,
} from "./ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Spinner } from "./ui/spinner";
import { LoadingSpinner } from "./ui/loading-spinner";
const formSchema = z.object({
description: z
@ -150,7 +150,7 @@ export default function ServerCustomize({
if (loading) {
return (
<>
<Spinner className="flex items-center" />
<LoadingSpinner className="flex items-center" />
<br />
</>
);
@ -233,7 +233,7 @@ export default function ServerCustomize({
success: "Report sent!",
loading: "Sending report...",
error: "Error while sending report",
},
}
);
}}
>
@ -261,7 +261,7 @@ export default function ServerCustomize({
success: "Owned server!",
loading: "Owning server...",
error: "Error while owning server",
},
}
);
setMinehutOwned(true);

@ -85,7 +85,7 @@ import { CommandIcon } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { toast } from "sonner";
import InfiniteScroll from "react-infinite-scroll-component";
import ClientFadeIn from "./ClientFadeIn";
import IconDisplay from "./IconDisplay";
@ -100,7 +100,9 @@ import { pageFind } from "./misc/Link";
import { Badge } from "./ui/badge";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Skeleton } from "./ui/skeleton";
import { Spinner } from "./ui/spinner";
import { affiliates } from "@/config/affiliates";
import { LoadingSpinner } from "./ui/loading-spinner";
import StickyTopbar from "./misc/StickyTopbar";
const features = [
{
@ -167,7 +169,7 @@ export default function ServerList() {
Array<(server: OnlineServer) => Promise<boolean>>
>([]);
const [randomData, setRandomData] = useState<OnlineServer | undefined>(
undefined,
undefined
);
const { resolvedTheme } = useTheme();
const [color, setColor] = useState("#ffffff");
@ -349,7 +351,7 @@ export default function ServerList() {
className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " +
"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]",
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
)}
onClick={() =>
router.push(pageFind(`Server:${server.name}`))
@ -383,7 +385,7 @@ export default function ServerList() {
className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " +
"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]",
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
)}
onClick={() => router.push(`/server/${server.name}`)}
>
@ -519,7 +521,8 @@ export default function ServerList() {
<br id="serverlist" className="pb-14" />
<Separator />
<ClientFadeIn delay={100}>
<Menubar className="mt-3 ml-2 border rounded p-2 shadow">
<StickyTopbar scrollElevation={250} className="p-2">
<Menubar className="mt-3 border rounded shadow">
<MenubarMenu>
<MenubarTrigger>Servers</MenubarTrigger>
<MenubarContent>
@ -558,7 +561,10 @@ export default function ServerList() {
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
stringList.push({
motd: b.motd,
server: b.name,
});
});
serverList.getMOTDs(stringList).then((c) => {
@ -566,7 +572,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
},
}
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -582,7 +588,7 @@ export default function ServerList() {
success: "Succesfully refreshed servers",
loading: "Refreshing...",
error: "Error while refreshing",
},
}
);
}}
>
@ -621,7 +627,10 @@ export default function ServerList() {
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
stringList.push({
motd: b.motd,
server: b.name,
});
});
serverList.getMOTDs(stringList).then((c) => {
@ -629,7 +638,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
},
}
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -660,7 +669,10 @@ export default function ServerList() {
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
stringList.push({
motd: b.motd,
server: b.name,
});
});
serverList.getMOTDs(stringList).then((c) => {
@ -668,7 +680,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
},
}
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -699,7 +711,10 @@ export default function ServerList() {
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
stringList.push({
motd: b.motd,
server: b.name,
});
});
serverList.getMOTDs(stringList).then((c) => {
@ -707,7 +722,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
},
}
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -720,7 +735,7 @@ export default function ServerList() {
error: "Error while changing filters",
loading: "Changing filters...",
success: "Changed filters!",
},
}
);
}}
value={(() => {
@ -738,8 +753,8 @@ export default function ServerList() {
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 />
Only allow servers that have the player range 7-15,
and cannot <br />
be Always Online.
</span>
</div>
@ -796,7 +811,10 @@ export default function ServerList() {
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
stringList.push({
motd: b.motd,
server: b.name,
});
});
serverList.getMOTDs(stringList).then((c) => {
@ -804,7 +822,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
},
}
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -887,21 +905,15 @@ export default function ServerList() {
value={ipr}
onValueChange={(v) => {
if (am)
toast(
<span>
These settings will not change over reloads
because you have account specific options enabled
<Button
variant="link"
className="p-0 m-0"
onClick={() =>
router.push("/account/settings/options")
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"),
},
}
>
Change your preferences
</Button>
</span>,
{ icon: "!" },
);
setIPR(v);
}}
@ -925,21 +937,15 @@ export default function ServerList() {
value={padding.toString()}
onValueChange={(v) => {
if (am)
toast(
<span>
These settings will not change over reloads
because you have account specific options enabled
<Button
variant="link"
className="p-0 m-0"
onClick={() =>
router.push("/account/settings/options")
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"),
},
}
>
Change your preferences
</Button>
</span>,
{ icon: "!" },
);
setPadding(v);
}}
@ -954,7 +960,10 @@ export default function ServerList() {
<MenubarRadioItem value="200">200px</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarCheckboxItem checked={pOS} onCheckedChange={setpOS}>
<MenubarCheckboxItem
checked={pOS}
onCheckedChange={setpOS}
>
Only use padding on servers
</MenubarCheckboxItem>
</MenubarSubContent>
@ -979,7 +988,10 @@ export default function ServerList() {
</MenubarSub>
<MenubarSeparator />
<SignedIn>
<MenubarCheckboxItem checked={hero} onCheckedChange={setHero}>
<MenubarCheckboxItem
checked={hero}
onCheckedChange={setHero}
>
Show Hero
</MenubarCheckboxItem>
</SignedIn>
@ -1002,6 +1014,7 @@ export default function ServerList() {
</MenubarContent>
</MenubarMenu>
</Menubar>
</StickyTopbar>
</ClientFadeIn>
<Dialog open={random} onOpenChange={setRandom}>
@ -1063,7 +1076,7 @@ export default function ServerList() {
onClick={() => {
setTextCopied(true);
clipboard.writeText(
randomData.name + ".mshf.minehut.gg",
randomData.name + ".mshf.minehut.gg"
);
toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000);
@ -1103,7 +1116,7 @@ export default function ServerList() {
setLoading(false);
});
}}
loader={<Spinner className="flex items-center" />}
loader={<LoadingSpinner className="flex items-center" />}
endMessage={
<p
style={{ textAlign: "center" }}
@ -1134,8 +1147,45 @@ export default function ServerList() {
" gap-4 sm:grid-cols-2"
}
>
{servers.map((b: any) => (
{servers.map((b: any, i: number) => (
<>
{i === Number(ipr) && affiliates.length != 0 && (
<div
className="border rounded h-[450px] shadow p-10 max-w-full dark:prose-invert prose grid grid-cols-3 max-xl:hidden"
style={{
gridColumn: `span ${ipr} / span ${ipr}`,
}}
>
<div className="dark:text-white text-black max-w-[300px] overflow-auto">
<h2 className="bg-gradient-to-bl from-yellow-300 via-yellow-500 to-yellow-100 bg-clip-text text-transparent">
Affiliates
</h2>
<p>
We have been able to partner with some servers that we
think are high-effort servers that need to be
recognized.
</p>
<p>
Please take some interest in a server you find
interesting and give them some much needed support.
</p>
<small>
These servers have absolutely no financial affiliation
with MHSF.
</small>
</div>
{affiliates
.filter((a) => a.mode.includes("server-list"))
.map((a) => (
<div
className="border rounded p-4 col-span-2"
key={a.name}
>
{a.name}
</div>
))}
</div>
)}
<ServerCard b={b} motd={motdList[b.name]} />
</>
))}

@ -31,15 +31,11 @@
"use client";
import { useState, useEffect } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
BetterHeader,
} from "@/components/ui/card";
import { motion } from "framer-motion";
import {
Tooltip,
TooltipContent,
@ -47,14 +43,23 @@ import {
} from "@/components/ui/tooltip";
import { Badge } from "./ui/badge";
import ServerSingle from "@/lib/single";
import { SignedIn, SignedOut } from "@clerk/nextjs";
import SignInPopoverButton from "./clerk/SignInPopoverButton";
import { Star, X } from "lucide-react";
import { favoriteServer, isFavorited } from "@/lib/api";
import { LoadingButton } from "./ui/loading-button";
import { motion } from "framer-motion";
import { Cake, Check, Heart, Star, Users, X } from "lucide-react";
import {
favoriteServer,
getCommunityServerFavorites,
isFavorited,
} from "@/lib/api";
import { useTheme } from "next-themes";
import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
import { Button } from "./ui/button";
import IconDisplay from "./IconDisplay";
import { useClerk, useUser } from "@clerk/nextjs";
import { LoaderIcon } from "react-hot-toast";
import { Separator } from "./ui/separator";
import { convert } from "@/components/NewChart";
import { LoadingSpinner } from "./ui/loading-spinner";
export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server));
@ -64,6 +69,9 @@ export default function ServerView(props: { server: string }) {
const [loadingFavorite, setLoadingFavorite] = useState(false);
const [randomText, setRandomText] = useState("");
const [lastOnline, setLastOnline] = useState(0);
const { isSignedIn } = useUser();
const [communityFavorited, setCommunityFavorited] = useState(0);
const clerk = useClerk();
const [format, setFormat] = useState("");
const [description, setDescription] = useState("");
const allText = [""];
@ -91,16 +99,19 @@ export default function ServerView(props: { server: string }) {
setLastOnline(online);
}
});
getCommunityServerFavorites(single.grabOffline()?.name as string).then(
(b) => {
setCommunityFavorited(b);
}
);
});
}, []);
if (loading) {
return (
<>
<div className="grid p-4 sm:grid-cols-3 gap-4">
<Skeleton className="sm:col-span-2 h-[245px]" />
<Skeleton className="h-[245px]" />
<div className="p-4">
<Skeleton className="sm:col-span-2 h-[155px]" />
</div>
</>
);
@ -125,8 +136,14 @@ export default function ServerView(props: { server: string }) {
</div>
)}
<FadeIn>
<div className="grid p-4 sm:grid-cols-3 gap-4">
<Card className="sm:col-span-2">
<div className="flex items-center">
<div className="bg-secondary p-4 rounded-lg ml-4">
<IconDisplay
server={single.grabOffline()}
className="flex items-center"
/>
</div>
<div className="block">
<BetterHeader>
<CardTitle className="flex items-center">
{single.grabOnline() == undefined &&
@ -173,103 +190,116 @@ export default function ServerView(props: { server: string }) {
</Tooltip>
)}
{single.getAuthor() != undefined && (
<p>by {single.getAuthor()}</p>
{single.getAuthor() != undefined ? (
<p className="text-lg flex items-center">
by {single.getAuthor()}{" "}
<Button
className="h-7 ml-2"
variant={favorited ? "outline" : "favorite"}
onClick={() => {
if (!isSignedIn) {
clerk.openSignUp();
return;
}
setLoadingFavorite(true);
favoriteServer(
single.grabOffline()?.name as string
).then(() => {
setLoadingFavorite(false);
setFavorited(!favorited);
});
}}
disabled={loadingFavorite}
>
{loadingFavorite && (
<LoadingSpinner className="mr-2 h-4 w-4" />
)}
{!favorited && !loadingFavorite && (
<motion.div
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.25, ease: "linear" }}
>
<Star size={16} className="mr-2" />
</motion.div>
)}
{favorited && !loadingFavorite && (
<motion.div
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.25, ease: "linear" }}
>
<Check size={16} className="mr-2" />
</motion.div>
)}
Favorite{favorited && "d"}
</Button>
</p>
) : (
<p className="text-lg flex items-center">
by Anonymous{" "}
<Button
className="h-7 ml-2"
variant={favorited ? "outline" : "favorite"}
onClick={() => {
if (!isSignedIn) {
clerk.openSignUp();
return;
}
setLoadingFavorite(true);
favoriteServer(
single.grabOffline()?.name as string
).then(() => {
setLoadingFavorite(false);
setFavorited(!favorited);
});
}}
disabled={loadingFavorite}
>
{loadingFavorite && <LoaderIcon className="mr-2" />}
{!favorited && !loadingFavorite && (
<motion.div
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.25, ease: "linear" }}
>
<Star size={16} className="mr-2" />
</motion.div>
)}
{favorited && !loadingFavorite && (
<motion.div
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.25, ease: "linear" }}
>
<Check size={16} className="mr-2" />
</motion.div>
)}
Favorite{favorited && "d"}
</Button>
</p>
)}
</CardDescription>
</BetterHeader>
<CardContent>
<p>
<strong>Time:</strong>
<br />
<i>Last online</i>{" "}
<Tooltip>
<TooltipTrigger>
<code>
{timeConverter(single.grabOffline()?.last_online)}
</code>
</TooltipTrigger>
<TooltipContent>
<code>{single.grabOffline()?.last_online}</code> in Unix
time
</TooltipContent>
</Tooltip>{" "}
<br />
<i>Created on</i>{" "}
<Tooltip>
<TooltipTrigger>
<code>{timeConverter(single.grabOffline()?.creation)}</code>
</TooltipTrigger>
<TooltipContent>
<code>{single.grabOffline()?.creation}</code> in Unix time
</TooltipContent>
</Tooltip>
<p className="text-md font-semibold text-muted-foreground flex items-center">
<>
<Heart className="mr-2" size={24} />
{convert(communityFavorited)}
<Separator orientation="vertical" className="ml-4 h-[30px]" />
</>
<>
<Users className="mr-2 ml-4" size={24} />{" "}
{convert(single.grabOffline()?.joins as number)}
<Separator orientation="vertical" className="ml-4 h-[30px]" />
</>
<>
<Cake className="mr-2 ml-4" size={24} />{" "}
{timeConverter(single.grabOffline()?.creation)}
</>
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Favorite the server?</CardTitle>
<CardDescription>
By favoriting the server, you can see it later.{" "}
<SignedOut>
<strong>You need to sign in to favorite a server.</strong>
</SignedOut>
</CardDescription>
</CardHeader>
<CardContent>
<SignedOut>
<SignInPopoverButton />
</SignedOut>
<SignedIn>
<LoadingButton
variant={resolvedTheme == "dark" ? "outline" : "default"}
loading={loadingFavorite}
onClick={() => {
setLoadingFavorite(true);
favoriteServer(single.grabOffline()?.name as string).then(
() => {
setFavorited(!favorited);
setLoadingFavorite(false);
}
);
}}
>
{favorited && (
<motion.div
animate={{ color: "yellow", fill: "yellow" }}
transition={{ duration: 2 }}
>
<Star
className="mr-2"
size="16"
color="yellow"
fill="yellow"
/>
</motion.div>
)}
{!favorited && (
<motion.div
transition={{ duration: 1 }}
animate={{ color: "yellow", fill: "yellow" }}
>
<Star className="mr-2" size="16" />
</motion.div>
)}
{favorited && "Unf"}
{!favorited && "F"}avorite Server
</LoadingButton>
</SignedIn>
</CardContent>
<CardFooter>
<small>
This is unlike voting. The{" "}
<i>amount of people who favorited are public</i>, but the server
doesn{"'"}t know who favorited, as Favorites are completely
anonymous.
</small>
</CardFooter>
</Card>
</div>
</div>
</FadeIn>
</>

@ -31,9 +31,21 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
declare global {
interface Document {
startViewTransition(updateCallback: () => void):
| {
finished: Promise<void>;
ready: Promise<void>;
updateCallbackDone: Promise<void>;
}
| undefined;
}
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [mounted, setMounted] = React.useState(false);
@ -45,3 +57,57 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
interface UseThemeTransitionResult {
theme: string | undefined;
changeTheme: (changeTheme: string) => void;
mounted: boolean;
}
export function useThemeTransition(): UseThemeTransitionResult {
const { theme, setTheme, systemTheme } = useTheme();
const [mounted, setMounted] = React.useState<boolean>(false);
React.useEffect(() => {
setMounted(true);
}, []);
const changeTheme = (changeTheme: string) => {
if (!mounted) return;
const resolvedTheme = theme === "system" ? systemTheme : changeTheme;
if (document.startViewTransition) {
document.startViewTransition(() => {
const root = document.documentElement;
root.style.setProperty(
"--current-background",
`var(--${resolvedTheme}-background)`
);
root.style.setProperty(
"--current-foreground",
`var(--${resolvedTheme}-foreground)`
);
setTheme(changeTheme);
});
} else {
setTheme(changeTheme);
}
};
React.useEffect(() => {
if (mounted && theme) {
const root = document.documentElement;
root.style.setProperty(
"--current-background",
`var(--${theme}-background)`
);
root.style.setProperty(
"--current-foreground",
`var(--${theme}-foreground)`
);
}
}, [mounted, theme]);
return { theme, changeTheme, mounted };
}

@ -29,10 +29,7 @@
*/
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
@ -41,27 +38,28 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useThemeTransition } from "./ThemeProvider";
export function ModeToggle() {
const { setTheme } = useTheme();
const { changeTheme } = useThemeTransition();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="mr-3">
<Button variant="ghost" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<DropdownMenuItem onClick={() => changeTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<DropdownMenuItem onClick={() => changeTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<DropdownMenuItem onClick={() => changeTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>

@ -36,6 +36,7 @@ import { Card, CardContent } from "../ui/card";
import { Skeleton } from "../ui/skeleton";
import A from "../misc/Link";
import { formalNames } from "@/config/achievements";
import NoItems from "../misc/NoItems";
export default function AchievementList({ server }: { server: string }) {
const [achievements, setAchievements] = useState<
@ -70,6 +71,7 @@ export default function AchievementList({ server }: { server: string }) {
Achievements are earned automatically when the server is online. See{" "}
<A alt="Achievement collection">Docs:Advanced/Achievements</A>
</span>
{achievements.length === 0 && <NoItems />}
{achievements
.filter(
(value, index) => listify(achievements).indexOf(value.type) === index
@ -109,8 +111,6 @@ export default function AchievementList({ server }: { server: string }) {
);
}
type WithInterval<K> = K & {
interval: number;
};

@ -31,7 +31,6 @@
"use client";
import IconDisplay from "@/components/IconDisplay";
import { Badge } from "@/components/ui/badge";
import { Spinner } from "@/components/ui/spinner";
import type { ServerResponse } from "@/lib/types/mh-server";
import { Copy, ExternalLink, ServerCrash } from "lucide-react";
import { notFound, useSearchParams } from "next/navigation";
@ -40,6 +39,7 @@ import { Button } from "../ui/button";
import { CheckmarkIcon } from "react-hot-toast";
import useClipboard from "@/lib/useClipboard";
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
import { LoadingSpinner } from "../ui/loading-spinner";
export default function Embed({ params }: { params: { server: string } }) {
const [serverFound, setServerFound] = useState(true);
@ -65,7 +65,7 @@ export default function Embed({ params }: { params: { server: string } }) {
}, [params]);
if (loading) {
return <Spinner />;
return <LoadingSpinner />;
}
if (!serverFound) {

@ -39,7 +39,7 @@ import { codeToHtml } from "shiki";
import { useTheme } from "next-themes";
import { Asterisk, Copy } from "lucide-react";
import useClipboard from "@/lib/useClipboard";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { Checkbox } from "../ui/checkbox";
import {
Select,
@ -64,7 +64,7 @@ export default function EmbedSelector({ server }: { server: string }) {
src="${url}"
width={390}
height={145}
style={{ borderRadius: 0.25 }}
style={{ borderRadius: "0.25rem" }}
allow="clipboard-write"
frameBorder={0}
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
@ -93,7 +93,7 @@ export default function EmbedSelector({ server }: { server: string }) {
src="${url}"
width={390}
height={145}
style={{ borderRadius: 0.25 }}
style={{ borderRadius: "0.25rem" }}
allow="clipboard-write"
frameBorder={0}
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"

@ -0,0 +1,57 @@
/*
* 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 QRCode from "react-qr-code";
import { DrawerFooter, DrawerTrigger } from "../ui/drawer";
import { Button } from "../ui/button";
import { useTheme } from "next-themes";
export default function QRCodeGenerator({ server }: { server: string }) {
const { resolvedTheme } = useTheme();
return (
<div className="w-full">
<QRCode
value={"https://mhsf.app/server/" + server + "?source=qrCode"}
className="flex flex-col items-center w-full py-4"
style={{
backgroundColor: resolvedTheme === "dark" ? "#fff" : undefined,
}}
/>
<DrawerFooter>
<DrawerTrigger asChild>
<Button>Close</Button>
</DrawerTrigger>
</DrawerFooter>
</div>
);
}

@ -1,5 +1,4 @@
"use client";
"use client";;
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
@ -15,8 +14,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { setCustomization } from "@/lib/api";
import { useEffect } from "react";
import toast from "react-hot-toast";
import { toast } from "sonner";
import ColorProvider from "../ColorProvider";
const FormSchema = z.object({

@ -47,7 +47,7 @@ import { Input } from "@/components/ui/input";
import { setCustomization } from "@/lib/api";
import { useEffect, useState } from "react";
import ColorProvider from "../ColorProvider";
import toast from "react-hot-toast";
import { toast } from "sonner";
const FormSchema = z.object({
id: z.string().min(2, {

@ -49,7 +49,7 @@ import {
InputOTPSlot,
} from "@/components/ui/input-otp";
import { linkMCAccount } from "@/lib/api";
import toast from "react-hot-toast";
import { toast } from "sonner";
import {
Dialog,
DialogContent,

@ -0,0 +1,48 @@
/*
* 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 { DatabaseZap } from "lucide-react";
export default function NoItems() {
return (
<>
<div className="flex flex-col items-center justify-center p-4 pt-10">
<DatabaseZap
className="text-2xl font-semibold text-gray-600"
size={32}
/>
<p className="text-xl text-gray-600 mt-2">
Huh, we tried to find something, but nothing was found.
</p>
</div>
</>
);
}

@ -0,0 +1,68 @@
/*
* 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 { useEffect, useState, ReactNode } from "react";
export default function StickyTopbar({
children,
scrollElevation,
className,
}: {
children: ReactNode;
scrollElevation: number;
className?: string;
}) {
const [isSticky, setIsSticky] = useState(false);
const handleScroll = () => {
if (window.scrollY > scrollElevation) {
setIsSticky(true);
} else {
setIsSticky(false);
}
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div
className={`transition-all duration-300 ${isSticky ? "fixed top-[70px] left-0 w-full backdrop-blur shadow-lg " + className : "block w-full bg-transparent"}`}
>
{children}
</div>
);
}

@ -31,7 +31,7 @@
"use client";
import { useState } from "react";
import { Button } from "../ui/button";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { Check } from "lucide-react";
import useClipboard from "@/lib/useClipboard";

@ -31,20 +31,10 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { Toaster } from "../ui/sonner";
export default function ThemedToaster() {
const { resolvedTheme } = useTheme();
return (
<Toaster
position="bottom-center"
reverseOrder={false}
toastOptions={
resolvedTheme == "dark"
? { style: { background: "#333", color: "#fff" } }
: undefined
}
/>
);
return <Toaster position="bottom-center" richColors />;
}

@ -50,6 +50,8 @@ const buttonVariants = cva(
ghost:
"hover:bg-accent hover:text-accent-foreground focus:ring-4 focus:ring-neutral-100 focus:ring-offset-current dark:focus:ring-neutral-900 duration-150 ease-in-out transition-all",
link: "text-primary underline-offset-4 hover:underline",
favorite:
"text-black rounded-lg hover:bg-primary/90 focus:ring-4 focus:ring-yellow-400/60 focus:ring-offset-current dark:focus:ring-yellow-400/60 duration-150 ease-in-out transition-all bg-gradient-to-bl from-yellow-300 via-yellow-500 to-yellow-100",
},
size: {
default: "h-10 px-4 py-2",

@ -0,0 +1,64 @@
/*
* 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 { cn } from "@/lib/utils";
export function LoadingSpinner({ className }: { className?: string }) {
return (
<div className={cn("h-5 w-5", className)}>
<div
style={{
position: "relative",
top: "50%",
left: "50%",
}}
className={cn("loading-spinner", "h-5 w-5", className)}
>
{[...Array(12)].map((_, i) => (
<div
key={i}
style={{
animationDelay: `${-1.2 + 0.1 * i}s`,
background: "gray",
position: "absolute",
borderRadius: "1rem",
width: "30%",
height: "8%",
left: "-10%",
top: "-4%",
transform: `rotate(${30 * i}deg) translate(120%)`,
}}
className="animate-spinner"
/>
))}
</div>
</div>
);
}

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

@ -59,7 +59,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}

42
src/config/affiliates.ts Normal file

@ -0,0 +1,42 @@
/*
* 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 { SVGProps } from "react";
export const affiliates: {
name: string;
shop: string;
line: string;
mode: string[];
otherLinks: {
name: string;
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
}[];
}[] = [];

@ -55,8 +55,30 @@ const FeatureList = ({
);
};
export const version = "1.4.0";
export const version = "1.6.0";
export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
{
id: "h9jr2cbxn7qwfvt5uypsdg",
name: "v1.6.0",
changelog: (
<FeatureList
features={[
"Completely redid top of server view",
"Favorite counts are now prominent on the server view",
"New theme transition (smooth)",
"New favorite button",
"Added more padding in the server view",
"Separated the tabs on the side for sharing actions",
"Added new QR code generator",
]}
title={
<strong className="flex items-center">
Version 1.6.0 (November 17th 2024)
</strong>
}
/>
),
},
{
id: "r9swempc7kaqd2j84nutv5",
name: "v1.5.0",

36
src/lib/head.ts Normal file

@ -0,0 +1,36 @@
/*
* 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.
*/
export async function getMinecraftHead(username: string) {
const uuidRequest = await fetch("https://api.mojang.com/users/profiles/minecraft/" + username);
const uuid = (await uuidRequest.json()).id;
return `https://crafatar.com/avatars/${uuid}`;
}

@ -29,7 +29,7 @@
*/
import { OnlineServer } from "./types/mh-server";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { getMOTDFromServer } from "./api";
var numberOfItemsInView = 20;

@ -30,7 +30,7 @@
import { serverOwned } from "./api";
import { OnlineServer, ServerResponse } from "./types/mh-server";
import toast from "react-hot-toast";
import { toast } from "sonner";
export default class ServerSingle {
private name = "";

@ -28,7 +28,7 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import toast from "react-hot-toast"
import { toast } from "sonner";
/** A hook to properly write text to the clipboard without triggering a client-side error
* @version 1.0

@ -178,6 +178,14 @@ const config = {
transform: "none",
},
},
spinner: {
"0%": {
opacity: "1",
},
"100%": {
opacity: "0",
},
},
shimmer: {
"0%, 90%, 100%": {
"background-position": "calc(-100% - var(--shimmer-width)) 0",
@ -194,6 +202,7 @@ const config = {
"accordion-up": "accordion-up 0.2s ease-out",
"image-glow": "image-glow 4100ms 600ms ease-out forwards",
shimmer: "shimmer 8s infinite",
spinner: "spinner 1.2s linear infinite",
"fade-in": "fade-in 1000ms var(--animation-delay, 0ms) ease forwards",
"caret-blink": "caret-blink 1.25s ease-out infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",

@ -4322,9 +4322,9 @@ globby@^11.1.0:
slash "^3.0.0"
goober@^2.1.10:
version "2.1.14"
resolved "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz"
integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==
version "2.1.16"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95"
integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==
gopd@^1.0.1:
version "1.0.1"
@ -6344,10 +6344,10 @@ next-css-obfuscator@^2.2.16:
recoverable-random "^1.0.5"
yargs "^17.7.2"
next-themes@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz"
integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==
next-themes@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.3.tgz#ea54552d5986936d177eed393ea50b658ae44800"
integrity sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA==
next@14.2.10:
version "14.2.10"
@ -6674,10 +6674,10 @@ periscopic@^3.0.0:
estree-walker "^3.0.0"
is-reference "^3.0.0"
picocolors@^1.0.0, picocolors@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
@ -6792,13 +6792,13 @@ postcss@8.4.31:
source-map-js "^1.0.2"
postcss@^8, postcss@^8.4.23, postcss@^8.4.38, postcss@^8.4.39:
version "8.4.39"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
version "8.4.49"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.1"
source-map-js "^1.2.0"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
@ -6847,6 +6847,11 @@ punycode@^2.1.0, punycode@^2.3.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
@ -6885,7 +6890,7 @@ react-hook-form@^7.52.2:
react-hot-toast@^2.4.1:
version "2.4.1"
resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==
dependencies:
goober "^2.1.10"
@ -6923,6 +6928,14 @@ react-markdown@^9.0.1:
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-qr-code@^2.0.15:
version "2.0.15"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.15.tgz#fbfc12952c504bcd64275647e9d1ea63251742ce"
integrity sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==
dependencies:
prop-types "^15.8.1"
qr.js "0.0.0"
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz"
@ -7409,10 +7422,15 @@ snakecase-keys@5.4.4:
snake-case "^3.0.4"
type-fest "^2.5.2"
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
sonner@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.0.tgz#f59a2a70e049a179b6fbd1bb1bf2619d5ced07c0"
integrity sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-resolve@^0.5.2:
version "0.5.3"