feat: fade-in + skeletons

This commit is contained in:
dvelo 2024-08-18 01:15:27 -05:00
parent d810a48dc8
commit f0584f0dfb
10 changed files with 933 additions and 827 deletions

@ -1,6 +1,6 @@
{ {
"name": "mh-stats", "name": "mh-stats",
"version": "0.10.0", "version": "0.10.2",
"private": true, "private": true,
"packageManager": "yarn@1.22.22", "packageManager": "yarn@1.22.22",
"scripts": { "scripts": {

@ -20,6 +20,7 @@ import { Inter as interFont } from "next/font/google";
import { CommandBarer } from "@/components/CommandBar"; import { CommandBarer } from "@/components/CommandBar";
import ThemedToaster from "@/components/misc/ThemedToaster"; import ThemedToaster from "@/components/misc/ThemedToaster";
import UnofficalDialog from "@/components/misc/UnofficalDialog"; import UnofficalDialog from "@/components/misc/UnofficalDialog";
import ClientFadeIn from "@/components/ClientFadeIn";
const inter = interFont({ variable: "--font-inter", subsets: ["latin"] }); const inter = interFont({ variable: "--font-inter", subsets: ["latin"] });
export default async function RootLayout({ export default async function RootLayout({
@ -63,7 +64,7 @@ export default async function RootLayout({
</div> </div>
<div> <div>
<NextTopLoader /> <NextTopLoader />
{children} <ClientFadeIn>{children}</ClientFadeIn>
</div>{" "} </div>{" "}
<ThemedToaster /> <ThemedToaster />
<CommandBarer /> <CommandBarer />

@ -4,11 +4,13 @@ import { useEffect, useState } from "react";
import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card"; import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import FadeIn from "react-fade-in/lib/FadeIn";
export default function AfterServerView({ server }: { server: string }) { export default function AfterServerView({ server }: { server: string }) {
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [discord, setDiscord] = useState(""); const [discord, setDiscord] = useState("");
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
getCustomization(server).then((b) => { getCustomization(server).then((b) => {
@ -16,36 +18,42 @@ export default function AfterServerView({ server }: { server: string }) {
setDescription(b.description == null ? "" : b.description); setDescription(b.description == null ? "" : b.description);
setDiscord(b.discord == null ? "" : b.discord); setDiscord(b.discord == null ? "" : b.discord);
} }
setLoading(false);
}); });
}, []); }, []);
if (loading) return <></>;
return ( return (
<div className="grid sm:grid-cols-4 pl-4 pr-4 gap-3.5"> <>
{description != "" && ( <FadeIn>
<Card className="sm:col-span-3"> <div className="grid sm:grid-cols-4 pl-4 pr-4 gap-3.5">
<CardDescription className="p-4 prose dark:prose-invert"> {description != "" && (
<Markdown disallowedElements={["img"]}>{description}</Markdown> <Card className="sm:col-span-3">
</CardDescription> <CardDescription className="p-4 prose dark:prose-invert">
</Card> <Markdown disallowedElements={["img"]}>{description}</Markdown>
)} </CardDescription>
{discord != "" && ( </Card>
<Card> )}
<CardHeader> {discord != "" && (
<CardTitle>Discord Server</CardTitle> <Card>
<CardDescription className="p-4 prose dark:prose-invert"> <CardHeader>
<iframe <CardTitle>Discord Server</CardTitle>
src={`https://discord.com/widget?id=${discord}&theme=${resolvedTheme}`} <CardDescription className="p-4 prose dark:prose-invert">
height="500" <iframe
allowTransparency={true} src={`https://discord.com/widget?id=${discord}&theme=${resolvedTheme}`}
className="rounded-lg lg:w-[350px]" height="500"
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts" allowTransparency={true}
/> className="rounded-lg lg:w-[350px]"
</CardDescription> sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
</CardHeader> />
</Card> </CardDescription>
)} </CardHeader>
</Card>
)}
<br /> <br />
</div> </div>
</FadeIn>
</>
); );
} }

@ -0,0 +1,12 @@
"use client";
import FadeIn from "react-fade-in";
export default function ClientFadeIn({
children,
delay = 0,
}: {
children: React.ReactNode;
delay?: number;
}) {
return <FadeIn delay={delay}>{children}</FadeIn>;
}

@ -18,6 +18,8 @@ import { Separator } from "./ui/separator";
import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card"; import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card";
import Link from "next/link"; import Link from "next/link";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
export default function FavoriteSortView() { export default function FavoriteSortView() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -55,7 +57,23 @@ export default function FavoriteSortView() {
}, []); }, []);
if (loading) { if (loading) {
return <Spinner className="flex items-center" />; return (
<>
<div className="grid grid-cols-2 gap-4">
<Skeleton className="h-[112px] rounded-xl" />
<Skeleton className="h-[112px] rounded-xl" />
</div>
<br />
<Separator />
<br />
<div className="grid grid-cols-4 gap-4">
<Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
</div>
</>
);
} }
return ( return (
@ -200,66 +218,68 @@ export default function FavoriteSortView() {
} }
style={{ overflow: "hidden", paddingLeft: 6 }} style={{ overflow: "hidden", paddingLeft: 6 }}
> >
<div className="grid sm:grid-cols-4 gap-4"> <FadeIn>
{allItems.map((v) => { <div className="grid sm:grid-cols-4 gap-4">
if (v.favorites == 0) { {allItems.map((v) => {
return <></>; if (v.favorites == 0) {
} return <></>;
}
if (online[v.server] != undefined) if (online[v.server] != undefined)
return ( return (
<ServerCard <ServerCard
mini mini
b={online[v.server]} b={online[v.server]}
favs={v.favorites} favs={v.favorites}
key={v.server} key={v.server}
/> />
); );
else else
return ( return (
<Card className="h-[226px]" key={v.server}> <Card className="h-[226px]" key={v.server}>
<CardHeader> <CardHeader>
<CardTitle>{v.server}</CardTitle> <CardTitle>{v.server}</CardTitle>
<CardDescription> <CardDescription>
{v.favorites} favorited {v.favorites} favorited
<br /> <br />
<Button <Button
size="icon" size="icon"
variant="secondary" variant="secondary"
className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden" className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
v.server + ".mshf.minehut.gg" v.server + ".mshf.minehut.gg"
); );
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >
<Copy size={18} /> <Copy size={18} />
<code className="ml-2">{v.server}</code> <code className="ml-2">{v.server}</code>
</Button> </Button>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Link href={"/server/" + v.server}> <Link href={"/server/" + v.server}>
<Button <Button
size="icon" size="icon"
variant="secondary" variant="secondary"
className="w-[32px] h-[32px] mt-2 ml-2 max-md:hidden" className="w-[32px] h-[32px] mt-2 ml-2 max-md:hidden"
> >
<Layers size={18} /> <Layers size={18} />
</Button> </Button>
</Link> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
Open up the server page to see more information about Open up the server page to see more information
the server about the server
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
); );
})} })}
</div> </div>
</FadeIn>
</InfiniteScroll> </InfiniteScroll>
</div> </div>
); );

