feat: inital commit

This commit is contained in:
dvelo 2024-07-23 18:49:21 -05:00
parent 870f3ec7a1
commit 9aeaebde54
70 changed files with 29225 additions and 555 deletions

@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
"extends": ["next/core-web-vitals"]
}

@ -1,36 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# MHSF
## Getting Started
An open-source Minehut server viewer/finder made in React + Next.js. Is much better than the Minehut one, as has many filters and is improving everyday.
First, run the development server:
## Tech-stack
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
[React](https://react.dev): Component based syntaxing for the web
[Next.js](https://nextjs.org): Handles API, server-based metadata, and so much more.
[Clerk](https://clerk.com): Authentication for MHSF (very cool library)
[TailwindCSS](https://tailwindcss.com): Styling without CSS, simplified
[shadcn/ui](https://ui.shadcn.com): Provides the awesome UI for MHSF
And many other smaller libraries are in use to make sure MHSF is feature-packed.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Contributing
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
If you'd like to contribute, you must follow the guidelines below:
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
- Make sure to lint (`npm run lint`) every time you finish lines of code
- Use common sense! Don't make comments that could offend someone, or just seems like a bad ideaa
- Be respectful when making your PR. We would love to take your code, but if you argue, it doesn't help.
## Learn More
## Building
To learn more about Next.js, take a look at the following resources:
Clone the repo!
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
First, you must supply the following services with API keys:
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
- [Clerk](https://clerk.com): Create an app and put the respective keys in `.env.local`
## Deploy on Vercel
Second, run `npm i` and `npm run build`. To start the app, run `npm run start`.
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
### Dev
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Swap `npm run build` for `npm run dev`.

17
components.json Normal file

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,6 @@
module.exports = {
enable: true,
mode: "simplify-seedable", // random | simplify | simplify-seedable
refreshClassConversionJson: false, // recommended set to true if not in production
allowExtensions: [".jsx", ".tsx", ".js", ".ts", ".html", ".rsc"],
};

@ -1,4 +1,14 @@
import plugin from "@unocss/webpack";
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "img.clerk.com",
},
],
},
};
export default nextConfig;

8027
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -6,21 +6,73 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"obfuscate": "next-css-obfuscator"
},
"dependencies": {
"@babel/parser": "^7.24.7",
"@clerk/nextjs": "^5.1.3",
"@clerk/themes": "^2.1.8",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@types/react-twemoji": "^0.4.3",
"@unocss/eslint-plugin": "^0.61.5",
"@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5",
"@unocss/webpack": "^0.61.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"framer-motion": "^11.3.8",
"geist": "^1.3.0",
"json-beautify": "^1.1.1",
"lucide-react": "^0.381.0",
"minimessage-2-html": "1.6.0",
"mongodb": "^6.8.0",
"next": "14.2.3",
"next-css-obfuscator": "^2.2.16",
"next-themes": "^0.3.0",
"postcss-obfuscator": "^1.6.1",
"prettier": "^3.3.1",
"react": "^18",
"react-dom": "^18",
"next": "14.2.3"
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.0.19",
"react-twemoji": "^0.5.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-patch": "^4.0.0",
"tw-to-css": "^0.0.12",
"unocss": "^0.61.5",
"unplugin-tailwindcss-mangle": "^3.0.1",
"vaul": "^0.9.1"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@unocss/eslint-config": "^0.61.5",
"@unocss/preset-uno": "^0.61.5",
"@unocss/transformer-compile-class": "^0.61.5",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.3"
"typescript": "^5"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

BIN
public/itemsCSS/icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

273
src/allTags.ts Normal file

@ -0,0 +1,273 @@
import { OnlineServer, ServerResponse } from "./components/ServerView";
const serverCache: any = {};
// allTags object
// name: name of the tag (server) => string
// condition: what condition will the tag appear. if the primary flag is false, just use a empty function (server) => boolean
// listCondition?: if this flag isn't primary, what condition will the tag appear if the server is offline
// tooltipDesc: the tooltip text when hovered (string)
// htmlDocs: when clicked, what appears (formatted in HTML, string, using the `` string format)
// docsName: name appearing on the title in the docs. (string)
// role?: the role used on the badge (https://ui.shadcn.com/docs/components/badge + some custom others, string)
// primary: does this tag appear **just** in the home screen (true), or **just** inside the server screen (false)
// __disab: you shouldn't mess with this flag
// __filter: if your name isn't static, set this to true
//
// You may also use `requestServer()` to grab the offline version of the server from the API, which may get you more information about the server (ServerResponse)
export var allTags: Array<{
name: (server: OnlineServer) => Promise<string>;
condition: (server: OnlineServer) => Promise<boolean>;
listCondition?: (server: ServerResponse) => Promise<boolean>;
tooltipDesc: string;
htmlDocs: string;
docsName: string;
primary: boolean;
role?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "red"
| "orange"
| "yellow"
| "green"
| "lime"
| "blue"
| "teal"
| "cyan"
| "violet"
| "indigo"
| "purple"
| "fuchsia"
| "pink";
__disab?: boolean;
__filter?: boolean;
}> = [
{
name: async () => "Always Online",
condition: async (b: any) => b.staticInfo.alwaysOnline,
tooltipDesc:
'"Always online" means that the server will not shut down until the plan associated with it expires.',
htmlDocs: `
This tag appears on servers where the plan they are under allows the server to be always online. However, if the plan associated with the tag expires, the server will no longer be Always Online. <em>This is in servers with one of the more expensive plans, or just a server that is external.</em>
`,
primary: true,
docsName: "Always Online",
role: "secondary",
__disab: true,
},
{
name: async (s) => s.staticInfo.planMaxPlayers + " max players",
condition: async (s) => s.staticInfo.planMaxPlayers != null,
tooltipDesc:
"This tag represents the maximum amount of players the server can have at one time.",
docsName: "Max Players",
htmlDocs:
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
primary: true,
role: "secondary",
__filter: true,
},
// deprecated
/**{
name: async () => "Velocity",
condition: async (s) => {
var type = await requestServer(s);
return type.server_version_type == "VELOCITY";
},
tooltipDesc:
"This server uses Velocity, or has multiple servers inside of it",
htmlDocs:
'Does this server use <a href="https://papermc.io/software/velocity">Velocity</a>? This means that the server has multiple minigames/other servers gamemodes that are private, and this server is the lobby.',
docsName: "Velocity",
primary: true,
role: "violet",
}, */
{
name: async () => "External",
condition: async (s) => {
return (await requestServer(s)).activeServerPlan == "External Server";
},
tooltipDesc: "Is this server not directly hosted on Minehuts servers?",
htmlDocs:
"Is this server not directly hosted on Minehuts servers, most likely because the server runs on an multi-server network (like Velocity or Bungeecord)",
docsName: "External",
primary: true,
role: "red",
},
];
export var allCategories: Array<{
name: string;
condition: (server: OnlineServer) => Promise<boolean>;
primary: boolean;
role?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "red"
| "orange"
| "yellow"
| "green"
| "lime"
| "blue"
| "teal"
| "cyan"
| "violet"
| "indigo"
| "purple"
| "fuchsia"
| "pink";
}> = [
{
name: "Farming",
condition: async (b: any) => {
return b.allCategories.includes("farming");
},
primary: true,
role: "secondary",
},
{
name: "SMP",
condition: async (b: any) => {
return b.allCategories.includes("smp");
},
primary: true,
role: "secondary",
},
{
name: "Factions",
condition: async (b: any) => {
return b.allCategories.includes("factions");
},
primary: true,
role: "secondary",
},
{
name: "Meme",
condition: async (b: any) => {
return b.allCategories.includes("meme");
},
primary: true,
role: "secondary",
},
{
name: "Puzzle",
condition: async (b: any) => {
return b.allCategories.includes("puzzle");
},
primary: true,
role: "secondary",
},
{
name: "Box",
condition: async (b: any) => {
return b.allCategories.includes("box");
},
primary: true,
role: "secondary",
},
{
name: "Minigames",
condition: async (b: any) => {
return b.allCategories.includes("minigames");
},
primary: true,
role: "secondary",
},
{
name: "RPG",
condition: async (b: any) => {
return b.allCategories.includes("rpg");
},
primary: true,
role: "secondary",
},
{
name: "Parkour",
condition: async (b: any) => {
return b.allCategories.includes("parkour");
},
primary: true,
role: "secondary",
},
{
name: "Lifesteal",
condition: async (b: any) => {
return b.allCategories.includes("lifesteal");
},
primary: true,
role: "secondary",
},
{
name: "Prison",
condition: async (b: any) => {
return b.allCategories.includes("prison");
},
primary: true,
role: "secondary",
},
{
name: "Gens",
condition: async (b: any) => {
return b.allCategories.includes("gens");
},
primary: true,
role: "secondary",
},
{
name: "Skyblock",
condition: async (b: any) => {
return b.allCategories.includes("skyblock");
},
primary: true,
role: "secondary",
},
{
name: "Roleplay",
condition: async (b: any) => {
return b.allCategories.includes("roleplay");
},
primary: true,
role: "secondary",
},
{
name: "PvP",
condition: async (b: any) => {
return b.allCategories.includes("pvp");
},
primary: true,
role: "secondary",
},
{
name: "Modded",
condition: async (b: any) => {
return b.allCategories.includes("modded");
},
primary: true,
role: "secondary",
},
{
name: "Creative",
condition: async (b: any) => {
return b.allCategories.includes("creative");
},
primary: true,
role: "secondary",
},
];
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
if (serverCache[s.name] == undefined) {
const re = await fetch(
"https://api.minehut.com/server/" + s.name + "?byName=true",
);
const json = await re.json();
serverCache[s.name] = json.server;
return json.server;
} else {
return serverCache[s.name];
}
}

@ -0,0 +1,50 @@
import { banner } from "@/banner";
import TopBar from "@/components/clerk/Topbar";
import FavoritesView from "@/components/FavoritesView";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Server } from "lucide-react";
export default function Favorites() {
return (
<main>
{banner.isBanner && (
<div className="bg-orange-600 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
{banner.bannerText}
</div>
)}
<div
className={
"w-screen h-12 border-b fixed backdrop-blur flex " +
(banner.isBanner == true ? "mt-8" : "")
}
>
<div className="me-auto mt-3 pl-7">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="max-sm:hidden">
<Server />
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbPage>Favorites</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<TopBar />
</div>
<div className=" pt-[60px] p-4">
<FavoritesView />
</div>
</main>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because it is too large Load Diff

@ -1,22 +1,44 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { GeistSans } from "geist/font/sans";
import { Github, CodeXml } from "lucide-react";
import "./globals.css";
import { Toaster } from "react-hot-toast";
import { headers } from "next/headers";
const inter = Inter({ subsets: ["latin"] });
import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/components/ThemeProvider";
import Image from "next/image";
import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider";
import { useEffectOnce } from "@/lib/useEffectOnce";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export async function generateMetadata(): Promise<Metadata> {
return {
title: `Currently online servers | MHSF`,
description: "View all servers currently on Minehut.",
icons: "/favicon.ico",
};
}
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
<ClerkThemeProvider className={GeistSans.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TooltipProvider>
<div>{children}</div>{" "}
<Toaster position="bottom-center" reverseOrder={false} />
</TooltipProvider>
</ThemeProvider>
</ClerkThemeProvider>
);
}

28
src/app/not-found.tsx Normal file

@ -0,0 +1,28 @@
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Server } from "lucide-react";
export default function NotFound() {
return (
<div className="w-screen h-12 border-b fixed backdrop-blur flex">
<div className="me-auto mt-3 pl-7">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="max-sm:hidden">
<Server />
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbPage>Not Found</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</div>
);
}

