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-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-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",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",

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

@ -130,7 +130,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="general" /> <TabServer server={params.server} tabDef="general" />
<div className="pt-8"> <div className="pt-8">

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

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

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

@ -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,22 @@ 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";
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 +68,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 +98,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 +135,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 +189,114 @@ 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 && <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> </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,7 +31,7 @@
"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";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
@ -45,3 +45,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;
}; };

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

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

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