@ -10,6 +10,8 @@ import toast from "react-hot-toast";
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";
import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
export default function FavoritesView() { export default function FavoritesView() {
const [apiFavorites, setApiFavorites] = useState<any>([]); const [apiFavorites, setApiFavorites] = useState<any>([]);
@ -40,8 +42,20 @@ export default function FavoritesView() {
if (loading) { if (loading) {
return ( return (
<> <>
<Spinner className="flex items-center" /> <div className="grid grid-cols-4 gap-4">
<br /> <Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
<Skeleton className="h-[147px] rounded-xl" />
</div>
</> </>
); );
} }
@ -54,53 +68,55 @@ export default function FavoritesView() {
Your favorites are empty. Maybe favorite a server! Your favorites are empty. Maybe favorite a server!
</div> </div>
)} )}
<div className="grid sm:grid-cols-4 gap-4"> <FadeIn>
{apiFavorites.map((server: ServerResponse) => ( <div className="grid sm:grid-cols-4 gap-4">
<Card key={server.name}> {apiFavorites.map((server: ServerResponse) => (
<CardHeader> <Card key={server.name}>
<CardTitle>{server.name}</CardTitle> <CardHeader>
<div> <CardTitle>{server.name}</CardTitle>
<Button <div>
size="icon" <Button
variant="secondary" size="icon"
className="min-w-[128px] max-w-[328px] mb-2 h-[32px] max-md:hidden" variant="secondary"
onClick={() => { className="min-w-[128px] max-w-[328px] mb-2 h-[32px] max-md:hidden"
navigator.clipboard.writeText( onClick={() => {
server.name + ".mshf.minehut.gg" navigator.clipboard.writeText(
); server.name + ".mshf.minehut.gg"
toast.success("Copied IP to clipboard"); );
}} toast.success("Copied IP to clipboard");
> }}
<Copy size={18} /> >
<code className="ml-2">{server.name}</code> <Copy size={18} />
</Button> <code className="ml-2">{server.name}</code>
<Tooltip> </Button>
<TooltipTrigger> <Tooltip>
<Button <TooltipTrigger>
size="icon" <Button
variant="secondary" size="icon"
className="w-[32px] h-[32px] mb-2 ml-2 max-md:hidden" variant="secondary"
onClick={() => { className="w-[32px] h-[32px] mb-2 ml-2 max-md:hidden"
router.push("/server/" + server.name); onClick={() => {
}} router.push("/server/" + server.name);
> }}
<Layers size={18} /> >
</Button> <Layers size={18} />
</TooltipTrigger> </Button>
<TooltipContent> </TooltipTrigger>
Open up the server page to see more information about the <TooltipContent>
server Open up the server page to see more information about the
</TooltipContent> server
</Tooltip> </TooltipContent>
</div> </Tooltip>
<code className="text-[14px]"> </div>
{convert(server.joins)} total joins {" "} <code className="text-[14px]">
{server.online ? "Online" : "Offline"} {convert(server.joins)} total joins {" "}
</code> {server.online ? "Online" : "Offline"}
</CardHeader> </code>
</Card> </CardHeader>
))} </Card>
</div> ))}
</div>
</FadeIn>
</> </>
); );
} }

