summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/FooterSection.tsx17
-rw-r--r--src/components/VCardExport.tsx88
-rw-r--r--src/components/VCardForm.tsx158
-rw-r--r--src/components/ui/button.tsx59
-rw-r--r--src/components/ui/card.tsx92
-rw-r--r--src/components/ui/checkbox.tsx32
-rw-r--r--src/components/ui/dropdown-menu.tsx257
-rw-r--r--src/components/ui/input.tsx21
8 files changed, 724 insertions, 0 deletions
diff --git a/src/components/FooterSection.tsx b/src/components/FooterSection.tsx
new file mode 100644
index 0000000..f9b4d64
--- /dev/null
+++ b/src/components/FooterSection.tsx
@@ -0,0 +1,17 @@
+import Link from "next/link";
+
+export default function FooterSection() {
+ return (
+ <footer className="text-white text-center py-4 mt-10">
+ <div className="space-x-4">
+ <Link href="/impressum" className="hover:underline">
+ Impressum
+ </Link>
+ <Link href="/datenschutz" className="hover:underline">
+ Datenschutz
+ </Link>
+ </div>
+ <p className="mt-2 text-sm">© 2025 Leo Götz</p>
+ </footer>
+ );
+}
diff --git a/src/components/VCardExport.tsx b/src/components/VCardExport.tsx
new file mode 100644
index 0000000..82afa59
--- /dev/null
+++ b/src/components/VCardExport.tsx
@@ -0,0 +1,88 @@
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+ CardFooter,
+} from "@/components/ui/card";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useEffect, useRef, useState } from "react";
+import QRCode from "qrcode";
+
+interface VCardExportProps {
+ sectionChange: (section: string) => void;
+ vcardText: any;
+}
+
+export default function VCardExport({
+ sectionChange,
+ vcardText,
+}: VCardExportProps) {
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
+ const [resolution, setResolution] = useState<string>("512");
+
+ useEffect(() => {
+ if (canvasRef.current) {
+ QRCode.toCanvas(canvasRef.current, vcardText, {
+ width: 256,
+ margin: 2,
+ }).catch(console.error);
+ }
+ }, [vcardText]);
+
+ const downloadQRCode = () => {
+ const offscreenCanvas = document.createElement("canvas");
+ QRCode.toCanvas(offscreenCanvas, vcardText, {
+ width: Number(resolution),
+ margin: 2,
+ })
+ .then(() => {
+ const pngUrl = offscreenCanvas.toDataURL("image/png");
+ const link = document.createElement("a");
+ link.href = pngUrl;
+ link.download = "vcard_qrcode.png";
+ link.click();
+ })
+ .catch(console.error);
+ };
+
+ return (
+ <Card className="w-[600px] mx-5">
+ <CardHeader>
+ <CardTitle>VCard Qrcode</CardTitle>
+ <CardDescription>
+ Sie können nun die Größe (Standard = 512x512px) auswählen und den
+ QRCode downloaden.
+ </CardDescription>
+ </CardHeader>
+ <CardFooter className="flex justify-between">
+ <Button onClick={() => sectionChange("form")}>Zurück</Button>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline">Größe anpassen</Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-56">
+ <DropdownMenuRadioGroup
+ value={resolution}
+ onValueChange={setResolution}
+ >
+ <DropdownMenuRadioItem value="1024">
+ 1024x1024
+ </DropdownMenuRadioItem>
+ <DropdownMenuRadioItem value="512">512x512</DropdownMenuRadioItem>
+ <DropdownMenuRadioItem value="256">256x256</DropdownMenuRadioItem>
+ </DropdownMenuRadioGroup>
+ </DropdownMenuContent>
+ </DropdownMenu>{" "}
+ <Button onClick={downloadQRCode}>Download</Button>
+ </CardFooter>
+ </Card>
+ );
+}
diff --git a/src/components/VCardForm.tsx b/src/components/VCardForm.tsx
new file mode 100644
index 0000000..3033cac
--- /dev/null
+++ b/src/components/VCardForm.tsx
@@ -0,0 +1,158 @@
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+ CardFooter,
+} from "@/components/ui/card";
+import { SelectionItem } from "@/lib/definitions";
+
+interface VCardFormProps {
+ selection: SelectionItem[];
+ onValueChange: (field: string, value: string) => void;
+ sectionChange: (section: string) => void;
+ onGenerate: any;
+}
+
+export default function VCardForm({
+ selection,
+ onValueChange,
+ sectionChange,
+ onGenerate,
+}: VCardFormProps) {
+ function generateVCardText(fields: SelectionItem[]) {
+ const vcard = [];
+ const getValue = (name: string) =>
+ fields.find((f) => f.field === name)?.value?.trim() || "";
+
+ // Hilfsfelder sammeln
+ const firstName = getValue("Vorname");
+ const lastName = getValue("Nachname");
+ const nickname = getValue("Spitzname");
+ const emailHome = getValue("Email (Privat)");
+ const emailWork = getValue("Email (Arbeit)");
+ const telHome = getValue("Telefonnummer (Zuhause)");
+ const telMobile = getValue("Telefonnummer (Mobile)");
+ const telWork = getValue("Telefonnummer (Arbeit)");
+ const country = getValue("Land");
+ const city = getValue("Ort");
+ const street = getValue("Straße");
+ const zip = getValue("Postleitzahl");
+ const bday = getValue("Geburtstag");
+ const org = getValue("Firma");
+ const department = getValue("Abteilung");
+ const title = getValue("Berufsbezeichnung");
+ const role = getValue("Rolle im Unternehmen");
+ const url = getValue("Homepage/Persönliche Webseite");
+ const note = getValue("Notizen");
+
+ // Start vCard
+ vcard.push("BEGIN:VCARD");
+ vcard.push("VERSION:3.0");
+
+ // N (strukturierter Name)
+ if (firstName || lastName) {
+ vcard.push(`N:${lastName};${firstName};;;`);
+ vcard.push(`FN:${firstName} ${lastName}`.trim());
+ }
+
+ // Spitzname
+ if (nickname) vcard.push(`NICKNAME:${nickname}`);
+
+ // Email
+ if (emailHome) vcard.push(`EMAIL;TYPE=HOME:${emailHome}`);
+ if (emailWork) vcard.push(`EMAIL;TYPE=WORK:${emailWork}`);
+
+ // Telefonnummer
+ if (telHome) vcard.push(`TEL;TYPE=HOME:${telHome}`);
+ if (telMobile) vcard.push(`TEL;TYPE=CELL:${telMobile}`);
+ if (telWork) vcard.push(`TEL;TYPE=WORK:${telWork}`);
+
+ // Geburtstag
+ if (bday) vcard.push(`BDAY:${bday}`);
+
+ // Organisation
+ if (org || department)
+ vcard.push(`ORG:${org}${department ? ";" + department : ""}`);
+
+ // Berufsbezeichnung
+ if (title) vcard.push(`TITLE:${title}`);
+
+ // Rolle
+ if (role) vcard.push(`ROLE:${role}`);
+
+ // URL
+ if (url) vcard.push(`URL:${url}`);
+
+ // Adresse
+ if (street || city || zip || country) {
+ vcard.push(`ADR:;;${street};${city};;${zip};${country}`);
+ }
+
+ // Notizen
+ if (note) vcard.push(`NOTE:${note}`);
+
+ // Ende der vCard
+ vcard.push("END:VCARD");
+
+ // Rückgabe
+ return vcard.join("\r\n");
+ }
+ const handleGenerate = () => {
+ const text = generateVCardText(selection);
+ onGenerate(text);
+ sectionChange("export");
+ };
+ return (
+ <Card className="w-[600] mx-5">
+ <CardHeader>
+ <CardTitle>VCard Daten eingeben</CardTitle>
+ <CardDescription>
+ Alle Eingaben werden ausschließlich lokal im Browser verarbeitet.
+ <br /> Es werden keine Daten an den Server gesendet.
+ <br />
+ (*) Felder sind pflicht!
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <form className="space-y-4">
+ {/* Loop trough selection and if field is required add a (*) */}
+ {selection.map((item) => (
+ <div key={item.field} className="space-y-2">
+ <label className="text-sm font-medium">
+ {item.field}
+ {item.required ? (
+ <span className="text-red-500 ml-1">*</span>
+ ) : null}
+ </label>
+ <Input
+ placeholder={item.placeholder}
+ value={item.value}
+ onChange={(e: any) => onValueChange(item.field, e.target.value)}
+ required={item.required}
+ />
+ </div>
+ ))}
+ </form>
+ </CardContent>
+ <CardFooter className="flex justify-end">
+ <Button
+ onClick={handleGenerate}
+ disabled={
+ !selection
+ .find((i) => i.field.toLowerCase() === "vorname")
+ ?.value.trim() ||
+ !selection
+ .find((i) => i.field.toLowerCase() === "nachname")
+ ?.value.trim()
+ }
+ >
+ Generieren
+ </Button>
+ </CardFooter>
+ </Card>
+ );
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps<typeof buttonVariants> & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ data-slot="button"
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props}
+ />
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..d05bbc6
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card"
+ className={cn(
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-header"
+ className={cn(
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-title"
+ className={cn("leading-none font-semibold", className)}
+ {...props}
+ />
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-description"
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-action"
+ className={cn(
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-content"
+ className={cn("px-6", className)}
+ {...props}
+ />
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-footer"
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+ {...props}
+ />
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..fa0e4b5
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { CheckIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
+ return (
+ <CheckboxPrimitive.Root
+ data-slot="checkbox"
+ className={cn(
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+ className
+ )}
+ {...props}
+ >
+ <CheckboxPrimitive.Indicator
+ data-slot="checkbox-indicator"
+ className="flex items-center justify-center text-current transition-none"
+ >
+ <CheckIcon className="size-3.5" />
+ </CheckboxPrimitive.Indicator>
+ </CheckboxPrimitive.Root>
+ )
+}
+
+export { Checkbox }
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..ec51e9c
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+ return (
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+ return (
+ <DropdownMenuPrimitive.Trigger
+ data-slot="dropdown-menu-trigger"
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+ return (
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ data-slot="dropdown-menu-content"
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+ className
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+ return (
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+ <DropdownMenuPrimitive.Item
+ data-slot="dropdown-menu-item"
+ data-inset={inset}
+ data-variant={variant}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+ return (
+ <DropdownMenuPrimitive.CheckboxItem
+ data-slot="dropdown-menu-checkbox-item"
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ className
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <CheckIcon className="size-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.CheckboxItem>
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+ return (
+ <DropdownMenuPrimitive.RadioGroup
+ data-slot="dropdown-menu-radio-group"
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+ return (
+ <DropdownMenuPrimitive.RadioItem
+ data-slot="dropdown-menu-radio-item"
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ className
+ )}
+ {...props}
+ >
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <CircleIcon className="size-2 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean
+}) {
+ return (
+ <DropdownMenuPrimitive.Label
+ data-slot="dropdown-menu-label"
+ data-inset={inset}
+ className={cn(
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+ return (
+ <DropdownMenuPrimitive.Separator
+ data-slot="dropdown-menu-separator"
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+ <span
+ data-slot="dropdown-menu-shortcut"
+ className={cn(
+ "text-muted-foreground ml-auto text-xs tracking-widest",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean
+}) {
+ return (
+ <DropdownMenuPrimitive.SubTrigger
+ data-slot="dropdown-menu-sub-trigger"
+ data-inset={inset}
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRightIcon className="ml-auto size-4" />
+ </DropdownMenuPrimitive.SubTrigger>
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+ return (
+ <DropdownMenuPrimitive.SubContent
+ data-slot="dropdown-menu-sub-content"
+ className={cn(
+ "bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..03295ca
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+ <input
+ type={type}
+ data-slot="input"
+ className={cn(
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Input }