@ -1,112 +1,69 @@
"use client";
import Image from "next/image";
import { GeistSans } from "geist/font/sans";
import ServerList from "@/components/ServerList";
import { Button } from "@/components/ui/button";
import { ModeToggle as ThemeSwitcher } from "@/components/ThemeSwitcher";
import {
SignInButton,
SignedIn,
SignedOut,
UserProfile,
useUser,
useClerk,
} from "@clerk/nextjs";
import { useEffect, useState } from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { X, UserCog, Server } from "lucide-react";
import TopBar from "@/components/clerk/Topbar";
import { banner } from "@/banner";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
<main>
{banner.isBanner && (
<div className="bg-orange-600 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
{banner.bannerText}
</div>
)}
<div
className={
"w-screen h-12 border-b fixed backdrop-blur flex " +
(banner.isBanner == true ? "mt-8" : "")
}
>
<div className="me-auto mt-3 pl-7">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="max-sm:hidden">
<Server />
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbPage>Home</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<TopBar />
</div>
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Docs{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Learn{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Templates{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Deploy{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
<div className=" pt-[60px] p-4">
<ServerList />
</div>
</main>
);

@ -0,0 +1,114 @@
import ServerView from "@/components/ServerView";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import TopBar from "@/components/clerk/Topbar";
import { Server } from "lucide-react";
import type { Metadata, ResolvingMetadata } from "next";
import { IconDisplayClient } from "@/components/IconDisplay";
import { banner } from "@/banner";
type Props = {
params: { server: string };
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise<Metadata> {
// read route params
const { server } = params;
const json = await (
await fetch("https://api.minehut.com/server/" + server + "?byName=true")
).json();
return {
title:
json.server == null
? "Server doesn't exist | MHSF"
: json.server.name +
", " +
(json.server.online
? json.server.playerCount +
(json.server.maxPlayers != 10
? "/" + json.server.maxPlayers
: "") +
" online"
: "Offline") +
" | MHSF",
description:
json.server == null
? `The server ${server} doesn't exist.`
: `View ${server} on Minehut Server Finder!`,
authors: json.server == null ? undefined : { name: json.server.owner },
applicationName: "MHSF (Minehut Server Finder)",
icons:
json.server == null
? undefined
: "https://mcapi.marveldc.me/item/" +
(json.server.icon == undefined ? "OAK_SIGN" : json.server.icon) +
"?width=64&height=64",
openGraph: {
type: "profile",
siteName: "MHSF (Minehut Server Finder)",
images: [
{
url:
"https://mcapi.marveldc.me/item/" +
json.server.icon +
"?width=64&height=64",
},
{
url: "/favicon.ico",
},
],
},
};
}
export default function ServerPage({ params }: { params: { server: string } }) {
return (
<main>
{banner.isBanner && (
<div className="bg-orange-600 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
{banner.bannerText}
</div>
)}
<div
className={
"w-screen h-12 border-b fixed backdrop-blur flex " +
(banner.isBanner == true ? "mt-8" : "")
}
>
<div className=" me-auto mt-3 pl-7">
<Breadcrumb className=" ">
<BreadcrumbList>
<BreadcrumbItem className=" max-sm:hidden">
<Server />
</BreadcrumbItem>
<BreadcrumbSeparator className=" max-sm:hidden" />
<BreadcrumbItem className=" max-sm:hidden">
Server
</BreadcrumbItem>
<BreadcrumbSeparator className=" max-sm:hidden" />
<BreadcrumbItem>
<BreadcrumbPage>{params.server}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<TopBar />
</div>
<div className=" pt-24">
<ServerView server={params.server} />
</div>
</main>
);
}

7
src/banner.tsx Normal file

@ -0,0 +1,7 @@
import { Server } from "lucide-react";
/** used when there is a outage */
export const banner = {
isBanner: false,
bannerText: <></>,
};

@ -0,0 +1,106 @@
"use client";
import { useEffect, useState } from "react";
import { Spinner } from "./ui/spinner";
import { Card, CardHeader, CardTitle } from "./ui/card";
import { ServerResponse } from "./ServerView";
import { useEffectOnce } from "@/lib/useEffectOnce";
import { Button } from "./ui/button";
import { Copy, Layers } from "lucide-react";
import toast from "react-hot-toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export default function FavoritesView() {
const [apiFavorites, setApiFavorites] = useState<any>([]);
const [loading, setLoading] = useState(true);
useEffectOnce(() => {
fetch("/api/accountLocked/getAllFavorites").then((c) => {
c.json().then((d) => {
let num = 0;
d.result.forEach((a: any, i: number) => {
fetch("https://api.minehut.com/server/" + a + "?byName=true").then(
(b) =>
b.json().then((c) => {
num++;
var apiClone = apiFavorites;
apiClone.push(c.server);
setApiFavorites(apiClone);
if (num == d.result.length) {
setLoading(false);
}
}),
);
});
});
});
});
if (loading) {
return (
<>
<Spinner className=" flex items-center" />
<br />
</>
);
}
return (
<>
<div className=" grid sm:grid-cols-4 gap-4">
{apiFavorites.map((server: ServerResponse) => (
<Card key={server.name}>
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<div>
<Button
size="icon"
variant="secondary"
className=" min-w-[128px] max-w-[328px] mb-2 h-[32px] max-md:hidden"
onClick={() => {
navigator.clipboard.writeText(server.name + ".minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
<Copy size={18} />
<code className="ml-2">{server.name}</code>
</Button>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
variant="secondary"
className=" w-[32px] h-[32px] mb-2 ml-2 max-md:hidden"
onClick={() => {
window.location.href = "/server/" + server.name;
}}
>
<Layers size={18} />
</Button>
</TooltipTrigger>
<TooltipContent>
Open up the server page to see more information about the
server
</TooltipContent>
</Tooltip>
</div>
<code className="text-[14px]">
{convert(server.joins)} total joins {" "}
{server.online ? "Online" : "Offline"}
</code>
</CardHeader>
</Card>
))}
</div>
</>
);
}
function convert(value: number) {
var result: string = "";
if (value >= 1000000) {
result = Math.floor(value / 1000000) + "m";
} else if (value >= 1000) {
result = Math.floor(value / 1000) + "k";
}
return result;
}

@ -0,0 +1,79 @@
"use client";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useState, useEffect } from "react";
export default function IconDisplay(props: { server: any }) {
return (
<Tooltip>
<TooltipTrigger>
<i
className={
props.server.icon != null
? "icon-minecraft icon-minecraft-" +
props.server.icon.replaceAll("_", "-").toLowerCase()
: "icon-minecraft icon-minecraft-oak-sign"
}
/>
</TooltipTrigger>
<TooltipContent>
<div className=" font-mono">
{props.server.icon != null
? props.server.icon.toLowerCase()
: "oak_sign"}
</div>
{props.server.icon == null && (
<div>
(this is the icon that loads in the lobby, the icon has not been
picked)
</div>
)}
</TooltipContent>
</Tooltip>
);
}
export function IconDisplayClient(props: { server: string }) {
const [icon, setIcon] = useState("");
useEffect(() => {
fetch(
"https://api.minehut.com/server/" + props.server + "?byName=true",
).then((b) => b.json().then((c) => setIcon(c.server.icon)));
}, []);
return (
<>
{icon != "" && (
<Tooltip>
<TooltipTrigger>
<i
className={
(icon != null
? "icon-minecraft icon-minecraft-" +
icon.replaceAll("_", "-").toLowerCase()
: "icon-minecraft icon-minecraft-oak-sign") +
" w-[16px] h-[16px]"
}
/>
</TooltipTrigger>
<TooltipContent>
<div className=" font-mono">
{icon != null ? icon.toLowerCase() : "oak_sign"}
</div>
{icon == null && (
<div>
(this is the icon that loads in the lobby, the icon has not been
picked)
</div>
)}
</TooltipContent>
</Tooltip>
)}
</>
);
}

@ -0,0 +1,203 @@
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuItem,
ContextMenuContent,
ContextMenuSeparator,
} from "@/components/ui/context-menu";
import toast from "react-hot-toast";
import {
CardHeader,
CardTitle,
CardDescription,
Card,
CardContent,
} from "./ui/card";
import IconDisplay from "./IconDisplay";
import { TagShower } from "./ServerList";
import { Copy, EllipsisVertical, Layers, MoveRight } from "lucide-react";
import { Button } from "./ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { useEffect, useState } from "react";
import { Tooltip } from "@radix-ui/react-tooltip";
import { TooltipContent, TooltipTrigger } from "./ui/tooltip";
export default function ServerCard({ b, motd }: any) {
return (
<ContextMenu>
<ContextMenuTrigger>
<Card key={b.name} className=" mb-4 flex items-center">
<CardHeader>
<CardTitle className=" m-0">
<IconDisplay server={b} /> {b.name}{" "}
<Drawer>
<DrawerTrigger>
<Button
size="icon"
className=" w-[24px] h-[24px] ml-2 md:hidden"
variant="secondary"
>
<EllipsisVertical size={16} />
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Actions</DrawerTitle>
</DrawerHeader>
<DrawerFooter>
<Button
variant="ghost"
onClick={() => {
navigator.clipboard.writeText(b.name + ".minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
Copy server IP
<Copy size={18} className=" ml-4" />
</Button>
<Button
variant="ghost"
onClick={() => {
window.location.href = "/server/" + b.name;
}}
>
Open server page
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
{b.author != undefined ? (
<div className=" text-sm text-muted-foreground">
by {b.author}
</div>
) : (
<br />
)}
<TagShower server={b} />
</CardTitle>
<CardDescription className=" float-left inline">
<span className=" flex items-center">
{b.playerData.playerCount == 0 ? (
<div
className=" items-center border"
style={{
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
) : (
<div
className=" items-center"
style={{
backgroundColor: "#0cce6b",
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
)}
<span className=" pl-1">
{b.playerData.playerCount}{" "}
{b.playerData.playerCount == 1 ? "player" : "players"}{" "}
currently online
</span>
</span>
<ContextMenu>
<ContextMenuTrigger>
<>
<Button
size="icon"
variant="secondary"
className=" min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => {
navigator.clipboard.writeText(b.name + ".minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
<Copy size={18} />
<code className="ml-2">{b.name}</code>
</Button>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
variant="secondary"
className=" w-[32px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => {
window.location.href = "/server/" + b.name;
}}
>
<Layers size={18} />
</Button>
</TooltipTrigger>
<TooltipContent>
Open up the server page to see more information about the server
</TooltipContent>
</Tooltip>
</>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
navigator.clipboard.writeText(b.name + ".minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
Copy server IP
<div className=" RightSlot">
<Copy size={18} />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onClick={() => {
window.location.href = "/server/" + b.name;
}}
>
Open server page
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</CardDescription>
<CardContent>
<div dangerouslySetInnerHTML={{ __html: motd }} />
</CardContent>
</CardHeader>
</Card>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
navigator.clipboard.writeText(b.name + ".minehut.gg");
toast.success("Copied IP to clipboard");
}}
>
Copy server IP
<div className=" RightSlot">
<Copy size={18} />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onClick={() => {
window.location.href = "/server/" + b.name;
}}
>
Open server page
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}

@ -0,0 +1,858 @@
"use client";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Badge } from "./ui/badge";
import ServersList from "@/lib/list";
import {
CircleUser,
Network,
Copy,
Sun,
SlidersHorizontal,
Check,
XIcon,
} from "lucide-react";
import Stat from "./Stat";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import toast from "react-hot-toast";
import { allTags, allCategories } from "@/allTags";
import IconDisplay from "./IconDisplay";
import InfiniteScroll from "react-infinite-scroll-component";
import Markdown from "react-markdown";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import remarkGfm from "remark-gfm";
import { Checkbox } from "@/components/ui/checkbox";
import { Spinner } from "./ui/spinner";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { OnlineServer, ServerResponse } from "./ServerView";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useEffectOnce } from "@/lib/useEffectOnce";
import ServerCard from "./ServerCard";
import { useHotkeys } from "react-hotkeys-hook";
export default function ServerList() {
const [loading, setLoading]: any = useState(true);
const [command, setCommand] = useState(false);
const [randomText, setRandomText] = useState("");
const [motdList, setMotdList] = useState<any>({});
const allText = [""];
const getRandomText = () => {
return allText[Math.floor(Math.random() * allText.length)];
};
const [searchRes, setSearchRes] = useState<any>(undefined);
const [templateFilter, setTemplateFilter] = useState(false);
const [random, setRandom] = useState(false);
const [serverList, setServerList] = useState(new ServersList([]));
const [textCopied, setTextCopied] = useState(false);
const bigger = async (server: OnlineServer) =>
server.playerData.playerCount > 15;
const smaller = async (server: OnlineServer) =>
!server.staticInfo.alwaysOnline &&
server.playerData.playerCount < 15 &&
server.playerData.playerCount > 7;
const [nameFilters, setNameFilters] = useState<any>({});
useHotkeys("ctrl+k", () => setCommand(true), []);
const [inErrState, setErrState] = useState(false);
const [servers, setServers] = useState<Array<OnlineServer>>([]);
const [filters, setFilters] = useState<
Array<(server: OnlineServer) => Promise<boolean>>
>([]);
const [randomData, setRandomData] = useState<OnlineServer | undefined>(
undefined,
);
useEffectOnce(() => {
setRandomText(getRandomText());
serverList
.fetchDataAndFilter()
.then(() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
setLoading(false);
});
})
.catch(() => setErrState(true));
});
if (inErrState) {
return (
<>
<div className="flex justify-center">
<XIcon />
<br />
</div>
<div className="flex justify-center">
Hmm. Something is wrong. Reload the page.
</div>
</>
);
}
if (loading) {
return (
<>
<Spinner className=" flex items-center" />
<br />
<div
className=" flex justify-center"
dangerouslySetInnerHTML={{ __html: randomText }}
></div>
</>
);
}
return (
<>
<div className=" max-lg:grid-cols-2 grid grid-cols-3 gap-4 ">
<Stat
title="Players online"
desc={serverList.getExtraData().total_players.toString()}
icon={CircleUser}
/>
<Stat
title="Servers online"
desc={serverList.getExtraData().total_servers.toString()}
icon={Network}
/>
<Stat
title="Current most popular server (in filter)"
className=" max-lg:col-span-2"
desc={
<>
{serverList.currentServers[0] != undefined
? serverList.currentServers[0].name
: "None"}{" "}
{serverList.currentServers[0] != undefined && (
<IconDisplay server={serverList.currentServers[0]} />
)}
</>
}
icon={Sun}
/>
</div>
<br />
<Separator />
<div className=" mt-3 ml-3">
<Button
onClick={() => setCommand(true)}
variant="secondary"
className=" max-lg:mb-3"
>
Search <code className="ml-2">Ctrl+K</code>
</Button>
<Popover>
<PopoverTrigger>
<Button className=" ml-3" variant="secondary">
Filter
<code className="ml-2">{filters.length}</code>
</Button>
</PopoverTrigger>
<PopoverContent className=" w-[390px] z-3">
<RadioGroup
onValueChange={(v) => {
toast.promise(
new Promise((g, b) => {
if (v == "smaller") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = true;
filt["bigger-tf"] = false;
setNameFilters(filt);
var filt2 = filters;
filt2.push(smaller);
if (filt2.includes(bigger)) {
filt2.splice(filt2.indexOf(bigger), 1);
}
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else if (v == "bigger") {
setTemplateFilter(true);
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = true;
setNameFilters(filt);
var filt2 = filters;
filt2.push(bigger);
filt2.splice(filt2.indexOf(smaller), 1);
setFilters(filt2);
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
} else {
var filt = nameFilters;
filt["smaller-tf"] = false;
filt["bigger-tf"] = false;
setNameFilters(filt);
setTemplateFilter(false);
var filt2 = filters;
filt2.splice(filt2.indexOf(smaller), 1);
filt2.splice(filt2.indexOf(bigger), 1);
setFilters(filt2);
console.log(filters, filters.includes(smaller));
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
g(undefined);
});
});
}
}),
{
error: "Error while changing filters",
loading: "Changing filters...",
success: "Changed filters!",
},
);
}}
defaultValue={(() => {
if (nameFilters["smaller-tf"]) {
return "smaller";
} else if (nameFilters["bigger-tf"]) {
return "bigger";
} else {
return "none";
}
})()}
>
<div className=" items-top flex space-x-2">
<RadioGroupItem id="smaller" value="smaller" />
<div className=" grid gap-1.5 leading-none">
<label
htmlFor="smaller"
className=" text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Only allow smaller servers
</label>
<p className=" text-sm text-muted-foreground">
Server have 15-7 players, cannot be{" "}
<Badge variant="secondary">Always Online</Badge>
</p>
</div>
</div>
<div className=" items-top flex space-x-2">
<RadioGroupItem id="bigger" value="bigger" />
<div className=" grid gap-1.5 leading-none">
<label
htmlFor="bigger"
className=" text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Only allow bigger servers
</label>
<p className=" text-sm text-muted-foreground">
Server has 16 players or more
</p>
</div>
</div>
<div className=" items-top flex space-x-2">
<RadioGroupItem id="none" value="none" />
<div className=" grid gap-1.5 leading-none">
<label
htmlFor="none"
className=" text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Custom/no requirements
</label>
</div>
</div>
</RadioGroup>
<br />
<br />
<strong className=" pb-2">Tags</strong>
<br />
{allTags.map((tag) => (
<div key={tag.docsName}>
{tag.docsName && tag.__filter == undefined && (
<div className=" items-top flex space-x-2 pb-1">
<Checkbox
disabled={templateFilter && tag.__disab != undefined}
id={tag.docsName}
checked={(() => {
return nameFilters["t-" + tag.docsName];
})()}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["t-" + tag.docsName] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(tag.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(tag.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{
server: string;
motd: string;
}> = [];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
/>
<div className=" grid gap-1.5 leading-none">
<label
htmlFor={tag.docsName}
className=" text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Badge variant={tag.role} className=" mr-1">
{tag.docsName}
</Badge>
</label>
</div>
</div>
)}
</div>
))}
<br />
<strong>Categories</strong>
<br />
{allCategories.map((categorie) => (
<div
className=" items-top flex space-x-2 pb-1"
key={categorie.name}
>
<Checkbox
id={categorie.name}
onCheckedChange={async (b) => {
var filt = nameFilters;
filt["c-" + categorie.name] = b;
setNameFilters(filt);
if (b) {
var filt2 = filters;
filt2.push(categorie.condition);
setFilters(filt2);
} else {
var filt2 = filters;
filt2.splice(filt2.indexOf(categorie.condition), 1);
setFilters(filt2);
}
serverList.editFilters(filters);
serverList.fetchDataAndFilter().then(() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> =
[];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
});
});
}}
checked={(() => {
return nameFilters["c-" + categorie.name];
})()}
/>
<div className=" grid gap-1.5 leading-none">
<label
htmlFor={categorie.name}
className=" text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Badge variant={categorie.role} className=" mr-1">
{categorie.name}
</Badge>
</label>
</div>
</div>
))}
</PopoverContent>
</Popover>
<Button
variant="secondary"
className=" md:ml-3 "
onClick={() => {
setRandomData(serverList.getRandomServer());
setRandom(true);
}}
>
Pick Random Server
</Button>
<Button
variant="secondary"
className=" ml-3"
onClick={() => {
toast.promise(
new Promise((s, e) => {
setLoading(true);
serverList
.fetchDataAndFilter()
.then(() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> =
[];
let obj: any = {};
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
setLoading(false);
s(false);
});
})
.catch(() => {
e();
});
}),
{
success: "Succesfully refreshed servers",
loading: "Refreshing...",
error: "Error while refreshing",
},
);
}}
>
Refresh
</Button>
<Dialog open={random} onOpenChange={setRandom}>
<DialogContent>
{randomData == undefined && <>No data to randomize</>}
{randomData != undefined && (
<DialogHeader>
<DialogTitle>
<IconDisplay server={randomData} /> {randomData.name}
{randomData.author != undefined ? (
<div className=" text-sm text-muted-foreground">
by {randomData.author}
</div>
) : (
<br />
)}
<TagShower server={randomData} />
</DialogTitle>
<DialogDescription className=" float-left inline">
<span className=" flex items-center">
{randomData.playerData.playerCount == 0 ? (
<div
className=" items-center border"
style={{
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
) : (
<div
className=" items-center"
style={{
backgroundColor: "#0cce6b",
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
)}
<span className=" pl-1">
{randomData.playerData.playerCount}{" "}
{randomData.playerData.playerCount == 1
? "player"
: "players"}{" "}
currently online
</span>
</span>
<br />
<strong>Server IP</strong>
<br />
<br />
<code className=" border p-3 rounded">
{randomData.name}.minehut.gg{" "}
<Button
size="icon"
className=" ml-1 h-[20px]"
onClick={() => {
setTextCopied(true);
navigator.clipboard.writeText(
randomData.name + ".minehut.gg",
);
toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000);
}}
>
{textCopied ? (
<Check size={16} className=" flex items-center" />
) : (
<p>Copy</p>
)}
</Button>
</code>
</DialogDescription>
</DialogHeader>
)}
</DialogContent>
</Dialog>
</div>
<CommandDialog open={command} onOpenChange={setCommand}>
<CommandInput
placeholder="Search for a server (offline or online)"
onValueChange={(c) => {
fetch("https://api.minehut.com/server/" + c + "?byName=true").then(
(l) => {
if (l.ok) {
console.log("found!");
l.json().then((m: any) => {
setSearchRes(m.server);
console.log(searchRes);
});
} else {
setSearchRes(undefined);
}
},
);
}}
/>
<CommandList>
<CommandEmpty>
No results found. (Minehut deleted legacy servers)
</CommandEmpty>
{searchRes == undefined ? (
""
) : (
<CommandGroup heading="Search Results">
<CommandItem
onSelect={() => {
window.location.replace("/server/" + searchRes.name);
}}
>
<div className="block">
<span className="font-medium">{searchRes.name}</span> <br />
<code className="text-gray-500 text-[14px]">
{searchRes.joins} total joins {" "}
{searchRes.online ? "Online" : "Offline"}
</code>
</div>
</CommandItem>
</CommandGroup>
)}
<CommandGroup heading="Popular Servers">
{serverList.currentServers.map((b: OnlineServer) => (
<CommandItem
key={b.name}
onSelect={() => {
window.location.replace("/server/" + b.name);
}}
>
<div className="block">
<span className="font-medium">{b.name}</span> <br />
<code className="text-gray-500 text-[14px]">
<TagShower server={b} />
</code>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
<br />
<InfiniteScroll
dataLength={serverList.currentServers.length}
hasMore={serverList.hasMore}
next={() => {
serverList.moveListDown();
let stringList: Array<{ server: string; motd: string }> = [];
serverList.currentServers.forEach((b) => {
stringList.push({ motd: b.motd, server: b.name });
});
serverList.getMOTDs(stringList).then((c) => {
var updatedSL = motdList;
c.forEach((b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
});
setMotdList(updatedSL);
setServers(serverList.currentServers);
setLoading(false);
});
}}
loader={<Spinner className=" flex items-center" />}
endMessage={
<p
style={{ textAlign: "center" }}
dangerouslySetInnerHTML={{
__html: randomText + "<br /> <strong>You've seen it all</strong>",
}}
/>
}
style={{ overflow: "hidden !important", paddingLeft: 6 }}
>
<div className=" grid sm:grid-cols-4 gap-4">
{servers.map((b: any) => (
<>
<ServerCard b={b} motd={motdList[b.name]} />
</>
))}
</div>
</InfiniteScroll>
</>
);
}
export function TagShower(props: { server: OnlineServer; className?: string }) {
const [loading, setLoading] = useState(true);
const [compatiableTags, setCompatiableTags] = useState<
Array<{
name: string;
docsName?: string;
tooltip: string;
htmlDocs: string;
role:
| "default"
| "destructive"
| "outline"
| "secondary"
| "red"
| "orange"
| "yellow"
| "green"
| "lime"
| "blue"
| "teal"
| "cyan"
| "violet"
| "indigo"
| "purple"
| "fuchsia"
| "pink";
}>
>([]);
useEffectOnce(() => {
if (loading) {
allTags.forEach((tag) => {
tag.condition(props.server).then((b) => {
if (b && tag.primary) {
tag.name(props.server).then((n) => {
compatiableTags.push({
name: n,
docsName: tag.docsName,
tooltip: tag.tooltipDesc,
htmlDocs: tag.htmlDocs,
role: tag.role == undefined ? "secondary" : tag.role,
});
setLoading(false);
});
}
});
});
}
});
if (loading) {
return <></>;
}
return (
<>
{compatiableTags.map((t) => (
<>
<Dialog key={t.name}>
<DialogTrigger>
<Tooltip>
<TooltipTrigger>
<Badge variant={t.role} className={"mr-1" + props.className}>
{t.name}
</Badge>
</TooltipTrigger>
<TooltipContent>
<div className=" font-normal">
{t.tooltip}
<br />
Click the tag to learn more about it.
</div>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{'"'}
{t.docsName == undefined ? t.name : t.docsName}
{'"'} documentation
</DialogTitle>
<DialogDescription
dangerouslySetInnerHTML={{
__html: t.htmlDocs,
}}
/>
</DialogHeader>
</DialogContent>
</Dialog>
</>
))}
</>
);
}

@ -0,0 +1,353 @@
"use client";
import { useState, useEffect } from "react";
import { Spinner } from "./ui/spinner";
import IconDisplay from "./IconDisplay";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
BetterHeader,
} from "@/components/ui/card";
import { motion, AnimatePresence } from "framer-motion";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import beautify from "json-beautify";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "./ui/badge";
import Editor from "@monaco-editor/react";
import { allTags } from "@/allTags";
import ServerSingle from "@/lib/single";
import { parse, parseExpression } from "@babel/parser";
import { NEXT_ROUTER_STATE_TREE } from "next/dist/client/components/app-router-headers";
import { Button } from "./ui/button";
import * as prettierPluginBabel from "prettier/plugins/babel";
import toast from "react-hot-toast";
import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs";
import SignInPopoverButton from "./clerk/SignInPopoverButton";
import { Sparkle, Star, X } from "lucide-react";
export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server));
const [loading, setLoading] = useState(true);
const [favorited, setFavorited] = useState(false);
const [randomText, setRandomText] = useState("");
const [lastOnline, setLastOnline] = useState(0);
const [format, setFormat] = useState("");
const allText = [""];
const getRandomText = () => {
return allText[Math.floor(Math.random() * allText.length)];
};
useEffect(() => {
setRandomText(getRandomText());
single.init().then(() => {
fetch("/api/accountLocked/isFavorited", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
server: single.grabOffline()?.name,
}),
})
.then((b) => {
b.json().then((c) => {
setFavorited(c.result);
setLoading(false);
var online = single.grabOffline()?.last_online;
if (online != undefined) {
setLastOnline(online);
}
});
})
.catch(() => {
setLoading(false);
var online = single.grabOffline()?.last_online;
if (online != undefined) {
setLastOnline(online);
}
});
});
}, []);
if (loading) {
return (
<>
<Spinner className="flex items-center" />
<br />
<div
className=" flex justify-center"
dangerouslySetInnerHTML={{ __html: randomText }}
></div>
</>
);
}
return (
<>
{single.grabOnline() == undefined && (
<div className=" grid pl-4 pr-4">
<div
className=" rounded p-2"
style={{ backgroundColor: "rgba(244, 63, 94, .16)" }}
>
<strong className="flex items-center">
This server is offline <X />
</strong>
<p>
This means that the server can{"'"}t loading some resources, like
tags, icons and authors. However, all historical data still is
stored and works.
</p>
</div>
</div>
)}
<div className=" grid p-4 sm:grid-cols-3 gap-4">
<Card className=" sm:col-span-2">
<BetterHeader>
<CardTitle className=" flex items-center">
{single.grabOnline() == undefined ? (
<div
className=" items-center mr-1"
style={{
width: ".75rem",
height: ".75rem",
borderRadius: "9999px",
backgroundColor: "#ff1744",
}}
/>
) : (
<div
className=" items-center mr-1"
style={{
width: ".75rem",
height: ".75rem",
borderRadius: "9999px",
backgroundColor: "#0cce6b",
}}
/>
)}
{props.server}
</CardTitle>
<CardDescription>
{/* 1704088800000 is the Unix time (in milliseconds) of (GMT) Monday, January 1, 2024 6:00:00 AM */}
{lastOnline < 1704088800000 &&
single.grabOnline() == undefined && (
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary">
Server too old to grab data
</Badge>
</TooltipTrigger>
<TooltipContent>
This server was last online before 1/1/24 or{" "}
<code>(GMT) Monday, January 1, 2024 6:00:00 AM</code>,
<br />
meaning data like tags, authors and other information
about the server cannot be seen.
</TooltipContent>
</Tooltip>
)}
{single.getAuthor() != undefined && (
<p>by {single.getAuthor()}</p>
)}
</CardDescription>
</BetterHeader>
<CardContent>
<p>
<strong>Time:</strong>
<br />
<i>Last online</i>{" "}
<Tooltip>
<TooltipTrigger>
<code>
{timeConverter(single.grabOffline()?.last_online)}
</code>
</TooltipTrigger>
<TooltipContent>
<code>{single.grabOffline()?.last_online}</code> in Unix time
</TooltipContent>
</Tooltip>{" "}
<br />
<i>Created on</i>{" "}
<Tooltip>
<TooltipTrigger>
<code>{timeConverter(single.grabOffline()?.creation)}</code>
</TooltipTrigger>
<TooltipContent>
<code>{single.grabOffline()?.creation}</code> in Unix time
</TooltipContent>
</Tooltip>
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Favorite the server?</CardTitle>
<CardDescription>
By favoriting the server, you can see it later.{" "}
<SignedOut>
<strong>You need to sign in to favorite a server.</strong>
</SignedOut>
</CardDescription>
</CardHeader>
<CardContent>
<SignedOut>
<SignInPopoverButton />
</SignedOut>
<SignedIn>
<Button
variant="outline"
onClick={() => {
fetch("/api/accountLocked/favoriteServer", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
server: single.grabOffline()?.name,
}),
method: "POST",
}).then(() => {});
setFavorited(!favorited);
}}
>
{favorited && (
<motion.div
animate={{ color: "yellow", fill: "yellow" }}
transition={{ duration: 2 }}
>
<Star
className="mr-2"
size="16"
color="yellow"
fill="yellow"
/>
</motion.div>
)}
{!favorited && (
<motion.div
transition={{ duration: 1 }}
animate={{ color: "yellow", fill: "yellow" }}
>
<Star className="mr-2" size="16" />
</motion.div>
)}
Favorite Server
</Button>
</SignedIn>
</CardContent>
<CardFooter>
<small>
This is unlike voting. The{" "}
<i>amount of people who favorited are public</i>, but the server
doesn{"'"}t know who favorited, as Favorites are completely
anonymous.
</small>
</CardFooter>
</Card>
</div>
</>
);
}
function timeConverter(UNIX_timestamp: any) {
var a = new Date(UNIX_timestamp);
var months = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
var year = a.getFullYear();
var month = months[a.getMonth()];
var date = a.getDate();
var time = month + "/" + date + "/" + year;
return time;
}
export interface ServerResponse {
__unix?: string;
deletion?: Deletion;
_id: string;
categories: string[];
inheritedCategories: any[];
purchased_icons: string[];
backup_slots: number;
suspended: boolean;
server_version_type: string;
proxy: boolean;
connectedServers: any[];
motd: string;
visibility: boolean;
server_plan: string;
storage_node: string;
default_banner_image: string;
default_banner_tint: string;
owner: string;
name: string;
name_lower: string;
creation: number;
platform: string;
credits_per_day: number;
in_game: boolean;
using_cosmetics: boolean;
__v: number;
port: number;
last_online: number;
joins: number;
active_icon: string;
expired: boolean;
icon: string;
online: boolean;
maxPlayers: number;
playerCount: number;
rawPlan: string;
activeServerPlan: string;
}
export interface Deletion {
started: boolean;
started_at: number;
reason: string;
completed: boolean;
completed_at: number;
storage_completed: boolean;
storage_completed_at: number;
}
export interface OnlineServer {
staticInfo: StaticInfo;
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: PlayerData;
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}
export interface StaticInfo {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
}
export interface PlayerData {
playerCount: number;
timeNoPlayers: number;
}