@ -19,6 +19,8 @@ import {
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import { ServerResponse } from "@/lib/types/mh-server"; import { ServerResponse } from "@/lib/types/mh-server";
import { getCommunityServerFavorites, getShortTermData } from "@/lib/api"; import { getCommunityServerFavorites, getShortTermData } from "@/lib/api";
import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
const chartConfig = { const chartConfig = {
player_count: { player_count: {
@ -37,6 +39,7 @@ export function NewChart({ server }: { server: string }) {
const [chartData, setChartData] = React.useState<any>([]); const [chartData, setChartData] = React.useState<any>([]);
const [joins, setJoins] = React.useState<any>(0); const [joins, setJoins] = React.useState<any>(0);
const [loading, setLoading] = React.useState(true);
const [favorites, setFavorites] = React.useState<any>(0); const [favorites, setFavorites] = React.useState<any>(0);
const allNums = { player_count: joins, favorites }; const allNums = { player_count: joins, favorites };
@ -52,99 +55,109 @@ export function NewChart({ server }: { server: string }) {
}); });
} }
); );
setLoading(false);
} }
); );
}); });
if (loading)
return (
<>
<Skeleton className="w-full h-[437px]" />
</>
);
return ( return (
<Card className="w-full"> <FadeIn>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row"> <Card className="w-full">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6"> <CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<CardTitle> <div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
{chartConfig[activeChart].label} Chart for {server} <CardTitle>
</CardTitle> {chartConfig[activeChart].label} Chart for {server}
<CardDescription>Showing the past 30 entries.</CardDescription> </CardTitle>
</div> <CardDescription>Showing the past 30 entries.</CardDescription>
<div className="flex"> </div>
{["player_count", "favorites"].map((key) => { <div className="flex">
const chart = key as keyof typeof chartConfig; {["player_count", "favorites"].map((key) => {
return ( const chart = key as keyof typeof chartConfig;
<button return (
key={chart} <button
data-active={activeChart === chart} key={chart}
className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6" data-active={activeChart === chart}
onClick={() => setActiveChart(chart)} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
> onClick={() => setActiveChart(chart)}
<span className="text-xs text-muted-foreground"> >
{chartConfig[chart].label} <span className="text-xs text-muted-foreground">
</span> {chartConfig[chart].label}
<span className="text-lg font-bold leading-none sm:text-3xl"> </span>
{convert(allNums[chart])} <span className="text-lg font-bold leading-none sm:text-3xl">
</span> {convert(allNums[chart])}
</button> </span>
); </button>
})} );
</div> })}
</CardHeader> </div>
<CardContent className="px-2 sm:p-6"> </CardHeader>
<ChartContainer <CardContent className="px-2 sm:p-6">
config={chartConfig} <ChartContainer
className="aspect-auto h-[250px] w-full" config={chartConfig}
> className="aspect-auto h-[250px] w-full"
<LineChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
> >
<CartesianGrid vertical={false} /> <LineChart
<XAxis accessibilityLayer
dataKey="date" data={chartData}
tickLine={false} margin={{
axisLine={false} left: 12,
tickMargin={8} right: 12,
minTickGap={32}
tickFormatter={(value) => {
return new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
});
}} }}
/> >
<YAxis <CartesianGrid vertical={false} />
dataKey={activeChart} <XAxis
tickLine={false} dataKey="date"
axisLine={false} tickLine={false}
tickFormatter={(value) => { axisLine={false}
return ( tickMargin={8}
value + minTickGap={32}
(activeChart == "player_count" tickFormatter={(value) => {
? ` plyr${value != 1 ? "s" : ""}.` return new Date(value).toLocaleTimeString("en-US", {
: ` ${value == 1 ? "favorite" : "favrts."}`) timeStyle: "short",
); });
}} }}
/> />
<ChartTooltip <YAxis
content={ dataKey={activeChart}
<ChartTooltipContent tickLine={false}
className="w-[150px]" axisLine={false}
nameKey={activeChart} tickFormatter={(value) => {
hideLabel return (
/> value +
} (activeChart == "player_count"
/> ? ` plyr${value != 1 ? "s" : ""}.`
<Line : ` ${value == 1 ? "favorite" : "favrts."}`)
dataKey={activeChart} );
type="monotone" }}
stroke={`var(--color-${activeChart})`} />
strokeWidth={2} <ChartTooltip
dot={false} content={
/> <ChartTooltipContent
</LineChart> className="w-[150px]"
</ChartContainer> nameKey={activeChart}
</CardContent> hideLabel
</Card> />
}
/>
<Line
dataKey={activeChart}
type="monotone"
stroke={`var(--color-${activeChart})`}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
</FadeIn>
); );
} }

