feat: finish home page

This commit is contained in:
dvelo 2025-04-21 23:25:26 -05:00
parent 697a5f2ed7
commit be55ec13db
13 changed files with 773 additions and 132 deletions

@ -57,6 +57,7 @@
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"cron": "^3.1.7", "cron": "^3.1.7",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"framer-motion": "^12.7.4",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"inngest": "^3.21.2", "inngest": "^3.21.2",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",

@ -9354,6 +9354,15 @@ body {
transform: rotate(-5deg) scale(0.9); transform: rotate(-5deg) scale(0.9);
} }
} }
@keyframes ripple {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
}
50% {
transform: translate(-50%, -50%) scale(0.9);
}
}
/* /*
---break--- ---break---
*/ */
@ -9368,9 +9377,15 @@ body {
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--animate-aurora: aurora 8s ease-in-out infinite alternate; --animate-aurora: aurora 8s ease-in-out infinite alternate;
--animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite; --animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite;
@keyframes ripple {
0%, 100% { --animate-line-shadow: line-shadow 15s linear infinite;
transform: translate(-50%, -50%) scale(1);}
50% { @keyframes line-shadow {
transform: translate(-50%, -50%) scale(0.9);}} 0% {
background-position: 0 0;
}
100% {
background-position: 100% -100%;
}
}
} }

@ -5,100 +5,107 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Github from "@/components/ui/github"; import Github from "@/components/ui/github";
import Image from "next/image" import Image from "next/image"
import { usePathname } from "next/navigation";
const hideFooterPages = ["/home"];
export function Footer() { export function Footer() {
return ( const pathname = usePathname();
<footer className="w-full border-t mt-15"> if (!hideFooterPages.includes(pathname ?? ""))
<div className="flex justify-between items-start p-[20px]"> return (
<span className="flex items-center gap-4 text-muted-foreground"> <footer className="w-full mt-15 border-t border-neutral-500/20 bg-neutral-100 dark:border-neutral-700/50 dark:bg-neutral-900 text-muted-foreground">
<Link href="Special:Root"> <div className="flex justify-between items-start p-[20px]">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" /> <span className="flex items-center gap-4">
</Link> <Link href="Special:Root">
<ul className="grid grid-cols-2 md:flex gap-4 p-0 m-0 w-full md:items-center items-start list-none"> <BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
<li className="text-sm">
<Link
href="/home"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Home
</Link>
</li>
<li className="text-sm">
<Link
href="/servers"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Servers
</Link>
</li>
<li className="text-sm">
<Link
href="/support"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Contact
</Link>
</li>
</ul>
</span>
<div className="block">
<div className="flex items-center mb-2 justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Discord className="w-[1.25em] h-[1.25em]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
<span className="block">
Minehut Discord
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
</span>
</DropdownMenuItem>
</Link>
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
<span className="block">
MHSF Discord
<small className="flex">A read-only server for updates related to MHSF.</small>
</span>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<Link href="https://github.com/DeveloLongScript/MHSF" noExtraIcons>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Github className="w-[1.25em] h-[1.25em]" />
</Button>
</Link> </Link>
<ul className="grid grid-cols-2 md:flex gap-4 p-0 m-0 w-full md:items-center items-start list-none">
<li className="text-sm">
<Link
href="/home"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Home
</Link>
</li>
<li className="text-sm">
<Link
href="/servers"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Servers
</Link>
</li>
<li className="text-sm">
<Link
href="/support"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Contact
</Link>
</li>
</ul>
</span>
<div className="block">
<div className="flex items-center mb-2 justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Discord className="w-[1.25em] h-[1.25em]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
<span className="block">
Minehut Discord
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
</span>
</DropdownMenuItem>
</Link>
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
<span className="block">
MHSF Discord
<small className="flex">A read-only server for updates related to MHSF.</small>
</span>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<Link href="https://github.com/DeveloLongScript/MHSF" noExtraIcons>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Github className="w-[1.25em] h-[1.25em]" />
</Button>
</Link>
</div>
<FooterStatus />
</div> </div>
<FooterStatus />
</div> </div>
</div> <span className="block px-4 -translate-y-12">
<span className="block border-t border-neutral-500/20 bg-neutral-100 px-7 py-10 dark:border-neutral-700/50 dark:bg-neutral-900"> <small className="text-[0.75rem]">
<small className="text-[0.75rem]"> MHSF is an open-source project licensed under the MIT license. MHSF is
MHSF is an open-source project licensed under the MIT license. MHSF is not officially affiliated with with Minehut, Super League Enterprise,
not officially affiliated with with Minehut, Super League Enterprise, or GamerSafer in any way. <br className="spacing-3" />
or GamerSafer in any way. <br className="spacing-3" /> Spamming, abusing or misusing the Minehut API and/or MHSF will get
Spamming, abusing or misusing the Minehut API and/or MHSF will get your IP blocked, we are not responsible for IP blocks.{" "}
your IP blocked, we are not responsible for IP blocks.{" "} <strong>You have been warned.</strong>
<strong>You have been warned.</strong> <br className="spacing-3" />
<br className="spacing-3" /> If you're worried, please review the{" "}
If you're worried, please review the{" "} <Link href="https://support.minehut.com/hc/en-us/articles/27075816947731-Minehut-Rules">
<Link href="https://support.minehut.com/hc/en-us/articles/27075816947731-Minehut-Rules"> Rules
Rules </Link>
</Link> , <Link href="https://minehut.com/terms-of-service">ToS</Link> &{" "}
, <Link href="https://minehut.com/terms-of-service">ToS</Link> &{" "} <Link href="https://t.mhsf.app/pr">Platform Rules</Link>.
<Link href="https://t.mhsf.app/pr">Platform Rules</Link>. </small>
</small> </span>
</span> </footer>
</footer> );
);
return null;
} }