23
src/components/Stat.tsx Normal file

@ -0,0 +1,23 @@
import { DollarSign } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function Component(props: {
title: string;
desc: string | JSX.Element;
icon: any;
className?: string;
}) {
return (
<Card className={props.className}>
<CardHeader className=" flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className=" text-sm font-medium m-0">
{props.title}
</CardTitle>
<props.icon className=" h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className=" text-2xl font-bold">{props.desc}</div>
</CardContent>
</Card>
);
}

@ -0,0 +1,17 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="mr-3">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

@ -0,0 +1,49 @@
"use client";
import { ClerkProvider } from "@clerk/nextjs";
import { useTheme } from "next-themes";
import { dark } from "@clerk/themes";
import { ThemeProvider } from "../ThemeProvider";
import { useEffect, useState } from "react";
export function ClerkThemeProvider({
children,
className,
}: {
children: JSX.Element;
className: string;
}) {
const [theme, setTheme] = useState<string | undefined>("");
return (
<ClerkProvider
appearance={{ baseTheme: theme == "dark" ? dark : undefined }}
>
<html lang="en" className={className}>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
{/** This *has* to be implemented in component form for the `useTheme` to load at the appropriate time. */}
<ThemeElement setTheme={setTheme} />
</ThemeProvider>
</body>
</html>
</ClerkProvider>
);
}
function ThemeElement({
setTheme,
}: {
setTheme: (update: string | undefined) => void;
}) {
const theme = useTheme();
useEffect(() => {
setTheme(theme.resolvedTheme);
}, [theme.resolvedTheme, setTheme]);
return <></>;
}

