feat: redid server view

This commit is contained in:
dvelo 2024-11-17 21:10:02 -06:00
parent fd40a8e143
commit 346f9d210e
18 changed files with 581 additions and 278 deletions

@ -53,6 +53,7 @@
"react-dom": "^18",
"react-fade-in": "^2.0.1",
"react-fast-marquee": "^1.6.5",
"react-qr-code": "^2.0.15",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0",

@ -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">

@ -130,7 +130,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="general" />
<div className="pt-8">

@ -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,7 +37,7 @@ 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";
@ -56,6 +56,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 +67,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 +108,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 +147,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 +181,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 +470,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

@ -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} />

@ -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";

@ -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,22 @@ 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";
export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server));
@ -64,6 +68,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 +98,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 +135,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 +189,114 @@ 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 && <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>
) : (
<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,7 +31,7 @@
"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";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
@ -45,3 +45,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;
};

@ -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>
);
}

@ -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>
</>
);
}

@ -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",

@ -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}`;
}

@ -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"
@ -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"