From b2bd3587bfd42ec8efdab12f090996f600720c0d Mon Sep 17 00:00:00 2001 From: Leo Goetz Date: Fri, 8 May 2026 15:43:50 +0200 Subject: inital commit --- src/components/FooterSection.tsx | 17 +++ src/components/VCardExport.tsx | 88 ++++++++++++ src/components/VCardForm.tsx | 158 ++++++++++++++++++++++ src/components/ui/button.tsx | 59 +++++++++ src/components/ui/card.tsx | 92 +++++++++++++ src/components/ui/checkbox.tsx | 32 +++++ src/components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++++++++++++++++++++ src/components/ui/input.tsx | 21 +++ 8 files changed, 724 insertions(+) create mode 100644 src/components/FooterSection.tsx create mode 100644 src/components/VCardExport.tsx create mode 100644 src/components/VCardForm.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/input.tsx (limited to 'src/components') 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 ( + + ); +} 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(null); + const [resolution, setResolution] = useState("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 ( + + + VCard Qrcode + + Sie können nun die Größe (Standard = 512x512px) auswählen und den + QRCode downloaden. + + + + + + + + + + + + 1024x1024 + + 512x512 + 256x256 + + + {" "} + + + + ); +} 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 ( + + + VCard Daten eingeben + + Alle Eingaben werden ausschließlich lokal im Browser verarbeitet. +
Es werden keine Daten an den Server gesendet. +
+ (*) Felder sind pflicht! +
+
+ +
+ {/* Loop trough selection and if field is required add a (*) */} + {selection.map((item) => ( +
+ + onValueChange(item.field, e.target.value)} + required={item.required} + /> +
+ ))} +
+
+ + + +
+ ); +} 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 & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +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 ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +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) { + return ( + + + + + + ) +} + +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) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +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 ( + + ) +} + +export { Input } -- cgit v1.3.1