This commit is contained in:
dvelo 2024-08-22 23:44:00 -05:00
parent 70687b3f35
commit 9be70112dc
20 changed files with 580 additions and 31 deletions

4
.gitignore vendored

@ -7,6 +7,10 @@
.yarn/install-state.gz .yarn/install-state.gz
.turbo .turbo
# cron
/cron/dist
/cron/node_modules
# testing # testing
/coverage /coverage

25
cron/README.md Normal file

@ -0,0 +1,25 @@
# MHSF Cron Tasks
In version 1.0, MHSF moved from using Inngest to collect statistics to a self-hosted `crontab` Node.js script.
## Why the move?
When running Inngest, on Vercel's servers, when doing the `/servers` Minehut API endpoint to grab the currently online servers, a Cloudflare pop-up appeared. This made it so the JSON data expected, was blocked. This appeared to only run on Vercel's servers, and the only real solution (without spending a lot of money) was to run a minimal script every 30 minutes to grab the server data.
## How do you run this?
If you're on a Unix based machine, just type the following:
```
# Make sure you already cloned the repo and are in the /cron directory.
# This project uses NPM instead of Yarn for the website
npm install
crontab -e
```
and in `vi` go into insert mode (type `i`) and type the following:
```
*/30 * * * * cd "<INSERT_REPO_DIR_HERE>/cron/" && npm start
```

175
cron/package-lock.json generated Normal file

@ -0,0 +1,175 @@
{
"name": "cron",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cron",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"chalk": "^5.3.0",
"dotenv": "^16.4.5",
"mongodb": "^6.8.0"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz",
"integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/bson": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
"integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
"engines": {
"node": ">=16.20.1"
}
},
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"node_modules/mongodb": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz",
"integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
"integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^13.0.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/tr46": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
"dependencies": {
"punycode": "^2.3.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
"dependencies": {
"tr46": "^4.1.1",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=16"
}
}
}
}

17
cron/package.json Normal file

@ -0,0 +1,17 @@
{
"name": "cron",
"version": "1.0.0",
"description": "In version 1.0, MHSF moved from using Inngest to collect statistics to a self-hosted `crontab` Node.js script.",
"main": "dist/index.js",
"scripts": {
"start": "tsc --build && node ./dist/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^5.3.0",
"dotenv": "^16.4.5",
"mongodb": "^6.8.0"
},
"type": "module"
}

123
cron/src/index.ts Normal file

@ -0,0 +1,123 @@
// MHSF crontab scripts
// by dvelo - licensed under MIT license
import chalk from "chalk";
console.log(chalk.yellow(chalk.bold("MHSF crontab scripts")));
console.log(chalk.yellow(chalk.bold("by dvelo - licensed under MIT license")));
console.log();
import { MongoClient } from "mongodb";
import { config } from "dotenv";
// set-up config
config({ path: "../.env.local" });
const mongo = new MongoClient(process.env.MONGO_DB as string);
main().catch((e) => {
console.log(chalk.red("[CRON] " + ERROR + " Error while running: "));
console.error(e);
});
const SUCCESS = chalk.green("SUCCESS");
const ERROR = chalk.red("ERROR");
const WARN = chalk.red("WARN");
const INFO = chalk.blueBright("INFO");
/**
* Main function that runs the script.
*
* @remarks
* Connects to the MongoDB instance, fetches the server data from the Minehut API, and inserts the total player and server count into the "mh" collection.
* Then, it iterates over each server and inserts the player count, server name, and date into the "history" collection.
* If an error occurs, it logs the error and closes the MongoDB connection.
*/
async function main() {
await mongo.connect();
try {
// No more mumbo jumbo
const mh = await (
await fetch("https://api.minehut.com/servers", {
headers: {
accept: "application/json",
"accept-language": Math.random().toString(),
priority: "u=1, i",
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"Content-Type": "application/json",
Referer: "http://localhost:3000/",
"Referrer-Policy": "strict-origin-when-cross-origin",
},
body: null,
method: "GET",
})
).json();
console.log("[CRON] " + SUCCESS + " Found", mh.servers.length, "servers");
const mha = mongo.db("mhsf").collection("mh");
const meta = mongo.db("mhsf").collection("meta");
const dbl = mongo.db("mhsf").collection("history");
await mha.insertOne({
total_players: mh.total_players,
total_servers: mh.total_servers,
date: new Date(),
});
let y = 0;
mh.servers.forEach(async (server: any, i: number) => {
const serverFavoritesObject = await meta.findOne({
server: server.name,
});
let favorites = 0;
if (serverFavoritesObject != undefined)
favorites = serverFavoritesObject.favorites;
await dbl.insertOne({
player_count: server.playerData.playerCount,
favorites,
server: server.name,
date: new Date(),
});
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(
"[CRON] " +
INFO +
" Remaining servers: " +
(y + "/" + mh.servers.length)
);
y++;
if (y == mh.servers.length) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(
"[CRON] " + SUCCESS + " Finished! Closing MongoDB connection."
);
// Close connection
await mongo
.close()
.catch((e) =>
console.log(
"[CRON] " + WARN + " Error while closing MongoDB connection:",
e
)
);
return;
}
});
} catch (e) {
await mongo.close();
console.log("[CRON] " + ERROR + " Error while parsing JSON:", e);
return;
}
}