@ -0,0 +1,36 @@
import { useClerk, useUser } from "@clerk/nextjs";
import { Button } from "../ui/button";
import { Star, UserCog, X } from "lucide-react";
export default function LoggedInPopover() {
const clerk = useClerk();
const { user } = useUser();
return (
<div className=" grid w-full">
<strong className=" text-center">Logged in as {user?.username}</strong>
<small className=" text-center">
Make comments about servers and favorite servers. Secured by Clerk
</small>
<br />
<Button variant={"ghost"} onClick={() => clerk.openUserProfile()}>
<UserCog size={18} className=" mr-2" />
Security/Profile settings
</Button>
<Button
variant={"ghost"}
onClick={() => window.location.replace("/account/favorites")}
>
<Star size={18} className=" mr-2" /> Favorites
</Button>
<Button
variant={"ghost"}
className=" text-red-500"
onClick={() => clerk.signOut()}
>
<X size={18} className=" mr-2" />
Logout
</Button>
</div>
);
}

@ -0,0 +1,43 @@
"use client";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "../ui/button";
import { AtSign, LogIn, UserCog } from "lucide-react";
import { useState } from "react";
import { SignIn, useClerk } from "@clerk/nextjs";
export default function SignInPopoverButton({
className,
}: {
className?: string;
}) {
const clerk = useClerk();
return (
<Popover>
<PopoverTrigger asChild>
<Button className={className}>Sign In</Button>
</PopoverTrigger>
<PopoverContent className="w-full">
<div className=" grid w-[200px]">
<strong className=" text-center">Login</strong>
<small className=" text-center">
Make comments about servers and favorite servers. Secured by Clerk
</small>
<br />
<Button variant={"ghost"} onClick={() => clerk.openSignIn()}>
<LogIn size={18} className="mr-2" />
Login
</Button>
<Button variant={"ghost"} onClick={() => clerk.openSignUp()}>
<AtSign size={18} className="mr-2" />
Sign-up
</Button>
</div>
</PopoverContent>
</Popover>
);
}