@ -1,7 +1,7 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "framer-motion";
import React, { import React, {
type ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
useEffect, useEffect,

@ -0,0 +1,199 @@
"use client";
import { cn } from "@/lib/utils";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
squareSize?: number;
gridGap?: number;
flickerChance?: number;
color?: string;
width?: number;
height?: number;
className?: string;
maxOpacity?: number;
}
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
...props
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,";
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
return `rgba(${r}, ${g}, ${b},`;
};
return toRGBA(color);
}, [color]);
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const cols = Math.floor(width / (squareSize + gridGap));
const rows = Math.floor(height / (squareSize + gridGap));
const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity;
}
return { cols, rows, squares, dpr };
},
[squareSize, gridGap, maxOpacity],
);
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity;
}
}
},
[flickerChance, maxOpacity],
);
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number,
) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr,
);
}
}
},
[memoizedColor, squareSize, gridGap],
);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let gridParams: ReturnType<typeof setupCanvas>;
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth;
const newHeight = height || container.clientHeight;
setCanvasSize({ width: newWidth, height: newHeight });
gridParams = setupCanvas(canvas, newWidth, newHeight);
};
updateCanvasSize();
let lastTime = 0;
const animate = (time: number) => {
if (!isInView) return;
const deltaTime = (time - lastTime) / 1000;
lastTime = time;
updateSquares(gridParams.squares, deltaTime);
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr,
);
animationFrameId = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize();
});
resizeObserver.observe(container);
const intersectionObserver = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
},
{ threshold: 0 },
);
intersectionObserver.observe(canvas);
if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return (
<div
ref={containerRef}
className={cn(`h-full w-full ${className}`)}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
</div>
);
};

@ -0,0 +1,64 @@
"use client";
import { AnimatePresence, motion, Variants, MotionProps } from "framer-motion";
import { cn } from "@/lib/utils";
import { ElementType } from "react";
import React from "react";
interface FlipTextProps extends MotionProps {
/** The duration of the animation */
duration?: number;
/** The delay between each character */
delayMultiple?: number;
/** The variants of the animation */
framerProps?: Variants;
/** The class name of the component */
className?: string;
/** The element type of the component */
as?: ElementType;
/** The children of the component */
children: React.ReactNode;
/** The variants of the animation */
variants?: Variants;
}
const defaultVariants: Variants = {
hidden: { rotateX: -90, opacity: 0 },
visible: { rotateX: 0, opacity: 1 },
};
export function FlipText({
children,
duration = 0.5,
delayMultiple = 0.08,
className,
as: Component = "span",
variants,
...props
}: FlipTextProps) {
const MotionComponent = motion.create(Component);
const characters = React.Children.toArray(children).join("").split("");
return (
<div className="flex justify-center">
<AnimatePresence mode="wait">
{characters.map((char, i) => (
<MotionComponent
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={variants || defaultVariants}
transition={{ duration, delay: i * delayMultiple }}
className={cn("origin-center drop-shadow-sm", className, char === " " ? "px-1" : "")}
{...props}
>
{char}
</MotionComponent>
))}
</AnimatePresence>
</div>
);
}