12
cron/tsconfig.json Normal file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES6",
"module": "NodeNext",
"outDir": "dist"
},
"include": ["src/**/*"]
}

@ -16,6 +16,7 @@
"@babel/parser": "^7.24.7", "@babel/parser": "^7.24.7",
"@clerk/nextjs": "^5.1.3", "@clerk/nextjs": "^5.1.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-menubar": "^1.1.1", "@radix-ui/react-menubar": "^1.1.1",
"@unocss/eslint-plugin": "^0.61.5", "@unocss/eslint-plugin": "^0.61.5",

@ -60,7 +60,15 @@ export default function Settings() {
<br /> <br />
<strong className="font-bold">Link Account</strong> <strong className="font-bold">Link Account</strong>
<div className="flex items-center"> <div className="flex items-center">
<p>Link a Minecraft account to customize a server you own.</p> <p>
Link a Minecraft account to customize a server you own.
<br />{" "}
{user?.publicMetadata.player != undefined && linked && (
<>
Currently linked to {user?.publicMetadata.player as string}
</>
)}
</p>
<Dialog> <Dialog>
<DialogTrigger> <DialogTrigger>

@ -100,6 +100,12 @@
backdrop-filter: blur(8px) !important; backdrop-filter: blur(8px) !important;
} }
body {
scrollbar-width: 10px;
scrollbar-color: #888;
scrollbar-gutter: transparent;
}
/** Cool scrollbar */ /** Cool scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;

@ -21,6 +21,7 @@ import { CommandBarer } from "@/components/CommandBar";
import ThemedToaster from "@/components/misc/ThemedToaster"; import ThemedToaster from "@/components/misc/ThemedToaster";
import UnofficalDialog from "@/components/misc/UnofficalDialog"; import UnofficalDialog from "@/components/misc/UnofficalDialog";
import ClientFadeIn from "@/components/ClientFadeIn"; import ClientFadeIn from "@/components/ClientFadeIn";
import toast from "react-hot-toast";
const inter = interFont({ variable: "--font-inter", subsets: ["latin"] }); const inter = interFont({ variable: "--font-inter", subsets: ["latin"] });
export default async function RootLayout({ export default async function RootLayout({

@ -15,7 +15,14 @@ import {
} from "./ui/card"; } from "./ui/card";
import IconDisplay from "./IconDisplay"; import IconDisplay from "./IconDisplay";
import { TagShower } from "./ServerList"; import { TagShower } from "./ServerList";
import { Copy, EllipsisVertical, Layers, Star } from "lucide-react"; import {
ArrowRight,
ChartArea,
Copy,
EllipsisVertical,
Layers,
Star,
} from "lucide-react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
Drawer, Drawer,
@ -33,9 +40,16 @@ import { useState } from "react";
import { favoriteServer, isFavorited } from "@/lib/api"; import { favoriteServer, isFavorited } from "@/lib/api";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import useClipboard from "@/lib/useClipboard";
export default function ServerCard({ b, motd, mini, favs }: any) { export default function ServerCard({ b, motd, mini, favs }: any) {
const router = useRouter(); const router = useRouter();
const clipboard = useClipboard();
const [favoriteStar, setFavoriteStar] = useState(false); const [favoriteStar, setFavoriteStar] = useState(false);
const [favoriteLoading, setFavoriteLoading] = useState(true); const [favoriteLoading, setFavoriteLoading] = useState(true);
const { isSignedIn } = useUser(); const { isSignedIn } = useUser();
@ -61,7 +75,51 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
> >
<CardHeader> <CardHeader>
<CardTitle className="m-0"> <CardTitle className="m-0">
<IconDisplay server={b} /> {b.name}{" "} <span>
<IconDisplay server={b} />
<HoverCard>
<HoverCardTrigger asChild>
<Link href={"/server/" + b.name}>
<Button
variant={"link"}
className="text-2xl px-0 pl-1 font-semibold"
>
{b.name}
</Button>
</Link>
</HoverCardTrigger>
<HoverCardContent className="w-80 font-normal tracking-normal">
<div className="flex justify-between space-x-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold">{b.name}</h4>
<p className="text-sm">
{motd && (
<span
dangerouslySetInnerHTML={{ __html: motd }}
className="w-[30px] text-center break-all overflow-hidden"
/>
)}
</p>
<div className="flex items-center pt-2">
<span className="text-xs text-muted-foreground flex items-center">
<ArrowRight size={16} className="mr-2" />
Open Server Page
</span>
</div>
<div className="flex items-center pt-2">
<span className="text-xs text-muted-foreground flex items-center">
<ChartArea size={16} className="mr-2" />
Running on{" "}
{b.staticInfo.serverPlan == undefined
? "Free Plan"
: b.staticInfo.serverPlan}
</span>
</div>
</div>
</div>
</HoverCardContent>
</HoverCard>
</span>
<Drawer> <Drawer>
<DrawerTrigger> <DrawerTrigger>
<Button <Button
@ -80,9 +138,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
navigator.clipboard.writeText( clipboard.writeText(b.name + ".mshf.minehut.gg");
b.name + ".mshf.minehut.gg"
);
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >
@ -101,7 +157,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
{b.author != undefined ? ( {b.author != undefined ? (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground font-normal tracking-normal">
by {b.author} by {b.author}
</div> </div>
) : ( ) : (
@ -147,9 +203,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
variant="secondary" variant="secondary"
className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden" className="min-w-[128px] max-w-[328px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => { onClick={() => {
navigator.clipboard.writeText( clipboard.writeText(b.name + ".mshf.minehut.gg");
b.name + ".mshf.minehut.gg"
);
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >
@ -178,9 +232,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
navigator.clipboard.writeText( clipboard.writeText(b.name + ".mshf.minehut.gg");
b.name + ".mshf.minehut.gg"
);
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >
@ -210,7 +262,7 @@ export default function ServerCard({ b, motd, mini, favs }: any) {
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
navigator.clipboard.writeText(b.name + ".mshf.minehut.gg"); clipboard.writeText(b.name + ".mshf.minehut.gg");
toast.success("Copied IP to clipboard"); toast.success("Copied IP to clipboard");
}} }}
> >

@ -48,6 +48,7 @@ import {
} from "@/components/ui/menubar"; } from "@/components/ui/menubar";
import ClientFadeIn from "./ClientFadeIn"; import ClientFadeIn from "./ClientFadeIn";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import useClipboard from "@/lib/useClipboard";
export default function ServerList() { export default function ServerList() {
const [loading, setLoading]: any = useState(true); const [loading, setLoading]: any = useState(true);
@ -70,6 +71,7 @@ export default function ServerList() {
const [nameFilters, setNameFilters] = useState<any>({}); const [nameFilters, setNameFilters] = useState<any>({});
const [inErrState, setErrState] = useState(false); const [inErrState, setErrState] = useState(false);
const [servers, setServers] = useState<Array<OnlineServer>>([]); const [servers, setServers] = useState<Array<OnlineServer>>([]);
const clipboard = useClipboard();
const router = useRouter(); const router = useRouter();
const [ipr, setIPR] = useState("4"); const [ipr, setIPR] = useState("4");
const [filters, setFilters] = useState< const [filters, setFilters] = useState<
@ -121,7 +123,7 @@ export default function ServerList() {
if (loading) { if (loading) {
return ( return (
<> <>
<div className="grid grid-cols-3 gap-4 max-lg:grid-cols-2"> <div className="md:grid md:grid-cols-3 gap-4 max-lg:grid-cols-2">
<Skeleton className="h-[112px] rounded-xl" /> <Skeleton className="h-[112px] rounded-xl" />
<Skeleton className="h-[112px] rounded-xl" /> <Skeleton className="h-[112px] rounded-xl" />
<Skeleton className="h-[112px] rounded-xl" /> <Skeleton className="h-[112px] rounded-xl" />
@ -129,7 +131,7 @@ export default function ServerList() {
<br /> <br />
<Separator /> <Separator />
<br /> <br />
<div className="grid grid-cols-4 gap-4"> <div className="md:grid md:grid-cols-4 gap-4">
<Skeleton className="h-[450px] rounded-xl" /> <Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" /> <Skeleton className="h-[450px] rounded-xl" />
<Skeleton className="h-[450px] rounded-xl" /> <Skeleton className="h-[450px] rounded-xl" />
@ -663,9 +665,7 @@ export default function ServerList() {
className="ml-1 h-[20px]" className="ml-1 h-[20px]"
onClick={() => { onClick={() => {
setTextCopied(true); setTextCopied(true);
navigator.clipboard.writeText( clipboard.writeText(randomData.name + ".mshf.minehut.gg");
randomData.name + ".mshf.minehut.gg"
);
toast.success("Copied!"); toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000); setTimeout(() => setTextCopied(false), 1000);
}} }}
@ -716,7 +716,11 @@ export default function ServerList() {
style={{ overflow: "hidden !important", paddingLeft: 6 }} style={{ overflow: "hidden !important", paddingLeft: 6 }}
> >
<ClientFadeIn delay={200}> <ClientFadeIn delay={200}>
<div className={" sm:grid " + "sm:grid-cols-" + ipr + " gap-4"}> <div
className={
" sm:grid " + "lg:grid-cols-" + ipr + " gap-4 sm:grid-cols-2"
}
>
{servers.map((b: any) => ( {servers.map((b: any) => (
<> <>
<ServerCard b={b} motd={motdList[b.name]} /> <ServerCard b={b} motd={motdList[b.name]} />
@ -788,7 +792,7 @@ export function TagShower(props: {
} }
return ( return (
<> <div className="font-normal tracking-normal">
{compatiableTags.map((t) => ( {compatiableTags.map((t) => (
<> <>
{props.unclickable && ( {props.unclickable && (
@ -832,6 +836,6 @@ export function TagShower(props: {
)} )}
</> </>
))} ))}
</> </div>
); );
} }

@ -11,7 +11,7 @@ export default function LoggedInPopover() {
return ( return (
<div className="grid w-full"> <div className="grid w-full">
<strong className="text-center">Logged in as {user?.username}</strong> <strong className="text-center">Logged in as {user?.username}</strong>
<small className="text-center"> <small className="text-center pb-6">
Make comments about servers and favorite servers. Secured by Clerk Make comments about servers and favorite servers. Secured by Clerk
</small> </small>
<br /> <br />

@ -33,7 +33,7 @@ export default function SignInPopoverButton({
<PopoverContent className="w-full"> <PopoverContent className="w-full">
<div className=" grid w-[200px]"> <div className=" grid w-[200px]">
<strong className="text-center">Login</strong> <strong className="text-center">Login</strong>
<small className="text-center"> <small className="text-center pb-6">
Make comments about servers and favorite servers. Secured by Clerk Make comments about servers and favorite servers. Secured by Clerk
</small> </small>
<br /> <br />

@ -19,7 +19,7 @@ export function ShowInfo() {
{open == true && ( {open == true && (
<> <>
<p> <p>
By claiming your account, you can add markdown descriptions and{" "} By claiming your account, you can add Markdown descriptions and{" "}
custom color schemes to your server (and more), making it stand out. custom color schemes to your server (and more), making it stand out.
To get started, join the server below on your Minecraft account. To get started, join the server below on your Minecraft account.
Enter the code in chat in the website, and you will link your Enter the code in chat in the website, and you will link your

@ -3,9 +3,11 @@ import { useState } from "react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import useClipboard from "@/lib/useClipboard";
export function TextCopyComp() { export function TextCopyComp() {
"use client"; "use client";
const clipboard = useClipboard();
const [textCopied, setTextCopied] = useState(false); const [textCopied, setTextCopied] = useState(false);
return ( return (
@ -16,7 +18,7 @@ export function TextCopyComp() {
className="ml-1 h-[20px]" className="ml-1 h-[20px]"
onClick={() => { onClick={() => {
setTextCopied(true); setTextCopied(true);
navigator.clipboard.writeText("MHSFPV.minehut.gg"); clipboard.writeText("MHSFPV.minehut.gg");
toast.success("Copied!"); toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000); setTimeout(() => setTextCopied(false), 1000);
}} }}

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 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}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

21
src/lib/useClipboard.ts Normal file

@ -0,0 +1,21 @@
import toast from "react-hot-toast"
/** A hook to properly write text to the clipboard without triggering a client-side error
* @version 1.0
*/
export default function useClipboard() {
const writeText = (text: string) => {
if (navigator.clipboard == undefined)
return toast.error("Clipboard doesn't exist");
navigator.clipboard.writeText(text);
}
const write = (text: ClipboardItems) => {
if (navigator.clipboard == undefined)
return toast.error("Clipboard doesn't exist")
navigator.clipboard.write(text);
}
return { writeText, write };
}