@ -46,6 +46,8 @@ import {
MenubarSubTrigger, MenubarSubTrigger,
MenubarTrigger, MenubarTrigger,
} from "@/components/ui/menubar"; } from "@/components/ui/menubar";
import ClientFadeIn from "./ClientFadeIn";
import { Skeleton } from "./ui/skeleton";
export default function ServerList() { export default function ServerList() {
const [loading, setLoading]: any = useState(true); const [loading, setLoading]: any = useState(true);
@ -119,471 +121,489 @@ export default function ServerList() {
if (loading) { if (loading) {
return ( return (
<> <>
<Spinner className="flex items-center" /> <div className="grid grid-cols-3 gap-4 max-lg:grid-cols-2">
<Skeleton className="h-[112px] rounded-xl" />
<Skeleton className="h-[112px] rounded-xl" />
<Skeleton className="h-[112px] rounded-xl" />
</div>
<br /> <br />
<div <Separator />
className="flex justify-center" <br />
dangerouslySetInnerHTML={{ __html: randomText }} <div className="grid grid-cols-4 gap-4">
></div> <Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" />
</div>
</> </>
); );
} }
return ( return (
<> <>
<div className="max-lg:grid-cols-2 grid grid-cols-3 gap-4 "> <ClientFadeIn>
<Stat <div className="max-lg:grid-cols-2 grid grid-cols-3 gap-4 ">
title="Players online" <Stat
desc={serverList.getExtraData().total_players.toString()} title="Players online"
icon={CircleUser} desc={serverList.getExtraData().total_players.toString()}
/> icon={CircleUser}
<Stat />
title={ <Stat
<div title={
className={
serverList.getExtraData().total_servers >= 3200
? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500"
: ""
}
>
Servers online{" "}
</div>
}
className="relative z-0"
desc={
<div className="flex items-center">
<div <div
className={ className={
serverList.getExtraData().total_servers >= 3200 serverList.getExtraData().total_servers >= 3200
? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500 " ? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500"
: "" : ""
} }
> >
{serverList.getExtraData().total_servers.toString()} Servers online{" "}
</div> </div>
{serverList.getExtraData().total_servers >= 3200 && ( }
<Tooltip> className="relative z-0"
<TooltipTrigger> desc={
<Info size={16} className="ml-2" /> <div className="flex items-center">
</TooltipTrigger> <div
<TooltipContent className="font-normal"> className={
The server amount is over 3.2k, meaning that new servers serverList.getExtraData().total_servers >= 3200
have to go into a queue before being able to be online.{" "} ? "bg-clip-text text-transparent bg-gradient-to-r from-cyan-500 to-blue-500 "
<br /> : ""
(the server count isn't entirely accurate, so sometimes you
might not go into a queue even when the server count is at
3.2k)
</TooltipContent>
</Tooltip>
)}
</div>
}
icon={Network}
>
{serverList.getExtraData().total_servers >= 3200 && (
<BorderBeam
size={135}
duration={12}
delay={9}
colorFrom="rgb(6 182 212)"
colorTo="rgb(59 130 246)"
/>
)}
</Stat>
<Stat
title="Current most popular server (in filter)"
className="max-lg:col-span-2"
desc={
<>
{serverList.currentServers[0] != undefined
? serverList.currentServers[0].name
: "None"}{" "}
{serverList.currentServers[0] != undefined && (
<IconDisplay server={serverList.currentServers[0]} />
)}
</>
}
icon={Sun}
/>
</div>
<br />
<Separator />
<Menubar className="mt-3 ml-2 border rounded p-2">
<MenubarMenu>
<MenubarTrigger>Servers</MenubarTrigger>
<MenubarContent>
<MenubarItem onSelect={() => events.emit("search-request-event")}>
Search Servers
<MenubarShortcut className="flex items-center ml-3">
<CommandIcon size={14} />
+Shift+K
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onSelect={() => {
setRandomData(serverList.getRandomServer());
setRandom(true);
}}
>
Pick Random Server
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onSelect={() => {
toast.promise(
new Promise((s, e) => {
setLoading(true);
serverList
.fetchDataAndFilter()
.then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
setLoading(false);
s(false);
});
})
.catch(() => {
e();
});
}),
{
success: "Succesfully refreshed servers",
loading: "Refreshing...",
error: "Error while refreshing",
}
);
}}
>
Refresh
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Filter</MenubarTrigger>
<MenubarContent className="max-h-[400px] overflow-auto">
<MenubarRadioGroup
onValueChange={(v) => {
toast.promise(
new Promise((g, b) => {
if (v == "smaller") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = true;
filt["bigger-tf"] = false;
setNameFilters(filt);
var filt2 = filters;
filt2.push(smaller);
if (filt2.includes(bigger)) {
filt2.splice(filt2.indexOf(bigger), 1);
}
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else if (v == "bigger") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = true;
setNameFilters(filt);
var filt2 = filters;
filt2.push(bigger);
filt2.splice(filt2.indexOf(smaller), 1);
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else {
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = false;
setNameFilters(filt);
setTemplateFilter(false);
var filt2 = filters;
filt2.splice(filt2.indexOf(smaller), 1);
filt2.splice(filt2.indexOf(bigger), 1);
setFilters(filt2);
console.log(filters, filters.includes(smaller));
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
}
}),
{
error: "Error while changing filters",
loading: "Changing filters...",
success: "Changed filters!",
}
);
}}
value={(() => {
if (nameFilters["smaller-tf"]) {
return "smaller";
} else if (nameFilters["bigger-tf"]) {
return "bigger";
} else {
return "none";
}
})()}
>
<MenubarRadioItem value="smaller">
<div className="block">
Only allow smaller servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers that have the player range 7-15, and
cannot <br />
be Always Online.
</span>
</div>
</MenubarRadioItem>
<MenubarRadioItem value="bigger">
<div className="block">
Only allow bigger servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers with more than 15 players.
</span>
</div>
</MenubarRadioItem>
<MenubarRadioItem value="none">
No/custom requirements
</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarSub>
<span className="text-sm text-muted-foreground ml-2">Tags</span>
</MenubarSub>
{allTags.map((tag) => (
<div key={tag.docsName}>
{tag.docsName && tag.__filter == undefined && (
<MenubarCheckboxItem
disabled={templateFilter && tag.__disab != undefined}
id={tag.docsName}
checked={(() => {
return nameFilters["t-" + tag.docsName];
})()}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["t-" + tag.docsName] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(tag.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(tag.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
>
<Badge variant={tag.role} className="mr-1">
{tag.docsName}
</Badge>
</MenubarCheckboxItem>
)}
</div>
))}
<MenubarSeparator />
<MenubarSub>
<span className="text-sm text-muted-foreground ml-2">
Categories
</span>
</MenubarSub>
{allCategories.map((categorie) => (
<MenubarCheckboxItem
id={categorie.name}
key={categorie.name}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["c-" + categorie.name] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(categorie.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(categorie.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> =
[];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
checked={(() => {
return nameFilters["c-" + categorie.name];
})()}
>
<Badge variant={categorie.role} className="mr-1">
{categorie.name}
</Badge>
</MenubarCheckboxItem>
))}
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarSub>
<MenubarSubTrigger>Grid</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup value={ipr} onValueChange={setIPR}>
<MenubarRadioItem value="4">4 items per row</MenubarRadioItem>
<MenubarRadioItem value="5">5 items per row</MenubarRadioItem>
<MenubarRadioItem value="6">6 items per row</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>Sort</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup
value="joins"
onValueChange={(c) =>
c == "favorites" && router.push("/sort/favorites")
} }
> >
<MenubarRadioItem value="joins"> {serverList.getExtraData().total_servers.toString()}
Players Online </div>
</MenubarRadioItem> {serverList.getExtraData().total_servers >= 3200 && (
<MenubarRadioItem value="favorites"> <Tooltip>
Favorites <TooltipTrigger>
</MenubarRadioItem> <Info size={16} className="ml-2" />
</MenubarRadioGroup> </TooltipTrigger>
</MenubarSubContent> <TooltipContent className="font-normal">
</MenubarSub> The server amount is over 3.2k, meaning that new servers
</MenubarContent> have to go into a queue before being able to be online.{" "}
</MenubarMenu> <br />
</Menubar> (the server count isn't entirely accurate, so sometimes
you might not go into a queue even when the server count
is at 3.2k)
</TooltipContent>
</Tooltip>
)}
</div>
}
icon={Network}
>
{serverList.getExtraData().total_servers >= 3200 && (
<BorderBeam
size={135}
duration={12}
delay={9}
colorFrom="rgb(6 182 212)"
colorTo="rgb(59 130 246)"
/>
)}
</Stat>
<Stat
title="Current most popular server (in filter)"
className="max-lg:col-span-2"
desc={
<>
{serverList.currentServers[0] != undefined
? serverList.currentServers[0].name
: "None"}{" "}
{serverList.currentServers[0] != undefined && (
<IconDisplay server={serverList.currentServers[0]} />
)}
</>
}
icon={Sun}
/>
</div>
</ClientFadeIn>
<br />
<Separator />
<ClientFadeIn delay={100}>
<Menubar className="mt-3 ml-2 border rounded p-2">
<MenubarMenu>
<MenubarTrigger>Servers</MenubarTrigger>
<MenubarContent>
<MenubarItem onSelect={() => events.emit("search-request-event")}>
Search Servers
<MenubarShortcut className="flex items-center ml-3">
<CommandIcon size={14} />
+Shift+K
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onSelect={() => {
setRandomData(serverList.getRandomServer());
setRandom(true);
}}
>
Pick Random Server
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onSelect={() => {
toast.promise(
new Promise((s, e) => {
setLoading(true);
serverList
.fetchDataAndFilter()
.then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
setLoading(false);
s(false);
});
})
.catch(() => {
e();
});
}),
{
success: "Succesfully refreshed servers",
loading: "Refreshing...",
error: "Error while refreshing",
}
);
}}
>
Refresh
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Filter</MenubarTrigger>
<MenubarContent className="max-h-[400px] overflow-auto">
<MenubarRadioGroup
onValueChange={(v) => {
toast.promise(
new Promise((g, b) => {
if (v == "smaller") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = true;
filt["bigger-tf"] = false;
setNameFilters(filt);
var filt2 = filters;
filt2.push(smaller);
if (filt2.includes(bigger)) {
filt2.splice(filt2.indexOf(bigger), 1);
}
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else if (v == "bigger") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = true;
setNameFilters(filt);
var filt2 = filters;
filt2.push(bigger);
filt2.splice(filt2.indexOf(smaller), 1);
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else {
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = false;
setNameFilters(filt);
setTemplateFilter(false);
var filt2 = filters;
filt2.splice(filt2.indexOf(smaller), 1);
filt2.splice(filt2.indexOf(bigger), 1);
setFilters(filt2);
console.log(filters, filters.includes(smaller));
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
}
}),
{
error: "Error while changing filters",
loading: "Changing filters...",
success: "Changed filters!",
}
);
}}
value={(() => {
if (nameFilters["smaller-tf"]) {
return "smaller";
} else if (nameFilters["bigger-tf"]) {
return "bigger";
} else {
return "none";
}
})()}
>
<MenubarRadioItem value="smaller">
<div className="block">
Only allow smaller servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers that have the player range 7-15, and
cannot <br />
be Always Online.
</span>
</div>
</MenubarRadioItem>
<MenubarRadioItem value="bigger">
<div className="block">
Only allow bigger servers
<br />
<span className="text-sm text-muted-foreground">
Only allow servers with more than 15 players.
</span>
</div>
</MenubarRadioItem>
<MenubarRadioItem value="none">
No/custom requirements
</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarSub>
<span className="text-sm text-muted-foreground ml-2">Tags</span>
</MenubarSub>
{allTags.map((tag) => (
<div key={tag.docsName}>
{tag.docsName && tag.__filter == undefined && (
<MenubarCheckboxItem
disabled={templateFilter && tag.__disab != undefined}
id={tag.docsName}
checked={(() => {
return nameFilters["t-" + tag.docsName];
})()}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["t-" + tag.docsName] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(tag.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(tag.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
>
<Badge variant={tag.role} className="mr-1">
{tag.docsName}
</Badge>
</MenubarCheckboxItem>
)}
</div>
))}
<MenubarSeparator />
<MenubarSub>
<span className="text-sm text-muted-foreground ml-2">
Categories
</span>
</MenubarSub>
{allCategories.map((categorie) => (
<MenubarCheckboxItem
id={categorie.name}
key={categorie.name}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["c-" + categorie.name] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(categorie.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(categorie.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> =
[];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
checked={(() => {
return nameFilters["c-" + categorie.name];
})()}
>
<Badge variant={categorie.role} className="mr-1">
{categorie.name}
</Badge>
</MenubarCheckboxItem>
))}
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarSub>
<MenubarSubTrigger>Grid</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup value={ipr} onValueChange={setIPR}>
<MenubarRadioItem value="4">
4 items per row
</MenubarRadioItem>
<MenubarRadioItem value="5">
5 items per row
</MenubarRadioItem>
<MenubarRadioItem value="6">
6 items per row
</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>Sort</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup
value="joins"
onValueChange={(c) =>
c == "favorites" && router.push("/sort/favorites")
}
>
<MenubarRadioItem value="joins">
Players Online
</MenubarRadioItem>
<MenubarRadioItem value="favorites">
Favorites
</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
</Menubar>
</ClientFadeIn>
<Dialog open={random} onOpenChange={setRandom}> <Dialog open={random} onOpenChange={setRandom}>
<DialogContent> <DialogContent>
@ -695,13 +715,15 @@ export default function ServerList() {
} }
style={{ overflow: "hidden !important", paddingLeft: 6 }} style={{ overflow: "hidden !important", paddingLeft: 6 }}
> >
<div className={" grid " + "grid-cols-" + ipr + " gap-4"}> <ClientFadeIn delay={200}>
{servers.map((b: any) => ( <div className={" grid " + "grid-cols-" + ipr + " gap-4"}>
<> {servers.map((b: any) => (
<ServerCard b={b} motd={motdList[b.name]} /> <>
</> <ServerCard b={b} motd={motdList[b.name]} />
))} </>
</div> ))}
</div>
</ClientFadeIn>
</InfiniteScroll> </InfiniteScroll>
</> </>
); );

@ -24,6 +24,8 @@ import { Star, X } from "lucide-react";
import { favoriteServer, isFavorited } from "@/lib/api"; import { favoriteServer, isFavorited } from "@/lib/api";
import { LoadingButton } from "./ui/loading-button"; import { LoadingButton } from "./ui/loading-button";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Skeleton } from "./ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
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));
@ -66,12 +68,11 @@ export default function ServerView(props: { server: string }) {
if (loading) { if (loading) {
return ( return (
<> <>
<Spinner className="flex items-center" /> <div className="grid p-4 sm:grid-cols-3 gap-4">
<br /> <Skeleton className="sm:col-span-2 h-[245px]" />
<div
className="flex justify-center" <Skeleton className="h-[245px]" />
dangerouslySetInnerHTML={{ __html: randomText }} </div>
></div>
</> </>
); );
} }
@ -94,151 +95,153 @@ export default function ServerView(props: { server: string }) {
</div> </div>
</div> </div>
)} )}
<FadeIn>
<div className="grid p-4 sm:grid-cols-3 gap-4"> <div className="grid p-4 sm:grid-cols-3 gap-4">
<Card className="sm:col-span-2"> <Card className="sm:col-span-2">
<BetterHeader> <BetterHeader>
<CardTitle className="flex items-center"> <CardTitle className="flex items-center">
{single.grabOnline() == undefined ? ( {single.grabOnline() == undefined ? (
<div <div
className="items-center mr-1" className="items-center mr-1"
style={{ style={{
width: ".75rem", width: ".75rem",
height: ".75rem", height: ".75rem",
borderRadius: "9999px", borderRadius: "9999px",
backgroundColor: "#ff1744", backgroundColor: "#ff1744",
}} }}
/> />
) : ( ) : (
<div <div
className="items-center mr-1" className="items-center mr-1"
style={{ style={{
width: ".75rem", width: ".75rem",
height: ".75rem", height: ".75rem",
borderRadius: "9999px", borderRadius: "9999px",
backgroundColor: "#0cce6b", backgroundColor: "#0cce6b",
}} }}
/> />
)}
{props.server}
</CardTitle>
<CardDescription>
{/* 1704088800000 is the Unix time (in milliseconds) of (GMT) Monday, January 1, 2024 6:00:00 AM */}
{lastOnline < 1704088800000 &&
single.grabOnline() == undefined && (
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary">
Server too old to grab data
</Badge>
</TooltipTrigger>
<TooltipContent>
This server was last online before 1/1/24 or{" "}
<code>(GMT) Monday, January 1, 2024 6:00:00 AM</code>,
<br />
meaning data like tags, authors and other information
about the server cannot be seen.
</TooltipContent>
</Tooltip>
)} )}
{props.server}
</CardTitle>
<CardDescription>
{/* 1704088800000 is the Unix time (in milliseconds) of (GMT) Monday, January 1, 2024 6:00:00 AM */}
{lastOnline < 1704088800000 &&
single.grabOnline() == undefined && (
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary">
Server too old to grab data
</Badge>
</TooltipTrigger>
<TooltipContent>
This server was last online before 1/1/24 or{" "}
<code>(GMT) Monday, January 1, 2024 6:00:00 AM</code>,
<br />
meaning data like tags, authors and other information
about the server cannot be seen.
</TooltipContent>
</Tooltip>
)}
{single.getAuthor() != undefined && ( {single.getAuthor() != undefined && (
<p>by {single.getAuthor()}</p> <p>by {single.getAuthor()}</p>
)} )}
</CardDescription> </CardDescription>
</BetterHeader> </BetterHeader>
<CardContent> <CardContent>
<p> <p>
<strong>Time:</strong> <strong>Time:</strong>
<br /> <br />
<i>Last online</i>{" "} <i>Last online</i>{" "}
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<code> <code>
{timeConverter(single.grabOffline()?.last_online)} {timeConverter(single.grabOffline()?.last_online)}
</code> </code>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<code>{single.grabOffline()?.last_online}</code> in Unix time <code>{single.grabOffline()?.last_online}</code> in Unix
</TooltipContent> time
</Tooltip>{" "} </TooltipContent>
<br /> </Tooltip>{" "}
<i>Created on</i>{" "} <br />
<Tooltip> <i>Created on</i>{" "}
<TooltipTrigger> <Tooltip>
<code>{timeConverter(single.grabOffline()?.creation)}</code> <TooltipTrigger>
</TooltipTrigger> <code>{timeConverter(single.grabOffline()?.creation)}</code>
<TooltipContent> </TooltipTrigger>
<code>{single.grabOffline()?.creation}</code> in Unix time <TooltipContent>
</TooltipContent> <code>{single.grabOffline()?.creation}</code> in Unix time
</Tooltip> </TooltipContent>
</p> </Tooltip>
</CardContent> </p>
</Card> </CardContent>
<Card> </Card>
<CardHeader> <Card>
<CardTitle>Favorite the server?</CardTitle> <CardHeader>
<CardDescription> <CardTitle>Favorite the server?</CardTitle>
By favoriting the server, you can see it later.{" "} <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> <SignedOut>
<strong>You need to sign in to favorite a server.</strong> <SignInPopoverButton />
</SignedOut> </SignedOut>
</CardDescription> <SignedIn>
</CardHeader> <LoadingButton
<CardContent> variant={resolvedTheme == "dark" ? "outline" : "default"}
<SignedOut> loading={loadingFavorite}
<SignInPopoverButton /> onClick={() => {
</SignedOut> setLoadingFavorite(true);
<SignedIn> favoriteServer(single.grabOffline()?.name as string).then(
<LoadingButton () => {
variant={resolvedTheme == "dark" ? "outline" : "default"} setFavorited(!favorited);
loading={loadingFavorite} setLoadingFavorite(false);
onClick={() => { }
setLoadingFavorite(true); );
favoriteServer(single.grabOffline()?.name as string).then( }}
() => { >
setFavorited(!favorited); {favorited && (
setLoadingFavorite(false); <motion.div
} animate={{ color: "yellow", fill: "yellow" }}
); transition={{ duration: 2 }}
}} >
> <Star
{favorited && ( className="mr-2"
<motion.div size="16"
animate={{ color: "yellow", fill: "yellow" }} color="yellow"
transition={{ duration: 2 }} fill="yellow"
> />
<Star </motion.div>
className="mr-2" )}
size="16" {!favorited && (
color="yellow" <motion.div
fill="yellow" transition={{ duration: 1 }}
/> animate={{ color: "yellow", fill: "yellow" }}
</motion.div> >
)} <Star className="mr-2" size="16" />
{!favorited && ( </motion.div>
<motion.div )}
transition={{ duration: 1 }} {favorited && "Unf"}
animate={{ color: "yellow", fill: "yellow" }} {!favorited && "F"}avorite Server
> </LoadingButton>
<Star className="mr-2" size="16" /> </SignedIn>
</motion.div> </CardContent>
)} <CardFooter>
{favorited && "Unf"} <small>
{!favorited && "F"}avorite Server This is unlike voting. The{" "}
</LoadingButton> <i>amount of people who favorited are public</i>, but the server
</SignedIn> doesn{"'"}t know who favorited, as Favorites are completely
</CardContent> anonymous.
<CardFooter> </small>
<small> </CardFooter>
This is unlike voting. The{" "} </Card>
<i>amount of people who favorited are public</i>, but the server </div>
doesn{"'"}t know who favorited, as Favorites are completely </FadeIn>
anonymous.
</small>
</CardFooter>
</Card>
</div>
</> </>
); );
} }

@ -1,7 +1,7 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
export const version = "b-0.10.0"; export const version = "b-0.10.2";
const User = ({ user }: { user: string }) => ( const User = ({ user }: { user: string }) => (
<span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]"> <span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]">
@ -44,6 +44,17 @@ export const Changelog = () => (
</code> </code>
</div> </div>
<br /> <br />
<div>
<strong className="flex items-center">
Version b-0.10.2 (August 18th 2024)
</strong>
<ul>
<li> Content fades-in on load</li>
<li> Instead of using spinners, now we are using Skeletons</li>
</ul>
</div>
<br />
<br />
<div> <div>
<strong className="flex items-center"> <strong className="flex items-center">
Version b-0.10.0 (August 17th 2024) Version b-0.10.0 (August 17th 2024)