@ -0,0 +1,90 @@
"use client";
import { useState, useEffect } from "react";
import { useClerk, useUser } from "@clerk/nextjs";
import { SignedOut, SignedIn, SignInButton } from "@clerk/nextjs";
import { Button } from "../ui/button";
import { ModeToggle as ThemeSwitcher } from "../ThemeSwitcher";
import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover";
import Image from "next/image";
import { UserCog, X } from "lucide-react";
import LoggedInPopover from "./LoggedInPopover";
import SignInPopoverButton from "./SignInPopoverButton";
import { version } from "@/version";
export default function TopBar() {
const [loading, setLoading] = useState(true);
const [isAuthenticating, setAuthenticating] = useState(false);
const clerk = useClerk();
const { user } = useUser();
useEffect(() => {
fetch("/api/isAuthenticating").then((b) => {
b.json().then((m) => {
setAuthenticating(m.message);
setLoading(false);
});
});
}, []);
return (
<>
<SignedOut>
<div className=" mt-1 gap-1 grid grid-cols-5">
{isAuthenticating && <SignInPopoverButton className="col-span-2" />}
{version}
<Button variant="ghost" size="icon">
<svg
viewBox="0 0 438.549 438.549"
className=" h-[1.2rem] w-[1.2rem]"
>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
</Button>
<ThemeSwitcher />
</div>
</SignedOut>
<SignedIn>
<div className=" mt-1 grid grid-cols-4">
<span className="flex items-center">{version}</span>
<Popover>
<PopoverTrigger>
{isAuthenticating && !loading && (
<Button size="icon" variant="ghost">
<Image
alt="Clerk Image"
src={
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"
: user?.imageUrl
}
width={32}
height={32}
/>
</Button>
)}
</PopoverTrigger>
<PopoverContent>
<LoggedInPopover />
</PopoverContent>
</Popover>
<Button variant="ghost" size="icon">
<svg
viewBox="0 0 438.549 438.549"
className=" h-[1.2rem] w-[1.2rem]"
>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
</Button>
<ThemeSwitcher />
</div>
</SignedIn>
</>
);
}