@ -1,13 +1,47 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Separator } from "./components/ui/separator";
import { Button } from "./components/ui/button";
import Confetti, { ConfettiButton } from "./components/effects/confetti";
export const version = "b-0.10.7"; export const version = "1.0";
const User = ({ user }: { user: string }) => ( const User = ({ user }: { user: string }) => (
<span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]"> <span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]">
{user} {user}
</span> </span>
); );
import confetti from "canvas-confetti";
const handleClick = () => {
const duration = 5 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
const randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min;
const interval = window.setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
confetti({
...defaults,
particleCount,
zIndex: 60,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
});
confetti({
...defaults,
particleCount,
zIndex: 60,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
});
}, 250);
};
export const Changelog = () => ( export const Changelog = () => (
<> <>
@ -43,6 +77,26 @@ export const Changelog = () => (
`| ${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE.substring(0, 24)}`} `| ${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE.substring(0, 24)}`}
</code> </code>
</div> </div>
<br />
<div>
<strong className="flex items-center">
Version 1.0.0 (August 22nd 2024)
</strong>
<ul>
<li>
1.0!{" "}
<Button className="h-[25px] w-[50px] ml-2" onClick={handleClick}>
woah!
</Button>
</li>
<li> New hover card on server title hover</li>
<li> Moving to self-hosted cron jobs</li>
<li> Fixing some mobile issues</li>
</ul>
</div>
<br />
<Separator />
<br /> <br />
<div> <div>
<strong className="flex items-center"> <strong className="flex items-center">

@ -740,6 +740,21 @@
"@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-hover-card@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.1.tgz#2982a5a91c7ae5a98e0cacd845fbdfbfdcdab355"
integrity sha512-IwzAOP97hQpDADYVKrEEHUH/b2LA+9MgB0LgdmnbFO2u/3M5hmEofjjr2M6CyzUblaAqJdFm6B7oFtU72DPXrA==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-icons@^1.3.0": "@radix-ui/react-icons@^1.3.0":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
@ -3028,9 +3043,9 @@ foreground-child@^3.1.0:
signal-exit "^4.0.1" signal-exit "^4.0.1"
framer-motion@^11.3.8: framer-motion@^11.3.8:
version "11.3.8" version "11.3.29"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.3.8.tgz#682df8cbac6a9667b48af427e5a8bdaea7203713" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.3.29.tgz#5ec10a350b89789d43ea7d9c6bde45b28470f196"
integrity sha512-1D+RDTsIp4Rz2dq/oToqSEc9idEQwgBRQyBq4rGpFba+0Z+GCbj9z1s0+ikFbanWe3YJ0SqkNlDe08GcpFGj5A== integrity sha512-uyDuUOeOElJEA3kbkbyoTNEf75Jih1EUg0ouLKYMlGDdt/LaJPmO+FyOGAGxM2HwKhHcAoKFNveR5A8peb7yhw==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"