diff --git a/apps/www/biome.json b/apps/www/biome.json deleted file mode 100644 index faa0495..0000000 --- a/apps/www/biome.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "linter": { - "rules": { - "style": { - "useTemplate": "off", - "useImportType": "warn" - }, - "suspicious": { - "noExplicitAny": "off", - "noDoubleEquals": "warn" - }, - "complexity": { - "noForEach": "off" - } - } - } -} diff --git a/apps/www/package.json b/apps/www/package.json index 3bc82a9..bdb82c8 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -47,6 +47,7 @@ "input-otp": "^1.2.4", "json-beautify": "^1.1.1", "lucide-react": "^0.454.0", + "mini-svg-data-uri": "^1.4.4", "minimessage-2-html": "1.6.0", "minimessage-js": "^1.1.3", "mongodb": "^6.8.0", diff --git a/apps/www/src/app/(main)/home/page.tsx b/apps/www/src/app/(main)/home/page.tsx new file mode 100644 index 0000000..3d07584 --- /dev/null +++ b/apps/www/src/app/(main)/home/page.tsx @@ -0,0 +1,17 @@ +import HomePageComponent from "@/components/feat/home-page/home-page"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + applicationName: "MHSF", + title: "The modern server list. ยท MHSF", + description: + "The open-source, modern and friendly server list wrapper for Minehut.", +}; + +export default function HomePage() { + return ( +
+ +
+ ); +} diff --git a/apps/www/src/app/(main)/layout.tsx b/apps/www/src/app/(main)/layout.tsx index 55c5b88..670b83f 100644 --- a/apps/www/src/app/(main)/layout.tsx +++ b/apps/www/src/app/(main)/layout.tsx @@ -41,6 +41,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { ThemeProvider } from "@/components/util/theme-provider"; import { FontBoundary } from "@/components/util/font-boundary"; import { ClerkProvider } from "@/components/util/clerk-provider"; +import { Toaster } from "sonner"; export default function RootLayout({ children, @@ -65,23 +66,26 @@ export default function RootLayout({ - - - - - - -
{children}
-
-
-
-
-
+ + + + + + + + +
{children}
+
+
+
+
+
+
); } diff --git a/apps/www/src/app/globals.css b/apps/www/src/app/globals.css index 9eeab6b..1f69270 100644 --- a/apps/www/src/app/globals.css +++ b/apps/www/src/app/globals.css @@ -1,12 +1,14 @@ @import "tailwindcss"; @plugin 'tailwindcss-animate'; +@config '../../tailwind-hero.config.ts'; @custom-variant dark (&:is(.dark *)); @theme { --animate-spin: spin 1s linear infinite; --animate-scale-in: scaleIn 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + --animate-fade-in: fade-in 1000ms var(--animation-delay, 0ms) ease forwards; --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); @@ -142,6 +144,7 @@ @layer base { :root { --background: 0 0% 100%; + --border: 214.3 31.8% 91.4%; --foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; @@ -174,6 +177,13 @@ --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; + + *, + ::before, + ::after { + /* Workaround for Tailwind being stupid */ + border-color: hsl(214.3 31.8% 91.4%); + } } .dark { --border: 216 34% 17%; @@ -287,6 +297,16 @@ clip-path: inset(0 0 0 0); } } + @keyframes fade-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: none; + } + } .dark::view-transition-new(root), .light::view-transition-new(root) { diff --git a/apps/www/src/app/layout.tsx b/apps/www/src/app/layout.tsx index b0d6958..cb227a8 100644 --- a/apps/www/src/app/layout.tsx +++ b/apps/www/src/app/layout.tsx @@ -31,8 +31,6 @@ "use client"; import "./globals.css"; import { useSearchParams } from "next/navigation"; -import { ThemeProvider } from "@/components/util/theme-provider"; -import { ClerkProvider } from "@/components/util/clerk-provider"; import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"] }); @@ -49,14 +47,7 @@ export default function RootLayout({ - - {children} - + {children} ); diff --git a/apps/www/src/components/feat/home-page/home-page.tsx b/apps/www/src/components/feat/home-page/home-page.tsx new file mode 100644 index 0000000..14bc5b5 --- /dev/null +++ b/apps/www/src/components/feat/home-page/home-page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useClerk, useUser } from "@clerk/nextjs"; +import { ArrowDown, GalleryVertical } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Gradient } from "stripe-gradient"; + +export default function HomePageComponent() { + const clerk = useClerk(); + const router = useRouter(); + const { isSignedIn } = useUser(); + const { resolvedTheme } = useTheme(); + const [gradientId, setGradientId] = useState("gradient-banner"); + + useEffect(() => { + setGradientId("gradient-banner"); + const gradient = new Gradient(); + gradient.initGradient("#" + gradientId); + }, [gradientId]); + + return ( +
+ +
+
+ +

+ Meet MHSF,
+ the modern server list +

+

+ MHSF is the next generation Minehut server list wrapper, with
+ interactive filters, customizable web-pages, a modern interface and{" "} +
+ everything in-between. +

+ + + + + +
+
+ See more + + + +
+
+
+
+ + For players + +
+ + +

+ Find what you want now.
+ Not later. +

+

+ MHSF is built for finding servers, and only that, along with
+ allowing for maximum customizability with
+ both your experience and the webpages you interact with.
+

+
+ +
+
+ ); +} diff --git a/apps/www/src/components/feat/icons/branding-icons.tsx b/apps/www/src/components/feat/icons/branding-icons.tsx index 6de76fb..14cc723 100644 --- a/apps/www/src/components/feat/icons/branding-icons.tsx +++ b/apps/www/src/components/feat/icons/branding-icons.tsx @@ -32,6 +32,22 @@ import { useTheme } from "next-themes"; import type { SVGProps } from "react"; +export const brandingIconClipboard = ` + + + + + + + + + + + + + +`; + /** * Returns a colorful version of the branding icon. * diff --git a/apps/www/src/components/feat/navbar/navbar.tsx b/apps/www/src/components/feat/navbar/navbar.tsx index c65312b..a35cda8 100644 --- a/apps/www/src/components/feat/navbar/navbar.tsx +++ b/apps/www/src/components/feat/navbar/navbar.tsx @@ -1,5 +1,8 @@ "use client"; -import { BrandingGenericIcon } from "@/components/feat/icons/branding-icons"; +import { + BrandingGenericIcon, + brandingIconClipboard, +} from "@/components/feat/icons/branding-icons"; import { Button } from "@/components/ui/button"; import { ContextMenu, @@ -11,6 +14,7 @@ import { import { DropdownMenu, DropdownMenuContent, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import Github from "@/components/ui/github"; @@ -18,18 +22,31 @@ import { Link } from "@/components/util/link"; import { version } from "@/config/version"; import { useScroll } from "@/lib/hooks/use-scroll"; import { cn } from "@/lib/utils"; -import { Menu, ServerCrash } from "lucide-react"; +import { Home, Image, Menu, ServerCrash } from "lucide-react"; import { MenuDropdown } from "./menu-dropdown"; +import useClipboard from "@/lib/useClipboard"; +import { toast } from "sonner"; +import { SignedIn, SignedOut, useUser } from "@clerk/nextjs"; +import NextImage from "next/image"; +import { usePathname } from "next/navigation"; + +const animatedTopbarPages = ["/home"]; export function NavBar() { const showBorder = useScroll(40); + const clipboard = useClipboard(); + const pathname = usePathname(); + const { user } = useUser(); return (
@@ -46,13 +63,34 @@ export function NavBar() { + Platform - Go home + Go to Dynamic Home Page + + + + + Go to Home Page + + + + + + { + clipboard.writeText(brandingIconClipboard); + toast.success("Copied icon to clipboard!"); + }} + > + + Copy Logo as SVG + + @@ -74,18 +112,62 @@ export function NavBar() { - + + + + + + + +
+ +
+
); diff --git a/apps/www/src/components/feat/server-list/server-card.tsx b/apps/www/src/components/feat/server-list/server-card.tsx index f4f33ca..faa8d88 100644 --- a/apps/www/src/components/feat/server-list/server-card.tsx +++ b/apps/www/src/components/feat/server-list/server-card.tsx @@ -6,17 +6,40 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { toast } from "sonner"; -export default function ServerCard({ server }: { server: OnlineServer }) { +export default function ServerCard({ + server, + motd, +}: { + server: OnlineServer; + motd: string | undefined; +}) { return ( - + toast.success("pluh")} + tabIndex={0} + onKeyDown={(e) => { + // Only send user when they hit "Enter" + if (e.key === "Enter") toast.success("keyboard"); + }} + > + + Hit{" "} + + Enter + {" "} + to go to {server.name} + {server.name} - + by {server.author || "Nobody"} @@ -35,6 +58,12 @@ export default function ServerCard({ server }: { server: OnlineServer }) { )} + {motd && ( + + )} ); } diff --git a/apps/www/src/components/feat/server-list/server-list.tsx b/apps/www/src/components/feat/server-list/server-list.tsx index db42967..0790539 100644 --- a/apps/www/src/components/feat/server-list/server-list.tsx +++ b/apps/www/src/components/feat/server-list/server-list.tsx @@ -6,11 +6,33 @@ import { Separator } from "@/components/ui/separator"; import { Statistics } from "./statistics"; import InfiniteScroll from "react-infinite-scroll-component"; import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling"; +import { useEffect, useState } from "react"; +import MiniMessage from "minimessage-js"; export function ServerList() { const { servers, loading, serverCount, playerCount } = useServers(); const { itemsLength, fetchMoreData, hasMoreData, data } = useInfiniteScrolling(servers); + const [motdList, setMotdList] = useState<{ name: string; motd: string }[]>( + [] + ); + + useEffect(() => { + const result: Array<{ name: string; motd: string }> = []; + const mm = MiniMessage.miniMessage(); + servers.forEach((c) => { + try { + result.push({ + name: c.name, + motd: mm.toHTML(mm.deserialize(c.motd)), + }); + } catch (e) { + console.log(e); + } + }); + + setMotdList(result); + }, [servers]); if (loading) return ( @@ -45,7 +67,11 @@ export function ServerList() { >
{data.map((c) => ( - + x.name === c.name)?.motd} + /> ))}
diff --git a/apps/www/src/components/feat/server-list/statistics.tsx b/apps/www/src/components/feat/server-list/statistics.tsx index fef1f61..bbfc9cb 100644 --- a/apps/www/src/components/feat/server-list/statistics.tsx +++ b/apps/www/src/components/feat/server-list/statistics.tsx @@ -49,7 +49,7 @@ export function Statistics({ }, []); return ( -
+
@@ -96,7 +96,7 @@ export function Statistics({ {averagesLoading && } - + Total Servers @@ -150,7 +150,7 @@ export function Statistics({ {averagesLoading && } - + Top Server {" "} diff --git a/apps/www/src/components/ui/skeleton.tsx b/apps/www/src/components/ui/skeleton.tsx index d7e45f7..fc7d600 100644 --- a/apps/www/src/components/ui/skeleton.tsx +++ b/apps/www/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Skeleton({ className, @@ -6,10 +6,10 @@ function Skeleton({ }: React.HTMLAttributes) { return (
- ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/apps/www/src/components/util/font-boundary.tsx b/apps/www/src/components/util/font-boundary.tsx index 265a7de..05a73bf 100644 --- a/apps/www/src/components/util/font-boundary.tsx +++ b/apps/www/src/components/util/font-boundary.tsx @@ -1,15 +1,16 @@ "use client"; - import { useSettingsStore } from "@/lib/hooks/use-settings-store"; import { Inter, Roboto } from "next/font/google"; import { useEffect, useState, type ReactNode } from "react"; import { GeistSans } from "geist/font/sans"; +import { usePathname } from "next/navigation"; const inter = Inter({ subsets: ["latin"] }); const roboto = Roboto({ subsets: ["latin"], weight: ["100", "300", "400", "500", "700", "900"], }); +const overflowXHiddenPages = ["/home"]; export function FontBoundary({ children, @@ -18,6 +19,7 @@ export function FontBoundary({ }) { const settingsStore = useSettingsStore(); const [fontFamily, setFontFamily] = useState("inter"); + const pathname = usePathname(); useEffect(() => { setFontFamily((settingsStore.get("font-family") ?? "inter") as string); @@ -39,7 +41,7 @@ export function FontBoundary({ default: return "system-ui-font--font-boundary"; } - })()}`} + })()} ${pathname !== null && overflowXHiddenPages.includes(pathname) ? "overflow-x-hidden" : ""}`} > {children} diff --git a/apps/www/src/lib/hooks/use-motd.tsx b/apps/www/src/lib/hooks/use-motd.tsx deleted file mode 100644 index 60cc66c..0000000 --- a/apps/www/src/lib/hooks/use-motd.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from "react"; -import type { OnlineServer } from "../types/mh-server"; -import MiniMessage from "minimessage-js"; - -export function useMOTD(servers: OnlineServer[]) { - const [motdList, setMotdList] = useState<{ name: string; motd: string }[]>( - [] - ); - - useEffect(() => { - setMotdList( - servers.map((server) => { - return { - name: server.name, - motd: MiniMessage.miniMessage().toHTML( - MiniMessage.miniMessage().deserialize(server.motd) - ), - }; - }) - ); - }, [servers]); - - return { - motdList, - getMotdForServer: (server: OnlineServer) => - motdList.find((c) => c.name === server.name)?.motd, - }; -} diff --git a/apps/www/src/middleware.ts b/apps/www/src/middleware.ts index a1cb1b4..0262859 100644 --- a/apps/www/src/middleware.ts +++ b/apps/www/src/middleware.ts @@ -29,17 +29,24 @@ */ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; -const isProtectedRoute = createRouteMatcher(["/account(.*)"]); -const isEmbed = createRouteMatcher(["/emebed(.*)"]); +// Thanks for the router matcher API Clerk <3 +const isRootRoute = createRouteMatcher(["/"]); export default process.env.NEXT_PUBLIC_IS_AUTH === "true" - ? clerkMiddleware((auth, req) => { - if (isProtectedRoute(req)) auth.protect(); - }) - : (request: NextRequest) => {}; + ? clerkMiddleware(async (auth, req) => { + if (isRootRoute(req)) { + switch ((await auth()).userId === null) { + case false: + return NextResponse.redirect(new URL("/servers", req.url)); + case true: + return NextResponse.redirect(new URL("/home", req.url)); + } + } + }) + : (request: NextRequest) => {}; export const config = { - matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], + matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], }; diff --git a/apps/www/tailwind-hero.config.ts b/apps/www/tailwind-hero.config.ts new file mode 100644 index 0000000..737932e --- /dev/null +++ b/apps/www/tailwind-hero.config.ts @@ -0,0 +1,54 @@ +import type { Config } from "tailwindcss"; + +import svgToDataUri from "mini-svg-data-uri"; + +import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; + +export default { + theme: { + extend: { + dropShadow: { + "card-hover": ["0 8px 12px #222A350d", "0 32px 80px #2f30370f"], + }, + }, + }, + plugins: [ + addVariablesForColors, + ({ matchUtilities, theme }: any) => { + matchUtilities( + { + "bg-grid": (value: any) => ({ + backgroundImage: `url("${svgToDataUri( + ``, + )}")`, + }), + "bg-grid-small": (value: any) => ({ + backgroundImage: `url("${svgToDataUri( + ``, + )}")`, + }), + "bg-dot": (value: any) => ({ + backgroundImage: `url("${svgToDataUri( + ``, + )}")`, + }), + }, + { + values: flattenColorPalette(theme("backgroundColor")), + type: "color", + }, + ); + }, + ], +} satisfies Config; + +function addVariablesForColors({ addBase, theme }: any) { + const allColors = flattenColorPalette(theme("colors")); + const newVars = Object.fromEntries( + Object.entries(allColors).map(([key, val]) => [`--${key}`, val]), + ); + + addBase({ + ":root": newVars, + }); +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..9ba4f49 --- /dev/null +++ b/biome.json @@ -0,0 +1,18 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "style": { + "useTemplate": "off", + "useImportType": "warn" + }, + "suspicious": { + "noExplicitAny": "off", + "noDoubleEquals": "warn" + }, + "complexity": { + "noForEach": "off" + } + } + } +} diff --git a/yarn.lock b/yarn.lock index 818e31f..738e948 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7572,6 +7572,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mini-svg-data-uri@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"