@ -0,0 +1,58 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
// All custom colors in MHSF
red: "border-transparent bg-red-300 text-primary-foreground hover:bg-red-300/80",
orange:
"border-transparent bg-orange-300 text-primary-foreground hover:bg-orange-300/80",
yellow:
"border-transparent bg-yellow-300 text-primary-foreground hover:bg-yellow-300/80",
green:
"border-transparent bg-green-300 text-primary-foreground hover:bg-green-300/80",
lime: "border-transparent bg-lime-300 text-primary-foreground hover:bg-lime-300/80",
blue: "border-transparent bg-blue-300 text-primary-foreground hover:bg-blue-300/80",
teal: "border-transparent bg-teal-300 text-primary-foreground hover:bg-teal-300/80",
cyan: "border-transparent bg-cyan-300 text-primary-foreground hover:bg-cyan-300/80",
violet:
"border-transparent bg-violet-300 text-primary-foreground hover:bg-violet-300/80",
indigo:
"border-transparent bg-indigo-300 text-primary-foreground hover:bg-indigo-300/80",
purple:
"border-transparent bg-purple-300 text-primary-foreground hover:bg-purple-300/80",
fuchsia:
"border-transparent bg-fuchsia-300 text-primary-foreground hover:bg-fuchsia-300/80",
pink: "border-transparent bg-pink-300 text-primary-foreground hover:bg-pink-300/80",
// End
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

