mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-08 05:04:58 -05:00
feat: progress (2/16)
This commit is contained in:
parent
5478276fd7
commit
e0095ca890
64
apps/www/src/components/feat/navbar/menu-dropdown.tsx
Normal file
64
apps/www/src/components/feat/navbar/menu-dropdown.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||||
|
import { LogIn, Settings, Ship, User, UserCog } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export function MenuDropdown() {
|
||||||
|
const clerk = useClerk();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator>Profile</DropdownMenuSeparator>
|
||||||
|
<SignedOut>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => clerk.openSignIn()}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogIn size={16} />
|
||||||
|
Login
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => clerk.openSignUp()}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<User size={16} />
|
||||||
|
Sign up
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
router.push(process.env.NEXT_PUBLIC_CLERK_SWITCH_DOMAIN as string)
|
||||||
|
}
|
||||||
|
className="hover:block group hover:py-1"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Ship size={16} />
|
||||||
|
Migrate your account
|
||||||
|
</span>
|
||||||
|
<small className="hidden group-hover:block break-words text-xs text-muted-foreground">
|
||||||
|
If you created your account before Jan. 29th 2025, you'll need to
|
||||||
|
migrate your account
|
||||||
|
</small>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</SignedOut>
|
||||||
|
<SignedIn>
|
||||||
|
<DropdownMenuItem onClick={() => clerk.openUserProfile()}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<UserCog size={16} />
|
||||||
|
User Settings
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</SignedIn>
|
||||||
|
<DropdownMenuSeparator>App</DropdownMenuSeparator>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Settings size={16} />
|
||||||
|
Settings
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { BrandingGenericIcon } from "@/components/feat/icons/branding-icons";
|
import { BrandingGenericIcon } from "@/components/feat/icons/branding-icons";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@ -7,14 +8,19 @@ import {
|
|||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import Github from "@/components/ui/github";
|
import Github from "@/components/ui/github";
|
||||||
import { ALegacy } from "@/components/util/link";
|
import { ALegacy } from "@/components/util/link";
|
||||||
import { ModeToggle } from "@/components/util/mode-toggle";
|
|
||||||
import { version } from "@/config/version";
|
import { version } from "@/config/version";
|
||||||
import { useScroll } from "@/lib/hooks/use-scroll";
|
import { useScroll } from "@/lib/hooks/use-scroll";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ServerCrash } from "lucide-react";
|
import { Menu, ServerCrash } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { MenuDropdown } from "./menu-dropdown";
|
||||||
|
|
||||||
export function NavBar() {
|
export function NavBar() {
|
||||||
const showBorder = useScroll(40);
|
const showBorder = useScroll(40);
|
||||||
@ -56,11 +62,31 @@ export function NavBar() {
|
|||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ALegacy>
|
</ALegacy>
|
||||||
|
<ALegacy href="Special:GitHub/releases">
|
||||||
|
<ContextMenuItem>
|
||||||
|
<span className="pl-2 flex gap-2 items-center">
|
||||||
|
<Github /> Open GitHub Releases
|
||||||
|
</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ALegacy>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</span>
|
</span>
|
||||||
<span className="mr-3">
|
<span className="mr-3 flex items-center">
|
||||||
<ModeToggle />
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
className="rounded-full flex items-center"
|
||||||
|
size="square-lg"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<Menu size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="max-w-[280px] w-[280px] mt-2 mr-2">
|
||||||
|
<MenuDropdown />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,61 @@
|
|||||||
import type { OnlineServer } from "@/lib/types/mh-server";
|
import type { OnlineServer } from "@/lib/types/mh-server";
|
||||||
import IconDisplay from "../icons/minecraft-icon-display";
|
import IconDisplay from "../icons/minecraft-icon-display";
|
||||||
import { Material } from "@/components/ui/material";
|
import { Material } from "@/components/ui/material";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export default function ServerCard({ server }: { server: OnlineServer }) {
|
export default function ServerCard({ server }: { server: OnlineServer }) {
|
||||||
return (
|
return (
|
||||||
<Material key={server.name}>
|
<Material key={server.name}>
|
||||||
<span className="flex gap-2 items-center">
|
<span className="flex gap-2 items-center">
|
||||||
<IconDisplay server={server} />
|
<IconDisplay server={server} />
|
||||||
{server.name}
|
<strong className="text-lg">{server.name}</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
by {server.author || "Nobody"}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="max-w-[390px] break-words">
|
||||||
|
{server.author ? (
|
||||||
|
<span>
|
||||||
|
{server.name} is owned by{" "}
|
||||||
|
<RankColoring author={server.author} rank={server.authorRank} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
This server doesn't have a recorded owner because the server owner
|
||||||
|
never linked their Minecraft account to their Minehut account.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</Material>
|
</Material>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RankColoring({ rank, author }: { rank: string; author: string }) {
|
||||||
|
switch (rank.toLocaleLowerCase()) {
|
||||||
|
case "default":
|
||||||
|
return <span className="text-muted-foreground">{author}</span>;
|
||||||
|
case "vip":
|
||||||
|
return <span className="text-green-700">[VIP] {author}</span>;
|
||||||
|
case "vip_plus":
|
||||||
|
return <span className="text-lime-500">[VIP+] {author}</span>;
|
||||||
|
case "pro":
|
||||||
|
return <span className="text-cyan-500">[PRO] {author}</span>;
|
||||||
|
case "legend":
|
||||||
|
return <span className="text-yellow-500">[LEGEND] {author}</span>;
|
||||||
|
case "patron":
|
||||||
|
return <span className="text-[#f355ff]">[PATRON] {author}</span>;
|
||||||
|
case "mod":
|
||||||
|
return <span className="text-yellow-200">[MOD] {author}</span>;
|
||||||
|
default:
|
||||||
|
return <span className="text-red-500">[STAFF] {author}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,9 +4,13 @@ import { useServers } from "@/lib/hooks/use-servers";
|
|||||||
import ServerCard from "./server-card";
|
import ServerCard from "./server-card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Statistics } from "./statistics";
|
import { Statistics } from "./statistics";
|
||||||
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
|
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
||||||
|
|
||||||
export function ServerList() {
|
export function ServerList() {
|
||||||
const { servers, loading, serverCount, playerCount } = useServers();
|
const { servers, loading, serverCount, playerCount } = useServers();
|
||||||
|
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||||
|
useInfiniteScrolling(servers);
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
@ -29,11 +33,18 @@ export function ServerList() {
|
|||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
||||||
Servers
|
Servers
|
||||||
</h1>
|
</h1>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-3">
|
<InfiniteScroll
|
||||||
{servers.map((c) => (
|
dataLength={itemsLength}
|
||||||
<ServerCard server={c} key={c.name} />
|
next={fetchMoreData}
|
||||||
))}
|
hasMore={hasMoreData}
|
||||||
</div>
|
loader={<>Loading...</>}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-3">
|
||||||
|
{data.map((c) => (
|
||||||
|
<ServerCard server={c} key={c.name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</InfiniteScroll>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"backdrop-blur-xl w-max z-[100] max-w-[280px] origin-top-left max-h-[32rem] overflow-auto list-none shadow-xl rounded-xl",
|
"backdrop-blur-xl w-max z-[100] max-w-[280px] origin-top-left max-h-[32rem] overflow-auto list-none shadow-xl rounded-xl select-none outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -96,7 +96,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full px-2 rounded-lg z-[100] min-h-[36px] font-normal flex items-center cursor-pointer hover:dark:bg-zinc-800/70",
|
"w-full px-2 rounded-lg z-[100] outline-none min-h-[36px] font-normal flex items-center cursor-pointer hover:dark:bg-zinc-800/70",
|
||||||
props.disabled ? "opacity-70 pointer-events-none cursor-not-allowed" : "",
|
props.disabled ? "opacity-70 pointer-events-none cursor-not-allowed" : "",
|
||||||
"duration-100 border border-transparent bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
"duration-100 border border-transparent bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
||||||
className
|
className
|
||||||
@ -176,9 +176,15 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn(
|
||||||
|
"text-slate-800 dark:text-zinc-200 text-xs font-medium py-2 px-2 flex items-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
<hr className="flex-shrink flex-1 border-slate-200 dark:border-zinc-800 border-opacity-70" />
|
||||||
|
</DropdownMenuPrimitive.Separator>
|
||||||
));
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,33 @@
|
|||||||
import { OnlineServer } from "../types/mh-server";
|
import { useEffect, useState } from "react";
|
||||||
|
import type { OnlineServer } from "../types/mh-server";
|
||||||
|
|
||||||
export function useInfiniteScrolling({
|
const itemsPerScroll = 40;
|
||||||
servers,
|
|
||||||
}: {
|
export function useInfiniteScrolling(servers: OnlineServer[]) {
|
||||||
servers: OnlineServer[];
|
const [currentOffset, setCurrentOffset] = useState(0);
|
||||||
}) {}
|
const [data, setData] = useState<OnlineServer[]>([]);
|
||||||
|
const [hasMoreData, setHasMoreData] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Start at the first `itemsPerScroll` servers (duh)
|
||||||
|
setData(servers.slice(0, itemsPerScroll));
|
||||||
|
}, [servers]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemsLength: currentOffset + itemsPerScroll,
|
||||||
|
fetchMoreData: () => {
|
||||||
|
setCurrentOffset(currentOffset + itemsPerScroll);
|
||||||
|
const currentData = data;
|
||||||
|
const dataSlice = servers.slice(
|
||||||
|
currentOffset,
|
||||||
|
currentOffset + itemsPerScroll
|
||||||
|
);
|
||||||
|
const newDataArray = [...currentData, ...dataSlice];
|
||||||
|
|
||||||
|
setData(newDataArray);
|
||||||
|
if (dataSlice.length !== itemsPerScroll) setHasMoreData(false);
|
||||||
|
},
|
||||||
|
hasMoreData,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user