mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 22:14:59 -05:00
feat: probably some changes
This commit is contained in:
parent
d93693dc2d
commit
0db9f5aba2
@ -32,9 +32,11 @@
|
||||
"@radix-ui/react-menubar": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-select": "2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/next": "^11.0.0",
|
||||
@ -51,6 +53,7 @@
|
||||
"@vercel/functions": "^2.0.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"ag-grid-react": "^33.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"contentlayer": "^0.3.4",
|
||||
"cron": "^3.1.7",
|
||||
"discord.js": "^14.15.3",
|
||||
@ -59,13 +62,14 @@
|
||||
"input-otp": "^1.2.4",
|
||||
"json-beautify": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.479.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"minimessage-2-html": "1.6.0",
|
||||
"minimessage-js": "^1.1.3",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"mongodb": "^6.8.0",
|
||||
"motion": "^12.7.4",
|
||||
"next": "15.2.0",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-css-obfuscator": "^2.2.16",
|
||||
|
||||
BIN
apps/www/public/branding/section-1/filter-demo-dark.png
Normal file
BIN
apps/www/public/branding/section-1/filter-demo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
apps/www/public/branding/section-1/filter-demo-light.png
Normal file
BIN
apps/www/public/branding/section-1/filter-demo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/www/public/branding/section-2/alert-demo-dark.png
Normal file
BIN
apps/www/public/branding/section-2/alert-demo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/www/public/branding/section-2/alert-demo-light.png
Normal file
BIN
apps/www/public/branding/section-2/alert-demo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/www/public/branding/section-2/interactive-demo-dark.png
Normal file
BIN
apps/www/public/branding/section-2/interactive-demo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/www/public/branding/section-2/interactive-demo-light.png
Normal file
BIN
apps/www/public/branding/section-2/interactive-demo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
148
apps/www/src/app/(dashboard)/dashboard/layout.tsx
Normal file
148
apps/www/src/app/(dashboard)/dashboard/layout.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
import "../../globals.css";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { Command, X } from "lucide-react";
|
||||
import { IsScript } from "@/components/util/is-script";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { NavBar } from "@/components/feat/navbar/navbar";
|
||||
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";
|
||||
import { Footer } from "@/components/feat/footer/footer";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { IframeProtector } from "@/components/util/iframe-protector";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams?.get("theme") || "light";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<noscript>
|
||||
<main className="flex justify-center items-center text-center min-h-screen h-max">
|
||||
<Placeholder
|
||||
icon={<X />}
|
||||
title="JavaScript is required for MHSF"
|
||||
description="MHSF cannot grab servers or do other external requests without JavaScript."
|
||||
>
|
||||
<Link href="https://www.enable-javascript.com/">
|
||||
<Button>Here's how</Button>
|
||||
</Link>
|
||||
</Placeholder>
|
||||
</main>
|
||||
</noscript>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ClerkProvider>
|
||||
<IsScript>
|
||||
<NuqsAdapter>
|
||||
<FontBoundary>
|
||||
<TooltipProvider>
|
||||
<SidebarProvider>
|
||||
<Sidebar variant="inset">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<Command className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
Acme Inc
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
Enterprise
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild size="sm">
|
||||
<a href="#">
|
||||
<span>a</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
<SidebarInset>
|
||||
<Toaster richColors position="bottom-center" />
|
||||
<div className="overflow-x-hidden">{children}</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</TooltipProvider>
|
||||
</FontBoundary>
|
||||
</NuqsAdapter>
|
||||
</IsScript>
|
||||
</ClerkProvider>
|
||||
</ThemeProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -28,20 +28,6 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export class MHSF {
|
||||
private favorites: number = 0;
|
||||
private customization: any = {};
|
||||
|
||||
getMHSF() {
|
||||
return {favorites: this.favorites, customization: this.customization}
|
||||
}
|
||||
|
||||
setFavorites(num: number) {
|
||||
this.favorites = num;
|
||||
|
||||
}
|
||||
|
||||
setCustomizations(num: object) {
|
||||
this.customization = num;
|
||||
}
|
||||
export default function Dashboard() {
|
||||
return <>Hello world</>
|
||||
}
|
||||
0
apps/www/src/app/(main)/home/actions.tsx
Normal file
0
apps/www/src/app/(main)/home/actions.tsx
Normal file
@ -45,6 +45,7 @@ import { Toaster } from "sonner";
|
||||
import { Footer } from "@/components/feat/footer/footer";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { IframeProtector } from "@/components/util/iframe-protector";
|
||||
import NextTopLoader from "@/components/util/top-loader";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@ -78,10 +79,12 @@ export default function RootLayout({
|
||||
<ClerkProvider>
|
||||
<IsScript>
|
||||
<NuqsAdapter>
|
||||
<FontBoundary className="max-w-[800px]">
|
||||
<FontBoundary>
|
||||
<IframeProtector>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors position="bottom-center" />
|
||||
|
||||
<NextTopLoader showSpinner={false} />
|
||||
<div className="overflow-x-hidden">{children}</div>
|
||||
</TooltipProvider>
|
||||
</IframeProtector>
|
||||
|
||||
@ -59,7 +59,7 @@ export default function ModificationPage({
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className="p-4">
|
||||
<div
|
||||
className="h-[150px] w-full rounded-xl p-2"
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
|
||||
@ -50,6 +50,8 @@ import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { serverModDB } from "@/config/sl-mod-db";
|
||||
import { useIframeCommunication } from "@/lib/hooks/use-iframe-communication";
|
||||
import { useRouter } from "@/lib/useRouter";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@ -71,6 +73,8 @@ export default function ModificationPage({
|
||||
}) {
|
||||
const { category, "custom-mod": mod } = use(params);
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const { fromIframe: communicator } = useIframeCommunication();
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
@ -99,7 +103,7 @@ export default function ModificationPage({
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className=" p-4">
|
||||
<div
|
||||
className="h-[150px] w-full rounded-xl p-2"
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
@ -116,7 +120,21 @@ export default function ModificationPage({
|
||||
you proud?)
|
||||
</Markdown>
|
||||
<div className="flex justify-between items-center">
|
||||
<Button className="mt-2">
|
||||
<Button className="mt-2" onClick={async () => {
|
||||
const newModObj = {
|
||||
...modObj,
|
||||
active: !modObj.active
|
||||
}
|
||||
const modificationArray = (user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ?? [];
|
||||
modificationArray[modIndex] = newModObj;
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
activatedModifications: modificationArray
|
||||
}
|
||||
});
|
||||
}}>
|
||||
{modObj?.active ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
@ -146,6 +164,7 @@ export default function ModificationPage({
|
||||
},
|
||||
});
|
||||
toast.success(`Deleted in ${Date.now() - time}ms`);
|
||||
router.push(backRoute);
|
||||
}}
|
||||
>
|
||||
<Trash size={16} /> Delete
|
||||
@ -185,9 +204,7 @@ export default function ModificationPage({
|
||||
<SettingTitle>File name</SettingTitle>
|
||||
</SettingMeta>
|
||||
<Link
|
||||
href={
|
||||
`"/servers/embedded/sl-modification-frame/file/${modObj.originalFileName}`
|
||||
}
|
||||
href={`/servers/embedded/sl-modification-frame/file/${encodeURIComponent(modObj.originalFileName)}`}
|
||||
className="text-blue-600"
|
||||
>
|
||||
<code className="flex items-center">
|
||||
|
||||
@ -51,9 +51,9 @@ export default async function ServerListCategoryFrame({
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||
);
|
||||
|
||||
``
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className=" p-4">
|
||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||
<Link href="/servers/embedded/sl-modification-frame">
|
||||
<ArrowLeft size={20} />
|
||||
@ -62,7 +62,7 @@ export default async function ServerListCategoryFrame({
|
||||
</h1>
|
||||
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
|
||||
|
||||
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
|
||||
<Material className="mt-10 p-4 grid lg:grid-cols-3 grid-cols-6 gap-2">
|
||||
{categoryObj?.entries.map((m) => (
|
||||
<Link
|
||||
key={m.name}
|
||||
|
||||
@ -1,3 +1,32 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { use, useEffect, useRef, useState } from "react";
|
||||
@ -20,7 +49,7 @@ import { cn } from "@/lib/utils";
|
||||
import { debounce } from "lodash";
|
||||
import { tryCatch } from "@/lib/try-catch";
|
||||
import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { CustomErrors } from "@/components/feat/server-list/modification/custom-files/custom-errors";
|
||||
import { CustomErrors, validateCode } from "@/components/feat/server-list/modification/custom-files/custom-errors";
|
||||
import { CustomLint } from "@/components/feat/server-list/modification/custom-files/custom-lint";
|
||||
import { CustomTest } from "@/components/feat/server-list/modification/custom-files/custom-test";
|
||||
|
||||
@ -149,14 +178,17 @@ export default function CustomFilePage({
|
||||
);
|
||||
}, 300);
|
||||
|
||||
(async () => setSyntaxErrors(await validateCode(monacoRef, filename)))();
|
||||
|
||||
// biome-ignore lint: L
|
||||
useEffect(() => {
|
||||
setSuccessfullyLinted(false);
|
||||
debouncedSave();
|
||||
(async () => setSyntaxErrors(await validateCode(monacoRef, filename)))();
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className=" p-4">
|
||||
<div className="w-full justify-between flex items-center gap-2 my-2">
|
||||
<strong className="flex items-center gap-1">
|
||||
<Link href="/servers/embedded/sl-modification-frame/files">
|
||||
@ -167,9 +199,7 @@ export default function CustomFilePage({
|
||||
<span className="flex items-center gap-2">
|
||||
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
|
||||
<CustomErrors
|
||||
filename={filename}
|
||||
value={value}
|
||||
monacoRef={monacoRef}
|
||||
syntaxErrors={syntaxErrors}
|
||||
/>
|
||||
)}
|
||||
<Tooltip>
|
||||
@ -192,6 +222,7 @@ export default function CustomFilePage({
|
||||
<CustomTest
|
||||
value={value}
|
||||
successfullyLinted={successfullyLinted}
|
||||
filename={filename}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
||||
@ -55,6 +55,8 @@ import {
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { findSupportedOperations } from "../file/[filename]/page";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export default function ServerListModificationFrame() {
|
||||
const { user } = useUser();
|
||||
@ -62,9 +64,8 @@ export default function ServerListModificationFrame() {
|
||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
|
||||
const operations = usePlatforms(files);
|
||||
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className="p-4">
|
||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||
<Link href="/servers/embedded/sl-modification-frame">
|
||||
<ArrowLeft size={16} />
|
||||
@ -81,20 +82,23 @@ export default function ServerListModificationFrame() {
|
||||
)}
|
||||
{files.map((c, i) => (
|
||||
<Link
|
||||
href={`/servers/embedded/sl-modification-frame/file/${c.name}`}
|
||||
href={`/servers/embedded/sl-modification-frame/file/${encodeURIComponent(c.name)}`}
|
||||
className="w-full py-1 px-2 rounded-xl flex items-center gap-1 justify-between hover:bg-slate-100 dark:hover:bg-zinc-700/30"
|
||||
key={c.name}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<FileCode size={16} />
|
||||
{operations.length !== 0 && (
|
||||
<>
|
||||
{operations[i].filter && <Filter size={16} />}
|
||||
{operations[i].sort && <SortAsc size={16} />}
|
||||
</>
|
||||
)}
|
||||
{c.name}.ts
|
||||
</span>
|
||||
<span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenu></DropdownMenu>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
|
||||
@ -126,9 +130,38 @@ export default function ServerListModificationFrame() {
|
||||
>
|
||||
<Trash size={16} /> Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const [name, setName] = useState("");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
<Pencil size={16} /> Rename
|
||||
</DropdownMenuItem>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Rename file</DialogTitle>
|
||||
|
||||
<div className="flex items-center w-full">
|
||||
<Input
|
||||
className="rounded-r-none w-full"
|
||||
placeholder="you-should-use-this-format-for-typescript-files-please"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
value={name}
|
||||
/>
|
||||
<span className="px-4 text-sm py-2 border border-l-none rounded-r-md">
|
||||
.ts
|
||||
</span>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
|
||||
@ -45,7 +45,7 @@ export default function ServerListModificationFrame() {
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<main className=" p-4">
|
||||
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
|
||||
<div className="flex items-center gap-2 my-2">
|
||||
<Button size="sm">Active modifications</Button>
|
||||
@ -60,7 +60,15 @@ export default function ServerListModificationFrame() {
|
||||
features, as well.
|
||||
</span>
|
||||
<Material className="mt-10 p-4">
|
||||
{serverModDB.map((c) => (
|
||||
{serverModDB.map(
|
||||
(c) =>
|
||||
(!c.__custom ||
|
||||
(c.__custom &&
|
||||
(
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||
[]
|
||||
).length !== 0)) && (
|
||||
<div key={c.displayTitle} className="my-4">
|
||||
<h2 className="text-lg font-bold pb-3 flex justify-between">
|
||||
{c.displayTitle}
|
||||
@ -72,7 +80,7 @@ export default function ServerListModificationFrame() {
|
||||
View more
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
<div className="grid grid-cols-6 lg:grid-cols-3 gap-2">
|
||||
{c.entries.map((m) => (
|
||||
<Material
|
||||
elevation="high"
|
||||
@ -126,7 +134,8 @@ export default function ServerListModificationFrame() {
|
||||
</SignedIn>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</Material>
|
||||
</main>
|
||||
);
|
||||
|
||||
@ -100,7 +100,7 @@ export async function GET(
|
||||
// Connect to MongoDB
|
||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||
await mongo.connect();
|
||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const db = mongo.db("mhsf");
|
||||
|
||||
// Get player data (last 60 entries)
|
||||
const historyCollection = db.collection("history");
|
||||
|
||||
@ -77,6 +77,8 @@
|
||||
/* Workaround for Tailwind being stupid */
|
||||
border-color: hsl(214.3 31.8% 91.4%);
|
||||
}
|
||||
|
||||
--sidebar: hsl(0 0% 98%)
|
||||
}
|
||||
.dark {
|
||||
--border: 216 34% 17%;
|
||||
@ -105,12 +107,6 @@
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
@apply border-zinc-800;
|
||||
}
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
@ -126,6 +122,14 @@
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
--sidebar: hsl(240 5.9% 10%);
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
@apply border-zinc-800;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
@ -9327,3 +9331,40 @@ body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
25% {
|
||||
background-position: 50% 100%;
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
transform: rotate(-3deg) scale(0.95);
|
||||
}
|
||||
75% {
|
||||
background-position: 50% 0%;
|
||||
transform: rotate(3deg) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
@theme inline {
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-aurora: aurora 8s ease-in-out infinite alternate;
|
||||
}
|
||||
72
apps/www/src/components/feat/home-page/animated-list.tsx
Normal file
72
apps/www/src/components/feat/home-page/animated-list.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import React, {
|
||||
type ComponentPropsWithoutRef,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
export function AnimatedListItem({ children }: { children: React.ReactNode }) {
|
||||
const animations = {
|
||||
initial: { scale: 0, opacity: 0 },
|
||||
animate: { scale: 1, opacity: 1, originY: 0 },
|
||||
exit: { scale: 0, opacity: 0 },
|
||||
transition: { type: "spring", stiffness: 350, damping: 40 },
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div {...animations} layout className="mx-auto w-full">
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AnimatedListProps extends ComponentPropsWithoutRef<"div"> {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const AnimatedList = React.memo(
|
||||
({ children, className, delay = 1000, ...props }: AnimatedListProps) => {
|
||||
const [index, setIndex] = useState(0);
|
||||
const childrenArray = useMemo(
|
||||
() => React.Children.toArray(children),
|
||||
[children],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (index < childrenArray.length - 1) {
|
||||
const timeout = setTimeout(() => {
|
||||
setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length);
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [index, delay, childrenArray.length]);
|
||||
|
||||
const itemsToShow = useMemo(() => {
|
||||
const result = childrenArray.slice(0, index + 1).reverse();
|
||||
return result;
|
||||
}, [index, childrenArray]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col items-center gap-4", className)}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{itemsToShow.map((item) => (
|
||||
<AnimatedListItem key={(item as React.ReactElement).key}>
|
||||
{item}
|
||||
</AnimatedListItem>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AnimatedList.displayName = "AnimatedList";
|
||||
43
apps/www/src/components/feat/home-page/aurora-text.tsx
Normal file
43
apps/www/src/components/feat/home-page/aurora-text.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
|
||||
interface AuroraTextProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
({
|
||||
children,
|
||||
className = "",
|
||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||
speed = 1,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||
colors[0]
|
||||
})`,
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
animationDuration: `${10 / speed}s`,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
className="relative animate-aurora bg-[length:200%_auto] bg-clip-text text-transparent"
|
||||
style={gradientStyle}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AuroraText.displayName = "AuroraText";
|
||||
49
apps/www/src/components/feat/home-page/avatar-circles.tsx
Normal file
49
apps/www/src/components/feat/home-page/avatar-circles.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface Avatar {
|
||||
imageUrl: string;
|
||||
profileUrl: string;
|
||||
}
|
||||
interface AvatarCirclesProps {
|
||||
className?: string;
|
||||
numPeople?: number;
|
||||
avatarUrls: Avatar[];
|
||||
}
|
||||
|
||||
export const AvatarCircles = ({
|
||||
numPeople,
|
||||
className,
|
||||
avatarUrls,
|
||||
}: AvatarCirclesProps) => {
|
||||
return (
|
||||
<div className={cn("z-10 flex -space-x-4 rtl:space-x-reverse", className)}>
|
||||
{avatarUrls.map((url, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={url.profileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
key={index}
|
||||
className="h-10 w-10 rounded-full border-2 border-white dark:border-gray-800"
|
||||
src={url.imageUrl}
|
||||
width={40}
|
||||
height={40}
|
||||
alt={`Avatar ${index + 1}`}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
{(numPeople ?? 0) > 0 && (
|
||||
<a
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-white bg-black text-center text-xs font-medium text-white hover:bg-gray-600 dark:border-gray-800 dark:bg-white dark:text-black"
|
||||
href=""
|
||||
>
|
||||
+{numPeople}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
172
apps/www/src/components/feat/home-page/example-chart.tsx
Normal file
172
apps/www/src/components/feat/home-page/example-chart.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.
|
||||
*/"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", player_count: 91 },
|
||||
{ date: "2024-04-02", player_count: 106 },
|
||||
{ date: "2024-04-03", player_count: 104 },
|
||||
{ date: "2024-04-04", player_count: 111 },
|
||||
{ date: "2024-04-05", player_count: 113 },
|
||||
{ date: "2024-04-06", player_count: 114 },
|
||||
{ date: "2024-04-07", player_count: 108 },
|
||||
{ date: "2024-04-08", player_count: 89 },
|
||||
{ date: "2024-04-09", player_count: 96 },
|
||||
{ date: "2024-04-10", player_count: 123 },
|
||||
{ date: "2024-04-11", player_count: 120 },
|
||||
{ date: "2024-04-12", player_count: 140 },
|
||||
{ date: "2024-04-13", player_count: 128 },
|
||||
{ date: "2024-04-14", player_count: 130 },
|
||||
{ date: "2024-04-15", player_count: 114 },
|
||||
{ date: "2024-04-16", player_count: 98 },
|
||||
{ date: "2024-04-17", player_count: 102 },
|
||||
{ date: "2024-04-18", player_count: 103 },
|
||||
{ date: "2024-04-19", player_count: 102 },
|
||||
{ date: "2024-04-20", player_count: 112 },
|
||||
{ date: "2024-04-21", player_count: 117 },
|
||||
{ date: "2024-04-22", player_count: 119 },
|
||||
{ date: "2024-04-23", player_count: 129 },
|
||||
{ date: "2024-04-24", player_count: 121 },
|
||||
{ date: "2024-04-25", player_count: 126 },
|
||||
{ date: "2024-04-26", player_count: 98 },
|
||||
{ date: "2024-04-27", player_count: 102 },
|
||||
{ date: "2024-04-28", player_count: 100 },
|
||||
{ date: "2024-04-29", player_count: 101 },
|
||||
{ date: "2024-04-30", player_count: 104 },
|
||||
{ date: "2024-05-01", player_count: 109 },
|
||||
{ date: "2024-05-02", player_count: 86 },
|
||||
{ date: "2024-05-03", player_count: 93 },
|
||||
{ date: "2024-05-04", player_count: 108 },
|
||||
{ date: "2024-05-05", player_count: 112 },
|
||||
{ date: "2024-05-06", player_count: 111 },
|
||||
{ date: "2024-05-07", player_count: 96 },
|
||||
{ date: "2024-05-08", player_count: 100 },
|
||||
{ date: "2024-05-09", player_count: 124 },
|
||||
{ date: "2024-05-10", player_count: 134 },
|
||||
{ date: "2024-05-11", player_count: 144 },
|
||||
{ date: "2024-05-12", player_count: 156 },
|
||||
{ date: "2024-05-13", player_count: 180 },
|
||||
{ date: "2024-05-14", player_count: 167 },
|
||||
{ date: "2024-05-15", player_count: 154 },
|
||||
{ date: "2024-05-16", player_count: 124 },
|
||||
{ date: "2024-05-17", player_count: 112 },
|
||||
{ date: "2024-05-18", player_count: 114 },
|
||||
{ date: "2024-05-19", player_count: 121 },
|
||||
{ date: "2024-05-20", player_count: 96 },
|
||||
{ date: "2024-05-21", player_count: 102 },
|
||||
{ date: "2024-05-22", player_count: 131 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
player_count: {
|
||||
label: "Players",
|
||||
color: "hsl(var(--chart-1))",
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ExampleChart() {
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="fillPlayers" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-player_count)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-player_count)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="player_count"
|
||||
type="natural"
|
||||
fill="url(#fillPlayers)"
|
||||
stroke="var(--color-player_count)"
|
||||
stackId="a"
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
@ -33,31 +33,59 @@
|
||||
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 { ArrowDown, GalleryVertical, Star } from "lucide-react";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { Gradient } from "stripe-gradient";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AuroraText } from "./aurora-text";
|
||||
import { AnimatedList } from "./animated-list";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExampleChart } from "./example-chart";
|
||||
import { Link } from "@/components/util/link";
|
||||
import {type Avatar, AvatarCircles } from "./avatar-circles";
|
||||
|
||||
const getGitHubDetails = async () => {
|
||||
const githubRepo = await (await fetch("https://api.github.com/repos/DeveloLongScript/mhsf")).json()
|
||||
const githubStargazers = await (await fetch("https://api.github.com/repos/DeveloLongScript/mhsf/stargazers")).json()
|
||||
|
||||
return {
|
||||
stars: githubRepo.stargazers_count as number,
|
||||
stargazers: (githubStargazers as Array<{avatar_url: string, html_url: string}>).map((c) => {return {imageUrl: c.avatar_url, profileUrl: c.html_url}})
|
||||
}
|
||||
}
|
||||
|
||||
export default function HomePageComponent() {
|
||||
const clerk = useClerk();
|
||||
const router = useRouter();
|
||||
const { isSignedIn } = useUser();
|
||||
const theme = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [stars, setStars] = useState(0);
|
||||
const [stargazers, setStargazers] = useState<Avatar[]>([]);
|
||||
const [gradientId, setGradientId] = useState("gradient-banner");
|
||||
|
||||
useEffect(() => {
|
||||
setGradientId("gradient-banner");
|
||||
const gradient = new Gradient();
|
||||
gradient.initGradient("#" + gradientId);
|
||||
gradient.initGradient(`#${gradientId}`);
|
||||
}, [gradientId]);
|
||||
|
||||
useEffect(() => {
|
||||
getGitHubDetails().then((c) => {
|
||||
setStars(c.stars);
|
||||
setStargazers(c.stargazers);
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="pt-20">
|
||||
<div className="lg:pt-10">
|
||||
<canvas
|
||||
id={gradientId}
|
||||
// Slightly outside of container to give a REALLY nice glow effect
|
||||
className="w-screen h-[610px] absolute blur-sm border-b z-1 opacity-0 animate-fade-in [--animation-delay:800ms]"
|
||||
className="md:w-[calc(100vw-206px)] max-md:w-full md:pl-50 h-[610px] absolute blur-sm border-b z-1 opacity-0 animate-fade-in [--animation-delay:800ms]"
|
||||
style={
|
||||
{
|
||||
"--gradient-color-1":
|
||||
@ -76,7 +104,7 @@ export default function HomePageComponent() {
|
||||
height="64"
|
||||
width={window.screen.width}
|
||||
/>
|
||||
<div className="rounded-lg p-[72px] dark:bg-grid-white/[0.2] bg-grid-black/[0.2] w-full mx-auto relative z-9 min-h-[600px]">
|
||||
<div className="border p-[72px] dark:bg-grid-white/[0.2] bg-grid-black/[0.2] md:w-[calc(100vw-400px)] mx-auto relative z-9 min-h-[600px]">
|
||||
<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">
|
||||
@ -86,8 +114,7 @@ export default function HomePageComponent() {
|
||||
<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 />
|
||||
interactive filters, customizable web-pages, a modern interface and{" "}
|
||||
<br />
|
||||
everything in-between.
|
||||
<br /> everything in-between.
|
||||
</p>
|
||||
|
||||
<span className="flex items-center gap-2 -translate-y-4">
|
||||
@ -107,33 +134,218 @@ export default function HomePageComponent() {
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-auto justify-center py-15 py-auto w-full text-center text-sm border border-dashed group h-[150px]">
|
||||
<span>See more</span>
|
||||
<span className="flex justify-center group-hover:translate-y-6 transition-all">
|
||||
<ArrowDown size={16} />
|
||||
</span>
|
||||
</div>
|
||||
<br className="md:hidden" />
|
||||
<span className="w-full flex flex-col items-center justify-center max-lg:hidden">
|
||||
<section className="md:w-[calc(100vw-400px)] border">
|
||||
<section className="border-b pb-25">
|
||||
<br />
|
||||
<br />
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<span className="animate-fade-in -translate-y-4 bg-green-400/60 px-4 py-2 rounded">
|
||||
For players
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<p className="text-center w-full font-bold text-sm">
|
||||
An open-source unofficial project brought to you by dvelo
|
||||
</p>
|
||||
</section>
|
||||
<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">
|
||||
For server hunters
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="mt-15 flex justify-between px-6 items-center">
|
||||
<section className="flex border-b">
|
||||
<div className="md:flex hidden border-r w-[50px] h-[500px] text-shadcn-primary/5 bg-[size:10px_10px] [background-image:repeating-linear-gradient(315deg,currentColor_0_1px,#0000_0_50%)] " />
|
||||
<span className="mt-15 md:flex md:justify-between md:items-center px-8 w-full">
|
||||
<span>
|
||||
<h1 className="animate-fade-in -translate-y-4 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">
|
||||
Find what you want now. <br />
|
||||
Not later.
|
||||
<AuroraText>Not later.</AuroraText>
|
||||
</h1>
|
||||
<p className="animate-fade-in mb-12 -translate-y-4 text-balance text-md tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
|
||||
MHSF is built for finding servers, and only that, along with <br />
|
||||
MHSF is built for finding servers, and only that, along with{" "}
|
||||
<br />
|
||||
allowing for maximum customizability with <br />
|
||||
both your experience and the webpages you interact with. <br />
|
||||
both your experience and the webpages you interact with.{" "}
|
||||
<br />
|
||||
</p>
|
||||
</span>
|
||||
<Skeleton className="h-[300px] w-[500px] rounded-xl" />
|
||||
<Material className="w-[450px] h-[320px] p-0 relative">
|
||||
{" "}
|
||||
<img
|
||||
src={`/branding/section-1/filter-demo-${theme.resolvedTheme}.png`}
|
||||
className="absolute bottom-0 right-0 rounded-br-lg"
|
||||
alt="Filter Demo"
|
||||
/>{" "}
|
||||
</Material>
|
||||
</span>
|
||||
<div className="border-l md:flex hidden w-[50px] h-[500px] text-shadcn-primary/5 bg-[size:10px_10px] [background-image:repeating-linear-gradient(315deg,currentColor_0_1px,#0000_0_50%)] " />
|
||||
</section>
|
||||
<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">
|
||||
Build your <AuroraText>dream</AuroraText> server list
|
||||
</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">
|
||||
Server lists are massive. Using custom filters and sorting
|
||||
systems <br />
|
||||
allow you to shrink the amount of information you see in the way{" "}
|
||||
<br />
|
||||
<strong>you</strong> want it.
|
||||
</p>
|
||||
</span>
|
||||
</section>
|
||||
<section className="flex w-full">
|
||||
<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">
|
||||
<AnimatedList className="p-4">
|
||||
{Array.from({ length: 100 }, () => [
|
||||
{ name: "Cannot find name 'flse'.", code: "2304" },
|
||||
{
|
||||
name: "Type 'string' is not assignable to type 'boolean'.",
|
||||
code: "2322",
|
||||
},
|
||||
{
|
||||
name: "'mhsf' has no exported member named 'Mincehut'. Did you mean 'Minehut'?",
|
||||
code: "2724",
|
||||
},
|
||||
{ name: "Cannot find namespace 'React'.", code: "2503" },
|
||||
{
|
||||
name: "'server' is declared but its value is never read.",
|
||||
code: "6133",
|
||||
},
|
||||
{
|
||||
name: "This comparison appears to be unintentional because the types 'string' and 'boolean' have no overlap",
|
||||
code: "2367",
|
||||
},
|
||||
])
|
||||
.flat()
|
||||
.reverse()
|
||||
.map((c) => (
|
||||
<TypeScriptError name={c.name} code={c.code} key={c.code} />
|
||||
))}
|
||||
</AnimatedList>
|
||||
|
||||
<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">
|
||||
Type-safety across the board
|
||||
</strong>
|
||||
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
|
||||
Completely safe TypeScript code is easily achieveable when
|
||||
using MHSF custom modification with fully functioning
|
||||
TypeScript error detection.
|
||||
</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">
|
||||
<img
|
||||
src={`/branding/section-2/alert-demo-${theme.resolvedTheme}.png`}
|
||||
className="flex justify-center max-lg:hidden rounded-lg "
|
||||
alt="Alert Demo"
|
||||
width={340}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<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">
|
||||
Lint your code instantly
|
||||
</strong>
|
||||
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
|
||||
Worried your code is broken? Run a simulation of your
|
||||
modification or lint it very quickly.
|
||||
</p>
|
||||
</span>
|
||||
</Material>
|
||||
<Material className="pl-4 relative border-0 rounded-none bg-transparent! hover:bg-white! hover:dark:bg-zinc-900! max-lg:h-[200px] transition-all">
|
||||
<img
|
||||
src={`/branding/section-2/interactive-demo-${theme.resolvedTheme}.png`}
|
||||
className="absolute bottom-[70px] right-0 max-lg:hidden rounded"
|
||||
alt="Interactive Demo"
|
||||
width={340}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<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">
|
||||
Interactively edit your code
|
||||
</strong>
|
||||
<p className="animate-fade-in mb-6 text-balance tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms]">
|
||||
MHSF uses the Monaco Editor as the editor of choice for all
|
||||
custom modifications; the same editor that powers the Visual
|
||||
Studio Code editor.
|
||||
</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>
|
||||
<div className="flex items-center justify-center border-y 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">
|
||||
For data hunters
|
||||
</Badge>
|
||||
</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">
|
||||
Your data? <AuroraText>No problem.</AuroraText>
|
||||
</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">
|
||||
Data for servers are openly accessible behind no paywall or{" "}
|
||||
<br />
|
||||
verification for thousands of servers over millions of total{" "}
|
||||
<br />
|
||||
entries.
|
||||
</p>
|
||||
<ExampleChart />
|
||||
<br />
|
||||
</span>
|
||||
</section>
|
||||
<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">
|
||||
Don't trust us? <AuroraText>We're open-source.</AuroraText>
|
||||
</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">
|
||||
MHSF's entire codebase from microservice to frontend is <br />
|
||||
completely open-source under the MIT License.
|
||||
</p>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Link href="Special:GitHub">
|
||||
<Button>Check it out!</Button>
|
||||
</Link>
|
||||
<span className="flex items-center gap-2 border rounded-lg px-2 py-1">
|
||||
<Star size={16} />
|
||||
<AvatarCircles numPeople={stars} avatarUrls={stargazers}/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</section>
|
||||
</section>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeScriptError({ name, code }: { name: string; code: string }) {
|
||||
return (
|
||||
<figure
|
||||
className={cn(
|
||||
"block break-words mx-auto max-w-full cursor-pointer rounded-2xl p-4",
|
||||
// animation styles
|
||||
"transition-all duration-200 ease-in-out hover:scale-[103%]",
|
||||
// light styles
|
||||
"bg-transparent [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
|
||||
// dark styles
|
||||
"transform-gpu dark:bg-transparent dark:backdrop-blur-md dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
|
||||
)}
|
||||
>
|
||||
<div className="block gap-3">
|
||||
<div className="overflow-hidden">
|
||||
<p className="break-words block max-w-full text-lg font-medium dark:text-white ">
|
||||
<span className="text-sm sm:text-lg">{name}</span>
|
||||
<span className="mx-1">·</span>
|
||||
<span className="text-xs text-gray-500">ts({code})</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,18 +51,8 @@ import type { MonacoRefType } from "@/app/(sl-modification-frame)/servers/embedd
|
||||
|
||||
export type SyntaxErrorInterface = languages.typescript.Diagnostic[] | null;
|
||||
|
||||
export function CustomErrors({
|
||||
value,
|
||||
monacoRef,
|
||||
filename
|
||||
}: {
|
||||
value: string;
|
||||
monacoRef: RefObject<MonacoRefType | null>;
|
||||
filename: string;
|
||||
}) {
|
||||
const [syntaxErrors, setSyntaxErrors] = useState<SyntaxErrorInterface>();
|
||||
|
||||
const validateCode = () => {
|
||||
export const validateCode = (monacoRef: RefObject<MonacoRefType | null>, filename: string) => {
|
||||
return new Promise<SyntaxErrorInterface>((re, rj) => {
|
||||
if (!monacoRef.current) return;
|
||||
|
||||
monacoRef.current.languages.typescript
|
||||
@ -78,16 +68,20 @@ export function CustomErrors({
|
||||
).toString(),
|
||||
)
|
||||
.then((diags) => {
|
||||
setSyntaxErrors(diags);
|
||||
re(diags);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
|
||||
validateCode();
|
||||
};
|
||||
|
||||
export function CustomErrors({
|
||||
syntaxErrors
|
||||
}: {
|
||||
syntaxErrors: SyntaxErrorInterface;
|
||||
}) {
|
||||
|
||||
// biome-ignore lint: L
|
||||
useEffect(validateCode, [value]);
|
||||
|
||||
if (syntaxErrors !== null && syntaxErrors !== undefined)
|
||||
return (
|
||||
@ -149,5 +143,5 @@ export function CustomErrors({
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
return null;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -40,12 +40,11 @@ import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CustomTestSuccess } from "./custom-test-success";
|
||||
|
||||
export function CustomTest({value, successfullyLinted}: {value: string, successfullyLinted: boolean}) {
|
||||
export function CustomTest({value, successfullyLinted, filename}: {value: string, successfullyLinted: boolean; filename: string}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [filterEnabled, setFilterEnabled] = useState(true);
|
||||
const [sortEnabled, setSortEnabled] = useState(true);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [testMode, setTestMode] = useState<"filter" | "sort" | "">("");
|
||||
|
||||
// biome-ignore lint: values needed (but not shown by linter)
|
||||
@ -173,7 +172,7 @@ export function CustomTest({value, successfullyLinted}: {value: string, successf
|
||||
{success && <Check size={16} />}Test
|
||||
</Button>
|
||||
{success && (
|
||||
<CustomTestSuccess filename={fileName} testMode={testMode} value={value} />
|
||||
<CustomTestSuccess filename={filename} testMode={testMode} value={value} />
|
||||
)}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@ -39,7 +39,7 @@ export function ModificationButton({disabled}: {disabled?: boolean}) {
|
||||
<Button disabled={disabled}>Filters & Sorting</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="p-0 h-[600px] w-[1000px] !max-w-[800px] overflow-x-hidden">
|
||||
<DialogContent className="p-0 h-[600px] lg:w-[1000px] lg:!max-w-[800px] overflow-x-hidden">
|
||||
<ModificationFrame />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -43,5 +43,5 @@ export function ModificationFrame() {
|
||||
})
|
||||
}, [ref])
|
||||
|
||||
return <iframe ref={ref} src="/servers/embedded/sl-modification-frame" height={800} width={800} title="Server-list Modification Frame" />
|
||||
return <iframe ref={ref} src="/servers/embedded/sl-modification-frame" height={800} title="Server-list Modification Frame" />
|
||||
}
|
||||
@ -39,11 +39,22 @@ import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
||||
import { ModificationButton } from "./modification/modification-button";
|
||||
import { useFilters } from "@/lib/hooks/use-filters";
|
||||
import { ServerTestModeSelector } from "./server-test-mode-selector";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export function ServerList() {
|
||||
const { servers, loading, serverCount, playerCount } = useServers();
|
||||
const { filteredData, testModeEnabled, testModeLoading, testModeStatus } =
|
||||
useFilters(servers);
|
||||
const {
|
||||
filteredData,
|
||||
testModeEnabled,
|
||||
testModeLoading,
|
||||
testModeStatus,
|
||||
filterCount,
|
||||
loading: filterLoading,
|
||||
} = useFilters(servers);
|
||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||
useInfiniteScrolling(filteredData);
|
||||
|
||||
@ -69,13 +80,23 @@ export function ServerList() {
|
||||
Servers
|
||||
</h1>
|
||||
<div className="flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ModificationButton disabled={testModeEnabled} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="backdrop-blur bg-transparent text-black dark:text-white ">{filterCount} modification(s) enabled</TooltipContent>
|
||||
</Tooltip>
|
||||
<ServerTestModeSelector
|
||||
testModeStatus={testModeStatus}
|
||||
testModeEnabled={testModeEnabled}
|
||||
testModeLoading={testModeLoading}
|
||||
/>
|
||||
</div>
|
||||
{filterLoading ? (
|
||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
dataLength={itemsLength}
|
||||
next={fetchMoreData}
|
||||
@ -92,6 +113,7 @@ export function ServerList() {
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ export function ReportingDialog({
|
||||
|
||||
return (
|
||||
<Drawer direction="left" open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||
<DrawerContent className="p-4 lg:min-w-[600px] max-lg:min-w-[400px] max-lg:max-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
||||
Report server
|
||||
</DrawerTitle>
|
||||
|
||||
@ -42,11 +42,11 @@ export function ServerMainPage({
|
||||
/>
|
||||
)}
|
||||
<span className="flex items-center gap-2 w-full relative">
|
||||
<div className="bg-secondary p-4 rounded-lg ml-4">
|
||||
<div className="bg-secondary p-4 rounded-lg lg:ml-4">
|
||||
<IconDisplay server={server} />
|
||||
</div>
|
||||
<p className="w-full">
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="lg:flex justify-between w-full">
|
||||
<h1 className="text-2xl font-bold">{server.name}</h1>
|
||||
<span>
|
||||
<ServerPageButtons server={server} mhsfData={mhsfData} />
|
||||
|
||||
@ -37,7 +37,7 @@ export function StatisticsMainRow({
|
||||
>
|
||||
<div className="p-4">
|
||||
<span className="flex gap-4 mb-2">
|
||||
<strong className="text-lg">Statistics</strong>
|
||||
<strong className="text-lg max-lg:hidden">Statistics</strong>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
@ -75,7 +75,7 @@ export function StatisticsMainRow({
|
||||
</span>
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 max-lg:mt-9">
|
||||
{!mhsfData.loading ? (
|
||||
<>
|
||||
{(statisticType === "playerCount"
|
||||
|
||||
726
apps/www/src/components/ui/sidebar.tsx
Normal file
726
apps/www/src/components/ui/sidebar.tsx
Normal file
@ -0,0 +1,726 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/lib/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="square-lg"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
@ -27,17 +27,15 @@
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// NextTopLoader.tsx
|
||||
'use client';
|
||||
|
||||
import Loader from 'nextjs-toploader';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {useEffect} from "react"
|
||||
import {ComponentProps, useEffect} from "react"
|
||||
import * as NProgress from "nprogress";
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
export default function NextTopLoader() {
|
||||
export default function NextTopLoader(props: ComponentProps<typeof Loader>) {
|
||||
const pathname = usePathname();
|
||||
const theme = useTheme()
|
||||
|
||||
@ -46,6 +44,6 @@ export default function NextTopLoader() {
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Loader color={theme.resolvedTheme == "dark" ? "white" : "black"} shadow={false}/>
|
||||
<Loader height={3} color={theme.resolvedTheme === "dark" ? "white" : "black"} shadow={false} {...props}/>
|
||||
)
|
||||
}
|
||||
@ -28,12 +28,21 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { OnlineServer } from "../types/mh-server";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { toast } from "sonner";
|
||||
import { tryCatch } from "../try-catch";
|
||||
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
|
||||
type EmbeddedFilter = {
|
||||
identifier: string;
|
||||
functionFilter: (server: OnlineServer) => boolean;
|
||||
};
|
||||
|
||||
type SortFunction<K> = (object1: K, object2: K) => number;
|
||||
|
||||
export function useFilters(data: OnlineServer[]) {
|
||||
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
|
||||
@ -43,9 +52,38 @@ export function useFilters(data: OnlineServer[]) {
|
||||
"Haven't connected thread yet (if stuck, select the other tab, and come back)",
|
||||
);
|
||||
const [testModeLoading, setTestModeLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<EmbeddedFilter[]>([]);
|
||||
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
||||
const { user, isSignedIn } = useUser();
|
||||
|
||||
const updateServers = (newFilters: EmbeddedFilter[]) => {
|
||||
const modificationMap = data.map((v) =>
|
||||
newFilters.map((c) => c.functionFilter(v)),
|
||||
);
|
||||
const resultData = data.filter(
|
||||
(_, i) => !modificationMap[i].includes(false),
|
||||
);
|
||||
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
||||
|
||||
console.table({ sortedData, modificationMap, resultData, data });
|
||||
|
||||
|
||||
if (sortedData.length !== 0)
|
||||
setFilteredData(sortedData);
|
||||
};
|
||||
|
||||
// biome-ignore lint: bruh
|
||||
useEffect(() => {
|
||||
if (filteredData.length === 0 || data.length === 0) {
|
||||
window.dispatchEvent(new Event("update-modification-stack"));
|
||||
} else setLoading(false);
|
||||
}, [data, filteredData.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredData.length === 0) setFilteredData(data);
|
||||
if (data.length === 0) {
|
||||
window.dispatchEvent(new Event("update-modification-stack"));
|
||||
} else setLoading(false);
|
||||
}, [data, filteredData.length]);
|
||||
|
||||
const testModeInit = (type: "filter" | "sort") => {
|
||||
@ -63,7 +101,7 @@ export function useFilters(data: OnlineServer[]) {
|
||||
);
|
||||
if (error) {
|
||||
setTestModeStatus(
|
||||
"Failed to transpile TypeScript! Error: " + error.message,
|
||||
`Failed to transpile TypeScript! Error: ${error.message},`,
|
||||
);
|
||||
setTestModeLoading(false);
|
||||
return;
|
||||
@ -112,14 +150,14 @@ export function useFilters(data: OnlineServer[]) {
|
||||
return;
|
||||
}
|
||||
if (typeof filterFunc === "function") {
|
||||
setTestModeStatus("Compiled in " + (Date.now() - startTime) + "ms");
|
||||
setTestModeStatus(`Compiled in ${Date.now() - startTime} ms`);
|
||||
toast.promise(
|
||||
async () => {
|
||||
let newServers = [];
|
||||
let newServers: OnlineServer[] = [];
|
||||
if (type === "filter") {
|
||||
newServers = data.filter((c) => filterFunc(c));
|
||||
setTestModeStatus(
|
||||
"Server count " + data.length + " -> " + newServers.length,
|
||||
`Server count ${data.length} -> ${newServers.length}`,
|
||||
);
|
||||
if (newServers.length === 0)
|
||||
setTestModeStatus(
|
||||
@ -129,9 +167,12 @@ export function useFilters(data: OnlineServer[]) {
|
||||
}
|
||||
if (type === "sort") {
|
||||
newServers = data.sort((a, b) => filterFunc(a, b));
|
||||
setTestModeStatus("Sorted " + newServers.length + " servers.");
|
||||
console.log(newServers, data.sort((a, b) => filterFunc(a, b)))
|
||||
console.log(filterFunc)
|
||||
setTestModeStatus(`Sorted ${newServers.length} servers.`);
|
||||
console.log(
|
||||
newServers,
|
||||
data.sort((a, b) => filterFunc(a, b)),
|
||||
);
|
||||
console.log(filterFunc);
|
||||
setFilteredData(() => [...newServers]);
|
||||
}
|
||||
|
||||
@ -155,6 +196,7 @@ export function useFilters(data: OnlineServer[]) {
|
||||
}
|
||||
};
|
||||
|
||||
// biome-ignore lint: I'm gonna turn this off :sob:
|
||||
useEffect(() => {
|
||||
if (data.length !== 0) {
|
||||
window.addEventListener("test-mode.enable.filter", () =>
|
||||
@ -166,5 +208,92 @@ export function useFilters(data: OnlineServer[]) {
|
||||
}
|
||||
}, [t, data]);
|
||||
|
||||
return { filteredData, testModeEnabled, testModeLoading, testModeStatus };
|
||||
// biome-ignore lint: I'm gonna turn this off :sob:
|
||||
useEffect(() => {
|
||||
if (!t)
|
||||
window.addEventListener("update-modification-stack", async () => {
|
||||
await user?.reload();
|
||||
let newFilters: EmbeddedFilter[] = [];
|
||||
if (isSignedIn) {
|
||||
const activatedModifications =
|
||||
(user.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||
[];
|
||||
const activeModifications = activatedModifications.filter(
|
||||
(c) => c.active && c.testMode === "filter",
|
||||
);
|
||||
|
||||
const resolvedModifications = (await Promise.all(
|
||||
activeModifications.map(async (c) => {
|
||||
const functionBody = c.transpiledContents
|
||||
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
|
||||
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
||||
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||
(async () =>
|
||||
c.testMode === "filter"
|
||||
? new Function(
|
||||
"server",
|
||||
`${functionBody}
|
||||
|
||||
return filter(server)`,
|
||||
)
|
||||
: new Function(
|
||||
"serverA",
|
||||
"serverB",
|
||||
`${functionBody}
|
||||
|
||||
return sort(serverA, serverB)`,
|
||||
))(),
|
||||
);
|
||||
|
||||
if (filterErr) {
|
||||
toast.error(
|
||||
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
||||
);
|
||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: () => true };
|
||||
}
|
||||
|
||||
if (typeof filterFunc === "function") {
|
||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: filterFunc };
|
||||
}
|
||||
|
||||
toast.error(
|
||||
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
||||
);
|
||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: () => true };
|
||||
}),
|
||||
)) as EmbeddedFilter[];
|
||||
|
||||
// avoid duplicates
|
||||
resolvedModifications.forEach((item) => {
|
||||
setFilters((c) => {
|
||||
if (c.findIndex((i) => i.identifier === item.identifier) === -1)
|
||||
return [
|
||||
...c,
|
||||
item
|
||||
]
|
||||
else return c;
|
||||
});
|
||||
})
|
||||
|
||||
newFilters = resolvedModifications.map((item) => {
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
console.log(newFilters);
|
||||
|
||||
updateServers(newFilters);
|
||||
});
|
||||
}, [data]);
|
||||
console.log(filters);
|
||||
|
||||
return {
|
||||
filteredData,
|
||||
testModeEnabled,
|
||||
testModeLoading,
|
||||
testModeStatus,
|
||||
filterCount: filters.filter((item, index, array) => array.indexOf(item) === index).length + (sort === null ? 1 : 0),
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,170 +0,0 @@
|
||||
/*
|
||||
* 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 { toast } from "sonner";
|
||||
import { OnlineServer } from "./types/mh-server";
|
||||
import MiniMessage from "minimessage-js";
|
||||
|
||||
var numberOfItemsInView = 20;
|
||||
|
||||
export default class ServersList {
|
||||
servers: Array<OnlineServer> = [];
|
||||
currentServers: Array<OnlineServer> = [];
|
||||
private filters: Array<(server: OnlineServer) => Promise<boolean>> = [];
|
||||
extraData: any = { total_players: 0, total_servers: 0 };
|
||||
private it = 0;
|
||||
hasMore = true;
|
||||
|
||||
constructor(filters: Array<(server: OnlineServer) => Promise<boolean>>) {
|
||||
this.filters = filters;
|
||||
}
|
||||
getRandomServer() {
|
||||
return this.servers[Math.floor(Math.random() * this.servers.length)];
|
||||
}
|
||||
getExtraData() {
|
||||
return this.extraData;
|
||||
}
|
||||
fetchDataAndFilter(): Promise<boolean> {
|
||||
return new Promise((g, bc) => {
|
||||
fetch("https://api.minehut.com/servers")
|
||||
.then((b) => {
|
||||
if (!b.ok) {
|
||||
console.log(
|
||||
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ",
|
||||
"font-weight: bold",
|
||||
b
|
||||
);
|
||||
toast.error(`
|
||||
Error while grabbing servers from API.
|
||||
If this is happening alot, make a new issue on GitHub
|
||||
`);
|
||||
bc();
|
||||
}
|
||||
if (b.ok)
|
||||
b.json().then((serversb) => {
|
||||
var serversWithoutFilt: Array<OnlineServer> = serversb.servers;
|
||||
var serversWithFilt: Array<OnlineServer> = [];
|
||||
this.extraData.total_players = serversb.total_players;
|
||||
this.extraData.total_servers = serversb.total_servers;
|
||||
serversWithoutFilt.forEach((server: OnlineServer, im) => {
|
||||
var good = true;
|
||||
const filterEach = () => {
|
||||
return new Promise((g, b) => {
|
||||
if (arrayEmpty(this.filters)) {
|
||||
g(true);
|
||||
serversWithFilt = serversWithoutFilt;
|
||||
}
|
||||
this.filters.forEach((filter, i) => {
|
||||
// Test for if filter is compatiable with server
|
||||
filter(server).then((b) => {
|
||||
if (!b) good = false;
|
||||
if (i == this.filters.length - 1 && good) {
|
||||
serversWithFilt.push(server);
|
||||
}
|
||||
if (i == this.filters.length - 1) {
|
||||
g(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
filterEach().then(() => {
|
||||
if (im == serversWithoutFilt.length - 1) {
|
||||
this.servers = serversWithFilt;
|
||||
g(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((b) => {
|
||||
toast.error(`
|
||||
Error while grabbing servers from API.
|
||||
If this is happening alot, make a new issue on GitHub
|
||||
`);
|
||||
console.log(
|
||||
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ",
|
||||
"font-weight: bold",
|
||||
b
|
||||
);
|
||||
bc();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveListDown() {
|
||||
const slicedArray = this.servers.slice(
|
||||
this.it * numberOfItemsInView,
|
||||
this.it * numberOfItemsInView + numberOfItemsInView
|
||||
);
|
||||
this.currentServers = this.currentServers.concat(slicedArray);
|
||||
this.it++;
|
||||
console.log(
|
||||
"%c[MHSF] Moved list down! Updated entries: ",
|
||||
"font-weight: bold",
|
||||
slicedArray
|
||||
);
|
||||
if (slicedArray.length != numberOfItemsInView) {
|
||||
this.hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
editFilters(newFilters: Array<(server: OnlineServer) => Promise<boolean>>) {
|
||||
this.filters = newFilters;
|
||||
this.servers = [];
|
||||
this.it = 0;
|
||||
this.currentServers = [];
|
||||
this.hasMore = true;
|
||||
}
|
||||
|
||||
async getMOTDs(
|
||||
list: Array<{ server: string; motd: string }>
|
||||
): Promise<Array<{ server: string; motd: string }>> {
|
||||
const result: Array<{ server: string; motd: string }> = [];
|
||||
const mm = MiniMessage.miniMessage();
|
||||
list.forEach((c) => {
|
||||
try {
|
||||
result.push({
|
||||
server: c.server,
|
||||
motd: mm.toHTML(mm.deserialize(c.motd)),
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function arrayEmpty(a: Array<any>) {
|
||||
return a.length == 0;
|
||||
}
|
||||
@ -1,242 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// rendering engine for MOTDs (aka Minehut)
|
||||
|
||||
const divList: any = {
|
||||
black: "black",
|
||||
dark_blue: "text-[#1D4ED8]",
|
||||
dark_green: "text-[#166634]",
|
||||
dark_red: "text-[#991b1b]",
|
||||
dark_purple: "text-[#6b21a8]",
|
||||
gold: "text-[#facc15]",
|
||||
gray: "text-[#4b5563]",
|
||||
dark_gray: "text-[#1f2937]",
|
||||
blue: "text-[#60a5fa]",
|
||||
green: "text-[#4ade80]",
|
||||
aqua: "text-[#22d3ee]",
|
||||
red: "text-[#f87171]",
|
||||
light_purple: "text-[#d8b4fe]",
|
||||
yellow: "text-[#facc15]",
|
||||
white: "text-white",
|
||||
strikethrough: "line-through",
|
||||
st: "line-through",
|
||||
u: "underline",
|
||||
underlined: "underline",
|
||||
italic: "italic",
|
||||
em: "italic",
|
||||
i: "italic",
|
||||
bold: "font-bold",
|
||||
b: "font-bold",
|
||||
};
|
||||
|
||||
export default function parseToHTML(m: string, tw?: boolean): Promise<string> {
|
||||
return new Promise<string>((g, b) => {
|
||||
fetch("https://webui.advntr.dev/api/mini-to-json", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
miniMessage: m,
|
||||
placeholders: { stringPlaceholders: {} },
|
||||
}),
|
||||
}).then((j) => {
|
||||
j.json().then((l) => {
|
||||
if (typeof l === "string") {
|
||||
g(l);
|
||||
} else {
|
||||
// This text has custom properties
|
||||
var allHTML = "";
|
||||
var root: Array<Element | string> = l.extra;
|
||||
if (root == undefined) {
|
||||
var curClass = "";
|
||||
var contents = "";
|
||||
if (l.color != undefined) {
|
||||
if (divList[l.color] == undefined) {
|
||||
curClass +=
|
||||
curClass == ""
|
||||
? "text-[" + l.color + "]"
|
||||
: " text-[" + l.color + "]";
|
||||
} else {
|
||||
curClass +=
|
||||
curClass == "" ? divList[l.color] : " " + divList[l.color];
|
||||
}
|
||||
}
|
||||
if (l.strikethrough == true) {
|
||||
curClass += curClass == "" ? "line-through" : " line-through";
|
||||
}
|
||||
if (l.bold == true) {
|
||||
curClass += curClass == "" ? "font-bold" : " font-bold";
|
||||
}
|
||||
if (l.italic == true) {
|
||||
curClass += curClass == "" ? "italic" : " italic";
|
||||
}
|
||||
allHTML += createHTML("span", curClass, l.text + contents);
|
||||
} else {
|
||||
root.forEach(function (i) {
|
||||
if (typeof i === "string") {
|
||||
allHTML += i;
|
||||
} else {
|
||||
var curClass = "";
|
||||
var contents = "";
|
||||
if (i.extra != undefined) {
|
||||
i.extra.forEach(function (m) {
|
||||
contents += objToHTML(m);
|
||||
});
|
||||
}
|
||||
if (i.color != undefined) {
|
||||
if (divList[i.color] == undefined) {
|
||||
curClass +=
|
||||
curClass == ""
|
||||
? "text-[" + i.color + "]"
|
||||
: " text-[" + i.color + "]";
|
||||
} else {
|
||||
curClass +=
|
||||
curClass == ""
|
||||
? divList[i.color]
|
||||
: " " + divList[i.color];
|
||||
}
|
||||
}
|
||||
if (i.strikethrough == true) {
|
||||
curClass += curClass == "" ? "line-through" : " line-through";
|
||||
}
|
||||
if (i.bold == true) {
|
||||
curClass += curClass == "" ? "font-bold" : " font-bold";
|
||||
}
|
||||
if (i.italic == true) {
|
||||
curClass += curClass == "" ? "italic" : " italic";
|
||||
}
|
||||
allHTML += createHTML("span", curClass, l.text + contents);
|
||||
}
|
||||
});
|
||||
}
|
||||
g("<span>" + allHTML + "</span>");
|
||||
}
|
||||
});
|
||||
if (!j.ok) {
|
||||
b("Problem while parsing MiniMessage");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function objToHTML(i: Element | string): string {
|
||||
if (typeof i == "string") {
|
||||
return i;
|
||||
}
|
||||
var curClass = "";
|
||||
var contents = "";
|
||||
if (i.extra != undefined) {
|
||||
i.extra.forEach((m) => {
|
||||
contents += objToHTML(m);
|
||||
});
|
||||
}
|
||||
if (i.color != undefined) {
|
||||
if (divList[i.color] == undefined) {
|
||||
curClass +=
|
||||
curClass == "" ? "text-[" + i.color + "]" : " text-[" + i.color + "]";
|
||||
} else {
|
||||
curClass += curClass == "" ? divList[i.color] : " " + divList[i.color];
|
||||
}
|
||||
}
|
||||
if (i.strikethrough == true) {
|
||||
curClass += curClass == "" ? "line-through" : " line-through";
|
||||
}
|
||||
if (i.bold == true) {
|
||||
curClass += curClass == "" ? "font-bold" : " font-bold";
|
||||
}
|
||||
if (i.italic == true) {
|
||||
curClass += curClass == "" ? "italic" : " italic";
|
||||
}
|
||||
|
||||
return createHTML("span", curClass, i.text + contents);
|
||||
}
|
||||
|
||||
function createHTML(
|
||||
tag: string,
|
||||
className: string,
|
||||
contents: string,
|
||||
tw?: boolean
|
||||
) {
|
||||
if (className == undefined) className = "";
|
||||
if (contents == undefined) contents = "";
|
||||
if (contents == "\n") contents = "<br>";
|
||||
|
||||
if (tw == false || tw == undefined) {
|
||||
return (
|
||||
"<" +
|
||||
tag +
|
||||
' style="' +
|
||||
colorConvert(className) +
|
||||
'">' +
|
||||
contents +
|
||||
"</" +
|
||||
tag +
|
||||
">"
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
"<" + tag + ' class="' + className + '">' + contents + "</" + tag + ">"
|
||||
);
|
||||
}
|
||||
}
|
||||
type Element = {
|
||||
text: string;
|
||||
extra: Array<Element>;
|
||||
color?: string;
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
strikethrough?: boolean;
|
||||
};
|
||||
|
||||
function colorConvert(className: string) {
|
||||
const classes = className.split(" ");
|
||||
let result = "";
|
||||
classes.forEach((classUnique) => {
|
||||
if (classUnique.startsWith("text-") && classUnique != "text-white") {
|
||||
if (classUnique.startsWith("text-[")) {
|
||||
result +=
|
||||
"color: " + classUnique.substring(6, classUnique.length - 1) + "; ";
|
||||
} else {
|
||||
result +=
|
||||
"color: " + classUnique.substring(5, classUnique.length) + "; ";
|
||||
}
|
||||
}
|
||||
if (classUnique.startsWith("font-bold")) {
|
||||
result += "font-weight: 700; ";
|
||||
}
|
||||
if (classUnique.startsWith("line-through")) {
|
||||
result += "text-decoration-line: line-through; ";
|
||||
}
|
||||
if (classUnique.startsWith("italic")) {
|
||||
result += "font-style: italic; ";
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 { serverOwned } from "./api";
|
||||
import { OnlineServer, ServerResponse } from "./types/mh-server";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default class ServerSingle {
|
||||
private name = "";
|
||||
private onlineObj: OnlineServer | undefined = undefined;
|
||||
private offlineObj: ServerResponse | undefined = undefined;
|
||||
online = false;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
setName(newName: string) {
|
||||
this.name = newName;
|
||||
}
|
||||
|
||||
isCustomized(): Promise<boolean> {
|
||||
return serverOwned(this.name);
|
||||
}
|
||||
|
||||
init(skipOnline?: boolean): Promise<boolean> {
|
||||
return new Promise<boolean>((g, bc) => {
|
||||
fetch("https://api.minehut.com/server/" + this.name + "?byName=true")
|
||||
.then((d) => {
|
||||
if (d.ok) {
|
||||
d.json().then((m) => {
|
||||
this.online = m.server.online;
|
||||
this.offlineObj = m.server;
|
||||
if (this.online == true && skipOnline != true) {
|
||||
fetch("https://api.minehut.com/servers").then((l) =>
|
||||
l.json().then((o) => {
|
||||
if (o.servers.find((j: OnlineServer) => j.name == this.name) == undefined) {
|
||||
g(true);
|
||||
}
|
||||
o.servers.forEach((j: OnlineServer) => {
|
||||
if (j.name == this.name) {
|
||||
this.onlineObj = j;
|
||||
g(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
} else g(true);
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ",
|
||||
"font-weight: bold",
|
||||
d
|
||||
);
|
||||
toast.error(`
|
||||
Error while grabbing servers from API.
|
||||
If this is happening alot, make a new issue on GitHub
|
||||
`);
|
||||
bc();
|
||||
}
|
||||
})
|
||||
.catch((b) => {
|
||||
toast.error(`
|
||||
Error while grabbing servers from API.
|
||||
If this is happening alot, make a new issue on GitHub
|
||||
`);
|
||||
console.log(
|
||||
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ",
|
||||
"font-weight: bold",
|
||||
b
|
||||
);
|
||||
bc();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAuthor(): string | undefined {
|
||||
if (this.onlineObj == undefined || this.onlineObj.author == undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return this.onlineObj.author;
|
||||
}
|
||||
}
|
||||
|
||||
grabOnline(): OnlineServer | undefined {
|
||||
return this.onlineObj;
|
||||
}
|
||||
grabOffline(): ServerResponse | undefined {
|
||||
if (this.offlineObj != undefined) {
|
||||
this.offlineObj.__unix =
|
||||
"Time in this file is defined in Unix time. Convert it in something like https://www.epochconverter.com/ (in milliseconds)";
|
||||
}
|
||||
return this.offlineObj;
|
||||
}
|
||||
}
|
||||
@ -45,6 +45,8 @@ export default async function handler(
|
||||
const result = await betterStackResult.json();
|
||||
const url = await betterStackURL.json();
|
||||
|
||||
console.log(result)
|
||||
|
||||
const filtered = result.data.filter(
|
||||
(c: any) =>
|
||||
c.attributes.ends_at === null &&
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
// Deprecated - moved exclusively to the client
|
||||
// const initalList: Array<{ server: string; motd: string }> = req.body.motd;
|
||||
// const resultedList: Array<{ server: string; motd: string }> = [];
|
||||
// var interval = 0;
|
||||
// if (initalList != undefined && initalList.forEach != undefined) {
|
||||
// initalList.forEach((c, i) => {
|
||||
// parseToHTML(c.motd)
|
||||
// .then((m) => {
|
||||
// interval++;
|
||||
// resultedList.push({ motd: m, server: c.server });
|
||||
// if (interval == initalList.length) {
|
||||
// res.send({ result: resultedList });
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// resultedList.push({ motd: "Error to grab MOTD", server: c.server });
|
||||
// if (i == initalList.length - 1) {
|
||||
// res.send({ result: resultedList });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// } else {
|
||||
// res.status(400).send({
|
||||
// message: "Wrong structure.. you might be using the legacy MOTD.",
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@ -82,23 +82,24 @@ export default async function handler(
|
||||
try {
|
||||
await mongo.connect();
|
||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const stats = mongo.db("mhsf")
|
||||
const userId = req.cookies.userId;
|
||||
|
||||
// Run queries in parallel
|
||||
const [favoriteData, customizationData, playerData, achievements] =
|
||||
await Promise.all([
|
||||
findFavoriteData(serverData.name, userId, db, {
|
||||
findFavoriteData(serverData.name, userId, stats, {
|
||||
maxFavoriteEntries,
|
||||
favoriteTimespanStart,
|
||||
favoriteTimespanEnd,
|
||||
}),
|
||||
findCustomizationData(serverData.name, userId, db),
|
||||
findPlayerData(serverData.name, db, {
|
||||
findPlayerData(serverData.name, stats, {
|
||||
maxPlayerEntries,
|
||||
playerTimespanStart,
|
||||
playerTimespanEnd,
|
||||
}),
|
||||
findAchievements(serverData.name, db, {
|
||||
findAchievements(serverData.name, stats, {
|
||||
maxAchievementEntries,
|
||||
achievementTimespanStart,
|
||||
achievementTimespanEnd,
|
||||
|
||||
@ -16,5 +16,11 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"cloc": "^2.4.0-cloc"
|
||||
}
|
||||
}
|
||||
|
||||
60
yarn.lock
60
yarn.lock
@ -2139,6 +2139,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
||||
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
||||
|
||||
"@radix-ui/react-compose-refs@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
|
||||
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
|
||||
|
||||
"@radix-ui/react-context-menu@^2.1.5", "@radix-ui/react-context-menu@^2.2.6":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz#752fd1d91f92bba287ef2b558770f4ca7d74523e"
|
||||
@ -2748,7 +2753,7 @@
|
||||
aria-hidden "^1.2.4"
|
||||
react-remove-scroll "^2.6.3"
|
||||
|
||||
"@radix-ui/react-separator@^1.1.0":
|
||||
"@radix-ui/react-separator@^1.1.0", "@radix-ui/react-separator@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.2.tgz#24c5450db20f341f2b743ed4b07b907e18579216"
|
||||
integrity sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==
|
||||
@ -2777,13 +2782,20 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
|
||||
"@radix-ui/react-slot@1.1.2", "@radix-ui/react-slot@^1.1.0", "@radix-ui/react-slot@^1.1.2":
|
||||
"@radix-ui/react-slot@1.1.2", "@radix-ui/react-slot@^1.1.0":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
|
||||
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
|
||||
"@radix-ui/react-slot@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.0.tgz#57727fc186ddb40724ccfbe294e1a351d92462ba"
|
||||
integrity sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
|
||||
"@radix-ui/react-switch@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3"
|
||||
@ -4598,7 +4610,7 @@ citty@^0.1.6:
|
||||
dependencies:
|
||||
consola "^3.2.3"
|
||||
|
||||
class-variance-authority@0.7.1, class-variance-authority@^0.7.0, class-variance-authority@^0.7.1:
|
||||
class-variance-authority@0.7.1, class-variance-authority@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
|
||||
integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
|
||||
@ -4650,6 +4662,11 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cloc@^2.4.0-cloc:
|
||||
version "2.4.0-cloc"
|
||||
resolved "https://registry.yarnpkg.com/cloc/-/cloc-2.4.0-cloc.tgz#d73e598c87c8cca57c391c79ec9e5268b7d9c225"
|
||||
integrity sha512-7SH5scbWgvVkg0LLUsGpCBnUnB9hCvryGFq9uZsMsuOm/JcxDs+6XB/EBuu446MzfCV0K7ydfTxB0bkEEbHHvw==
|
||||
|
||||
clone@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
@ -6448,6 +6465,15 @@ framer-motion@^11.3.8:
|
||||
motion-utils "^11.18.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
framer-motion@^12.7.4:
|
||||
version "12.7.4"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.7.4.tgz#50aeb8b5b5a672dea931bdb74956d7b526bf0b4b"
|
||||
integrity sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==
|
||||
dependencies:
|
||||
motion-dom "^12.7.4"
|
||||
motion-utils "^12.7.2"
|
||||
tslib "^2.4.0"
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
@ -8169,10 +8195,10 @@ lucide-react@^0.474.0:
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
|
||||
integrity sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==
|
||||
|
||||
lucide-react@^0.479.0:
|
||||
version "0.479.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.479.0.tgz#7321f979a389ec5dd86747b2deb6444cf0922f8d"
|
||||
integrity sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==
|
||||
lucide-react@^0.487.0:
|
||||
version "0.487.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.487.0.tgz#c18a463404e8ef106d46a7c9cceddf9fc8b9ff6b"
|
||||
integrity sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==
|
||||
|
||||
luxon@~3.5.0:
|
||||
version "3.5.0"
|
||||
@ -9476,11 +9502,31 @@ motion-dom@^11.18.1:
|
||||
dependencies:
|
||||
motion-utils "^11.18.1"
|
||||
|
||||
motion-dom@^12.7.4:
|
||||
version "12.7.4"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.7.4.tgz#80f5f8d706e94bc29f6f4f4afa300ff9e1f976a2"
|
||||
integrity sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==
|
||||
dependencies:
|
||||
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:
|
||||
version "12.7.2"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.7.2.tgz#99b673d8851583b325bd0c8b0f04c5bf42b9b818"
|
||||
integrity sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==
|
||||
|
||||
motion@^12.7.4:
|
||||
version "12.7.4"
|
||||
resolved "https://registry.yarnpkg.com/motion/-/motion-12.7.4.tgz#d0a6032ccc6323e996caf0bc2ba6d0262fcc6214"
|
||||
integrity sha512-MBGrMbYageHw4iZJn+pGTr7abq5n53jCxYkhFC1It3vYukQPRWg5zij46MnwYGpLR8KG465MLHSASXot9edYOw==
|
||||
dependencies:
|
||||
framer-motion "^12.7.4"
|
||||
tslib "^2.4.0"
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user