mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-07-07 01:11:03 -05:00
feat: inital commit
This commit is contained in:
parent
870f3ec7a1
commit
9aeaebde54
@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["next/core-web-vitals"]
|
||||
}
|
||||
|
||||
44
README.md
44
README.md
@ -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
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"
|
||||
}
|
||||
}
|
||||
3513
css-obfuscator/conversion.json
Normal file
3513
css-obfuscator/conversion.json
Normal file
File diff suppressed because it is too large
Load Diff
6
next-css-obfuscator.config.ts
Normal file
6
next-css-obfuscator.config.ts
Normal file
@ -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
8027
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@ -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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 B |
0
public/itemsCSS/blockEntity.css
Normal file
0
public/itemsCSS/blockEntity.css
Normal file
BIN
public/itemsCSS/icons.png
Normal file
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
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];
|
||||
}
|
||||
}
|
||||
50
src/app/account/favorites/page.tsx
Normal file
50
src/app/account/favorites/page.tsx
Normal file
@ -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 |
9016
src/app/globals.css
9016
src/app/globals.css
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
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>
|
||||
);
|
||||
}
|
||||
163
src/app/page.tsx
163
src/app/page.tsx
@ -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
|
||||
<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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Learn about Next.js in an interactive course with 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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</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>
|
||||
);
|
||||
|
||||
114
src/app/server/[server]/page.tsx
Normal file
114
src/app/server/[server]/page.tsx
Normal file
@ -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
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: <></>,
|
||||
};
|
||||
106
src/components/FavoritesView.tsx
Normal file
106
src/components/FavoritesView.tsx
Normal file
@ -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;
|
||||
}
|
||||
79
src/components/IconDisplay.tsx
Normal file
79
src/components/IconDisplay.tsx
Normal file
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
203
src/components/ServerCard.tsx
Normal file
203
src/components/ServerCard.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
858
src/components/ServerList.tsx
Normal file
858
src/components/ServerList.tsx
Normal file
@ -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>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
353
src/components/ServerView.tsx
Normal file
353
src/components/ServerView.tsx
Normal file
@ -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
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>
|
||||
);
|
||||
}
|
||||
17
src/components/ThemeProvider.tsx
Normal file
17
src/components/ThemeProvider.tsx
Normal file
@ -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>;
|
||||
}
|
||||
40
src/components/ThemeSwitcher.tsx
Normal file
40
src/components/ThemeSwitcher.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
49
src/components/clerk/ClerkThemeProvider.tsx
Normal file
49
src/components/clerk/ClerkThemeProvider.tsx
Normal file
@ -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 <></>;
|
||||
}
|
||||
36
src/components/clerk/LoggedInPopover.tsx
Normal file
36
src/components/clerk/LoggedInPopover.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
43
src/components/clerk/SignInPopoverButton.tsx
Normal file
43
src/components/clerk/SignInPopoverButton.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
90
src/components/clerk/Topbar.tsx
Normal file
90
src/components/clerk/Topbar.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
src/components/ui/badge.tsx
Normal file
58
src/components/ui/badge.tsx
Normal file
@ -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 };
|
||||
115
src/components/ui/breadcrumb.tsx
Normal file
115
src/components/ui/breadcrumb.tsx
Normal file
@ -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,
|
||||
}
|
||||
56
src/components/ui/button.tsx
Normal file
56
src/components/ui/button.tsx
Normal file
@ -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
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,
|
||||
};
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@ -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 }
|
||||
155
src/components/ui/command.tsx
Normal file
155
src/components/ui/command.tsx
Normal file
@ -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,
|
||||
}
|
||||
200
src/components/ui/context-menu.tsx
Normal file
200
src/components/ui/context-menu.tsx
Normal file
@ -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,
|
||||
}
|
||||
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal file
@ -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,
|
||||
};
|
||||
118
src/components/ui/drawer.tsx
Normal file
118
src/components/ui/drawer.tsx
Normal file
@ -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,
|
||||
}
|
||||
200
src/components/ui/dropdown-menu.tsx
Normal file
200
src/components/ui/dropdown-menu.tsx
Normal file
@ -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,
|
||||
}
|
||||
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
@ -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 }
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -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 }
|
||||
129
src/components/ui/navigation-menu.tsx
Normal file
129
src/components/ui/navigation-menu.tsx
Normal file
@ -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,
|
||||
};
|
||||
31
src/components/ui/popover.tsx
Normal file
31
src/components/ui/popover.tsx
Normal file
@ -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 }
|
||||
44
src/components/ui/radio-group.tsx
Normal file
44
src/components/ui/radio-group.tsx
Normal file
@ -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 }
|
||||
45
src/components/ui/resizable.tsx
Normal file
45
src/components/ui/resizable.tsx
Normal file
@ -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 }
|
||||
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal file
@ -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 }
|
||||
50
src/components/ui/spinner.tsx
Normal file
50
src/components/ui/spinner.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal file
@ -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
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
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
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
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
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
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)(.*)"],
|
||||
};
|
||||
1
src/page/api/uuidToUser.ts
Normal file
1
src/page/api/uuidToUser.ts
Normal file
@ -0,0 +1 @@
|
||||
|
||||
66
src/pages/api/accountLocked/favoriteServer.ts
Normal file
66
src/pages/api/accountLocked/favoriteServer.ts
Normal file
@ -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;
|
||||
}
|
||||
33
src/pages/api/accountLocked/getAllFavorites.ts
Normal file
33
src/pages/api/accountLocked/getAllFavorites.ts
Normal file
@ -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;
|
||||
}
|
||||
70
src/pages/api/accountLocked/getCommunityNum.ts
Normal file
70
src/pages/api/accountLocked/getCommunityNum.ts
Normal file
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/pages/api/accountLocked/isFavorited.ts
Normal file
32
src/pages/api/accountLocked/isFavorited.ts
Normal file
@ -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
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." });
|
||||
}
|
||||
}
|
||||
8
src/pages/api/isAuthenticating.ts
Normal file
8
src/pages/api/isAuthenticating.ts
Normal file
@ -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
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
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user