mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 14:54:58 -05:00
feat: redid server view
This commit is contained in:
parent
fd40a8e143
commit
346f9d210e
@ -53,6 +53,7 @@
|
|||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-fade-in": "^2.0.1",
|
"react-fade-in": "^2.0.1",
|
||||||
"react-fast-marquee": "^1.6.5",
|
"react-fast-marquee": "^1.6.5",
|
||||||
|
"react-qr-code": "^2.0.15",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
|
|||||||
@ -43,9 +43,9 @@ import NewDomainDialog from "@/components/misc/NewDomainDialog";
|
|||||||
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 {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { banner } from "@/config/banner";
|
import { banner } from "@/config/banner";
|
||||||
@ -55,79 +55,74 @@ import { Inter as interFont } from "next/font/google";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const extraMetadata = {
|
export const extraMetadata = {
|
||||||
twitter: {
|
twitter: {
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: "/imgs/icon-cf.png",
|
url: "/imgs/icon-cf.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
themeColor: "#000000",
|
themeColor: "#000000",
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: "/imgs/icon-cf.png",
|
url: "/imgs/icon-cf.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} satisfies Metadata;
|
} satisfies Metadata;
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: "black",
|
themeColor: "black",
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
};
|
};
|
||||||
|
|
||||||
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({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<ClerkThemeProvider className={GeistSans.className}>
|
<ClerkThemeProvider className={GeistSans.className}>
|
||||||
<ThemeProvider
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
attribute="class"
|
<TooltipProvider>
|
||||||
defaultTheme="system"
|
{banner.isBanner && (
|
||||||
enableSystem
|
<div className="bg-orange-600 z-10 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
|
||||||
disableTransitionOnChange
|
{banner.bannerText}
|
||||||
>
|
</div>
|
||||||
<TooltipProvider>
|
)}
|
||||||
{banner.isBanner && (
|
<div
|
||||||
<div className="bg-orange-600 z-10 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
|
className={
|
||||||
{banner.bannerText}
|
"w-screen h-[3rem] border-b fixed backdrop-blur flex z-10 " +
|
||||||
</div>
|
(banner.isBanner == true ? "mt-8" : "")
|
||||||
)}
|
}
|
||||||
<div
|
>
|
||||||
className={
|
<div className="items-center me-auto mt-2 pl-7 max-sm:mt-3">
|
||||||
"w-screen h-[3rem] border-b fixed backdrop-blur flex z-10 " +
|
<Breadcrumb>
|
||||||
(banner.isBanner == true ? "mt-8" : "")
|
<BreadcrumbList>
|
||||||
}
|
<Link href="/">
|
||||||
>
|
<BreadcrumbPage className="max-sm:hidden">
|
||||||
<div className="items-center me-auto mt-2 pl-7 max-sm:mt-3">
|
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] " />
|
||||||
<Breadcrumb>
|
</BreadcrumbPage>
|
||||||
<BreadcrumbList>
|
</Link>
|
||||||
<Link href="/">
|
<TextFromPathname />
|
||||||
<BreadcrumbPage className="max-sm:hidden">
|
</BreadcrumbList>
|
||||||
<BrandingGenericIcon className="max-w-[32px] max-h-[32px] " />
|
</Breadcrumb>
|
||||||
</BreadcrumbPage>
|
</div>
|
||||||
</Link>
|
<TopBar inter={inter.className} />
|
||||||
<TextFromPathname />
|
</div>
|
||||||
</BreadcrumbList>
|
<div className={banner.isBanner ? "pt-8" : undefined}>
|
||||||
</Breadcrumb>
|
<NextTopLoader />
|
||||||
</div>
|
<ClientFadeIn>{children}</ClientFadeIn>
|
||||||
<TopBar inter={inter.className} />
|
</div>{" "}
|
||||||
</div>
|
<ThemedToaster />
|
||||||
<div className={banner.isBanner ? "pt-8" : undefined}>
|
<CommandBarer />
|
||||||
<NextTopLoader />
|
<SpeedInsights />
|
||||||
<ClientFadeIn>{children}</ClientFadeIn>
|
<Analytics />
|
||||||
</div>{" "}
|
<NewDomainDialog />
|
||||||
<ThemedToaster />
|
<UnofficalDialog />
|
||||||
<CommandBarer />
|
</TooltipProvider>
|
||||||
<SpeedInsights />
|
</ThemeProvider>
|
||||||
<Analytics />
|
</ClerkThemeProvider>
|
||||||
<NewDomainDialog />
|
);
|
||||||
<UnofficalDialog />
|
|
||||||
</TooltipProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</ClerkThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,7 +130,7 @@ export default function ServerPage({ params }: { params: { server: string } }) {
|
|||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<ColorProvider server={params.server}>
|
<ColorProvider server={params.server}>
|
||||||
<div className={"pt-16"}>
|
<div className={"pt-16 xl:px-[100px]"}>
|
||||||
<Banner server={params.server} />
|
<Banner server={params.server} />
|
||||||
<TabServer server={params.server} tabDef="general" />
|
<TabServer server={params.server} tabDef="general" />
|
||||||
<div className="pt-8">
|
<div className="pt-8">
|
||||||
|
|||||||
@ -37,80 +37,80 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import type { Metadata, ResolvingMetadata } from "next";
|
import type { Metadata, ResolvingMetadata } from "next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { server: string };
|
params: { server: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params }: Props,
|
{ params }: Props,
|
||||||
parent: ResolvingMetadata,
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
// read route params
|
// read route params
|
||||||
const { server } = params;
|
const { server } = params;
|
||||||
const json = await (
|
const json = await (
|
||||||
await fetch("https://api.minehut.com/server/" + server + "?byName=true")
|
await fetch("https://api.minehut.com/server/" + server + "?byName=true")
|
||||||
).json();
|
).json();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title:
|
title:
|
||||||
json.server == null
|
json.server == null
|
||||||
? "Server doesn't exist | MHSF"
|
? "Server doesn't exist | MHSF"
|
||||||
: json.server.name +
|
: json.server.name +
|
||||||
", " +
|
", " +
|
||||||
(json.server.online
|
(json.server.online
|
||||||
? json.server.playerCount +
|
? json.server.playerCount +
|
||||||
(json.server.maxPlayers != 10
|
(json.server.maxPlayers != 10
|
||||||
? "/" + json.server.maxPlayers
|
? "/" + json.server.maxPlayers
|
||||||
: "") +
|
: "") +
|
||||||
" online"
|
" online"
|
||||||
: "Offline") +
|
: "Offline") +
|
||||||
" | MHSF",
|
" | MHSF",
|
||||||
description:
|
description:
|
||||||
json.server == null
|
json.server == null
|
||||||
? `The server ${server} doesn't exist.`
|
? `The server ${server} doesn't exist.`
|
||||||
: `View ${server} on Minehut Server Finder!`,
|
: `View ${server} on Minehut Server Finder!`,
|
||||||
authors: json.server == null ? undefined : { name: json.server.owner },
|
authors: json.server == null ? undefined : { name: json.server.owner },
|
||||||
applicationName: "MHSF (Minehut Server Finder)",
|
applicationName: "MHSF (Minehut Server Finder)",
|
||||||
icons:
|
icons:
|
||||||
json.server == null
|
json.server == null
|
||||||
? undefined
|
? undefined
|
||||||
: "https://mcapi.marveldc.me/item/" +
|
: "https://mcapi.marveldc.me/item/" +
|
||||||
(json.server.icon == undefined ? "OAK_SIGN" : json.server.icon) +
|
(json.server.icon == undefined ? "OAK_SIGN" : json.server.icon) +
|
||||||
"?width=64&height=64",
|
"?width=64&height=64",
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "profile",
|
type: "profile",
|
||||||
siteName: "MHSF (Minehut Server Finder)",
|
siteName: "MHSF (Minehut Server Finder)",
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url:
|
url:
|
||||||
"https://mcapi.marveldc.me/item/" +
|
"https://mcapi.marveldc.me/item/" +
|
||||||
json.server.icon +
|
json.server.icon +
|
||||||
"?width=64&height=64",
|
"?width=64&height=64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "/favicon.ico",
|
url: "/favicon.ico",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServerPage({ params }: { params: { server: string } }) {
|
export default function ServerPage({ params }: { params: { server: string } }) {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<ColorProvider server={params.server}>
|
<ColorProvider server={params.server}>
|
||||||
<div className={"pt-16"}>
|
<div className={"pt-16 xl:px-[100px]"}>
|
||||||
<Banner server={params.server} />
|
<Banner server={params.server} />
|
||||||
<TabServer server={params.server} tabDef="statistics" />
|
<TabServer server={params.server} tabDef="statistics" />
|
||||||
<div className="pt-8">
|
<div className="pt-8">
|
||||||
<ServerView server={params.server} />
|
<ServerView server={params.server} />
|
||||||
<Separator />
|
<Separator />
|
||||||
<br />
|
<br />
|
||||||
<div className="p-4 gap-4">
|
<div className="p-4 gap-4">
|
||||||
<NewChart server={params.server} />
|
<NewChart server={params.server} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ColorProvider>
|
</ColorProvider>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,6 +147,37 @@
|
|||||||
/* }*/
|
/* }*/
|
||||||
/*}*/
|
/*}*/
|
||||||
|
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes clip-down {
|
||||||
|
from {
|
||||||
|
clip-path: inset(0 0 100% 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark::view-transition-new(root),
|
||||||
|
.light::view-transition-new(root) {
|
||||||
|
animation: 0.7s clip-down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.backdrop-blur {
|
.backdrop-blur {
|
||||||
-webkit-backdrop-filter: blur(8px) !important;
|
-webkit-backdrop-filter: blur(8px) !important;
|
||||||
backdrop-filter: blur(8px) !important;
|
backdrop-filter: blur(8px) !important;
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import {
|
|||||||
getIndexFromRarity,
|
getIndexFromRarity,
|
||||||
getMinehutIcons,
|
getMinehutIcons,
|
||||||
} from "@/lib/types/server-icon";
|
} from "@/lib/types/server-icon";
|
||||||
import { Copy, ExternalLink, Info } from "lucide-react";
|
import { Copy, Info, QrCode, Share2 } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import FadeIn from "react-fade-in/lib/FadeIn";
|
import FadeIn from "react-fade-in/lib/FadeIn";
|
||||||
@ -56,6 +56,9 @@ import {
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "./ui/drawer";
|
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "./ui/drawer";
|
||||||
import EmbedSelector from "./feat/EmbedSelector";
|
import EmbedSelector from "./feat/EmbedSelector";
|
||||||
|
import { Separator } from "./ui/separator";
|
||||||
|
import QRCodeGenerator from "./feat/QRCodeGen";
|
||||||
|
import NoItems from "./misc/NoItems";
|
||||||
|
|
||||||
export default function AfterServerView({ server }: { server: string }) {
|
export default function AfterServerView({ server }: { server: string }) {
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
@ -64,6 +67,7 @@ export default function AfterServerView({ server }: { server: string }) {
|
|||||||
const [icons, setIcons] = useState<MinehutIcon[]>();
|
const [icons, setIcons] = useState<MinehutIcon[]>();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [qrCodeOpen, setQrCodeOpen] = useState(false);
|
||||||
const [view, setView] = useState(
|
const [view, setView] = useState(
|
||||||
description !== "" || discord !== "" ? "desc" : "extra"
|
description !== "" || discord !== "" ? "desc" : "extra"
|
||||||
);
|
);
|
||||||
@ -104,6 +108,14 @@ export default function AfterServerView({ server }: { server: string }) {
|
|||||||
<EmbedSelector server={server} />
|
<EmbedSelector server={server} />
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
<Drawer open={qrCodeOpen} onOpenChange={setQrCodeOpen}>
|
||||||
|
<DrawerContent className="max-w-md w-full mx-auto rounded-t-[10px]">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>QR Code generator</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<QRCodeGenerator server={server} />
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<div className="grid sm:grid-cols-6 h-full pl-4 pr-4 ">
|
<div className="grid sm:grid-cols-6 h-full pl-4 pr-4 ">
|
||||||
<div className="ml-5 mb-2 flex items-center sm:hidden overflow-auto w-[calc(100vw-5rem)]">
|
<div className="ml-5 mb-2 flex items-center sm:hidden overflow-auto w-[calc(100vw-5rem)]">
|
||||||
@ -135,9 +147,10 @@ export default function AfterServerView({ server }: { server: string }) {
|
|||||||
>
|
>
|
||||||
Purchased Icons
|
Purchased Icons
|
||||||
</Button>
|
</Button>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
|
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
|
||||||
Embed Creator
|
<Share2 className="h-[1rem] w-[1rem] mr-2" />
|
||||||
<ExternalLink className="h-[1.2rem] w-[1.2rem] ml-1" />
|
Embeds
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-sm:hidden">
|
<div className="max-sm:hidden">
|
||||||
@ -168,9 +181,16 @@ export default function AfterServerView({ server }: { server: string }) {
|
|||||||
>
|
>
|
||||||
Purchased Icons
|
Purchased Icons
|
||||||
</Button>
|
</Button>
|
||||||
|
<br />
|
||||||
|
<Separator />
|
||||||
|
<br />
|
||||||
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
|
<Button variant="ghost" onClick={() => setEmbedOpened(true)}>
|
||||||
Embed Creator
|
<Share2 className="h-[1rem] w-[1rem] mr-2" />
|
||||||
<ExternalLink className="h-[1.2rem] w-[1.2rem] ml-1" />
|
Embeds
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" onClick={() => setQrCodeOpen(true)}>
|
||||||
|
<QrCode className="h-[1rem] w-[1rem] mr-2" />
|
||||||
|
QR Code
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -450,6 +470,7 @@ export default function AfterServerView({ server }: { server: string }) {
|
|||||||
ownership, they may or may not available at that certain
|
ownership, they may or may not available at that certain
|
||||||
moment either.
|
moment either.
|
||||||
</p>
|
</p>
|
||||||
|
{serverObject?.purchased_icons.length == 0 && <NoItems />}
|
||||||
{serverObject?.purchased_icons.map((icon) => (
|
{serverObject?.purchased_icons.map((icon) => (
|
||||||
<Card key={icon} className="my-4">
|
<Card key={icon} className="my-4">
|
||||||
<CardContent
|
<CardContent
|
||||||
|
|||||||
@ -29,27 +29,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Banner from "./Banner";
|
import Banner from "./Banner";
|
||||||
import ServerCustomize from "./ServerCustomize";
|
import ServerCustomize from "./ServerCustomize";
|
||||||
import TabServer from "./misc/TabServer";
|
import TabServer from "./misc/TabServer";
|
||||||
|
|
||||||
export default function CustomizeRoot({
|
export default function CustomizeRoot({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { server: string };
|
params: { server: string };
|
||||||
}) {
|
}) {
|
||||||
const [color, setColor] = useState("");
|
const [color, setColor] = useState("");
|
||||||
return (
|
return (
|
||||||
<div className={"pt-16 theme-" + color}>
|
<div className={"pt-16 xl:px-[100px] theme-" + color}>
|
||||||
<Banner server={params.server} />
|
<Banner server={params.server} />
|
||||||
<TabServer server={params.server} tabDef="customize" />
|
<TabServer server={params.server} tabDef="customize" />
|
||||||
<Separator />
|
<br />
|
||||||
<br />
|
<div className="pl-[40px] pr-[40px]">
|
||||||
<div className="pl-[40px] pr-[40px]">
|
<ServerCustomize server={params.server} cs={color} setCS={setColor} />
|
||||||
<ServerCustomize server={params.server} cs={color} setCS={setColor} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -196,7 +196,7 @@ export function NewChart({ server }: { server: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert(value: number) {
|
export function convert(value: number) {
|
||||||
var result: string = value.toString();
|
var result: string = value.toString();
|
||||||
if (value >= 1000000) {
|
if (value >= 1000000) {
|
||||||
result = Math.floor(value / 1000000) + "m";
|
result = Math.floor(value / 1000000) + "m";
|
||||||
|
|||||||
@ -31,15 +31,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
CardTitle,
|
||||||
BetterHeader,
|
BetterHeader,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -47,14 +43,22 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import ServerSingle from "@/lib/single";
|
import ServerSingle from "@/lib/single";
|
||||||
import { SignedIn, SignedOut } from "@clerk/nextjs";
|
import { motion } from "framer-motion";
|
||||||
import SignInPopoverButton from "./clerk/SignInPopoverButton";
|
import { Cake, Check, Heart, Star, Users, X } from "lucide-react";
|
||||||
import { Star, X } from "lucide-react";
|
import {
|
||||||
import { favoriteServer, isFavorited } from "@/lib/api";
|
favoriteServer,
|
||||||
import { LoadingButton } from "./ui/loading-button";
|
getCommunityServerFavorites,
|
||||||
|
isFavorited,
|
||||||
|
} from "@/lib/api";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Skeleton } from "./ui/skeleton";
|
import { Skeleton } from "./ui/skeleton";
|
||||||
import FadeIn from "react-fade-in/lib/FadeIn";
|
import FadeIn from "react-fade-in/lib/FadeIn";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import IconDisplay from "./IconDisplay";
|
||||||
|
import { useClerk, useUser } from "@clerk/nextjs";
|
||||||
|
import { LoaderIcon } from "react-hot-toast";
|
||||||
|
import { Separator } from "./ui/separator";
|
||||||
|
import { convert } from "@/components/NewChart";
|
||||||
|
|
||||||
export default function ServerView(props: { server: string }) {
|
export default function ServerView(props: { server: string }) {
|
||||||
const [single, setSingle] = useState(new ServerSingle(props.server));
|
const [single, setSingle] = useState(new ServerSingle(props.server));
|
||||||
@ -64,6 +68,9 @@ export default function ServerView(props: { server: string }) {
|
|||||||
const [loadingFavorite, setLoadingFavorite] = useState(false);
|
const [loadingFavorite, setLoadingFavorite] = useState(false);
|
||||||
const [randomText, setRandomText] = useState("");
|
const [randomText, setRandomText] = useState("");
|
||||||
const [lastOnline, setLastOnline] = useState(0);
|
const [lastOnline, setLastOnline] = useState(0);
|
||||||
|
const { isSignedIn } = useUser();
|
||||||
|
const [communityFavorited, setCommunityFavorited] = useState(0);
|
||||||
|
const clerk = useClerk();
|
||||||
const [format, setFormat] = useState("");
|
const [format, setFormat] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const allText = [""];
|
const allText = [""];
|
||||||
@ -91,16 +98,19 @@ export default function ServerView(props: { server: string }) {
|
|||||||
setLastOnline(online);
|
setLastOnline(online);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
getCommunityServerFavorites(single.grabOffline()?.name as string).then(
|
||||||
|
(b) => {
|
||||||
|
setCommunityFavorited(b);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid p-4 sm:grid-cols-3 gap-4">
|
<div className="p-4">
|
||||||
<Skeleton className="sm:col-span-2 h-[245px]" />
|
<Skeleton className="sm:col-span-2 h-[155px]" />
|
||||||
|
|
||||||
<Skeleton className="h-[245px]" />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -125,8 +135,14 @@ export default function ServerView(props: { server: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<div className="grid p-4 sm:grid-cols-3 gap-4">
|
<div className="flex items-center">
|
||||||
<Card className="sm:col-span-2">
|
<div className="bg-secondary p-4 rounded-lg ml-4">
|
||||||
|
<IconDisplay
|
||||||
|
server={single.grabOffline()}
|
||||||
|
className="flex items-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="block">
|
||||||
<BetterHeader>
|
<BetterHeader>
|
||||||
<CardTitle className="flex items-center">
|
<CardTitle className="flex items-center">
|
||||||
{single.grabOnline() == undefined &&
|
{single.grabOnline() == undefined &&
|
||||||
@ -173,103 +189,114 @@ export default function ServerView(props: { server: string }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{single.getAuthor() != undefined && (
|
{single.getAuthor() != undefined ? (
|
||||||
<p>by {single.getAuthor()}</p>
|
<p className="text-lg flex items-center">
|
||||||
|
by {single.getAuthor()}{" "}
|
||||||
|
<Button
|
||||||
|
className="h-7 ml-2"
|
||||||
|
variant={favorited ? "outline" : "favorite"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isSignedIn) {
|
||||||
|
clerk.openSignUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingFavorite(true);
|
||||||
|
favoriteServer(
|
||||||
|
single.grabOffline()?.name as string
|
||||||
|
).then(() => {
|
||||||
|
setLoadingFavorite(false);
|
||||||
|
setFavorited(!favorited);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={loadingFavorite}
|
||||||
|
>
|
||||||
|
{loadingFavorite && <LoaderIcon className="mr-2" />}
|
||||||
|
{!favorited && !loadingFavorite && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
initial={{ opacity: 0, scale: 0.3 }}
|
||||||
|
transition={{ duration: 0.25, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Star size={16} className="mr-2" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{favorited && !loadingFavorite && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
initial={{ opacity: 0, scale: 0.3 }}
|
||||||
|
transition={{ duration: 0.25, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Check size={16} className="mr-2" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
Favorite{favorited && "d"}
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-lg flex items-center">
|
||||||
|
by Anonymous{" "}
|
||||||
|
<Button
|
||||||
|
className="h-7 ml-2"
|
||||||
|
variant={favorited ? "outline" : "favorite"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isSignedIn) {
|
||||||
|
clerk.openSignUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingFavorite(true);
|
||||||
|
favoriteServer(
|
||||||
|
single.grabOffline()?.name as string
|
||||||
|
).then(() => {
|
||||||
|
setLoadingFavorite(false);
|
||||||
|
setFavorited(!favorited);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={loadingFavorite}
|
||||||
|
>
|
||||||
|
{loadingFavorite && <LoaderIcon className="mr-2" />}
|
||||||
|
{!favorited && !loadingFavorite && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
initial={{ opacity: 0, scale: 0.3 }}
|
||||||
|
transition={{ duration: 0.25, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Star size={16} className="mr-2" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{favorited && !loadingFavorite && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
initial={{ opacity: 0, scale: 0.3 }}
|
||||||
|
transition={{ duration: 0.25, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Check size={16} className="mr-2" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
Favorite{favorited && "d"}
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</BetterHeader>
|
</BetterHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p>
|
<p className="text-md font-semibold text-muted-foreground flex items-center">
|
||||||
<strong>Time:</strong>
|
<>
|
||||||
<br />
|
<Heart className="mr-2" size={24} />
|
||||||
<i>Last online</i>{" "}
|
{convert(communityFavorited)}
|
||||||
<Tooltip>
|
<Separator orientation="vertical" className="ml-4 h-[30px]" />
|
||||||
<TooltipTrigger>
|
</>
|
||||||
<code>
|
<>
|
||||||
{timeConverter(single.grabOffline()?.last_online)}
|
<Users className="mr-2 ml-4" size={24} />{" "}
|
||||||
</code>
|
{convert(single.grabOffline()?.joins as number)}
|
||||||
</TooltipTrigger>
|
<Separator orientation="vertical" className="ml-4 h-[30px]" />
|
||||||
<TooltipContent>
|
</>
|
||||||
<code>{single.grabOffline()?.last_online}</code> in Unix
|
<>
|
||||||
time
|
<Cake className="mr-2 ml-4" size={24} />{" "}
|
||||||
</TooltipContent>
|
{timeConverter(single.grabOffline()?.creation)}
|
||||||
</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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</div>
|
||||||
<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>
|
|
||||||
<LoadingButton
|
|
||||||
variant={resolvedTheme == "dark" ? "outline" : "default"}
|
|
||||||
loading={loadingFavorite}
|
|
||||||
onClick={() => {
|
|
||||||
setLoadingFavorite(true);
|
|
||||||
favoriteServer(single.grabOffline()?.name as string).then(
|
|
||||||
() => {
|
|
||||||
setFavorited(!favorited);
|
|
||||||
setLoadingFavorite(false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
{favorited && "Unf"}
|
|
||||||
{!favorited && "F"}avorite Server
|
|
||||||
</LoadingButton>
|
|
||||||
</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>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
@ -45,3 +45,57 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|||||||
|
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UseThemeTransitionResult {
|
||||||
|
theme: string | undefined;
|
||||||
|
changeTheme: (changeTheme: string) => () => void;
|
||||||
|
mounted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useThemeTransition(): UseThemeTransitionResult {
|
||||||
|
const { theme, setTheme, systemTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeTheme = (changeTheme: string) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
const resolvedTheme = theme === "system" ? systemTheme : changeTheme;
|
||||||
|
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty(
|
||||||
|
"--current-background",
|
||||||
|
`var(--${resolvedTheme}-background)`
|
||||||
|
);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--current-foreground",
|
||||||
|
`var(--${resolvedTheme}-foreground)`
|
||||||
|
);
|
||||||
|
setTheme(changeTheme);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTheme(changeTheme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (mounted && theme) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty(
|
||||||
|
"--current-background",
|
||||||
|
`var(--${theme}-background)`
|
||||||
|
);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--current-foreground",
|
||||||
|
`var(--${theme}-foreground)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [mounted, theme]);
|
||||||
|
|
||||||
|
return { theme, changeTheme, mounted };
|
||||||
|
}
|
||||||
|
|||||||
@ -29,10 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -41,27 +38,28 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { useThemeTransition } from "./ThemeProvider";
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme } = useTheme();
|
const { changeTheme } = useThemeTransition();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="mr-3">
|
<Button variant="ghost" size="icon">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<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" />
|
<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>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
<DropdownMenuItem onClick={() => changeTheme("light")}>
|
||||||
Light
|
Light
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
<DropdownMenuItem onClick={() => changeTheme("dark")}>
|
||||||
Dark
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
<DropdownMenuItem onClick={() => changeTheme("system")}>
|
||||||
System
|
System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import { Card, CardContent } from "../ui/card";
|
|||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import A from "../misc/Link";
|
import A from "../misc/Link";
|
||||||
import { formalNames } from "@/config/achievements";
|
import { formalNames } from "@/config/achievements";
|
||||||
|
import NoItems from "../misc/NoItems";
|
||||||
|
|
||||||
export default function AchievementList({ server }: { server: string }) {
|
export default function AchievementList({ server }: { server: string }) {
|
||||||
const [achievements, setAchievements] = useState<
|
const [achievements, setAchievements] = useState<
|
||||||
@ -70,6 +71,7 @@ export default function AchievementList({ server }: { server: string }) {
|
|||||||
Achievements are earned automatically when the server is online. See{" "}
|
Achievements are earned automatically when the server is online. See{" "}
|
||||||
<A alt="Achievement collection">Docs:Advanced/Achievements</A>
|
<A alt="Achievement collection">Docs:Advanced/Achievements</A>
|
||||||
</span>
|
</span>
|
||||||
|
{achievements.length === 0 && <NoItems />}
|
||||||
{achievements
|
{achievements
|
||||||
.filter(
|
.filter(
|
||||||
(value, index) => listify(achievements).indexOf(value.type) === index
|
(value, index) => listify(achievements).indexOf(value.type) === index
|
||||||
@ -109,8 +111,6 @@ export default function AchievementList({ server }: { server: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type WithInterval<K> = K & {
|
type WithInterval<K> = K & {
|
||||||
interval: number;
|
interval: number;
|
||||||
};
|
};
|
||||||
|
|||||||
57
src/components/feat/QRCodeGen.tsx
Normal file
57
src/components/feat/QRCodeGen.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import QRCode from "react-qr-code";
|
||||||
|
import { DrawerFooter, DrawerTrigger } from "../ui/drawer";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
export default function QRCodeGenerator({ server }: { server: string }) {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<QRCode
|
||||||
|
value={"https://mhsf.app/server/" + server + "?source=qrCode"}
|
||||||
|
className="flex flex-col items-center w-full py-4"
|
||||||
|
style={{
|
||||||
|
backgroundColor: resolvedTheme === "dark" ? "#fff" : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DrawerFooter>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button>Close</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
</DrawerFooter>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/components/misc/NoItems.tsx
Normal file
48
src/components/misc/NoItems.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { DatabaseZap } from "lucide-react";
|
||||||
|
|
||||||
|
export default function NoItems() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center justify-center p-4 pt-10">
|
||||||
|
<DatabaseZap
|
||||||
|
className="text-2xl font-semibold text-gray-600"
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
<p className="text-xl text-gray-600 mt-2">
|
||||||
|
Huh, we tried to find something, but nothing was found.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -50,6 +50,8 @@ const buttonVariants = cva(
|
|||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground focus:ring-4 focus:ring-neutral-100 focus:ring-offset-current dark:focus:ring-neutral-900 duration-150 ease-in-out transition-all",
|
"hover:bg-accent hover:text-accent-foreground focus:ring-4 focus:ring-neutral-100 focus:ring-offset-current dark:focus:ring-neutral-900 duration-150 ease-in-out transition-all",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
favorite:
|
||||||
|
"text-black rounded-lg hover:bg-primary/90 focus:ring-4 focus:ring-yellow-400/60 focus:ring-offset-current dark:focus:ring-yellow-400/60 duration-150 ease-in-out transition-all bg-gradient-to-bl from-yellow-300 via-yellow-500 to-yellow-100",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
|
|||||||
@ -55,8 +55,30 @@ const FeatureList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const version = "1.4.0";
|
export const version = "1.6.0";
|
||||||
export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
|
export const changelog: { name: string; id: string; changelog: ReactNode }[] = [
|
||||||
|
{
|
||||||
|
id: "h9jr2cbxn7qwfvt5uypsdg",
|
||||||
|
name: "v1.6.0",
|
||||||
|
changelog: (
|
||||||
|
<FeatureList
|
||||||
|
features={[
|
||||||
|
"Completely redid top of server view",
|
||||||
|
"Favorite counts are now prominent on the server view",
|
||||||
|
"New theme transition (smooth)",
|
||||||
|
"New favorite button",
|
||||||
|
"Added more padding in the server view",
|
||||||
|
"Separated the tabs on the side for sharing actions",
|
||||||
|
"Added new QR code generator",
|
||||||
|
]}
|
||||||
|
title={
|
||||||
|
<strong className="flex items-center">
|
||||||
|
Version 1.6.0 (November 17th 2024)
|
||||||
|
</strong>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "r9swempc7kaqd2j84nutv5",
|
id: "r9swempc7kaqd2j84nutv5",
|
||||||
name: "v1.5.0",
|
name: "v1.5.0",
|
||||||
|
|||||||
36
src/lib/head.ts
Normal file
36
src/lib/head.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getMinecraftHead(username: string) {
|
||||||
|
const uuidRequest = await fetch("https://api.mojang.com/users/profiles/minecraft/" + username);
|
||||||
|
const uuid = (await uuidRequest.json()).id;
|
||||||
|
|
||||||
|
return `https://crafatar.com/avatars/${uuid}`;
|
||||||
|
}
|
||||||
13
yarn.lock
13
yarn.lock
@ -6847,6 +6847,11 @@ punycode@^2.1.0, punycode@^2.3.0:
|
|||||||
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
|
qr.js@0.0.0:
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
|
||||||
|
integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
|
||||||
|
|
||||||
queue-microtask@^1.2.2:
|
queue-microtask@^1.2.2:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||||
@ -6923,6 +6928,14 @@ react-markdown@^9.0.1:
|
|||||||
unist-util-visit "^5.0.0"
|
unist-util-visit "^5.0.0"
|
||||||
vfile "^6.0.0"
|
vfile "^6.0.0"
|
||||||
|
|
||||||
|
react-qr-code@^2.0.15:
|
||||||
|
version "2.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.15.tgz#fbfc12952c504bcd64275647e9d1ea63251742ce"
|
||||||
|
integrity sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.8.1"
|
||||||
|
qr.js "0.0.0"
|
||||||
|
|
||||||
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6:
|
react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6:
|
||||||
version "2.3.6"
|
version "2.3.6"
|
||||||
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz"
|
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user