100
src/components/ui/card.tsx Normal file

@ -0,0 +1,100 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "./button";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const BetterHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 pl-6 pt-6 pb-2", className)}
{...props}
/>
));
BetterHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
BetterHeader,
};

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

@ -0,0 +1,122 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 backdrop-blur",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

@ -0,0 +1,45 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

@ -0,0 +1,50 @@
import React from "react";
import { cn } from "@/lib/utils";
import { VariantProps, cva } from "class-variance-authority";
import { Loader2 } from "lucide-react";
const spinnerVariants = cva("flex-col items-center justify-center", {
variants: {
show: {
true: "flex",
false: "hidden",
},
},
defaultVariants: {
show: true,
},
});
const loaderVariants = cva("animate-spin text-primary", {
variants: {
size: {
small: "size-6",
medium: "size-8",
large: "size-12",
},
},
defaultVariants: {
size: "medium",
},
});
interface SpinnerContentProps
extends VariantProps<typeof spinnerVariants>,
VariantProps<typeof loaderVariants> {
className?: string;
children?: React.ReactNode;
}
export function Spinner({
size,
show,
children,
className,
}: SpinnerContentProps) {
return (
<span className={spinnerVariants({ show })}>
<Loader2 className={cn(loaderVariants({ size }), className)} />
{children}
</span>
);
}

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

