Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods

This commit is contained in:
Bram Suurd 2024-11-13 15:55:34 +01:00
parent 948f8b0058
commit bdce4f778d
7 changed files with 855 additions and 2 deletions

View File

@ -14,8 +14,10 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@vercel/analytics": "^1.2.2",
@ -39,7 +41,8 @@
"sharp": "^0.33.5",
"simple-icons": "^13.5.0",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0"
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22",
@ -1028,6 +1031,12 @@
"node": ">=14"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
@ -1602,6 +1611,49 @@
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
"integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
@ -1643,6 +1695,35 @@
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz",
"integrity": "sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
@ -7550,6 +7631,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@ -24,8 +24,10 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@vercel/analytics": "^1.2.2",
@ -49,7 +51,8 @@
"sharp": "^0.33.5",
"simple-icons": "^13.5.0",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0"
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22",

View File

@ -0,0 +1,490 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { fetchCategories } from "@/lib/data";
import { Category } from "@/lib/types";
import { PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { z } from "zod";
const scriptSchema = z.object({
name: z.string().min(1),
slug: z.string().min(1),
categories: z.array(z.number()),
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
type: z.enum(["vm", "ct", "misc"]),
updateable: z.boolean(),
privileged: z.boolean(),
interface_port: z.number().nullable(),
documentation: z.string().nullable(),
website: z.string().url().nullable(),
logo: z.string().url().nullable(),
description: z.string().min(1),
install_methods: z.array(
z.object({
type: z.enum(["default", "alpine"]),
script: z.string().min(1),
resources: z.object({
cpu: z.number().nullable(),
ram: z.number().nullable(),
hdd: z.number().nullable(),
os: z.string().nullable(),
version: z.number().nullable(),
}),
}),
),
default_credentials: z.object({
username: z.string().nullable(),
password: z.string().nullable(),
}),
notes: z.array(
z.object({
text: z.string().min(1),
type: z.string().min(1),
}),
),
});
type Script = z.infer<typeof scriptSchema>;
export default function JSONGenerator() {
const [script, setScript] = useState<Script>({
name: "",
slug: "",
categories: [],
date_created: "",
type: "vm",
updateable: false,
privileged: false,
interface_port: null,
documentation: null,
website: null,
logo: null,
description: "",
install_methods: [
{
type: "default",
script: "",
resources: {
cpu: null,
ram: null,
hdd: null,
os: null,
version: null,
},
},
],
default_credentials: {
username: null,
password: null,
},
notes: [
{
text: "",
type: "",
},
],
});
const [isValid, setIsValid] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => {
fetchCategories()
.then((data: Category[]) => {
setCategories(data);
})
.catch((error) => console.error("Error fetching categories:", error));
}, []);
const updateScript = (key: keyof Script, value: Script[keyof Script]) => {
setScript((prev) => {
const updated = { ...prev, [key]: value };
const result = scriptSchema.safeParse(updated);
setIsValid(result.success);
return updated;
});
};
const addInstallMethod = () => {
setScript((prev) => ({
...prev,
install_methods: [
...prev.install_methods,
{
type: "default",
script: "",
resources: {
cpu: null,
ram: null,
hdd: null,
os: null,
version: null,
},
},
],
}));
};
const updateInstallMethod = (
index: number,
key: keyof Script["install_methods"][number],
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
) => {
setScript((prev) => {
const updated = {
...prev,
install_methods: prev.install_methods.map((method, i) =>
i === index ? { ...method, [key]: value } : method,
),
};
const result = scriptSchema.safeParse(updated);
setIsValid(result.success);
return updated;
});
};
const removeInstallMethod = (index: number) => {
setScript((prev) => ({
...prev,
install_methods: prev.install_methods.filter((_, i) => i !== index),
}));
};
const addNote = () => {
setScript((prev) => ({
...prev,
notes: [...prev.notes, { text: "", type: "" }],
}));
};
const updateNote = (
index: number,
key: keyof Script["notes"][number],
value: string,
) => {
setScript((prev) => {
const updated = {
...prev,
notes: prev.notes.map((note, i) =>
i === index ? { ...note, [key]: value } : note,
),
};
const result = scriptSchema.safeParse(updated);
setIsValid(result.success);
return updated;
});
};
const removeNote = (index: number) => {
setScript((prev) => ({
...prev,
notes: prev.notes.filter((_, i) => i !== index),
}));
};
const addCategory = (categoryId: number) => {
setScript((prev) => ({
...prev,
categories: [...new Set([...prev.categories, categoryId])],
}));
};
const removeCategory = (categoryId: number) => {
setScript((prev) => ({
...prev,
categories: prev.categories.filter((id) => id !== categoryId),
}));
};
return (
<div className="flex h-screen">
<div className="w-1/2 p-4 overflow-y-auto">
<h2 className="text-2xl font-bold mb-4">JSON Generator</h2>
<form className="space-y-4">
<Input
placeholder="Name"
value={script.name}
onChange={(e) => updateScript("name", e.target.value)}
/>
<Input
placeholder="Slug"
value={script.slug}
onChange={(e) => updateScript("slug", e.target.value)}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Categories
</label>
<Select onValueChange={(value) => addCategory(Number(value))}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="mt-2 flex flex-wrap gap-2">
{script.categories.map((categoryId) => {
const category = categories.find((c) => c.id === categoryId);
return category ? (
<span
key={categoryId}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{category.name}
<button
type="button"
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
onClick={() => removeCategory(categoryId)}
>
<span className="sr-only">Remove</span>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</span>
) : null;
})}
</div>
</div>
<Input
placeholder="Date Created (YYYY-MM-DD)"
value={script.date_created}
onChange={(e) => updateScript("date_created", e.target.value)}
/>
<Select
value={script.type}
onValueChange={(value) => updateScript("type", value)}
>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="vm">VM</SelectItem>
<SelectItem value="ct">CT</SelectItem>
<SelectItem value="misc">Misc</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center space-x-2">
<Switch
checked={script.updateable}
onCheckedChange={(checked) => updateScript("updateable", checked)}
/>
<label>Updateable</label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={script.privileged}
onCheckedChange={(checked) => updateScript("privileged", checked)}
/>
<label>Privileged</label>
</div>
<Input
placeholder="Interface Port"
type="number"
value={script.interface_port || ""}
onChange={(e) =>
updateScript(
"interface_port",
e.target.value ? Number(e.target.value) : null,
)
}
/>
<Input
placeholder="Documentation URL"
value={script.documentation || ""}
onChange={(e) =>
updateScript("documentation", e.target.value || null)
}
/>
<Input
placeholder="Website URL"
value={script.website || ""}
onChange={(e) => updateScript("website", e.target.value || null)}
/>
<Input
placeholder="Logo URL"
value={script.logo || ""}
onChange={(e) => updateScript("logo", e.target.value || null)}
/>
<Textarea
placeholder="Description"
value={script.description}
onChange={(e) => updateScript("description", e.target.value)}
/>
<h3 className="text-xl font-semibold">Install Methods</h3>
{script.install_methods.map((method, index) => (
<div key={index} className="space-y-2 border p-4 rounded">
<Select
value={method.type}
onValueChange={(value) =>
updateInstallMethod(index, "type", value)
}
>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="alpine">Alpine</SelectItem>
</SelectContent>
</Select>
<Textarea
placeholder="Script"
value={method.script}
onChange={(e) =>
updateInstallMethod(index, "script", e.target.value)
}
/>
<Input
placeholder="CPU"
type="number"
value={method.resources.cpu || ""}
onChange={(e) =>
updateInstallMethod(index, "resources", {
...method.resources,
cpu: e.target.value ? Number(e.target.value) : null,
})
}
/>
<Input
placeholder="RAM"
type="number"
value={method.resources.ram || ""}
onChange={(e) =>
updateInstallMethod(index, "resources", {
...method.resources,
ram: e.target.value ? Number(e.target.value) : null,
})
}
/>
<Input
placeholder="HDD"
type="number"
value={method.resources.hdd || ""}
onChange={(e) =>
updateInstallMethod(index, "resources", {
...method.resources,
hdd: e.target.value ? Number(e.target.value) : null,
})
}
/>
<Input
placeholder="OS"
value={method.resources.os || ""}
onChange={(e) =>
updateInstallMethod(index, "resources", {
...method.resources,
os: e.target.value || null,
})
}
/>
<Input
placeholder="Version"
type="number"
value={method.resources.version || ""}
onChange={(e) =>
updateInstallMethod(index, "resources", {
...method.resources,
version: e.target.value ? Number(e.target.value) : null,
})
}
/>
<Button
variant="destructive"
onClick={() => removeInstallMethod(index)}
>
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
</Button>
</div>
))}
<Button onClick={addInstallMethod}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
</Button>
<h3 className="text-xl font-semibold">Default Credentials</h3>
<Input
placeholder="Username"
value={script.default_credentials.username || ""}
onChange={(e) =>
updateScript("default_credentials", {
...script.default_credentials,
username: e.target.value || null,
})
}
/>
<Input
placeholder="Password"
value={script.default_credentials.password || ""}
onChange={(e) =>
updateScript("default_credentials", {
...script.default_credentials,
password: e.target.value || null,
})
}
/>
<h3 className="text-xl font-semibold">Notes</h3>
{script.notes.map((note, index) => (
<div key={index} className="space-y-2 border p-4 rounded">
<Input
placeholder="Note Text"
value={note.text}
onChange={(e) => updateNote(index, "text", e.target.value)}
/>
<Input
placeholder="Note Type"
value={note.type}
onChange={(e) => updateNote(index, "type", e.target.value)}
/>
<Button variant="destructive" onClick={() => removeNote(index)}>
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
</Button>
</div>
))}
<Button onClick={addNote}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
</Button>
</form>
</div>
<div className="w-1/2 p-4 bg-gray-100 overflow-y-auto">
<Alert className={isValid ? "bg-green-100" : "bg-red-100"}>
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
<AlertDescription>
{isValid
? "The current JSON is valid according to the schema."
: "The current JSON does not match the required schema."}
</AlertDescription>
</Alert>
<pre className="mt-4 p-4 bg-white rounded shadow">
{JSON.stringify(script, null, 2)}
</pre>
</div>
</div>
);
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/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-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
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, AlertTitle, AlertDescription }

View 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 "@/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-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
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,
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }