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

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

@ -84,12 +84,7 @@ export default async function RootLayout({
}>) { }>) {
return ( return (
<ClerkThemeProvider className={GeistSans.className}> <ClerkThemeProvider className={GeistSans.className}>
<ThemeProvider <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TooltipProvider> <TooltipProvider>
{banner.isBanner && ( {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"> <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 Banner from "@/components/Banner";
import ColorProvider from "@/components/ColorProvider"; import ColorProvider from "@/components/ColorProvider";
import ServerView from "@/components/ServerView"; import ServerView from "@/components/ServerView";
import StickyTopbar from "@/components/misc/StickyTopbar";
import TabServer from "@/components/misc/TabServer"; import TabServer from "@/components/misc/TabServer";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
@ -130,9 +131,11 @@ export default function ServerPage({ params }: { params: { server: string } }) {
return ( return (
<main> <main>
<ColorProvider server={params.server}> <ColorProvider server={params.server}>
<div className={"pt-16"}> <div className={"pt-16 xl:px-[100px]"}>
<Banner server={params.server} /> <Banner server={params.server} />
<StickyTopbar scrollElevation={100} className="pt-4">
<TabServer server={params.server} tabDef="general" /> <TabServer server={params.server} tabDef="general" />
</StickyTopbar>
<div className="pt-8"> <div className="pt-8">
<ServerView server={params.server} /> <ServerView server={params.server} />
</div> </div>

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

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

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

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

@ -30,13 +30,12 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Spinner } from "./ui/spinner";
import { Card, CardHeader, CardTitle } from "./ui/card"; import { Card, CardHeader, CardTitle } from "./ui/card";
import type { ServerResponse } from "@/lib/types/mh-server"; import type { ServerResponse } from "@/lib/types/mh-server";
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Copy, Layers, XIcon } from "lucide-react"; 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 { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { getAccountFavorites } from "@/lib/api"; import { getAccountFavorites } from "@/lib/api";
import { useRouter } from "@/lib/useRouter"; 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(); var result: string = value.toString();
if (value >= 1000000) { if (value >= 1000000) {
result = Math.floor(value / 1000000) + "m"; result = Math.floor(value / 1000000) + "m";

@ -45,7 +45,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "./ui/switch"; import { Switch } from "./ui/switch";
import { setAccountSL } from "@/lib/api"; import { setAccountSL } from "@/lib/api";
import toast from "react-hot-toast"; import { toast } from "sonner";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
export function SLCustomize() { 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 { MiniJoinsChart } from "@/components/misc/MiniJoinsChart";
import { import {
ContextMenu, ContextMenu,
@ -66,7 +35,8 @@ import {
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; 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 IconDisplay from "./IconDisplay";
import { TagShower } from "./ServerList"; import { TagShower } from "./ServerList";
import { Button } from "./ui/button"; import { Button } from "./ui/button";

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

@ -85,7 +85,7 @@ import { CommandIcon } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast"; import { toast } from "sonner";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import ClientFadeIn from "./ClientFadeIn"; import ClientFadeIn from "./ClientFadeIn";
import IconDisplay from "./IconDisplay"; import IconDisplay from "./IconDisplay";
@ -100,7 +100,9 @@ import { pageFind } from "./misc/Link";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Skeleton } from "./ui/skeleton"; 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 = [ const features = [
{ {
@ -167,7 +169,7 @@ export default function ServerList() {
Array<(server: OnlineServer) => Promise<boolean>> Array<(server: OnlineServer) => Promise<boolean>>
>([]); >([]);
const [randomData, setRandomData] = useState<OnlineServer | undefined>( const [randomData, setRandomData] = useState<OnlineServer | undefined>(
undefined, undefined
); );
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const [color, setColor] = useState("#ffffff"); const [color, setColor] = useState("#ffffff");
@ -349,7 +351,7 @@ export default function ServerList() {
className={cn( className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " + "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] " + "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={() => onClick={() =>
router.push(pageFind(`Server:${server.name}`)) router.push(pageFind(`Server:${server.name}`))
@ -383,7 +385,7 @@ export default function ServerList() {
className={cn( className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " + "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] " + "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}`)} onClick={() => router.push(`/server/${server.name}`)}
> >
@ -519,7 +521,8 @@ export default function ServerList() {
<br id="serverlist" className="pb-14" /> <br id="serverlist" className="pb-14" />
<Separator /> <Separator />
<ClientFadeIn delay={100}> <ClientFadeIn delay={100}>
<Menubar className="mt-3 ml-2 border rounded p-2 shadow"> <StickyTopbar scrollElevation={250} className="p-2">
<Menubar className="mt-3 border rounded shadow">
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>Servers</MenubarTrigger> <MenubarTrigger>Servers</MenubarTrigger>
<MenubarContent> <MenubarContent>
@ -558,7 +561,10 @@ export default function ServerList() {
let obj: any = {}; let obj: any = {};
serverList.currentServers.forEach((b) => { 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) => { serverList.getMOTDs(stringList).then((c) => {
@ -566,7 +572,7 @@ export default function ServerList() {
c.forEach( c.forEach(
(b: { server: string; motd: string }) => { (b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd; updatedSL[b.server] = b.motd;
}, }
); );
setMotdList(updatedSL); setMotdList(updatedSL);
setServers(serverList.currentServers); setServers(serverList.currentServers);
@ -582,7 +588,7 @@ export default function ServerList() {
success: "Succesfully refreshed servers", success: "Succesfully refreshed servers",
loading: "Refreshing...", loading: "Refreshing...",
error: "Error while refreshing", error: "Error while refreshing",
}, }
); );
}} }}
> >
@ -621,7 +627,10 @@ export default function ServerList() {
let obj: any = {}; let obj: any = {};
serverList.currentServers.forEach((b) => { 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) => { serverList.getMOTDs(stringList).then((c) => {
@ -629,7 +638,7 @@ export default function ServerList() {
c.forEach( c.forEach(
(b: { server: string; motd: string }) => { (b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd; updatedSL[b.server] = b.motd;
}, }
); );
setMotdList(updatedSL); setMotdList(updatedSL);
setServers(serverList.currentServers); setServers(serverList.currentServers);
@ -660,7 +669,10 @@ export default function ServerList() {
let obj: any = {}; let obj: any = {};
serverList.currentServers.forEach((b) => { 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) => { serverList.getMOTDs(stringList).then((c) => {
@ -668,7 +680,7 @@ export default function ServerList() {
c.forEach( c.forEach(
(b: { server: string; motd: string }) => { (b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd; updatedSL[b.server] = b.motd;
}, }
); );
setMotdList(updatedSL); setMotdList(updatedSL);
setServers(serverList.currentServers); setServers(serverList.currentServers);
@ -699,7 +711,10 @@ export default function ServerList() {
let obj: any = {}; let obj: any = {};
serverList.currentServers.forEach((b) => { 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) => { serverList.getMOTDs(stringList).then((c) => {
@ -707,7 +722,7 @@ export default function ServerList() {
c.forEach( c.forEach(
(b: { server: string; motd: string }) => { (b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd; updatedSL[b.server] = b.motd;
}, }
); );
setMotdList(updatedSL); setMotdList(updatedSL);
setServers(serverList.currentServers); setServers(serverList.currentServers);
@ -720,7 +735,7 @@ export default function ServerList() {
error: "Error while changing filters", error: "Error while changing filters",
loading: "Changing filters...", loading: "Changing filters...",
success: "Changed filters!", success: "Changed filters!",
}, }
); );
}} }}
value={(() => { value={(() => {
@ -738,8 +753,8 @@ export default function ServerList() {
Only allow smaller servers Only allow smaller servers
<br /> <br />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Only allow servers that have the player range 7-15, and Only allow servers that have the player range 7-15,
cannot <br /> and cannot <br />
be Always Online. be Always Online.
</span> </span>
</div> </div>
@ -796,7 +811,10 @@ export default function ServerList() {
let obj: any = {}; let obj: any = {};
serverList.currentServers.forEach((b) => { 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) => { serverList.getMOTDs(stringList).then((c) => {
@ -804,7 +822,7 @@ export default function ServerList() {
c.forEach( c.forEach(
(b: { server: string; motd: string }) => { (b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd; updatedSL[b.server] = b.motd;
}, }
); );
setMotdList(updatedSL); setMotdList(updatedSL);
setServers(serverList.currentServers); setServers(serverList.currentServers);
@ -887,21 +905,15 @@ export default function ServerList() {
value={ipr} value={ipr}
onValueChange={(v) => { onValueChange={(v) => {
if (am) if (am)
toast( toast.warning(
<span> "These settings will not change over reloads because you have account specific options enabled",
These settings will not change over reloads {
because you have account specific options enabled action: {
<Button label: "Check settings",
variant="link" onClick: () =>
className="p-0 m-0" router.push("/account/settings/options"),
onClick={() => },
router.push("/account/settings/options")
} }
>
Change your preferences
</Button>
</span>,
{ icon: "!" },
); );
setIPR(v); setIPR(v);
}} }}
@ -925,21 +937,15 @@ export default function ServerList() {
value={padding.toString()} value={padding.toString()}
onValueChange={(v) => { onValueChange={(v) => {
if (am) if (am)
toast( toast.warning(
<span> "These settings will not change over reloads because you have account specific options enabled",
These settings will not change over reloads {
because you have account specific options enabled action: {
<Button label: "Check settings",
variant="link" onClick: () =>
className="p-0 m-0" router.push("/account/settings/options"),
onClick={() => },
router.push("/account/settings/options")
} }
>
Change your preferences
</Button>
</span>,
{ icon: "!" },
); );
setPadding(v); setPadding(v);
}} }}
@ -954,7 +960,10 @@ export default function ServerList() {
<MenubarRadioItem value="200">200px</MenubarRadioItem> <MenubarRadioItem value="200">200px</MenubarRadioItem>
</MenubarRadioGroup> </MenubarRadioGroup>
<MenubarSeparator /> <MenubarSeparator />
<MenubarCheckboxItem checked={pOS} onCheckedChange={setpOS}> <MenubarCheckboxItem
checked={pOS}
onCheckedChange={setpOS}
>
Only use padding on servers Only use padding on servers
</MenubarCheckboxItem> </MenubarCheckboxItem>
</MenubarSubContent> </MenubarSubContent>
@ -979,7 +988,10 @@ export default function ServerList() {
</MenubarSub> </MenubarSub>
<MenubarSeparator /> <MenubarSeparator />
<SignedIn> <SignedIn>
<MenubarCheckboxItem checked={hero} onCheckedChange={setHero}> <MenubarCheckboxItem
checked={hero}
onCheckedChange={setHero}
>
Show Hero Show Hero
</MenubarCheckboxItem> </MenubarCheckboxItem>
</SignedIn> </SignedIn>
@ -1002,6 +1014,7 @@ export default function ServerList() {
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
</Menubar> </Menubar>
</StickyTopbar>
</ClientFadeIn> </ClientFadeIn>
<Dialog open={random} onOpenChange={setRandom}> <Dialog open={random} onOpenChange={setRandom}>
@ -1063,7 +1076,7 @@ export default function ServerList() {
onClick={() => { onClick={() => {
setTextCopied(true); setTextCopied(true);
clipboard.writeText( clipboard.writeText(
randomData.name + ".mshf.minehut.gg", randomData.name + ".mshf.minehut.gg"
); );
toast.success("Copied!"); toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000); setTimeout(() => setTextCopied(false), 1000);
@ -1103,7 +1116,7 @@ export default function ServerList() {
setLoading(false); setLoading(false);
}); });
}} }}
loader={<Spinner className="flex items-center" />} loader={<LoadingSpinner className="flex items-center" />}
endMessage={ endMessage={
<p <p
style={{ textAlign: "center" }} style={{ textAlign: "center" }}
@ -1134,8 +1147,45 @@ export default function ServerList() {
" gap-4 sm:grid-cols-2" " 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]} /> <ServerCard b={b} motd={motdList[b.name]} />
</> </>
))} ))}

@ -31,15 +31,11 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { import {
Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter,
CardHeader,
CardTitle, CardTitle,
BetterHeader, BetterHeader,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { motion } from "framer-motion";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -47,14 +43,23 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import ServerSingle from "@/lib/single"; import ServerSingle from "@/lib/single";
import { SignedIn, SignedOut } from "@clerk/nextjs"; import { motion } from "framer-motion";
import SignInPopoverButton from "./clerk/SignInPopoverButton"; import { Cake, Check, Heart, Star, Users, X } from "lucide-react";
import { Star, X } from "lucide-react"; import {
import { favoriteServer, isFavorited } from "@/lib/api"; favoriteServer,
import { LoadingButton } from "./ui/loading-button"; getCommunityServerFavorites,
isFavorited,
} from "@/lib/api";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn"; 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 }) { export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server)); 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 [loadingFavorite, setLoadingFavorite] = useState(false);
const [randomText, setRandomText] = useState(""); const [randomText, setRandomText] = useState("");
const [lastOnline, setLastOnline] = useState(0); const [lastOnline, setLastOnline] = useState(0);
const { isSignedIn } = useUser();
const [communityFavorited, setCommunityFavorited] = useState(0);
const clerk = useClerk();
const [format, setFormat] = useState(""); const [format, setFormat] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const allText = [""]; const allText = [""];
@ -91,16 +99,19 @@ export default function ServerView(props: { server: string }) {
setLastOnline(online); setLastOnline(online);
} }
}); });
getCommunityServerFavorites(single.grabOffline()?.name as string).then(
(b) => {
setCommunityFavorited(b);
}
);
}); });
}, []); }, []);
if (loading) { if (loading) {
return ( return (
<> <>
<div className="grid p-4 sm:grid-cols-3 gap-4"> <div className="p-4">
<Skeleton className="sm:col-span-2 h-[245px]" /> <Skeleton className="sm:col-span-2 h-[155px]" />
<Skeleton className="h-[245px]" />
</div> </div>
</> </>
); );
@ -125,8 +136,14 @@ export default function ServerView(props: { server: string }) {
</div> </div>
)} )}
<FadeIn> <FadeIn>
<div className="grid p-4 sm:grid-cols-3 gap-4"> <div className="flex items-center">
<Card className="sm:col-span-2"> <div className="bg-secondary p-4 rounded-lg ml-4">
<IconDisplay
server={single.grabOffline()}
className="flex items-center"
/>
</div>
<div className="block">
<BetterHeader> <BetterHeader>
<CardTitle className="flex items-center"> <CardTitle className="flex items-center">
{single.grabOnline() == undefined && {single.grabOnline() == undefined &&
@ -173,103 +190,116 @@ export default function ServerView(props: { server: string }) {
</Tooltip> </Tooltip>
)} )}
{single.getAuthor() != undefined && ( {single.getAuthor() != undefined ? (
<p>by {single.getAuthor()}</p> <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> </CardDescription>
</BetterHeader> </BetterHeader>
<CardContent> <CardContent>
<p> <p className="text-md font-semibold text-muted-foreground flex items-center">
<strong>Time:</strong> <>
<br /> <Heart className="mr-2" size={24} />
<i>Last online</i>{" "} {convert(communityFavorited)}
<Tooltip> <Separator orientation="vertical" className="ml-4 h-[30px]" />
<TooltipTrigger> </>
<code> <>
{timeConverter(single.grabOffline()?.last_online)} <Users className="mr-2 ml-4" size={24} />{" "}
</code> {convert(single.grabOffline()?.joins as number)}
</TooltipTrigger> <Separator orientation="vertical" className="ml-4 h-[30px]" />
<TooltipContent> </>
<code>{single.grabOffline()?.last_online}</code> in Unix <>
time <Cake className="mr-2 ml-4" size={24} />{" "}
</TooltipContent> {timeConverter(single.grabOffline()?.creation)}
</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> </p>
</CardContent> </CardContent>
</Card> </div>
<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> </FadeIn>
</> </>

@ -31,9 +31,21 @@
"use client"; "use client";
import * as React from "react"; 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"; 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) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [mounted, setMounted] = React.useState(false); const [mounted, setMounted] = React.useState(false);
@ -45,3 +57,57 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; 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"; "use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -41,27 +38,28 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useThemeTransition } from "./ThemeProvider";
export function ModeToggle() { export function ModeToggle() {
const { setTheme } = useTheme(); const { changeTheme } = useThemeTransition();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <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" /> <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" /> <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> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}> <DropdownMenuItem onClick={() => changeTheme("light")}>
Light Light
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}> <DropdownMenuItem onClick={() => changeTheme("dark")}>
Dark Dark
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}> <DropdownMenuItem onClick={() => changeTheme("system")}>
System System
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

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

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

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

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

@ -49,7 +49,7 @@ import {
InputOTPSlot, InputOTPSlot,
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import { linkMCAccount } from "@/lib/api"; import { linkMCAccount } from "@/lib/api";
import toast from "react-hot-toast"; import { toast } from "sonner";
import { import {
Dialog, Dialog,
DialogContent, 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"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import toast from "react-hot-toast"; import { toast } from "sonner";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import useClipboard from "@/lib/useClipboard"; import useClipboard from "@/lib/useClipboard";

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

@ -50,6 +50,8 @@ const buttonVariants = cva(
ghost: 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", "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", 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: { size: {
default: "h-10 px-4 py-2", 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 <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...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 }[] = [ 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", id: "r9swempc7kaqd2j84nutv5",
name: "v1.5.0", 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 { OnlineServer } from "./types/mh-server";
import toast from "react-hot-toast"; import { toast } from "sonner";
import { getMOTDFromServer } from "./api"; import { getMOTDFromServer } from "./api";
var numberOfItemsInView = 20; var numberOfItemsInView = 20;

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

@ -28,7 +28,7 @@
* OTHER DEALINGS IN THE SOFTWARE. * 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 /** A hook to properly write text to the clipboard without triggering a client-side error
* @version 1.0 * @version 1.0

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

@ -4322,9 +4322,9 @@ globby@^11.1.0:
slash "^3.0.0" slash "^3.0.0"
goober@^2.1.10: goober@^2.1.10:
version "2.1.14" version "2.1.16"
resolved "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz" resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95"
integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg== integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
@ -6344,10 +6344,10 @@ next-css-obfuscator@^2.2.16:
recoverable-random "^1.0.5" recoverable-random "^1.0.5"
yargs "^17.7.2" yargs "^17.7.2"
next-themes@^0.3.0: next-themes@^0.4.3:
version "0.3.0" version "0.4.3"
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.3.tgz#ea54552d5986936d177eed393ea50b658ae44800"
integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== integrity sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA==
next@14.2.10: next@14.2.10:
version "14.2.10" version "14.2.10"
@ -6674,10 +6674,10 @@ periscopic@^3.0.0:
estree-walker "^3.0.0" estree-walker "^3.0.0"
is-reference "^3.0.0" is-reference "^3.0.0"
picocolors@^1.0.0, picocolors@^1.0.1: picocolors@^1.0.0, picocolors@^1.1.1:
version "1.0.1" version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
@ -6792,13 +6792,13 @@ postcss@8.4.31:
source-map-js "^1.0.2" source-map-js "^1.0.2"
postcss@^8, postcss@^8.4.23, postcss@^8.4.38, postcss@^8.4.39: postcss@^8, postcss@^8.4.23, postcss@^8.4.38, postcss@^8.4.39:
version "8.4.39" version "8.4.49"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
dependencies: dependencies:
nanoid "^3.3.7" nanoid "^3.3.7"
picocolors "^1.0.1" picocolors "^1.1.1"
source-map-js "^1.2.0" source-map-js "^1.2.1"
prelude-ls@^1.2.1: prelude-ls@^1.2.1:
version "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" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== 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: queue-microtask@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" 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: react-hot-toast@^2.4.1:
version "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== integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==
dependencies: dependencies:
goober "^2.1.10" goober "^2.1.10"
@ -6923,6 +6928,14 @@ react-markdown@^9.0.1:
unist-util-visit "^5.0.0" unist-util-visit "^5.0.0"
vfile "^6.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: 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" version "2.3.6"
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz" 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" snake-case "^3.0.4"
type-fest "^2.5.2" type-fest "^2.5.2"
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: sonner@^1.7.0:
version "1.2.0" version "1.7.0"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.0.tgz#f59a2a70e049a179b6fbd1bb1bf2619d5ced07c0"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== 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: source-map-resolve@^0.5.2:
version "0.5.3" version "0.5.3"