132
src/lib/list.ts Normal file

@ -0,0 +1,132 @@
import { OnlineServer } from "@/components/ServerView";
import toast from "react-hot-toast";
var numberOfItemsInView = 20;
export default class ServersList {
private 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 }>) {
let response = await fetch("/api/getMOTD", {
body: JSON.stringify({ motd: list }),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
let json = await response.json();
return json.result;
}
}
function arrayEmpty(a: Array<any>) {
return a.length == 0;
}

205
src/lib/miniMessage2HTML.ts Normal file

@ -0,0 +1,205 @@
import { twi } from "tw-to-css";
const divList: any = {
black: "000000",
dark_blue: "002bff",
dark_green: "text-green-800",
dark_red: "text-red-800",
dark_purple: "text-purple-800",
gold: "text-yellow-400",
gray: "text-gray-600",
dark_gray: "text-gray-800",
blue: "text-blue-400",
green: "text-green-400",
aqua: "text-cyan-400",
red: "text-red-400",
light_purple: "text-purple-300",
yellow: "text-yellow-400",
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("<div>" + allHTML + "</div>");
}
});
if (!j.ok) {
b("Problem while parsing MiniMessage");
}
});
});
}
function objToHTML(i: Element): string {
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 == "undefined") return "<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-")) {
if (classUnique.startsWith("text-[")) {
result +=
"color: " + classUnique.substring(6, classUnique.length - 1) + "; ";
}
}
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;
}

80
src/lib/single.ts Normal file

@ -0,0 +1,80 @@
import { OnlineServer, ServerResponse } from "@/components/ServerView";
import toast from "react-hot-toast";
export default class ServerSingle {
private name = "";
private onlineObj: OnlineServer | undefined = undefined;
private offlineObj: ServerResponse | undefined = undefined;
online = false;
constructor(name: string) {
this.name = name;
}
init(): 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) {
fetch("https://api.minehut.com/servers").then((l) =>
l.json().then((o) => {
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;
}
}

35
src/lib/useEffectOnce.ts Normal file

@ -0,0 +1,35 @@
"use client";
import { useEffect, useRef, useState } from "react";
export const useEffectOnce = (effect: () => void | (() => void)) => {
const destroyFunc = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const [val, setVal] = useState<number>(0);
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffect(() => {
// only execute the effect first time around
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}
// this forces one render after the effect is run
setVal((val) => val + 1);
return () => {
// if the comp didn't render since the useEffect was called,
// we know it's the dummy React cycle
if (!renderAfterCalled.current) {
return;
}
if (destroyFunc.current) {
destroyFunc.current();
}
};
}, []);
};

6
src/lib/utils.ts Normal file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

14
src/middleware.ts Normal file

@ -0,0 +1,14 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextRequest } from "next/server";
const isProtectedRoute = createRouteMatcher(["/account(.*)"]);
export default process.env.IS_AUTH == "true"
? clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
})
: (request: NextRequest) => {};
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

@ -0,0 +1 @@

@ -0,0 +1,66 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
import { decreaseNum, increaseNum } from "./getCommunityNum";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = checkForInfoOrLeave(res, req.body.server);
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) {
collection.insertOne({ user: userId, favorites: [server] });
await increaseNum(client, server);
res.send({ message: "Favorited " + server });
} else {
const collect = find[0];
let existingFavorites: Array<string> = collect.favorites;
console.log(collect);
if (existingFavorites.includes(server)) {
// remove that favorite from the list
const index = existingFavorites.indexOf(server);
await decreaseNum(client, server);
if (index > -1) {
existingFavorites.splice(index, 1);
}
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
},
);
res.send({ message: "Unfavorited " + server });
} else {
existingFavorites.push(server);
await increaseNum(client, server);
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
},
);
res.send({ message: "Favorited " + server });
}
}
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -0,0 +1,33 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) {
res.send({ result: [] });
} else {
res.send({ result: find[0].favorites });
}
client.close();
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -0,0 +1,70 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = checkForInfoOrLeave(res, req.body.server);
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length != 0) {
const entry = find[0];
res.send({ result: entry.favorites });
} else {
res.send({ result: 0 });
}
client.close();
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}
export async function increaseNum(client: MongoClient, server: string) {
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length == 0) {
collection.insertOne({ server: server, favorites: 1 });
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites + 1 },
);
}
}
export async function decreaseNum(client: MongoClient, server: string) {
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length == 0) {
return;
// Physically is impossible
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites - 1 },
);
}
}

@ -0,0 +1,32 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = checkForInfoOrLeave(res, req.body.server);
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) res.send({ result: false });
else {
res.send({ result: find[0].favorites.includes(server) });
}
client.close();
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

36
src/pages/api/getMOTD.ts Normal file

@ -0,0 +1,36 @@
import { NextApiRequest, NextApiResponse } from "next";
import parseToHTML from "@/lib/miniMessage2HTML";
let num = 0;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
num++;
var body: Array<{ server: string; motd: string }> = req.body.motd;
var list: Array<{ server: string; motd: string }> = [];
var yes = 0;
if (body != undefined && body.forEach != undefined) {
body.forEach((c, i) => {
parseToHTML(c.motd)
.then((m) => {
yes++;
list.push({ motd: m, server: c.server });
if (yes == body.length) {
res.send({ result: list });
}
})
.catch(() => {
list.push({ motd: "Error to grab MOTD", server: c.server });
if (i == body.length - 1) {
res.send({ result: list });
}
});
});
} else {
res
.status(400)
.send({ mes: "Wrong structure.. you might be using the legacy MOTD." });
}
}

@ -0,0 +1,8 @@
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.send({ message: process.env.IS_AUTH == "true" });
}

8
src/types/index.d.ts vendored Normal file

@ -0,0 +1,8 @@
declare module "json-beautify" {
export default function beautify(
value: any,
replacer: Function | object | any[] | null,
space: number | string,
limit?: number,
): string;
}

1
src/version.ts Normal file

@ -0,0 +1 @@
export const version = "b-0.21";

@ -1,20 +1,80 @@
import type { Config } from "tailwindcss";
const config: Config = {
const config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx,json}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
};
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
} satisfies Config;
export default config;

4133
yarn.lock

File diff suppressed because it is too large Load Diff