@ -0,0 +1,105 @@
/*
* 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) 2025 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 { useEffectOnce } from "@/lib/useEffectOnce";
import {
Inter,
Roboto,
Montserrat,
Ubuntu_Condensed,
Ubuntu,
Poppins,
} from "next/font/google";
import { GeistSans } from "geist/font/sans";
import { useEffect, useMemo, useState, type JSX } from "react";
import { cn } from "@/lib/utils";
const inter = Inter({ subsets: ["latin"] });
const roboto = Roboto({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "700", "900"],
});
const montserrat = Montserrat({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "700", "900"],
});
const ubuntuCondensed = Ubuntu_Condensed({
subsets: ["latin"],
weight: ["400"],
});
const ubuntu = Ubuntu({
subsets: ["latin"],
weight: ["400"],
});
const poppins = Poppins({
subsets: ["latin"],
weight: ["400"],
});
const fonts = [
inter.style,
roboto.style,
montserrat.style,
GeistSans.style,
ubuntuCondensed.style,
ubuntu.style,
poppins.style,
];
export function FontChanger({
children,
nounderline
}: { children: JSX.Element | JSX.Element[] | string, nounderline?: boolean }) {
const [position, setPosition] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setPosition((position) => {
if (position === fonts.length - 1) return 0;
return position + 1;
});
}, 1000);
return () => clearInterval(interval);
}, []);
console.log(position);
return (
<div className="h-[1.2em] overflow-hidden flex items-center justify-center">
<span
className={cn("leading-[1.2] inline-block text-black dark:text-white font-normal h-[1.2em]", !nounderline ? "underline decoration-black dark:decoration-white" : "")}
style={fonts[position]}
>
{children}
</span>
</div>
);
}

@ -47,6 +47,13 @@ import { ExampleChart } from "./example-chart";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { type Avatar, AvatarCircles } from "./avatar-circles"; import { type Avatar, AvatarCircles } from "./avatar-circles";
import { Ripple } from "./ripple"; import { Ripple } from "./ripple";
import { FontChanger } from "./font-changer";
import { FlickeringGrid } from "./flickering-grid";
import { DiscordWordmark } from "@/components/ui/discord";
import { Separator } from "@/components/ui/separator";
import { LineShadowText } from "@/components/feat/home-page/line-shadow-text";
import { FlipText } from "./flip-text";
import { InteractiveHoverButton } from "./interactive-hover-button";
const getGitHubDetails = async () => { const getGitHubDetails = async () => {
const githubRepo = await ( const githubRepo = await (
@ -71,6 +78,7 @@ export default function HomePageComponent() {
const router = useRouter(); const router = useRouter();
const { isSignedIn } = useUser(); const { isSignedIn } = useUser();
const theme = useTheme(); const theme = useTheme();
const shadowColor = theme.resolvedTheme === "dark" ? "white" : "black";
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const [stars, setStars] = useState(0); const [stars, setStars] = useState(0);
const [stargazers, setStargazers] = useState<Avatar[]>([]); const [stargazers, setStargazers] = useState<Avatar[]>([]);
@ -117,7 +125,10 @@ export default function HomePageComponent() {
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-[rgb(10,10,10)] bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] " /> <div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-[rgb(10,10,10)] bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] " />
<h1 className="bg-clip-text animate-fade-in -translate-y-4 bg-gradient-to-br from-black from-30% to-black/40 pb-6 text-5xl font-semibold tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-5xl md:text-6xl lg:text-7xl dark:from-white dark:to-white/40"> <h1 className="bg-clip-text animate-fade-in -translate-y-4 bg-gradient-to-br from-black from-30% to-black/40 pb-6 text-5xl font-semibold tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-5xl md:text-6xl lg:text-7xl dark:from-white dark:to-white/40">
The missing half of Minehut The missing half of{" "}
<LineShadowText shadowColor="#407ce5" className="text-[#488aff]">
Minehut
</LineShadowText>
</h1> </h1>
<p className="animate-fade-in mb-12 -translate-y-4 text-balance text-lg tracking-tight text-neutral-400 opacity-0 [--animation-delay:400ms] md:text-xl "> <p className="animate-fade-in mb-12 -translate-y-4 text-balance text-lg tracking-tight text-neutral-400 opacity-0 [--animation-delay:400ms] md:text-xl ">
MHSF is the next generation Minehut server list wrapper, with <br /> MHSF is the next generation Minehut server list wrapper, with <br />
@ -150,8 +161,18 @@ export default function HomePageComponent() {
<br /> <br />
<br /> <br />
<br /> <br />
<p className="text-center w-full font-bold text-sm"> <p className="text-center w-full font-bold justify-center text-sm flex items-center gap-2">
An open-source unofficial project brought to you by dvelo An open-source unofficial project brought to you by{" "}
<Link
href="https://giftedly.dev"
className="flex gap-1 items-center"
>
<img
src="https://avatars.githubusercontent.com/u/52332868?v=4"
className="w-[24px] h-[24px] rounded-full border"
/>
dvelo
</Link>
</p> </p>
</section> </section>
<div className="flex items-center justify-center border-b text-shadcn-primary/5 min-h-[50px] z-0"> <div className="flex items-center justify-center border-b text-shadcn-primary/5 min-h-[50px] z-0">
@ -228,10 +249,7 @@ export default function HomePageComponent() {
.reverse() .reverse()
.flat() .flat()
.map((c) => ( .map((c) => (
<TypeScriptError <TypeScriptError name={c.name} code={c.code} />
name={c.name}
code={c.code}
/>
))} ))}
</AnimatedList> </AnimatedList>
@ -306,10 +324,10 @@ export default function HomePageComponent() {
entries. entries.
</p> </p>
</span> </span>
<div className="w-full"> <div className="w-full">
<ExampleChart /> <ExampleChart />
</div> </div>
<br /> <br />
</section> </section>
<section className="md:flex relative overflow-hidden h-[500px] md:justify-center md:items-center px-8 w-full text-center border-b"> <section className="md:flex relative overflow-hidden h-[500px] md:justify-center md:items-center px-8 w-full text-center border-b">
<span> <span>
@ -329,17 +347,145 @@ export default function HomePageComponent() {
<AvatarCircles numPeople={stars} avatarUrls={stargazers} /> <AvatarCircles numPeople={stars} avatarUrls={stargazers} />
</span> </span>
</span> </span>
<br /> <br />
</span> </span>
<Ripple mainCircleSize={700}/> <Ripple mainCircleSize={700} />
</section> </section>
<div className="flex items-center justify-center border-b text-shadcn-primary/5 min-h-[50px] z-0"> <div className="flex items-center justify-center border-b text-shadcn-primary/5 min-h-[50px] z-0">
<Badge className="animate-fade-in my-2 rounded-xl px-4 py-2 relative z-1 text-shadcn-primary"> <Badge className="animate-fade-in my-2 rounded-xl px-4 py-2 relative z-1 text-shadcn-primary">
For server <AuroraText>owners</AuroraText> For server <AuroraText>owners</AuroraText>
</Badge> </Badge>
</div> </div>
<section className="md:flex mt-15 md:justify-center md:items-center px-8 w-full text-center border-b">
<span>
<h1 className="animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 text-2xl font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-2xl md:text-3xl lg:text-4xl dark:from-white dark:to-white/40">
Showcase <FontChanger>your server</FontChanger>
</h1>
<p className="animate-fade-in mb-6 mt-6 -translate-y-4 text-balance text-md tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
Make your server stand out from the crowd with options to <br />
display what your server is.
</p>
</span>
</section>
<section className="flex w-full border-b">
<div className="lg:flex hidden border-r w-[50px] h-[350px] text-shadcn-primary/5 bg-[size:10px_10px] [background-image:repeating-linear-gradient(315deg,currentColor_0_1px,#0000_0_50%)] " />
<div className="lg:grid grid-cols-3 max-h-[350px] max-lg:h-full w-full">
<Material className="border-0 relative p-0! max-h-[350px] overflow-hidden rounded-none bg-transparent! hover:bg-white! hover:dark:bg-zinc-900! max-lg:h-[200px] transition-all">
<img
src="https://i.imgur.com/5dUxh0c.png"
style={{
maskImage: "linear-gradient(to top, transparent, black)",
}}
/>
<span className="mt-auto absolute bottom-0 backdrop-blur-lg px-4 pt-2">
<strong className="animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] dark:from-white dark:to-white/40">
Show. Not tell.
</strong>
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
Use a static banner that can show what your server is about.
</p>
</span>
</Material>
<Material className="border-0 p-4 relative rounded-none border-r border-l bg-transparent! hover:bg-white! hover:dark:bg-zinc-900! max-lg:h-[200px] transition-all">
<div className="h-[calc(100%-100px)] w-[calc(100%-32px)] mx-4 my-2 rounded-lg bg-[#5865F2] max-2xl:hidden">
<span className="flex items-center justify-between text-white p-4">
<DiscordWordmark fill="white" className="h-[16px]" />
<Separator orientation="vertical" />
<span>
<strong>300k+</strong> online currently
</span>
</span>
<div className="bg-[#454FBF] h-[calc(100%-50px)] w-full p-4 rounded-lg grid">
<Skeleton className="w-full h-[20px]" />
<Skeleton className="w-full h-[20px]" />
<Skeleton className="w-full h-[20px]" />
<Skeleton className="w-full h-[20px]" />
</div>
</div>
<span className="mt-auto absolute bottom-0 backdrop-blur-lg">
<strong className="animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] dark:from-white dark:to-white/40">
Link your community.
</strong>
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
Quickly enable a embed of your Discord server to show
directly on your server page.
</p>
</span>
</Material>
<Material className="!p-0 z-0 relative border-0 rounded-none bg-transparent! hover:bg-white! hover:dark:bg-zinc-900! max-lg:h-[200px] transition-all">
<span className="h-[calc(100%-100px)] flex justify-center items-center my-auto">
<FlickeringGrid
className="absolute inset-0 z-0 w-full h-full"
squareSize={2}
gridGap={4}
color="#6B7280"
maxOpacity={0.5}
flickerChance={0.1}
/>
<Material className="relative z-0 w-[300px]">
<span className="text-2xl">
<FontChanger nounderline>
<AuroraText>My Server</AuroraText>
</FontChanger>
</span>
Lorem ipsum dolor sit amet consectetur adipiscing elit.
Consectetur adipiscing elit quisque faucibus ex sapien
vitae.
</Material>
</span>
<span className="mt-auto absolute bottom-0 backdrop-blur-lg pl-4 pt-2">
<strong className="animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] dark:from-white dark:to-white/40">
Whats your server?
</strong>
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
Describe your server using Markdown to include bold, italic
& images directly in the description of your server.
</p>
</span>
</Material>
</div>
<div className="border-l lg:flex hidden w-[50px] h-[350px] text-shadcn-primary/5 bg-[size:10px_10px] [background-image:repeating-linear-gradient(315deg,currentColor_0_1px,#0000_0_50%)] " />
</section>
<section className="flex w-full items-center justify-center py-16 text-center">
<span>
<h1>
<FlipText className="text-balance bg-gradient-to-br pb-6 text-2xl font-semibold leading-none tracking-tighter sm:text-2xl md:text-3xl lg:text-4xl text-black dark:text-white">
The modern server list
</FlipText>
</h1>
<p className="mb-6 text-balance tracking-tight text-gray-400">
Whether you are a player, data hunter or server owner, MHSF{" "}
<br />
always has a solution to the community side of Minehut.
</p>
<Link href="/servers">
<InteractiveHoverButton>Find servers</InteractiveHoverButton>
</Link>
</span>
</section>
</section> </section>
</span> </span>
<div className="hidden md:block relative sm:max-w-[1158px] mx-auto w-full -z-10 h-[300px] overflow-clip mt-[100px]">
<div
className={cn(
"hidden md:block absolute left-1/2 z-10 -translate-x-1/2 translate-y-[125px]",
"text-center text-[255px] font-bold leading-none before:bg-gradient-to-b before:from-gray-200 before:dark:from-gray-700",
"before:to-gray-100/30 before:dark:to-gray-500/30 before:to-80% before:bg-clip-text before:text-transparent before:content-['MHSF']",
"after:absolute after:inset-0 after:bg-gray-300/70 after:dark:bg-gray-700/70 after:bg-clip-text after:text-transparent after:mix-blend-darken",
"after:content-['MHSF'] after:[text-shadow:0_1px_0_white] after:dark:[text-shadow:0_1px_0_black] dark:text-gray-700",
)}
/>
<FlickeringGrid
className="opacity-80 absolute z-5 translate-y-[8px]"
color="#6B7280"
style={{
maskImage: "linear-gradient(to bottom, transparent, black)",
}}
/>
</div>
</div> </div>
); );
} }

@ -0,0 +1,35 @@
import React from "react";
import { cn } from "@/lib/utils";
import { ArrowRightIcon } from "@radix-ui/react-icons";
interface InteractiveHoverButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
export const InteractiveHoverButton = React.forwardRef<
HTMLButtonElement,
InteractiveHoverButtonProps
>(({ children, className, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold",
className,
)}
{...props}
>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-shadcn-primary transition-all duration-300 group-hover:scale-[100.8]" />
<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0">
{children}
</span>
</div>
<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-shadcn-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100">
<span>{children}</span>
<ArrowRightIcon />
</div>
</button>
);
});
InteractiveHoverButton.displayName = "InteractiveHoverButton";

@ -0,0 +1,42 @@
import { cn } from "@/lib/utils";
import { motion, type MotionProps } from "framer-motion";
interface LineShadowTextProps
extends Omit<React.HTMLAttributes<HTMLElement>, keyof MotionProps>,
MotionProps {
shadowColor?: string;
as?: React.ElementType;
}
export function LineShadowText({
children,
shadowColor = "black",
className,
as: Component = "span",
...props
}: LineShadowTextProps) {
const MotionComponent = motion.create(Component);
const content = typeof children === "string" ? children : null;
if (!content) {
throw new Error("LineShadowText only accepts string content");
}
return (
<MotionComponent
style={{ "--shadow-color": shadowColor } as React.CSSProperties}
className={cn(
"relative z-0 inline-flex",
"after:absolute after:left-[0.04em] after:top-[0.04em] after:content-[attr(data-text)]",
"after:bg-[linear-gradient(45deg,transparent_45%,var(--shadow-color)_45%,var(--shadow-color)_55%,transparent_0)]",
"after:-z-10 after:bg-[length:0.06em_0.06em] after:bg-clip-text after:text-transparent",
"after:animate-line-shadow",
className,
)}
data-text={content}
{...props}
>
{content}
</MotionComponent>
);
}

@ -73,7 +73,8 @@ export function NavBar() {
className={cn( className={cn(
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex", "w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between", "items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
showBorder ? "border-b backdrop-blur-xl" : "", "lg:top-0 max-lg:bottom-0",
showBorder ? "border-b backdrop-blur-xl" : "max-lg:border-b max-lg:backdrop-blur-xl",
pathname !== null && animatedTopbarPages.includes(pathname) pathname !== null && animatedTopbarPages.includes(pathname)
? "[--animation-delay:1000ms] opacity-0 animate-fade-in" ? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
: "" : ""
@ -187,7 +188,7 @@ export function NavBar() {
</DropdownMenu> </DropdownMenu>
<SignedIn> <SignedIn>
<div <div
className="absolute right-0 z-10 h-full className="absolute right-0 -z-10 h-full
overflow-hidden w-full ml-auto" overflow-hidden w-full ml-auto"
style={{ borderRadius: "inherit" }} style={{ borderRadius: "inherit" }}
> >

File diff suppressed because one or more lines are too long

@ -6456,15 +6456,6 @@ forwarded@0.2.0:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
framer-motion@^11.3.8:
version "11.18.2"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718"
integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==
dependencies:
motion-dom "^11.18.1"
motion-utils "^11.18.1"
tslib "^2.4.0"
framer-motion@^12.7.4: framer-motion@^12.7.4:
version "12.7.4" version "12.7.4"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.7.4.tgz#50aeb8b5b5a672dea931bdb74956d7b526bf0b4b" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.7.4.tgz#50aeb8b5b5a672dea931bdb74956d7b526bf0b4b"
@ -9495,13 +9486,6 @@ mongodb@^6.12.0, mongodb@^6.8.0:
bson "^6.10.3" bson "^6.10.3"
mongodb-connection-string-url "^3.0.0" mongodb-connection-string-url "^3.0.0"
motion-dom@^11.18.1:
version "11.18.1"
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==
dependencies:
motion-utils "^11.18.1"
motion-dom@^12.7.4: motion-dom@^12.7.4:
version "12.7.4" version "12.7.4"
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.7.4.tgz#80f5f8d706e94bc29f6f4f4afa300ff9e1f976a2" resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.7.4.tgz#80f5f8d706e94bc29f6f4f4afa300ff9e1f976a2"
@ -9509,11 +9493,6 @@ motion-dom@^12.7.4:
dependencies: dependencies:
motion-utils "^12.7.2" motion-utils "^12.7.2"
motion-utils@^11.18.1:
version "11.18.1"
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b"
integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==
motion-utils@^12.7.2: motion-utils@^12.7.2:
version "12.7.2" version "12.7.2"
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.7.2.tgz#99b673d8851583b325bd0c8b0f04c5bf42b9b818" resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.7.2.tgz#99b673d8851583b325bd0c8b0f04c5bf42b9b818"