mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 17:44:59 -05:00
feat(v2): some changes
This commit is contained in:
parent
f21ce33b27
commit
b62f79e010
@ -1,135 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -28,14 +28,8 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
import { Placeholder } from "@/components/ui/placeholder";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { IsScript } from "@/components/util/is-script";
|
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 { NavBar } from "@/components/feat/navbar/navbar";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { ThemeProvider } from "@/components/util/theme-provider";
|
import { ThemeProvider } from "@/components/util/theme-provider";
|
||||||
@ -50,8 +44,6 @@ export default function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const search = searchParams?.get("theme") || "light";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -134,6 +134,7 @@ export default function ModificationPage({
|
|||||||
activatedModifications: modificationArray
|
activatedModifications: modificationArray
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
communicator.send("rerender-servers", {});
|
||||||
}}>
|
}}>
|
||||||
{modObj?.active ? "Disable" : "Enable"}
|
{modObj?.active ? "Disable" : "Enable"}
|
||||||
</Button>
|
</Button>
|
||||||
@ -165,6 +166,7 @@ export default function ModificationPage({
|
|||||||
});
|
});
|
||||||
toast.success(`Deleted in ${Date.now() - time}ms`);
|
toast.success(`Deleted in ${Date.now() - time}ms`);
|
||||||
router.push(backRoute);
|
router.push(backRoute);
|
||||||
|
communicator.send("rerender-servers", {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash size={16} /> Delete
|
<Trash size={16} /> Delete
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
import { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface BentoGridProps extends ComponentPropsWithoutRef<"div"> {
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BentoCardProps extends ComponentPropsWithoutRef<"div"> {
|
|
||||||
name: string;
|
|
||||||
className: string;
|
|
||||||
background: ReactNode;
|
|
||||||
Icon: React.ElementType;
|
|
||||||
description: string;
|
|
||||||
href: string;
|
|
||||||
cta: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BentoGrid = ({ children, className, ...props }: BentoGridProps) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"grid w-full auto-rows-[22rem] grid-cols-3 gap-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BentoCard = ({
|
|
||||||
name,
|
|
||||||
className,
|
|
||||||
background,
|
|
||||||
Icon,
|
|
||||||
description,
|
|
||||||
href,
|
|
||||||
cta,
|
|
||||||
...props
|
|
||||||
}: BentoCardProps) => (
|
|
||||||
<div
|
|
||||||
key={name}
|
|
||||||
className={cn(
|
|
||||||
"group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl",
|
|
||||||
// light styles
|
|
||||||
"bg-background [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-background dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div>{background}</div>
|
|
||||||
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 p-6 transition-all duration-300 group-hover:-translate-y-10">
|
|
||||||
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
|
|
||||||
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
|
|
||||||
{name}
|
|
||||||
</h3>
|
|
||||||
<p className="max-w-lg text-neutral-400">{description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export { BentoCard, BentoGrid };
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
import { BrandingGenericIcon, Discord } from "../icons/branding-icons";
|
import { BrandingGenericIcon, Discord } from "../icons/branding-icons";
|
||||||
import { Link } from "../../util/link";
|
import { Link } from "../../util/link";
|
||||||
import { FooterStatus } from "./status";
|
import { FooterStatus } from "./status";
|
||||||
@ -7,10 +8,11 @@ import Github from "@/components/ui/github";
|
|||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const hideFooterPages = ["/home"];
|
const hideFooterPages = ["/home"]
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
if (!hideFooterPages.includes(pathname ?? ""))
|
if (!hideFooterPages.includes(pathname ?? ""))
|
||||||
return (
|
return (
|
||||||
<footer className="w-full mt-15 border-t border-neutral-500/20 bg-neutral-100 dark:border-neutral-700/50 dark:bg-neutral-900 text-muted-foreground">
|
<footer className="w-full mt-15 border-t border-neutral-500/20 bg-neutral-100 dark:border-neutral-700/50 dark:bg-neutral-900 text-muted-foreground">
|
||||||
@ -50,7 +52,7 @@ export function Footer() {
|
|||||||
<div className="flex items-center mb-2 justify-end gap-2">
|
<div className="flex items-center mb-2 justify-end gap-2">
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="tertiary" size="square-md" className="flex items-center">
|
<Button variant="tertiary" size="square-md" className="flex items-center">
|
||||||
<Discord className="w-[1.25em] h-[1.25em]" />
|
<Discord className="w-[1.25em] h-[1.25em]" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -90,8 +90,6 @@ export function FontChanger({
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
console.log(position);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[1.2em] overflow-hidden flex items-center justify-center">
|
<div className="h-[1.2em] overflow-hidden flex items-center justify-center">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -106,13 +106,13 @@ export default function HomePageComponent() {
|
|||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--gradient-color-1":
|
"--gradient-color-1":
|
||||||
resolvedTheme === "dark" ? "#043D5D" : "#1F9EA3",
|
resolvedTheme === "dark" ? "#470061" : "#610034",
|
||||||
"--gradient-color-2":
|
"--gradient-color-2":
|
||||||
resolvedTheme === "dark" ? "#032E46" : "#F8BD97",
|
resolvedTheme === "dark" ? "#001299" : "#700099",
|
||||||
"--gradient-color-3":
|
"--gradient-color-3":
|
||||||
resolvedTheme === "dark" ? "#23B684" : "#9E5428",
|
resolvedTheme === "dark" ? "#8d00eb" : "#eb00ac",
|
||||||
"--gradient-color-4":
|
"--gradient-color-4":
|
||||||
resolvedTheme === "dark" ? "#0F595E" : "#EEEEEE",
|
resolvedTheme === "dark" ? "#009de0" : "#0007e0",
|
||||||
webKitMaskImage:
|
webKitMaskImage:
|
||||||
"linear-gradient(to top, black, black, transparent)",
|
"linear-gradient(to top, black, black, transparent)",
|
||||||
maskImage: "linear-gradient(to top, black, black, transparent)",
|
maskImage: "linear-gradient(to top, black, black, transparent)",
|
||||||
|
|||||||
@ -30,22 +30,22 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
BrandingGenericIcon,
|
BrandingGenericIcon,
|
||||||
brandingIconClipboard,
|
brandingIconClipboard,
|
||||||
} from "@/components/feat/icons/branding-icons";
|
} from "@/components/feat/icons/branding-icons";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import Github from "@/components/ui/github";
|
import Github from "@/components/ui/github";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
@ -63,143 +63,147 @@ import { usePathname } from "next/navigation";
|
|||||||
const animatedTopbarPages = ["/home"];
|
const animatedTopbarPages = ["/home"];
|
||||||
|
|
||||||
export function NavBar() {
|
export function NavBar() {
|
||||||
const showBorder = useScroll(40);
|
const showBorder = useScroll(40);
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
|
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
|
||||||
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
||||||
"lg:top-0 max-lg:bottom-0",
|
"lg:top-0 max-lg:bottom-0",
|
||||||
showBorder ? "border-b backdrop-blur-xl" : "max-lg:border-b max-lg:backdrop-blur-xl",
|
showBorder
|
||||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
? "border-b backdrop-blur-xl"
|
||||||
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
: "max-lg:border-b max-lg:backdrop-blur-xl",
|
||||||
: ""
|
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||||
)}
|
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
||||||
>
|
: "",
|
||||||
<span>
|
)}
|
||||||
<ContextMenu>
|
>
|
||||||
<ContextMenuTrigger>
|
<span>
|
||||||
<Link className="gap-5 flex items-center " href="/">
|
<ContextMenu>
|
||||||
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] mt-0.5" />
|
<ContextMenuTrigger>
|
||||||
<span className="gap-2 flex group hover:text-blue-500 hover:underline transition-all">
|
<Link className="gap-5 flex items-center " href="/">
|
||||||
<strong className="">MHSF</strong>
|
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] mt-0.5" />
|
||||||
<span className="text-muted-foreground group-hover:text-blue-500 transition-all">
|
<span className="gap-2 flex group hover:text-blue-500 hover:underline transition-all">
|
||||||
v{version}
|
<strong className="">MHSF</strong>
|
||||||
</span>
|
<span className="text-muted-foreground group-hover:text-blue-500 transition-all">
|
||||||
</span>
|
v{version}
|
||||||
</Link>
|
</span>
|
||||||
</ContextMenuTrigger>
|
</span>
|
||||||
<ContextMenuContent className="overflow-hidden min-w-[300px]">
|
</Link>
|
||||||
<DropdownMenuSeparator>Platform</DropdownMenuSeparator>
|
</ContextMenuTrigger>
|
||||||
<Link href="Special:Root">
|
<ContextMenuContent className="overflow-hidden min-w-[300px]">
|
||||||
<ContextMenuItem>
|
<DropdownMenuSeparator>Platform</DropdownMenuSeparator>
|
||||||
<span className="pl-2 flex gap-2 items-center">
|
<Link href="Special:Root">
|
||||||
<ServerCrash size={16} /> Go to Dynamic Home Page
|
<ContextMenuItem>
|
||||||
</span>
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
</ContextMenuItem>
|
<ServerCrash size={16} /> Go to Dynamic Home Page
|
||||||
</Link>
|
</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link href="/home">
|
<Link href="/home">
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<span className="pl-2 flex gap-2 items-center">
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
<Home size={16} /> Go to Home Page
|
<Home size={16} /> Go to Home Page
|
||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.writeText(brandingIconClipboard);
|
clipboard.writeText(brandingIconClipboard);
|
||||||
toast.success("Copied icon to clipboard!");
|
toast.success("Copied icon to clipboard!");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="pl-2 flex gap-2 items-center">
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
<Image size={16} /> Copy Logo as SVG
|
<Image size={16} /> Copy Logo as SVG
|
||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
<Link href="Special:GitHub">
|
<Link href="Special:GitHub">
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<span className="pl-2 flex gap-2 items-center">
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
<Github /> Open GitHub
|
<Github /> Open GitHub
|
||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="Special:GitHub/releases">
|
<Link href="Special:GitHub/releases">
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<span className="pl-2 flex gap-2 items-center">
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
<Github /> Open GitHub Releases
|
<Github /> Open GitHub Releases
|
||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</span>
|
</span>
|
||||||
<span className="mr-3 flex items-center">
|
<span className="mr-3 flex items-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<SignedOut>
|
||||||
<SignedOut>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full flex items-center",
|
"rounded-full flex items-center",
|
||||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||||
? "[--animation-delay:2000ms] opacity-0 animate-fade-in"
|
? "[--animation-delay:2000ms] opacity-0 animate-fade-in"
|
||||||
: ""
|
: "",
|
||||||
)}
|
)}
|
||||||
size="square-lg"
|
size="square-lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
<Menu size={16} />
|
<Menu size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</SignedOut>
|
</DropdownMenuTrigger>
|
||||||
<SignedIn>
|
</SignedOut>
|
||||||
<Button
|
<SignedIn>
|
||||||
size="square-lg"
|
<DropdownMenuTrigger asChild>
|
||||||
variant="tertiary"
|
<Button
|
||||||
className={cn(
|
size="square-lg"
|
||||||
"rounded-full flex items-center",
|
variant="tertiary"
|
||||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
className={cn(
|
||||||
? "[--animation-delay:2000ms] opacity-0 animate-fade-in"
|
"rounded-full flex items-center",
|
||||||
: ""
|
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||||
)}
|
? "[--animation-delay:2000ms] opacity-0 animate-fade-in"
|
||||||
>
|
: "",
|
||||||
<NextImage
|
)}
|
||||||
alt="Clerk Image"
|
>
|
||||||
src={
|
<NextImage
|
||||||
user?.imageUrl === undefined
|
alt="Clerk Image"
|
||||||
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
|
src={
|
||||||
: user?.imageUrl
|
user?.imageUrl === undefined
|
||||||
}
|
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
|
||||||
width={26}
|
: user?.imageUrl
|
||||||
height={26}
|
}
|
||||||
className="rounded-full"
|
width={26}
|
||||||
/>
|
height={26}
|
||||||
</Button>
|
className="rounded-full"
|
||||||
</SignedIn>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent className="max-w-[280px] w-[280px] mt-2 mr-2">
|
</DropdownMenuTrigger>
|
||||||
<MenuDropdown />
|
</SignedIn>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuContent className="max-w-[280px] w-[280px] mt-2 mr-2">
|
||||||
</DropdownMenu>
|
<MenuDropdown />
|
||||||
<SignedIn>
|
</DropdownMenuContent>
|
||||||
<div
|
</DropdownMenu>
|
||||||
className="absolute right-0 -z-10 h-full
|
<SignedIn>
|
||||||
|
<div
|
||||||
|
className="absolute right-0 -z-10 h-full
|
||||||
overflow-hidden w-full ml-auto"
|
overflow-hidden w-full ml-auto"
|
||||||
style={{ borderRadius: "inherit" }}
|
style={{ borderRadius: "inherit" }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={user?.imageUrl ?? ""}
|
src={user?.imageUrl ?? ""}
|
||||||
className="blur-2xl -z-10 object-cover w-48 h-48 opacity-20 dark:opacity-50 ml-auto"
|
className="blur-2xl -z-10 object-cover w-48 h-48 opacity-20 dark:opacity-50 ml-auto"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,135 @@ import { Button } from "@/components/ui/button";
|
|||||||
import type { Filter } from "@/lib/types/filter";
|
import type { Filter } from "@/lib/types/filter";
|
||||||
import type { Sort } from "@/lib/types/sort";
|
import type { Sort } from "@/lib/types/sort";
|
||||||
import { ModificationFileCreationDialog } from "./modification-file-creation-dialog";
|
import { ModificationFileCreationDialog } from "./modification-file-creation-dialog";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useIframeCommunication } from "@/lib/hooks/use-iframe-communication";
|
||||||
|
|
||||||
type Action = Filter | Sort | { customAction: string };
|
type Action = Filter | Sort | { customAction: string };
|
||||||
|
|
||||||
|
export type ClerkEmbeddedFilter<T> = {
|
||||||
|
type: string;
|
||||||
|
metadata: T;
|
||||||
|
};
|
||||||
|
|
||||||
export function ModificationAction({ value }: { value?: Action }) {
|
export function ModificationAction({ value }: { value?: Action }) {
|
||||||
return (
|
const { isSignedIn, user } = useUser();
|
||||||
<>
|
const [applied, setApplied] = useState<number | undefined>();
|
||||||
{value !== undefined && "customAction" in value ? (
|
const communication = useIframeCommunication();
|
||||||
<ModificationFileCreationDialog
|
|
||||||
type={value.customAction.endsWith("sort") ? "sort" : "filter"}
|
const findExisting = () => {
|
||||||
>
|
if (!(value !== undefined && "customAction" in value)) {
|
||||||
<Button size="sm" className="mt-1">
|
const filter = value as Filter;
|
||||||
{value.customAction === "custom-sort"
|
let existing = -1;
|
||||||
? "Create Sort"
|
if (isSignedIn)
|
||||||
: "Create Filter"}
|
existing = (
|
||||||
</Button>
|
(user.unsafeMetadata.filters as Array<
|
||||||
</ModificationFileCreationDialog>
|
ClerkEmbeddedFilter<unknown>
|
||||||
) : (
|
>) ?? []
|
||||||
<Button size="sm">Apply</Button>
|
).findIndex(
|
||||||
)}
|
(c) =>
|
||||||
</>
|
JSON.stringify(c.metadata) ===
|
||||||
);
|
JSON.stringify(filter.toIdentifier()) &&
|
||||||
|
c.type === filter.getSpecificFilterId(),
|
||||||
|
);
|
||||||
|
else
|
||||||
|
existing = (
|
||||||
|
(JSON.parse(localStorage.getItem("mhsf__filters") ?? "[]") as Array<
|
||||||
|
ClerkEmbeddedFilter<unknown>
|
||||||
|
>) ?? []
|
||||||
|
).findIndex(
|
||||||
|
(c) =>
|
||||||
|
JSON.stringify(c.metadata) === JSON.stringify(filter.toIdentifier()) &&
|
||||||
|
c.type === filter.getSpecificFilterId(),
|
||||||
|
);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => setApplied(findExisting()))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{value !== undefined && "customAction" in value ? (
|
||||||
|
<ModificationFileCreationDialog
|
||||||
|
type={value.customAction.endsWith("sort") ? "sort" : "filter"}
|
||||||
|
>
|
||||||
|
<Button size="sm" className="mt-1">
|
||||||
|
{value.customAction === "custom-sort"
|
||||||
|
? "Create Sort"
|
||||||
|
: "Create Filter"}
|
||||||
|
</Button>
|
||||||
|
</ModificationFileCreationDialog>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="mt-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (value?.type() === "filter") {
|
||||||
|
const filter = value as Filter;
|
||||||
|
const existing = findExisting();
|
||||||
|
|
||||||
|
if (isSignedIn) {
|
||||||
|
const existingArray =
|
||||||
|
(user.unsafeMetadata.filters as Array<
|
||||||
|
ClerkEmbeddedFilter<unknown>
|
||||||
|
>) ?? [];
|
||||||
|
existingArray.splice(existing, 1);
|
||||||
|
if (existing === -1)
|
||||||
|
await user.update({
|
||||||
|
unsafeMetadata: {
|
||||||
|
...user.unsafeMetadata,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: filter.getSpecificFilterId(),
|
||||||
|
metadata: filter.toIdentifier(),
|
||||||
|
},
|
||||||
|
...((user.unsafeMetadata.filters as Array<
|
||||||
|
ClerkEmbeddedFilter<unknown>
|
||||||
|
>) ?? []),
|
||||||
|
] as Array<ClerkEmbeddedFilter<unknown>>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
else
|
||||||
|
await user.update({
|
||||||
|
unsafeMetadata: {
|
||||||
|
filters: existingArray,
|
||||||
|
...user.unsafeMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existingArray =
|
||||||
|
(JSON.parse(
|
||||||
|
localStorage.getItem("mhsf__filters") ?? "[]",
|
||||||
|
) as Array<ClerkEmbeddedFilter<unknown>>) ?? [];
|
||||||
|
existingArray.splice(existing, 1);
|
||||||
|
|
||||||
|
if (existing === -1)
|
||||||
|
localStorage.setItem(
|
||||||
|
"mhsf__filters",
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
type: filter.getSpecificFilterId(),
|
||||||
|
metadata: filter.toIdentifier(),
|
||||||
|
},
|
||||||
|
...((JSON.parse(
|
||||||
|
localStorage.getItem("mhsf__filters") ?? "[]",
|
||||||
|
) as Array<ClerkEmbeddedFilter<unknown>>) ?? []),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
else
|
||||||
|
localStorage.setItem("mhsf__filters", JSON.stringify(existingArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
setApplied(findExisting());
|
||||||
|
}
|
||||||
|
communication.fromIframe.send("rerender-servers", {});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{applied === -1 ? "A" : "Una"}pply
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,9 @@ export function ModificationFrame() {
|
|||||||
if (c.from === "iframe")
|
if (c.from === "iframe")
|
||||||
communication.toIframe.send("ping", {from: "top-layer"})
|
communication.toIframe.send("ping", {from: "top-layer"})
|
||||||
})
|
})
|
||||||
|
communication.toIframe.handle("rerender-servers", (c) => {
|
||||||
|
window.dispatchEvent(new Event("update-modification-stack"))
|
||||||
|
})
|
||||||
}, [ref])
|
}, [ref])
|
||||||
|
|
||||||
return <iframe ref={ref} src="/servers/embedded/sl-modification-frame" height={800} title="Server-list Modification Frame" />
|
return <iframe ref={ref} src="/servers/embedded/sl-modification-frame" height={800} title="Server-list Modification Frame" />
|
||||||
|
|||||||
@ -89,28 +89,6 @@ export function MOTDRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex gap-2"
|
|
||||||
onSelect={() =>
|
|
||||||
window.dispatchEvent(new Event("open-rearrange-menu"))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Shuffle size={16} /> Rearrange server items
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Button
|
|
||||||
className="flex items-center"
|
|
||||||
size="square-md"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<EllipsisVertical size={16} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</DropdownMenu>
|
|
||||||
</span>
|
</span>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
{tab === "motd" && (
|
{tab === "motd" && (
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
import { Editor } from "@monaco-editor/react";
|
||||||
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
|
export function ClerkMetadataPopup({children}: {children: ReactNode | ReactNode[] | undefined}) {
|
||||||
|
const [mdType, setMDType] = useState<"public" | "unsafe" | null>(null)
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const {user} = useUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("open-public-clerkmd", () => {
|
||||||
|
setMDType("public")
|
||||||
|
setOpen(true);
|
||||||
|
})
|
||||||
|
window.addEventListener("open-unsafe-clerkmd", () => {
|
||||||
|
setMDType("unsafe")
|
||||||
|
setOpen(true);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="min-w-[850px]">
|
||||||
|
<DialogTitle>{mdType?.toLocaleUpperCase()} metadata</DialogTitle>
|
||||||
|
<Editor options={{domReadOnly: true, readOnly: true}} height={500} width={800} language="json" value={JSON.stringify(mdType === "public" ? user?.publicMetadata : user?.unsafeMetadata, null, 2)}/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -29,38 +29,77 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
import { Material } from "@/components/ui/material";
|
||||||
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "./setting";
|
import {
|
||||||
|
Setting,
|
||||||
|
SettingContent,
|
||||||
|
SettingDescription,
|
||||||
|
SettingMeta,
|
||||||
|
SettingTitle,
|
||||||
|
} from "./setting";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AnimatedText } from "@/components/ui/animated-text";
|
import { AnimatedText } from "@/components/ui/animated-text";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { loadingList } from "../server-page/util";
|
import { loadingList } from "../server-page/util";
|
||||||
|
import { ClerkMetadataPopup } from "./clerk-metadata-popup";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Link } from "@/components/util/link";
|
||||||
|
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||||
|
|
||||||
export function DebugSettings() {
|
export function DebugSettings() {
|
||||||
const [randomText, setRandomText] = useState("")
|
const [randomText, setRandomText] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material className="mt-6 grid gap-4">
|
<Material className="mt-6 grid gap-4">
|
||||||
<h2 className="text-xl font-semibold text-inherit">Debug Settings</h2>
|
<h2 className="text-xl font-semibold text-inherit">Debug Settings</h2>
|
||||||
<Setting>
|
<Setting>
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>
|
<SettingTitle>Generate loading text</SettingTitle>
|
||||||
Generate loading text
|
<SettingDescription>
|
||||||
</SettingTitle>
|
Generate a random loading text
|
||||||
<SettingDescription>
|
</SettingDescription>
|
||||||
Generate a random loading text
|
</SettingMeta>
|
||||||
</SettingDescription>
|
<div className="block pb-6">
|
||||||
</SettingMeta>
|
<Button
|
||||||
<div className="block pb-6">
|
onClick={() => {
|
||||||
<Button onClick={() => {
|
setRandomText(
|
||||||
setRandomText(loadingList[Math.floor(Math.random() * loadingList.length)])
|
loadingList[Math.floor(Math.random() * loadingList.length)],
|
||||||
}}>
|
);
|
||||||
Generate
|
}}
|
||||||
</Button>
|
>
|
||||||
<AnimatedText className="font-bold" text={randomText + "..."}/>
|
Generate
|
||||||
</div>
|
</Button>
|
||||||
</SettingContent>
|
<AnimatedText className="font-bold" text={randomText + "..."} />
|
||||||
</Setting>
|
</div>
|
||||||
</Material>
|
</SettingContent>
|
||||||
);
|
</Setting>
|
||||||
|
<ClerkMetadataPopup>
|
||||||
|
<Setting>
|
||||||
|
<SettingContent>
|
||||||
|
<SettingMeta>
|
||||||
|
<SettingTitle>View Clerk metadata</SettingTitle>
|
||||||
|
<SettingDescription>
|
||||||
|
View any Clerk metadata for your user.
|
||||||
|
</SettingDescription>
|
||||||
|
</SettingMeta>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button>Open metadata</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={() => window.dispatchEvent(new Event("open-public-clerkmd"))}>Public</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => window.dispatchEvent(new Event("open-unsafe-clerkmd"))}>Unsafe</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SettingContent>
|
||||||
|
</Setting>
|
||||||
|
</ClerkMetadataPopup>
|
||||||
|
</Material>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "@/lib/hooks/use-theme";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export const serverModDB: ModDBCategory[] = [
|
|||||||
name: "Always Online",
|
name: "Always Online",
|
||||||
description: "All servers that are always online.",
|
description: "All servers that are always online.",
|
||||||
color: "#a380e0",
|
color: "#a380e0",
|
||||||
value: new TagFilter(0),
|
value: new TagFilter(2),
|
||||||
icon: ServerCog
|
icon: ServerCog
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -36,10 +36,12 @@ import { tryCatch } from "../try-catch";
|
|||||||
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||||
|
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||||
|
import { supportedFilters } from "../types/filter";
|
||||||
|
|
||||||
type EmbeddedFilter = {
|
type EmbeddedFilter = {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
functionFilter: (server: OnlineServer) => boolean;
|
functionFilter: (server: OnlineServer) => (boolean | Promise<boolean>);
|
||||||
};
|
};
|
||||||
|
|
||||||
type SortFunction<K> = (object1: K, object2: K) => number;
|
type SortFunction<K> = (object1: K, object2: K) => number;
|
||||||
@ -57,23 +59,20 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
||||||
const { user, isSignedIn } = useUser();
|
const { user, isSignedIn } = useUser();
|
||||||
|
|
||||||
const updateServers = (newFilters: EmbeddedFilter[]) => {
|
const updateServers = async (newFilters: EmbeddedFilter[]) => {
|
||||||
const modificationMap = data.map((v) =>
|
const modificationMap = await Promise.all(data.map((v) =>
|
||||||
newFilters.map((c) => c.functionFilter(v)),
|
Promise.all(newFilters.map(async (c) => c.functionFilter(v))),
|
||||||
);
|
));
|
||||||
const resultData = data.filter(
|
const resultData = data.filter(
|
||||||
(_, i) => !modificationMap[i].includes(false),
|
(_, i) => !modificationMap[i].includes(false),
|
||||||
);
|
);
|
||||||
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
||||||
|
|
||||||
console.table({ sortedData, modificationMap, resultData, data });
|
console.log({ sortedData, modificationMap, resultData, data, newFilters });
|
||||||
|
|
||||||
|
if (sortedData.length !== 0) setFilteredData(sortedData);
|
||||||
if (sortedData.length !== 0)
|
|
||||||
setFilteredData(sortedData);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// biome-ignore lint: bruh
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredData.length === 0 || data.length === 0) {
|
if (filteredData.length === 0 || data.length === 0) {
|
||||||
window.dispatchEvent(new Event("update-modification-stack"));
|
window.dispatchEvent(new Event("update-modification-stack"));
|
||||||
@ -129,18 +128,18 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
(async () =>
|
(async () =>
|
||||||
type === "filter"
|
type === "filter"
|
||||||
? new Function(
|
? new Function(
|
||||||
"server",
|
"server",
|
||||||
`${functionBody}
|
`${functionBody}
|
||||||
|
|
||||||
return filter(server)`,
|
return filter(server)`,
|
||||||
)
|
)
|
||||||
: new Function(
|
: new Function(
|
||||||
"serverA",
|
"serverA",
|
||||||
"serverB",
|
"serverB",
|
||||||
`${functionBody}
|
`${functionBody}
|
||||||
|
|
||||||
return sort(serverA, serverB)`,
|
return sort(serverA, serverB)`,
|
||||||
))(),
|
))(),
|
||||||
);
|
);
|
||||||
if (filterErr) {
|
if (filterErr) {
|
||||||
setTestModeStatus(
|
setTestModeStatus(
|
||||||
@ -213,7 +212,13 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
if (!t)
|
if (!t)
|
||||||
window.addEventListener("update-modification-stack", async () => {
|
window.addEventListener("update-modification-stack", async () => {
|
||||||
await user?.reload();
|
await user?.reload();
|
||||||
|
setLoading(true);
|
||||||
let newFilters: EmbeddedFilter[] = [];
|
let newFilters: EmbeddedFilter[] = [];
|
||||||
|
const filters =
|
||||||
|
((isSignedIn ? user.unsafeMetadata.filters : JSON.parse(localStorage.getItem("mhsf__filters") ?? "[]")) as Array<
|
||||||
|
ClerkEmbeddedFilter<unknown>
|
||||||
|
>) ?? [];
|
||||||
|
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
const activatedModifications =
|
const activatedModifications =
|
||||||
(user.unsafeMetadata
|
(user.unsafeMetadata
|
||||||
@ -232,58 +237,84 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
(async () =>
|
(async () =>
|
||||||
c.testMode === "filter"
|
c.testMode === "filter"
|
||||||
? new Function(
|
? new Function(
|
||||||
"server",
|
"server",
|
||||||
`${functionBody}
|
`${functionBody}
|
||||||
|
|
||||||
return filter(server)`,
|
return filter(server)`,
|
||||||
)
|
)
|
||||||
: new Function(
|
: new Function(
|
||||||
"serverA",
|
"serverA",
|
||||||
"serverB",
|
"serverB",
|
||||||
`${functionBody}
|
`${functionBody}
|
||||||
|
|
||||||
return sort(serverA, serverB)`,
|
return sort(serverA, serverB)`,
|
||||||
))(),
|
))(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filterErr) {
|
if (filterErr) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
||||||
);
|
);
|
||||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: () => true };
|
return {
|
||||||
|
identifier: `file-${c.originalFileName}.ts`,
|
||||||
|
functionFilter: () => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof filterFunc === "function") {
|
if (typeof filterFunc === "function") {
|
||||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: filterFunc };
|
return {
|
||||||
|
identifier: `file-${c.originalFileName}.ts`,
|
||||||
|
functionFilter: filterFunc,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(
|
toast.error(
|
||||||
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
`Couldn't enable modification '${c.friendlyName}'. Please lint and test again.`,
|
||||||
);
|
);
|
||||||
return { identifier: `file-${c.originalFileName}.ts`, functionFilter: () => true };
|
return {
|
||||||
|
identifier: `file-${c.originalFileName}.ts`,
|
||||||
|
functionFilter: () => true,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
)) as EmbeddedFilter[];
|
)) as EmbeddedFilter[];
|
||||||
|
|
||||||
// avoid duplicates
|
// avoid duplicates
|
||||||
|
// biome-ignore lint/complexity/noForEach:
|
||||||
resolvedModifications.forEach((item) => {
|
resolvedModifications.forEach((item) => {
|
||||||
setFilters((c) => {
|
setFilters((c) => {
|
||||||
if (c.findIndex((i) => i.identifier === item.identifier) === -1)
|
if (c.findIndex((i) => i.identifier === item.identifier) === -1)
|
||||||
return [
|
return [...c, item];
|
||||||
...c,
|
return c;
|
||||||
item
|
|
||||||
]
|
|
||||||
else return c;
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
newFilters = resolvedModifications.map((item) => {
|
newFilters = resolvedModifications.map((item) => {
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noForEach:
|
||||||
|
filters.forEach((filter) => {
|
||||||
|
// Get back the filter type from the namespace
|
||||||
|
const filterType = supportedFilters.find(
|
||||||
|
(t) => filter.type === t.ns,
|
||||||
|
);
|
||||||
|
// Get back a filter with associated metadata
|
||||||
|
const parsedFilter = filterType?.fi(
|
||||||
|
filter.metadata as {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
newFilters.push({
|
||||||
|
identifier: filterType?.ns + (Math.random() * Math.random() * Math.random()).toString(),
|
||||||
|
functionFilter: (server: OnlineServer) => parsedFilter?.applyToServer({ online: server }) ?? true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
console.log(newFilters);
|
console.log(newFilters);
|
||||||
|
|
||||||
updateServers(newFilters);
|
await updateServers(newFilters);
|
||||||
});
|
});
|
||||||
}, [data]);
|
}, [data]);
|
||||||
console.log(filters);
|
console.log(filters);
|
||||||
@ -293,7 +324,9 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
testModeEnabled,
|
testModeEnabled,
|
||||||
testModeLoading,
|
testModeLoading,
|
||||||
testModeStatus,
|
testModeStatus,
|
||||||
filterCount: filters.filter((item, index, array) => array.indexOf(item) === index).length + (sort === null ? 1 : 0),
|
filterCount:
|
||||||
|
filters.filter((item, index, array) => array.indexOf(item) === index)
|
||||||
|
.length + (sort === null ? 1 : 0),
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,17 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Dashboard() {
|
import { allTags } from "@/config/tags"
|
||||||
return <>Hello world</>
|
import { useUser } from "@clerk/nextjs"
|
||||||
|
|
||||||
|
export function useModificationsChange() {
|
||||||
|
const {isSignedIn} = useUser()
|
||||||
|
|
||||||
|
return {
|
||||||
|
addFilterData: async (namespace: string, identifier: { [key: string]: string | number | boolean }) => {
|
||||||
|
if (isSignedIn) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -74,3 +74,24 @@ export function useSettingsStore() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An exact copy of the API without Clerk
|
||||||
|
export function useNoClerkSettingsStore() {
|
||||||
|
return {
|
||||||
|
get: (key: string) => {
|
||||||
|
if (localStorage.getItem(key) === "true")
|
||||||
|
return true;
|
||||||
|
if (localStorage.getItem(key) === "false")
|
||||||
|
return false;
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
},
|
||||||
|
set: async (
|
||||||
|
key: string,
|
||||||
|
value: string | boolean,
|
||||||
|
userEntry: boolean,
|
||||||
|
__unsafeMetadata = false
|
||||||
|
) => {
|
||||||
|
localStorage.setItem(key, value.toString());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import { allTags } from "@/config/tags";
|
|||||||
import type { OnlineServer, ServerResponse } from "./mh-server";
|
import type { OnlineServer, ServerResponse } from "./mh-server";
|
||||||
import type { MHSFData } from "./data";
|
import type { MHSFData } from "./data";
|
||||||
import { TagFilter } from "./filters/tag-filter";
|
import { TagFilter } from "./filters/tag-filter";
|
||||||
|
import { CategoryFilter } from "./filters/category-filter";
|
||||||
|
|
||||||
/* Any filter that can be converted back and forth from a string or a Filter object */
|
/* Any filter that can be converted back and forth from a string or a Filter object */
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
@ -49,11 +50,17 @@ export interface Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const supportedFilters: {
|
export const supportedFilters: {
|
||||||
t: (implementation: unknown) => Filter;
|
|
||||||
ns: string;
|
ns: string;
|
||||||
|
fi: (identifier: {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}) => Filter;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
t: (i) => new TagFilter(i as string | number),
|
|
||||||
ns: "app.mhsf.filter.tagFilter",
|
ns: "app.mhsf.filter.tagFilter",
|
||||||
|
fi: new TagFilter(0, false).fromIdentifier
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ns: "app.mhsf.filter.categoryFilter",
|
||||||
|
fi: new CategoryFilter(0).fromIdentifier
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -28,25 +28,37 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { allCategories } from "@/config/tags";
|
||||||
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
import type { MHSFData } from "../data";
|
||||||
import { MongoClient } from "mongodb";
|
import type { Filter } from "../filter";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import type { OnlineServer, ServerResponse } from "../mh-server";
|
||||||
import { supportedFilters } from "@/lib/types/filter";
|
|
||||||
|
|
||||||
const supportedNamespaces = supportedFilters.map((c) => c.ns);
|
export class CategoryFilter implements Filter {
|
||||||
|
categoryIndex: number;
|
||||||
|
|
||||||
export default async function handler(
|
type(): "filter" {
|
||||||
req: NextApiRequest,
|
return "filter";
|
||||||
res: NextApiResponse,
|
}
|
||||||
) {
|
|
||||||
const { userId } = getAuth(req);
|
|
||||||
|
|
||||||
if (!userId) {
|
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return { categoryIndex: this.categoryIndex };
|
||||||
}
|
}
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
await client.connect();
|
|
||||||
|
|
||||||
|
fromIdentifier(identifier: { [key: string]: string | number | boolean; }): Filter {
|
||||||
|
return new CategoryFilter(identifier.categoryIndex as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpecificFilterId(): string {
|
||||||
|
return "app.mhsf.filter.categoryFilter";
|
||||||
|
}
|
||||||
|
|
||||||
|
applyToServer(server: { online?: OnlineServer; server?: ServerResponse; mhsfData?: MHSFData; }): Promise<boolean> {
|
||||||
|
if (server.online !== undefined)
|
||||||
|
return allCategories[this.categoryIndex].condition(server.online);
|
||||||
|
return new Promise((r) => r(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(categoryIndex: number) {
|
||||||
|
this.categoryIndex = categoryIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -35,13 +35,14 @@ import type { Filter } from "../filter";
|
|||||||
|
|
||||||
export class TagFilter implements Filter {
|
export class TagFilter implements Filter {
|
||||||
tagId: string;
|
tagId: string;
|
||||||
|
opposite: boolean;
|
||||||
|
|
||||||
type(): "filter" {
|
type(): "filter" {
|
||||||
return "filter";
|
return "filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
toIdentifier(): { [key: string]: string | number | boolean } {
|
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||||
return { tagId: this.tagId };
|
return { tagId: this.tagId, opposite: this.opposite };
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpecificFilterId(): string {
|
getSpecificFilterId(): string {
|
||||||
@ -51,12 +52,13 @@ export class TagFilter implements Filter {
|
|||||||
fromIdentifier(identifier: {
|
fromIdentifier(identifier: {
|
||||||
[key: string]: string | number | boolean;
|
[key: string]: string | number | boolean;
|
||||||
}): Filter {
|
}): Filter {
|
||||||
return new TagFilter(identifier.tagId as string);
|
return new TagFilter(identifier.tagId as string, identifier.opposite as boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(tagIndex: number | string) {
|
constructor(tagIndex: number | string, opposite: boolean) {
|
||||||
if (typeof tagIndex === "string") this.tagId = tagIndex;
|
if (typeof tagIndex === "string") this.tagId = tagIndex;
|
||||||
else this.tagId = btoa(allTags[tagIndex].docsName);
|
else this.tagId = btoa(allTags[tagIndex].docsName);
|
||||||
|
this.opposite = opposite;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyToServer(server: {
|
applyToServer(server: {
|
||||||
@ -72,8 +74,14 @@ export class TagFilter implements Filter {
|
|||||||
).condition ?? (() => true)
|
).condition ?? (() => true)
|
||||||
)(server);
|
)(server);
|
||||||
|
|
||||||
|
console.log(result, server.online?.name, (
|
||||||
|
allTags.find((c) => btoa(c.docsName) === this.tagId) ?? {
|
||||||
|
condition: () => true,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
if (typeof result === "boolean")
|
if (typeof result === "boolean")
|
||||||
return new Promise((r) => r(result))
|
return new Promise((r) => r(!result))
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,8 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
|||||||
? clerkMiddleware(async (auth, req) => {
|
? clerkMiddleware(async (auth, req) => {
|
||||||
const authRes = await auth();
|
const authRes = await auth();
|
||||||
const client = await clerkClient();
|
const client = await clerkClient();
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
requestHeaders.set("x-url", req.url);
|
||||||
|
|
||||||
if (isRootRoute(req)) {
|
if (isRootRoute(req)) {
|
||||||
switch (authRes.userId === null) {
|
switch (authRes.userId === null) {
|
||||||
@ -70,6 +72,12 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
|||||||
new URL(`/server/v2/minehut/${minehutRes.server._id}`, req.url),
|
new URL(`/server/v2/minehut/${minehutRes.server._id}`, req.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
})
|
})
|
||||||
: (request: NextRequest) => {};
|
: (request: NextRequest) => {};
|
||||||
|
|
||||||
|
|||||||
@ -45,8 +45,6 @@ export default async function handler(
|
|||||||
const result = await betterStackResult.json();
|
const result = await betterStackResult.json();
|
||||||
const url = await betterStackURL.json();
|
const url = await betterStackURL.json();
|
||||||
|
|
||||||
console.log(result)
|
|
||||||
|
|
||||||
const filtered = result.data.filter(
|
const filtered = result.data.filter(
|
||||||
(c: any) =>
|
(c: any) =>
|
||||||
c.attributes.ends_at === null &&
|
c.attributes.ends_at === null &&
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user