server route!
All checks were successful
Deploy Frontend / docker (17, 3.8.5) (push) Successful in 2m59s
All checks were successful
Deploy Frontend / docker (17, 3.8.5) (push) Successful in 2m59s
This commit is contained in:
parent
367c974cb3
commit
56563802be
Binary file not shown.
@ -40,13 +40,13 @@
|
|||||||
{
|
{
|
||||||
"name": "Player Lookup",
|
"name": "Player Lookup",
|
||||||
"description": "Wanna find a player? You can locate them here by their username or UUID.",
|
"description": "Wanna find a player? You can locate them here by their username or UUID.",
|
||||||
"image": "/media/featured/server.png",
|
"image": "/media/featured/server.jpg",
|
||||||
"href": "/player"
|
"href": "/player"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Server Lookup",
|
"name": "Server Lookup",
|
||||||
"description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.",
|
"description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.",
|
||||||
"image": "/media/featured/server.png",
|
"image": "/media/featured/server.jpg",
|
||||||
"href": "/player"
|
"href": "/player"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@radix-ui/react-context-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
BIN
Frontend/public/media/featured/server.jpg
Normal file
BIN
Frontend/public/media/featured/server.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
Before Width: | Height: | Size: 384 KiB |
BIN
Frontend/public/media/full-ping.png
Normal file
BIN
Frontend/public/media/full-ping.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
BIN
Frontend/public/media/server-background.png
Normal file
BIN
Frontend/public/media/server-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
@ -1,13 +1,14 @@
|
|||||||
import Embed from "@/components/embed";
|
import Embed from "@/components/embed";
|
||||||
import PlayerResult from "@/components/player/player-result";
|
import PlayerResult from "@/components/player/player-result";
|
||||||
import PlayerSearch from "@/components/player/player-search";
|
import PlayerSearch from "@/components/player/player-search";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { minecrafter } from "@/font/fonts";
|
import { minecrafter } from "@/font/fonts";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageProps } from "@/types/page";
|
import { PageProps } from "@/types/page";
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Image from "next/image";
|
|
||||||
import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib";
|
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page to lookup a player.
|
* The page to lookup a player.
|
||||||
@ -29,18 +30,7 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
|
|||||||
// Render the page
|
// Render the page
|
||||||
return (
|
return (
|
||||||
<main className="px-3 h-screen flex justify-center items-center">
|
<main className="px-3 h-screen flex justify-center items-center">
|
||||||
<div className="mt-0 sm:mt-[45rem] xl:mt-0 flex flex-col xl:flex-row xl:gap-24 2xl:gap-48 transition-all transform-gpu">
|
<div className="flex flex-col gap-7">
|
||||||
{/* Banner */}
|
|
||||||
<Image
|
|
||||||
className="hidden sm:flex xl:my-auto h-[28rem] pointer-events-none"
|
|
||||||
src="/media/players.webp"
|
|
||||||
alt="Minecraft Players"
|
|
||||||
width={632}
|
|
||||||
height={632}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="pb-16 xl:pb-0 flex flex-col gap-7">
|
|
||||||
<h1
|
<h1
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none",
|
"mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none",
|
||||||
@ -52,7 +42,13 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-5 px-10 xs:px-0">
|
<div className="flex flex-col gap-5 px-10 xs:px-0">
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<ExclamationCircleIcon width={20} height={20} />
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<PlayerSearch query={query} />
|
<PlayerSearch query={query} />
|
||||||
@ -61,7 +57,6 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
|
|||||||
{/* Player Result */}
|
{/* Player Result */}
|
||||||
{result && <PlayerResult query={query} player={result} />}
|
{result && <PlayerResult query={query} player={result} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -93,13 +88,13 @@ export const generateMetadata = async ({
|
|||||||
return Embed({
|
return Embed({
|
||||||
title: "Invalid Player",
|
title: "Invalid Player",
|
||||||
color: "#EB4034",
|
color: "#EB4034",
|
||||||
description: "The player you searched for is invalid.",
|
description: `The player ${query} is invalid.`,
|
||||||
});
|
});
|
||||||
} else if (code === 404) {
|
} else if (code === 404) {
|
||||||
return Embed({
|
return Embed({
|
||||||
title: "Player Not Found",
|
title: "Player Not Found",
|
||||||
color: "#EB4034",
|
color: "#EB4034",
|
||||||
description: "The player you searched for was not found.",
|
description: `The player ${query} was not found.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
93
Frontend/src/app/(pages)/server/[[...slug]]/page.tsx
Normal file
93
Frontend/src/app/(pages)/server/[[...slug]]/page.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import ServerResult from "@/components/server/server-result";
|
||||||
|
import ServerSearch from "@/components/server/server-search";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { minecrafter } from "@/font/fonts";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { PageProps } from "@/types/page";
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import {
|
||||||
|
CachedBedrockMinecraftServer,
|
||||||
|
CachedJavaMinecraftServer,
|
||||||
|
ServerPlatform,
|
||||||
|
getMinecraftServer,
|
||||||
|
type RestfulMCAPIError,
|
||||||
|
} from "restfulmc-lib";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The page to lookup a server.
|
||||||
|
*
|
||||||
|
* @returns the page jsx
|
||||||
|
*/
|
||||||
|
const ServerPage = async ({ params }: PageProps): Promise<ReactElement> => {
|
||||||
|
let error: string | undefined = undefined; // The error to display
|
||||||
|
let result:
|
||||||
|
| CachedJavaMinecraftServer
|
||||||
|
| CachedBedrockMinecraftServer
|
||||||
|
| undefined = undefined; // The server to display
|
||||||
|
const platform: string | undefined = params.slug?.[0]; // The platform to search for
|
||||||
|
const hostname: string | undefined = params.slug?.[1]; // The hostname of the server to search for
|
||||||
|
|
||||||
|
// Try and get the server to display
|
||||||
|
try {
|
||||||
|
result =
|
||||||
|
platform && hostname
|
||||||
|
? await getMinecraftServer(platform as ServerPlatform, hostname)
|
||||||
|
: undefined;
|
||||||
|
} catch (err) {
|
||||||
|
error = (err as RestfulMCAPIError).message; // Set the error message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the page
|
||||||
|
return (
|
||||||
|
<main className="px-3 h-screen flex justify-center items-center">
|
||||||
|
<div className="flex flex-col gap-7">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none",
|
||||||
|
minecrafter.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Server Lookup
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5 px-10 xs:px-16 transition-all transform-gpu">
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<ExclamationCircleIcon width={20} height={20} />
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<ServerSearch platform={platform} hostname={hostname} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Result */}
|
||||||
|
{result && (
|
||||||
|
<div className="flex justify-center scale-90 xs:scale-100 transition-all transform-gpu">
|
||||||
|
<ServerResult server={result} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate metadata for this page.
|
||||||
|
*
|
||||||
|
* @param params the route params
|
||||||
|
* @param searchParams the search params
|
||||||
|
* @returns the generated metadata
|
||||||
|
*/
|
||||||
|
export const generateMetadata = async ({
|
||||||
|
params,
|
||||||
|
}: PageProps): Promise<Metadata> => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerPage;
|
@ -5,6 +5,16 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for a player search.
|
||||||
|
*/
|
||||||
|
type PlayerSearchProps = {
|
||||||
|
/**
|
||||||
|
* The original query to search for.
|
||||||
|
*/
|
||||||
|
query: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component for searching for a player.
|
* A component for searching for a player.
|
||||||
*
|
*
|
||||||
@ -13,16 +23,14 @@ import { ReactElement } from "react";
|
|||||||
*/
|
*/
|
||||||
const PlayerSearch = ({
|
const PlayerSearch = ({
|
||||||
query,
|
query,
|
||||||
}: {
|
}: PlayerSearchProps): ReactElement => {
|
||||||
query: string | undefined;
|
|
||||||
}): ReactElement => {
|
|
||||||
const handleRedirect = async (form: FormData): Promise<void> => {
|
const handleRedirect = async (form: FormData): Promise<void> => {
|
||||||
"use server";
|
"use server";
|
||||||
redirect(`/player/${form.get("query")}`);
|
redirect(`/player/${form.get("query")}`);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-7 justify-center items-center"
|
className="flex flex-col gap-7 items-center"
|
||||||
action={handleRedirect}
|
action={handleRedirect}
|
||||||
>
|
>
|
||||||
<div className="w-full flex flex-col gap-3">
|
<div className="w-full flex flex-col gap-3">
|
||||||
@ -32,6 +40,7 @@ const PlayerSearch = ({
|
|||||||
name="query"
|
name="query"
|
||||||
placeholder="Query..."
|
placeholder="Query..."
|
||||||
defaultValue={query}
|
defaultValue={query}
|
||||||
|
required
|
||||||
maxLength={36}
|
maxLength={36}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
72
Frontend/src/app/components/server/server-result.tsx
Normal file
72
Frontend/src/app/components/server/server-result.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
CachedBedrockMinecraftServer,
|
||||||
|
CachedJavaMinecraftServer,
|
||||||
|
} from "restfulmc-lib";
|
||||||
|
import config from "../../config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for a server result.
|
||||||
|
*/
|
||||||
|
type ServerResultProps = {
|
||||||
|
/**
|
||||||
|
* The result of a search.
|
||||||
|
*/
|
||||||
|
server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a server search.
|
||||||
|
*
|
||||||
|
* @param server the server to display
|
||||||
|
* @returns the server result jsx
|
||||||
|
*/
|
||||||
|
const ServerResult = ({ server }: ServerResultProps): ReactNode => {
|
||||||
|
const favicon: string | undefined = (server as CachedJavaMinecraftServer)
|
||||||
|
.favicon?.url; // The favicon of the server (TODO: bedrock)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-[29rem] relative p-2 flex gap-2 rounded-lg",
|
||||||
|
'bg-[url("/media/server-background.png")]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Favicon */}
|
||||||
|
<Image
|
||||||
|
src={favicon || `${config.apiEndpoint}/server/icon/fallback`}
|
||||||
|
alt={`${server.hostname}'s Favicon`}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Name & MOTD */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1>{server.hostname}</h1>
|
||||||
|
{server.motd.html.map((line, index) => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={index}
|
||||||
|
dangerouslySetInnerHTML={{ __html: line }}
|
||||||
|
></p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ping */}
|
||||||
|
<div className="absolute top-0.5 right-0.5 flex gap-1 items-center">
|
||||||
|
<p>
|
||||||
|
{server.players.online}/{server.players.max}
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/media/full-ping.png"
|
||||||
|
alt="Ping!"
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ServerResult;
|
92
Frontend/src/app/components/server/server-search.tsx
Normal file
92
Frontend/src/app/components/server/server-search.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import MinecraftButton from "@/components/minecraft-button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { ServerPlatform } from "restfulmc-lib";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for a server search.
|
||||||
|
*/
|
||||||
|
type ServerSearchProps = {
|
||||||
|
/**
|
||||||
|
* The original platform to query for.
|
||||||
|
*/
|
||||||
|
platform: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original hostname to query for.
|
||||||
|
*/
|
||||||
|
hostname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for searching for a server.
|
||||||
|
*
|
||||||
|
* @param query the query to search for
|
||||||
|
* @returns the search component jsx
|
||||||
|
*/
|
||||||
|
const ServerSearch = ({
|
||||||
|
platform,
|
||||||
|
hostname,
|
||||||
|
}: ServerSearchProps): ReactElement => {
|
||||||
|
const handleRedirect = async (form: FormData): Promise<void> => {
|
||||||
|
"use server";
|
||||||
|
redirect(`/server/${form.get("platform")}/${form.get("hostname")}`);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-7 items-center"
|
||||||
|
action={handleRedirect}
|
||||||
|
>
|
||||||
|
<div className="w-full flex gap-2">
|
||||||
|
{/* Platform Selection */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="platform">Platform</Label>
|
||||||
|
<Select
|
||||||
|
name="platform"
|
||||||
|
defaultValue={platform || "java"}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-28">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.values(ServerPlatform).map(
|
||||||
|
(platform, index) => (
|
||||||
|
<SelectItem key={index} value={platform}>
|
||||||
|
{platform.charAt(0).toUpperCase() +
|
||||||
|
platform.substring(1)}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hostname Query */}
|
||||||
|
<div className="w-full flex flex-col gap-3">
|
||||||
|
<Label htmlFor="hostname">Hostname</Label>
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
name="hostname"
|
||||||
|
placeholder="Query..."
|
||||||
|
defaultValue={hostname}
|
||||||
|
required
|
||||||
|
maxLength={36}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MinecraftButton type="submit">Search</MinecraftButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ServerSearch;
|
62
Frontend/src/app/components/ui/alert.tsx
Normal file
62
Frontend/src/app/components/ui/alert.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/app/lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-red-600/50 text-red-600 dark:border-red-600 [&>svg]:text-red-600",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mb-1 font-medium leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
|
export { Alert, AlertDescription, AlertTitle };
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/app/lib/utils"
|
import { cn } from "@/app/lib/utils";
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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",
|
"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-1 focus-visible:ring-zinc-700 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all transform-gpu",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
160
Frontend/src/app/components/ui/select.tsx
Normal file
160
Frontend/src/app/components/ui/select.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/app/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-zinc-700 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 transition-all transform-gpu",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full 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">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user