feat: new banner system

This commit is contained in:
dvelo 2024-12-21 17:14:29 -06:00
parent b144bafd03
commit c8fe0a6051
29 changed files with 1129 additions and 125 deletions

1
.tool-versions Normal file

@ -0,0 +1 @@
nodejs 23.3.0

@ -20,6 +20,7 @@
"@emotion/is-prop-valid": "^1.3.0",
"@linear/sdk": "^31.0.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-aspect-ratio": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-hover-card": "^1.1.1",

@ -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.
*/
import SignUpPage from "@/components/clerk/SignInForm";
import { ServerCrash } from "lucide-react";
export default function Page() {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-[rgb(244,_244,_245)] p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="/" className="flex items-center gap-2 self-center font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<ServerCrash className="size-4" />
</div>
MHSF
</a>
<SignUpPage />
</div>
</div>
);
}

22
src/app/(auth)/layout.tsx Normal file

@ -0,0 +1,22 @@
import { ClerkProvider } from "@clerk/nextjs";
import "@/app/globals.css";
import { GeistSans } from "geist/font/sans";
export const metadata = {
title: "Next.js",
description: "Generated by Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={GeistSans.className}>
<body>
<ClerkProvider>{children}</ClerkProvider>
</body>
</html>
);
}

@ -32,27 +32,17 @@ import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { GeistSans } from "geist/font/sans";
import "../globals.css";
import ClientFadeIn from "@/components/ClientFadeIn";
import { CommandBarer } from "@/components/CommandBar";
import { BrandingGenericIcon } from "@/components/Icon";
import TextFromPathname from "@/components/TextFromPathname";
import { ThemeProvider } from "@/components/ThemeProvider";
import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider";
import TopBar from "@/components/clerk/Topbar";
import NewDomainDialog from "@/components/misc/NewDomainDialog";
import ThemedToaster from "@/components/misc/ThemedToaster";
import UnofficalDialog from "@/components/misc/UnofficalDialog";
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { TooltipProvider } from "@/components/ui/tooltip";
import { banner } from "@/config/banner";
import NextTopLoader from "@/lib/top-loader";
import type { Metadata, Viewport } from "next";
import { Inter as interFont } from "next/font/google";
import Link from "next/link";
import LayoutPart from "@/components/feat/LayoutPart";
import AllBanners from "@/components/feat/AllBanners";
export const extraMetadata = {
twitter: {
@ -86,35 +76,8 @@ export default async function RootLayout({
<ClerkThemeProvider className={GeistSans.className}>
<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">
{banner.bannerText}
</div>
)}
<div
className={
"w-screen h-[3rem] border-b fixed backdrop-blur flex z-10 " +
(banner.isBanner == true ? "mt-8" : "")
}
>
<div className="items-center me-auto mt-2 pl-7 max-sm:mt-3">
<Breadcrumb>
<BreadcrumbList>
<Link href="/">
<BreadcrumbPage className="max-sm:hidden">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] " />
</BreadcrumbPage>
</Link>
<TextFromPathname />
</BreadcrumbList>
</Breadcrumb>
</div>
<TopBar inter={inter.className} />
</div>
<div className={banner.isBanner ? "pt-8" : undefined}>
<NextTopLoader />
<ClientFadeIn>{children}</ClientFadeIn>
</div>{" "}
<AllBanners />
<LayoutPart>{children}</LayoutPart>
<ThemedToaster />
<CommandBarer />
<SpeedInsights />

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

@ -35,6 +35,7 @@ import ServerView from "@/components/ServerView";
import TabServer from "@/components/misc/TabServer";
import { Separator } from "@/components/ui/separator";
import type { Metadata, ResolvingMetadata } from "next";
import StickyTopbar from "@/components/misc/StickyTopbar";
type Props = {
params: { server: string };
@ -98,18 +99,21 @@ export default function ServerPage({ params }: { params: { server: string } }) {
return (
<main>
<ColorProvider server={params.server}>
<div className={"pt-16 xl:px-[100px]"}>
<div className={"pt-[300px] xl:px-[100px]"}>
<Banner server={params.server} />
<TabServer server={params.server} tabDef="statistics" />
<div className="pt-8">
<div className="pt-8 z-10 relative">
<ServerView server={params.server} />
</div>
<StickyTopbar scrollElevation={100} className="pt-4">
<TabServer server={params.server} tabDef="statistics" />
</StickyTopbar>
<Separator />
<br />
<div className="p-4 gap-4">
<NewChart server={params.server} />
</div>
</div>
</div>
</ColorProvider>
</main>
);

@ -31,10 +31,12 @@
"use client";
import { getCustomization } from "@/lib/api";
import { useEffect, useState } from "react";
import useTotalBannerSize from "@/lib/hooks/use-total-banner-size";
export default function Banner({ server }: { server: string }) {
const [bannerURL, setBannerURL] = useState("");
const [loading, setLoading] = useState(true);
const { bannerSize } = useTotalBannerSize();
useEffect(() => {
getCustomization(server).then((c) => {
@ -64,8 +66,16 @@ export default function Banner({ server }: { server: string }) {
? bannerURL
: "wsrv.nl/?url=" + encodeURIComponent(bannerURL) + "?n=-1"
}
className="rounded align-middle block ml-auto mr-auto w-[50%] max-h-[150px]"
className="rounded align-middle block ml-auto mr-auto absolute left-0 z-0 max-h-[400px] w-full object-fill"
alt="User-provided banner for this server."
style={
{
"-webkit-mask-image":
"linear-gradient(to top, transparent, black)",
maskImage: "linear-gradient(to top, transparent, black)",
top: `${bannerSize * 32 + 36}px`,
} as React.CSSProperties
}
/>
)}
<br />

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

@ -0,0 +1,311 @@
/*
* 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 * as Clerk from "@clerk/elements/common";
import * as SignUp from "@clerk/elements/sign-up";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { LoadingSpinner } from "../ui/loading-spinner";
import Discord from "../ui/discord";
import Github from "../ui/github";
export default function SignUpPage() {
return (
<div className="flex flex-col gap-6">
<SignUp.Root
fallback={
<Card className="flex items-center justify-center float-center absolute left-[50%] translate-x-[-50%] size-[50px]">
<LoadingSpinner />
</Card>
}
>
<Clerk.Loading>
{(isGlobalLoading) => (
<>
{isGlobalLoading && (
<Card className="flex items-center justify-center float-center absolute left-[50%] translate-x-[-50%] size-[50px]">
<LoadingSpinner />
</Card>
)}
<SignUp.Step name="start">
<Card className="w-full sm:w-96 ">
<CardHeader className="text-center">
<CardTitle>Create your account</CardTitle>
<CardDescription>
Welcome! Please fill in the details to get started.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<div className="grid gap-2 grid-cols-2">
<Clerk.Connection name="discord" asChild>
<Button
size="sm"
variant="outline"
type="button"
className="flex items-center"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:discord">
{(isLoading) =>
isLoading ? (
<LoadingSpinner className="size-4 " />
) : (
<>
<Discord className="mr-2 size-4" />
Discord
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
<Clerk.Connection name="github" asChild>
<Button
size="sm"
variant="outline"
type="button"
className="flex items-center"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:github">
{(isLoading) =>
isLoading ? (
<LoadingSpinner className="size-4 " />
) : (
<>
<Github
className="mr-2 size-4"
fill="black"
/>
GitHub
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
</div>
<p className="flex items-center gap-x-3 text-sm text-muted-foreground before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">
or
</p>
<Clerk.Field name="emailAddress" className="space-y-2">
<Clerk.Label asChild>
<Label>Email address</Label>
</Clerk.Label>
<Clerk.Input type="email" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
<Clerk.Field name="password" className="space-y-2">
<Clerk.Label asChild>
<Label>Password</Label>
</Clerk.Label>
<Clerk.Input type="password" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Captcha className="empty:hidden" />
<SignUp.Action submit asChild>
<Button
disabled={isGlobalLoading}
className="flex items-center"
>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<LoadingSpinner className="size-4 " />
) : (
"Continue"
);
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
<Button variant="link" size="sm" asChild>
<Clerk.Link navigate="sign-in">
Already have an account? Sign in
</Clerk.Link>
</Button>
</div>
</CardFooter>
</Card>
</SignUp.Step>
<SignUp.Step name="continue">
<Card className="w-full sm:w-96 ">
<CardHeader>
<CardTitle>Continue registration</CardTitle>
</CardHeader>
<CardContent>
<Clerk.Field name="username" className="space-y-2">
<Clerk.Label>
<Label>Username</Label>
</Clerk.Label>
<Clerk.Input type="text" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Action submit asChild>
<Button
disabled={isGlobalLoading}
className="flex items-center"
>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<LoadingSpinner className="size-4 " />
) : (
"Continue"
);
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
</div>
</CardFooter>
</Card>
</SignUp.Step>
<SignUp.Step name="verifications">
<SignUp.Strategy name="email_code">
<Card className="w-full sm:w-96 ">
<CardHeader>
<CardTitle>Verify your email</CardTitle>
<CardDescription>
Use the verification link sent to your email address
</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<div className="grid items-center justify-center gap-y-2">
<Clerk.Field name="code" className="space-y-2">
<Clerk.Label className="sr-only">
Email address
</Clerk.Label>
<div className="flex justify-center text-center">
<Clerk.Input
type="otp"
className="flex justify-center has-[:disabled]:opacity-50"
autoSubmit
render={({ value, status }) => {
return (
<div
data-status={status}
className={cn(
"relative flex size-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
{
"z-10 ring-2 ring-ring ring-offset-background":
status === "cursor" ||
status === "selected",
}
)}
>
{value}
{status === "cursor" && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
}}
/>
</div>
<Clerk.FieldError className="block text-center text-sm text-destructive" />
</Clerk.Field>
<SignUp.Action
asChild
resend
className="text-muted-foreground"
fallback={({ resendableAfter }) => (
<Button variant="link" size="sm" disabled>
Didn&apos;t receive a code? Resend (
<span className="tabular-nums">
{resendableAfter}
</span>
)
</Button>
)}
>
<Button type="button" variant="link" size="sm">
Didn&apos;t receive a code? Resend
</Button>
</SignUp.Action>
</div>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Action submit asChild>
<Button
disabled={isGlobalLoading}
className="flex items-center"
>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<LoadingSpinner className="size-4 " />
) : (
"Continue"
);
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
</div>
</CardFooter>
</Card>
</SignUp.Strategy>
</SignUp.Step>
</>
)}
</Clerk.Loading>
</SignUp.Root>
</div>
);
}

@ -0,0 +1,73 @@
/*
* 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 React, { useEffect, useState } from "react";
import { Gradient } from "stripe-gradient";
export default function GradientBanner({
children,
}: {
children?: React.ReactNode;
}) {
const [gradientId, setGradientId] = useState("gradient-banner");
useEffect(() => {
setGradientId("gradient-banner");
const gradient = new Gradient();
gradient.initGradient("#" + gradientId);
}, [gradientId]);
return (
<div className="fixed top-0 left-0 backdrop-blur">
<canvas
id={gradientId}
data-js-darken-top
className="w-screen blur-sm h-[4rem] border-b z-1"
style={
{
"--gradient-color-1": "#6ec3f4",
"--gradient-color-2": "#3a3aff",
"--gradient-color-3": "#ff61ab",
"--gradient-color-4": "#E63946",
webKitMaskImage: "linear-gradient(to top, transparent, black)",
maskImage: "linear-gradient(to top, transparent, black)",
} as React.CSSProperties
}
height="64"
width={window.screen.width}
/>{" "}
<div className="fixed top-0 left-0 z-2 p-2 text-left text-black dark:text-white">
{children}
</div>
</div>
);
}

@ -1,7 +1,7 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://list.mlnehut.com/docs/legal/external-content-agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
@ -28,26 +28,16 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { Button } from "@/components/ui/button";
import Link from "next/link";
"use client";
/** used when there is a outage */
export const banner = {
isBanner:
process.env.NEXT_PUBLIC_VERCEL_ENV !== "production"
? true
: /** Set this to true when outage --->*/ false,
bannerText:
process.env.NEXT_PUBLIC_VERCEL_ENV !== "production" ? (
<>
Your not in production!{" "}
<Link href="https://list.mlnehut.com">
<Button variant="link" className="dark:text-black">
Go to production
</Button>
</Link>
</>
) : (
<>{/** Set this to an explanation! */}</>
),
};
import { useBanners } from "@/lib/hooks/use-banners";
export default function AllBanners() {
const { banners } = useBanners();
return (
<div className="fixed grid grid-cols-1 z-10">
{banners.map((banner) => banner.bannerContent)}
</div>
);
}

@ -0,0 +1,51 @@
/*
* 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 useTotalBannerSize from "@/lib/hooks/use-total-banner-size";
export default function BannerContainer({
children,
className,
style,
}: {
children: React.ReactNode;
className?: string;
style: (size: number) => React.CSSProperties;
}) {
const { bannerSize } = useTotalBannerSize();
return (
<div className={className} style={style(bannerSize)}>
{children}
</div>
);
}

@ -0,0 +1,55 @@
"use client";
import ClientFadeIn from "@/components/ClientFadeIn";
import { BrandingGenericIcon } from "@/components/Icon";
import TextFromPathname from "@/components/TextFromPathname";
import TopBar from "@/components/clerk/Topbar";
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import NextTopLoader from "@/lib/top-loader";
import Link from "next/link";
import BannerContainer from "@/components/feat/BannerContainer";
import { Inter } from "next/font/google";
const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
export default function LayoutPart({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<BannerContainer
className={"w-screen h-[3rem] border-b fixed backdrop-blur flex z-10"}
style={(size: number) => ({
marginTop: `${2 * size}rem`,
})}
>
<div className="items-center me-auto mt-2 pl-7 max-sm:mt-3">
<Breadcrumb>
<BreadcrumbList>
<Link href="/">
<BreadcrumbPage className="max-sm:hidden">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] " />
</BreadcrumbPage>
</Link>
<TextFromPathname />
</BreadcrumbList>
</Breadcrumb>
</div>
<TopBar inter={inter.className} />
</BannerContainer>
<BannerContainer
style={(size: number) => ({
paddingTop: `${2 * size}rem`,
})}
>
<NextTopLoader />
<ClientFadeIn>{children}</ClientFadeIn>
</BannerContainer>
</>
);
}

@ -0,0 +1,50 @@
/*
* 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 default function MainBanner({
className,
size = 1,
children,
}: {
className?: string;
size?: number;
children?: React.ReactNode;
}) {
return (
<div
className={`w-screen border-b text-black flex items-center text-center font-medium pl-2 ${className}`}
style={{
height: `${size * 2}rem`,
}}
>
{children}
</div>
);
}

@ -0,0 +1,29 @@
/*
* 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.
*/

@ -0,0 +1,83 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://list.mlnehut.com/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { SignedIn } from "@clerk/clerk-react";
import { useUser } from "@clerk/nextjs";
import {
KnockProvider,
KnockFeedProvider,
NotificationIconButton,
NotificationFeedPopover,
NotificationCell,
} from "@knocklabs/react";
import { useRef, useState } from "react";
import "@knocklabs/react/dist/index.css";
import { Button } from "../ui/button";
import { Bell } from "lucide-react";
import { useTheme } from "next-themes";
export default function KnockNotification() {
const [isVisible, setIsVisible] = useState(false);
const notifButtonRef = useRef(null);
const { user } = useUser();
const { resolvedTheme } = useTheme();
return (
<SignedIn>
<KnockProvider
apiKey={process.env.NEXT_PUBLIC_KNOCK_KEY as string}
userId={user?.id as string}
>
<KnockFeedProvider
feedId={process.env.NEXT_PUBLIC_CHANNEL_ID as string}
colorMode={resolvedTheme}
>
<>
<Button
size="icon"
variant="ghost"
className="mb-1"
ref={notifButtonRef}
onClick={() => setIsVisible(!isVisible)}
>
<Bell className="h-[1.2rem] w-[1.2rem]" />
</Button>
<NotificationFeedPopover
buttonRef={notifButtonRef}
isVisible={isVisible}
onClose={() => setIsVisible(false)}
/>
</>
</KnockFeedProvider>
</KnockProvider>
</SignedIn>
);
}

@ -0,0 +1,99 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import { ChevronDown } from "lucide-react";
export function ServerListNav() {
return (
<div className="w-full max-w-6xl mx-auto p-4">
<div className="space-y-4">
<h1 className="text-xl font-semibold">Links</h1>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-between gap-2 sm:gap-8">
<div className="grid grid-cols-2 sm:flex sm:flex-row items-stretch sm:items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full gap-2">
Filter
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem>All links</DropdownMenuItem>
<DropdownMenuItem>Active links</DropdownMenuItem>
<DropdownMenuItem>Archived links</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full gap-2">
Display
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem>List view</DropdownMenuItem>
<DropdownMenuItem>Grid view</DropdownMenuItem>
<DropdownMenuItem>Compact view</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="search" placeholder="Search..." className="pl-8" />
</div>
<Button className="shrink-0 gap-2">
Create link
<kbd className="pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
C
</kbd>
</Button>
<Button variant="ghost" size="icon" className="shrink-0">
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More options</span>
</Button>
</div>
</div>
</div>
</div>
);
}

@ -29,8 +29,7 @@
*/
"use client";
import { banner } from "@/config/banner";
import useTotalBannerSize from "@/lib/hooks/use-total-banner-size";
import { useEffect, useState, ReactNode } from "react";
export default function StickyTopbar({
@ -43,6 +42,7 @@ export default function StickyTopbar({
className?: string;
}) {
const [isSticky, setIsSticky] = useState(false);
const { bannerSize } = useTotalBannerSize();
const handleScroll = () => {
if (window.scrollY > scrollElevation) {
@ -61,7 +61,10 @@ export default function StickyTopbar({
return (
<div
className={`transition-all duration-300 ${isSticky ? "fixed left-0 w-full backdrop-blur shadow-lg " + (banner.isBanner == true ? "top-[70px] " : "top-[38px] ") + className : "block w-full bg-transparent"}`}
className={`transition-all duration-300 ${isSticky ? "fixed left-0 w-full backdrop-blur shadow-lg " + className : "block w-full bg-transparent"}`}
style={{
top: isSticky ? `${bannerSize * 32 + 38}px` : undefined,
}}
>
{children}
</div>

@ -0,0 +1,7 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

@ -0,0 +1,17 @@
import type { SVGProps } from "react";
const Discord = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 199"
width="1em"
height="1em"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}
>
<path
d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
fill="#5865F2"
/>
</svg>
);
export default Discord;

@ -0,0 +1,15 @@
import type { SVGProps } from "react";
const Github = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 250"
width="1em"
height="1em"
fill="#fff"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}
>
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
export default Github;

41
src/config/banners.tsx Normal file

@ -0,0 +1,41 @@
export const defaultBanners: {
bannerSpace: number;
bannerContent: React.ReactNode;
}[] = [
// The sponsor banner ALWAYS has to be first.
// {
// bannerSpace: 2,
// bannerContent: (
// <MainBanner size={2} className="max-h-[4rem] border-0">
// {" "}
// <GradientBanner>
// <strong>???</strong> — <i>an official affiliate of MHSF</i>{" "}
// <br />
// Lorem ipsum odor amet, consectetuer adipiscing elit. — check it out
// </GradientBanner>
// </MainBanner>
// ),
// },
];
export const bannerHooks: (() =>
| { bannerSpace: number; bannerContent: React.ReactNode }
| undefined)[] = [
() => {
// if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production")
// return {
// bannerSpace: 1,
// bannerContent: (
// <MainBanner className="bg-orange-600">
// Your not in production!{" "}
// <Link href="https://mhsf.app">
// <Button variant="link" className="dark:text-black">
// Go to production
// </Button>
// </Link>
// </MainBanner>
// ),
// };
return undefined;
},
];

@ -65,7 +65,7 @@ export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
features={[
"New MOTD engine that is over 3,000% faster, runs client-side, and doesn't need any requests to run.",
"Fixed issue where GitHub link was broken if you were signed-out",
"",
"Adding snowfall finally (better late then ever)",
]}
title={
<strong className="flex items-center">

@ -62,13 +62,17 @@ async function apiConstructor<K>(
export async function getMOTDFromServer(
list: Array<{ server: string; motd: string }>,
): Promise<Array<{ server: string; motd: string }>> {
const result = await fetch(connector("/motd", { version: 1 }), {
const result = await fetch(
process.env.NEXT_PUBLIC_ALTERNATE_MOTD_ENDPOINT ??
connector("/motd", { version: 1 }),
{
body: JSON.stringify({ motd: list }),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
},
);
let json = await result.json();
return json.result;

@ -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.
*/
import { bannerHooks, defaultBanners } from "@/config/banners";
import { useEffect, useState } from "react";
import { useIsMobile } from "./use-mobile";
export function useBanners() {
const [banners, setBanners] = useState<
{
bannerSpace: number;
bannerContent: React.ReactNode;
}[]
>(defaultBanners);
const isOnMobile = useIsMobile();
useEffect(() => {
if (isOnMobile) {
setBanners([]);
return;
}
setBanners(defaultBanners);
bannerHooks.forEach((hook) => {
const run = hook();
if (run !== undefined) setBanners((oldBanners) => [...oldBanners, run]);
});
}, [isOnMobile]);
return { banners };
}

@ -0,0 +1,61 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { bannerHooks, defaultBanners } from "@/config/banners";
import { useEffect, useState } from "react";
import { useIsMobile } from "./use-mobile";
export default function useTotalBannerSize() {
const [bannerSize, setBannerSize] = useState(0);
const isOnMobile = useIsMobile();
useEffect(() => {
setBannerSize(0);
if (isOnMobile) return;
const allBanners = [];
// First push the default banners
allBanners.push(...defaultBanners);
// Then push the banner hooks
bannerHooks.forEach((hook) => {
allBanners.push(hook());
});
setBannerSize(
allBanners.reduce(
(acc, banner) => acc + (banner ?? { bannerSpace: 0 }).bannerSpace,
0
) ?? 0
);
}, [isOnMobile]);
return { bannerSize };
}

5
src/lib/types/stripe-gradient.d.ts vendored Normal file

@ -0,0 +1,5 @@
declare module "stripe-gradient" {
declare class Gradient {
initGradient(id: string): void;
};
}

@ -1178,6 +1178,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-aspect-ratio@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.1.tgz#95d7692e61bab5eb7fec91f241ea993899593313"
integrity sha512-kNU4FIpcFMBLkOUcgeIteH06/8JLBcYY6Le1iKenDGCYNYFX3TQqCZjzkOsz37h7r94/99GTb7YhEr98ZBJibw==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-avatar@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz#5848d2ed5f34d18b36fc7e2d227c41fca8600ea1"