summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/datenschutz/page.tsx387
-rw-r--r--src/app/globals.css168
-rw-r--r--src/app/impressum/page.tsx71
-rw-r--r--src/app/layout.tsx31
-rw-r--r--src/app/page.tsx155
-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
-rw-r--r--src/lib/definitions.ts7
-rw-r--r--src/lib/utils.ts6
15 files changed, 1549 insertions, 0 deletions
diff --git a/src/app/datenschutz/page.tsx b/src/app/datenschutz/page.tsx
new file mode 100644
index 0000000..d412d6f
--- /dev/null
+++ b/src/app/datenschutz/page.tsx
@@ -0,0 +1,387 @@
+export default function Datenschutz() {
+ return (
+ <div className="container mx-auto px-4 py-10">
+ <div className="card shadow-lg bg-base-100 p-8">
+ <h1 className="text-4xl font-bold text-center mb-8">
+ Datenschutzerklärung
+ </h1>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 1. Verantwortlicher für die Datenverarbeitung
+ </h2>
+ <p>
+ <strong>Leo Götz</strong>
+ <p>c/o IP-Management #9337</p>
+ <p>Ludwig-Erhard-Str. 18</p>
+ <p>20459 Hamburg</p>
+ </p>
+ <p>E-Mail: info [at] leogtz.de</p>
+ <p>
+ Ein gesetzlicher Datenschutzbeauftragter ist nicht erforderlich, da
+ die gesetzlichen Voraussetzungen gemäß Art. 37 DSGVO nicht erfüllt
+ sind. Bei Fragen zum Datenschutz können Sie sich jedoch jederzeit an
+ den oben genannten Verantwortlichen wenden.
+ </p>
+ </section>
+
+ <section className="mb-8" id="section-2">
+ <h2 className="text-2xl font-semibold mb-4">
+ 2. Allgemeine Informationen zur Datenverarbeitung
+ </h2>
+ <h3 className="text-xl font-semibold mb-2">
+ Umfang der Verarbeitung personenbezogener Daten
+ </h3>
+ <p>
+ Ich verarbeite personenbezogene Daten der Nutzer grundsätzlich nur,
+ soweit dies zur Bereitstellung einer funktionsfähigen Website sowie
+ meiner Inhalte und Leistungen erforderlich ist. Die Verarbeitung
+ personenbezogener Daten erfolgt regelmäßig nur nach Einwilligung des
+ Nutzers oder wenn die Verarbeitung durch gesetzliche Vorschriften
+ gestattet ist.
+ </p>
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Rechtsgrundlagen der Datenverarbeitung
+ </h3>
+ <p>
+ Die Verarbeitung personenbezogener Daten erfolgt auf Basis der
+ folgenden Rechtsgrundlagen:
+ </p>
+ <ul className="list-disc ml-6">
+ <li>
+ <strong>Art. 6 Abs. 1 lit. f DSGVO</strong> – Verarbeitung erfolgt
+ aufgrund meines berechtigten Interesses an der Sicherheit und
+ Stabilität der Website sowie zur Fehleranalyse und Optimierung.
+ </li>
+ </ul>
+ <h2 className="font-semibold mt-6 mb-4">
+ Hosting und Server-Logfiles
+ </h2>
+ <p>Diese Website wird gehostet bei:</p>
+ <p>
+ <strong>IP-Projects GmbH & Co. KG</strong>
+ <br />
+ </p>
+ Am Vogelherd 14
+ <br />D - 97295 Waldbrunn
+ <p>
+ <a
+ href="https://www.ip-projects.de"
+ target="_blank"
+ rel="noopener noreferrer"
+ className="link"
+ >
+ ip-projects.de
+ </a>
+ </p>
+ <p>
+ Beim Aufruf meiner Website werden durch den Webserver automatisch
+ Informationen in sogenannten Server-Logfiles erfasst und
+ gespeichert. Diese Daten werden von Ihrem Browser automatisch
+ übermittelt und umfassen:
+ </p>
+ <ul className="list-disc ml-6">
+ <li>IP-Adresse des anfragenden Geräts</li>
+ <li>Datum und Uhrzeit des Zugriffs</li>
+ <li>Aufgerufene URL (Seite oder Datei)</li>
+ <li>HTTP-Methode (z. B. GET, POST)</li>
+ <li>HTTP-Statuscode der Antwort</li>
+ <li>Übertragene Datenmenge in Bytes</li>
+ <li>Referrer-URL (zuvor besuchte Seite, sofern übermittelt)</li>
+ <li>
+ Browser-Kennung (User-Agent), einschließlich verwendetem Browser,
+ Betriebssystem und Spracheinstellungen
+ </li>
+ </ul>
+ <p>
+ Die Verarbeitung dieser Daten ist technisch notwendig, um die
+ Website korrekt ausliefern zu können, die Systemsicherheit und
+ -stabilität zu gewährleisten sowie Fehler zu diagnostizieren. Eine
+ Zusammenführung dieser Daten mit anderen Datenquellen findet nicht
+ statt.
+ </p>
+ <p>
+ Speicherdauer: Die Logfiles werden nach spätestens 7 Tagen
+ automatisch gelöscht, sofern keine sicherheitsrelevanten Vorfälle
+ eine längere Aufbewahrung erfordern
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 3. Erhebung und Speicherung personenbezogener Daten sowie Art und
+ Zweck der Verwendung
+ </h2>
+ <p>
+ Beim Besuch dieser Website werden grundsätzlich keine
+ personenbezogenen Daten aktiv erhoben oder gespeichert. Die
+ Erstellung der vCard-Daten (Vorname, Nachname, Telefonnummer usw.)
+ sowie der zugehörige QR-Code erfolgt vollständig clientseitig im
+ Browser des Nutzers. Es findet keine Übermittlung, Speicherung oder
+ Verarbeitung dieser Daten auf meinem Server oder durch Dritte statt.
+ Eine darüber hinausgehende Verarbeitung personenbezogener Daten
+ findet nicht statt.
+ </p>
+ <p>
+ Die einzigen Daten, die im Rahmen der technischen Bereitstellung
+ meiner Website erfasst werden, sind in den Server-Logfiles
+ gespeichert (siehe Abschnitt 2:
+ <a href="#section-2" className="link">
+ Allgemeine Informationen zur Datenverarbeitung
+ </a>
+ ). Diese Daten dienen ausschließlich der Sicherstellung des
+ störungsfreien Betriebs der Website sowie zur Fehleranalyse und
+ werden nicht mit anderen Datenquellen zusammengeführt.
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 4. Weitergabe von Daten an Dritte
+ </h2>
+ <p>Ich übermittle keine personenbezogenen Daten an Dritte.</p>
+ <p>
+ Es erfolgt weder eine Verarbeitung durch externe Dienstleister noch
+ eine Weitergabe der Daten an Drittländer. Mein Server befindet sich
+ in Deutschland, und es werden keine externen Tracking- oder
+ Analyse-Dienste wie Google Analytics verwendet.
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 5. Rechte der betroffenen Personen
+ </h2>
+ <p>
+ Als betroffene Person haben Sie nach der Datenschutz-Grundverordnung
+ (DSGVO) folgende Rechte in Bezug auf die Verarbeitung Ihrer
+ personenbezogenen Daten:
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Auskunft (Art. 15 DSGVO)
+ </h3>
+ <p>
+ Sie haben das Recht, eine Bestätigung darüber zu verlangen, ob
+ personenbezogene Daten über Sie verarbeitet werden. Ist dies der
+ Fall, können Sie Auskunft über diese Daten sowie weitere
+ Informationen zur Verarbeitung erhalten.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Berichtigung (Art. 16 DSGVO)
+ </h3>
+ <p>
+ Falls Ihre personenbezogenen Daten unrichtig oder unvollständig
+ sind, haben Sie das Recht, die unverzügliche Berichtigung oder
+ Vervollständigung dieser Daten zu verlangen.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Löschung („Recht auf Vergessenwerden“) (Art. 17 DSGVO)
+ </h3>
+ <p>
+ Sie können verlangen, dass Ihre personenbezogenen Daten gelöscht
+ werden, sofern einer der folgenden Gründe zutrifft:
+ </p>
+ <ul className="list-disc ml-6">
+ <li>
+ Die Daten sind für die Zwecke, für die sie erhoben wurden, nicht
+ mehr erforderlich.
+ </li>
+ <li>
+ Sie widerrufen Ihre Einwilligung und es gibt keine andere
+ Rechtsgrundlage für die Verarbeitung.
+ </li>
+ <li>
+ Sie legen Widerspruch gegen die Verarbeitung ein (siehe Art. 21
+ DSGVO).
+ </li>
+ <li>Die Daten wurden unrechtmäßig verarbeitet.</li>
+ <li>
+ Die Löschung ist zur Erfüllung einer rechtlichen Verpflichtung
+ erforderlich.
+ </li>
+ </ul>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)
+ </h3>
+ <p>
+ Unter bestimmten Voraussetzungen können Sie die Einschränkung der
+ Verarbeitung Ihrer personenbezogenen Daten verlangen, z. B. wenn die
+ Richtigkeit der Daten bestritten wird oder die Verarbeitung
+ unrechtmäßig ist, Sie aber keine Löschung wünschen.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Datenübertragbarkeit (Art. 20 DSGVO)
+ </h3>
+ <p>
+ Falls die Verarbeitung auf Ihrer Einwilligung oder einem Vertrag
+ beruht und automatisiert erfolgt, haben Sie das Recht, Ihre
+ personenbezogenen Daten in einem strukturierten, gängigen und
+ maschinenlesbaren Format zu erhalten oder diese an einen anderen
+ Verantwortlichen übertragen zu lassen.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Widerspruchsrecht gegen die Verarbeitung (Art. 21 DSGVO)
+ </h3>
+ <p>
+ Sie haben das Recht, aus Gründen, die sich aus Ihrer besonderen
+ Situation ergeben, jederzeit Widerspruch gegen die Verarbeitung
+ Ihrer personenbezogenen Daten einzulegen, sofern die Verarbeitung
+ auf einem berechtigten Interesse (Art. 6 Abs. 1 lit. f DSGVO)
+ beruht. Ich werde Ihre Daten dann nicht mehr verarbeiten, es sei
+ denn, es liegen zwingende schutzwürdige Gründe für die Verarbeitung
+ vor.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Recht auf Beschwerde bei einer Aufsichtsbehörde (Art. 77 DSGVO)
+ </h3>
+ <p>
+ Falls Sie der Ansicht sind, dass die Verarbeitung Ihrer
+ personenbezogenen Daten gegen die DSGVO verstößt, haben Sie das
+ Recht, sich bei einer Datenschutzaufsichtsbehörde zu beschweren.
+ </p>
+
+ <h3 className="text-xl font-semibold mt-4 mb-2">
+ Zuständige Aufsichtsbehörde für Datenschutz in Deutschland:
+ </h3>
+ <p>
+ <strong>
+ Bayerisches Landesamt für Datenschutzaufsicht (BayLDA)
+ </strong>
+ <br />
+ Promenade 18<br></br>
+ 91522 Ansbach<br></br>
+ Deutschland
+ </p>
+ <p>
+ Telefon: +49 (0) 981 180093-0<br></br>
+ E-Mail:
+ <a href="mailto:poststelle@lda.bayern.de" className="link">
+ poststelle@lda.bayern.de
+ </a>
+ <br></br>
+ Website:
+ <a
+ href="https://www.lda.bayern.de"
+ target="_blank"
+ rel="noopener noreferrer"
+ className="link"
+ >
+ https://www.lda.bayern.de
+ </a>
+ </p>
+ <p>
+ Eine Liste aller Datenschutzaufsichtsbehörden in Deutschland finden
+ Sie hier:<br></br>
+ <a
+ href="https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ className="link"
+ >
+ https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html
+ </a>
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 6. Verwendung von Cookies und Tracking-Technologien
+ </h2>
+ <p>
+ Meine Website verwendet keine Cookies und setzt keine
+ Tracking-Technologien ein, die personenbezogene Daten speichern oder
+ an Dritte weitergeben.
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 7. Einsatz von Drittanbietern und Tools
+ </h2>
+ <p>
+ Ich verwende keine externen Drittanbieter oder Cloud-Dienste zur
+ Verarbeitung personenbezogener Daten.
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">8. Datensicherheit</h2>
+ <p>
+ Ich treffe geeignete technische und organisatorische Maßnahmen, um
+ die Sicherheit Ihrer personenbezogenen Daten zu gewährleisten und
+ sie vor unbefugtem Zugriff, Verlust oder Manipulation zu schützen.
+ </p>
+ <p>
+ Zu den von mir eingesetzten Sicherheitsmaßnahmen gehören unter
+ anderem:
+ </p>
+ <ul className="list-disc ml-6">
+ <li>
+ <strong>Regelmäßige Aktualisierung des Servers:</strong> Mein VPS
+ wird kontinuierlich mit Sicherheitsupdates versorgt, um bekannte
+ Sicherheitslücken zu schließen.
+ </li>
+ <li>
+ <strong>SSL/TLS-Verschlüsselung:</strong> Die Übertragung von
+ Daten zwischen Ihrem Browser und meiner Website erfolgt
+ verschlüsselt über das SSL/TLS-Protokoll. Dadurch werden Ihre
+ Daten während der Übertragung vor unbefugtem Zugriff geschützt.
+ </li>
+ <li>
+ <strong>Firewall-Schutz:</strong> Zum Schutz gegen unbefugte
+ Zugriffe und Angriffe setze ich Firewalls ein, die den
+ Datenverkehr überwachen und verdächtige Aktivitäten blockieren.
+ </li>
+ <li>
+ <strong>DDoS-Schutz:</strong> Maßnahmen zum Schutz gegen
+ Distributed-Denial-of-Service (DDoS)-Angriffe sorgen dafür, dass
+ meine Website auch bei unerwartet hohem Datenverkehr erreichbar
+ bleibt.
+ </li>
+ </ul>
+ <p>
+ Trotz aller Sicherheitsmaßnahmen weise ich darauf hin, dass die
+ Übertragung von Daten im Internet (z. B. bei der Kommunikation per
+ E-Mail) Sicherheitslücken aufweisen kann. Ein vollständiger Schutz
+ der Daten vor dem Zugriff durch Dritte ist nicht möglich.
+ </p>
+ <p>
+ Falls Sie Fragen zur Sicherheit meiner Website haben, können Sie
+ mich jederzeit unter
+ <a href="mailto:info@leogtz.de" className="link">
+ info@leogtz.de
+ </a>{" "}
+ kontaktieren.
+ </p>
+ </section>
+
+ <section className="mb-8">
+ <h2 className="text-2xl font-semibold mb-4">
+ 9. Änderungen der Datenschutzerklärung
+ </h2>
+ <p>
+ Ich behalte mir vor, diese Datenschutzerklärung jederzeit zu ändern,
+ um sie an rechtliche Anforderungen oder Änderungen meines Angebots
+ anzupassen. Die jeweils aktuelle Version der Datenschutzerklärung
+ ist jederzeit auf dieser Website abrufbar.
+ </p>
+ <p>
+ Falls durch eine Änderung der Datenschutzerklärung eine Mitwirkung
+ Ihrerseits erforderlich wird (z. B. eine erneute Einwilligung),
+ werde ich Sie rechtzeitig darüber informieren.
+ </p>
+ <p>
+ <strong>Letzte Aktualisierung:</strong> 21. März 2026
+ </p>
+ </section>
+ </div>
+ </div>
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..d81e27c
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,168 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.3211 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.3211 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.3211 0 0);
+ --primary: oklch(0.6231 0.188 259.8145);
+ --primary-foreground: oklch(1 0 0);
+ --secondary: oklch(0.967 0.0029 264.5419);
+ --secondary-foreground: oklch(0.4461 0.0263 256.8018);
+ --muted: oklch(0.9846 0.0017 247.8389);
+ --muted-foreground: oklch(0.551 0.0234 264.3637);
+ --accent: oklch(0.9514 0.025 236.8242);
+ --accent-foreground: oklch(0.3791 0.1378 265.5222);
+ --destructive: oklch(0.6368 0.2078 25.3313);
+ --destructive-foreground: oklch(1 0 0);
+ --border: oklch(0.9276 0.0058 264.5313);
+ --input: oklch(0.9276 0.0058 264.5313);
+ --ring: oklch(0.6231 0.188 259.8145);
+ --chart-1: oklch(0.6231 0.188 259.8145);
+ --chart-2: oklch(0.5461 0.2152 262.8809);
+ --chart-3: oklch(0.4882 0.2172 264.3763);
+ --chart-4: oklch(0.4244 0.1809 265.6377);
+ --chart-5: oklch(0.3791 0.1378 265.5222);
+ --sidebar: oklch(0.9846 0.0017 247.8389);
+ --sidebar-foreground: oklch(0.3211 0 0);
+ --sidebar-primary: oklch(0.6231 0.188 259.8145);
+ --sidebar-primary-foreground: oklch(1 0 0);
+ --sidebar-accent: oklch(0.9514 0.025 236.8242);
+ --sidebar-accent-foreground: oklch(0.3791 0.1378 265.5222);
+ --sidebar-border: oklch(0.9276 0.0058 264.5313);
+ --sidebar-ring: oklch(0.6231 0.188 259.8145);
+ --font-sans: Inter, sans-serif;
+ --font-serif: Source Serif 4, serif;
+ --font-mono: JetBrains Mono, monospace;
+ --radius: 0.375rem;
+ --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-sm:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow-md:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
+ --shadow-lg:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
+ --shadow-xl:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
+ --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
+}
+
+.dark {
+ --background: oklch(0.2046 0 0);
+ --foreground: oklch(0.9219 0 0);
+ --card: oklch(0.2686 0 0);
+ --card-foreground: oklch(0.9219 0 0);
+ --popover: oklch(0.2686 0 0);
+ --popover-foreground: oklch(0.9219 0 0);
+ --primary: oklch(0.6231 0.188 259.8145);
+ --primary-foreground: oklch(1 0 0);
+ --secondary: oklch(0.2686 0 0);
+ --secondary-foreground: oklch(0.9219 0 0);
+ --muted: oklch(0.2686 0 0);
+ --muted-foreground: oklch(0.7155 0 0);
+ --accent: oklch(0.3791 0.1378 265.5222);
+ --accent-foreground: oklch(0.8823 0.0571 254.1284);
+ --destructive: oklch(0.6368 0.2078 25.3313);
+ --destructive-foreground: oklch(1 0 0);
+ --border: oklch(0.3715 0 0);
+ --input: oklch(0.3715 0 0);
+ --ring: oklch(0.6231 0.188 259.8145);
+ --chart-1: oklch(0.7137 0.1434 254.624);
+ --chart-2: oklch(0.6231 0.188 259.8145);
+ --chart-3: oklch(0.5461 0.2152 262.8809);
+ --chart-4: oklch(0.4882 0.2172 264.3763);
+ --chart-5: oklch(0.4244 0.1809 265.6377);
+ --sidebar: oklch(0.2046 0 0);
+ --sidebar-foreground: oklch(0.9219 0 0);
+ --sidebar-primary: oklch(0.6231 0.188 259.8145);
+ --sidebar-primary-foreground: oklch(1 0 0);
+ --sidebar-accent: oklch(0.3791 0.1378 265.5222);
+ --sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284);
+ --sidebar-border: oklch(0.3715 0 0);
+ --sidebar-ring: oklch(0.6231 0.188 259.8145);
+ --font-sans: Inter, sans-serif;
+ --font-serif: Source Serif 4, serif;
+ --font-mono: JetBrains Mono, monospace;
+ --radius: 0.375rem;
+ --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-sm:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow-md:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
+ --shadow-lg:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
+ --shadow-xl:
+ 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
+ --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+ --font-sans: var(--font-sans);
+ --font-mono: var(--font-mono);
+ --font-serif: var(--font-serif);
+
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+
+ --shadow-2xs: var(--shadow-2xs);
+ --shadow-xs: var(--shadow-xs);
+ --shadow-sm: var(--shadow-sm);
+ --shadow: var(--shadow);
+ --shadow-md: var(--shadow-md);
+ --shadow-lg: var(--shadow-lg);
+ --shadow-xl: var(--shadow-xl);
+ --shadow-2xl: var(--shadow-2xl);
+}
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx
new file mode 100644
index 0000000..57268a1
--- /dev/null
+++ b/src/app/impressum/page.tsx
@@ -0,0 +1,71 @@
+export default function Impressum() {
+ return (
+ <div className="container mx-auto px-4 py-10">
+ <div className="card shadow-lg bg-base-100 p-8">
+ <h1 className="text-4xl font-bold text-center mb-8">Impressum</h1>
+ {/* Angaben gemäß § 5 TMG */}
+ <h2 className="text-2xl font-semibold mt-4">Angaben gemäß § 5 TMG</h2>
+ <p>
+ <strong>Leo Götz</strong>
+ </p>
+ <p>c/o IP-Management #9337</p>
+ <p>Ludwig-Erhard-Str. 18</p>
+ <p>20459 Hamburg</p>
+ {/* Kontakt */}
+ <h2 className="text-2xl font-semibold mt-6">Kontakt</h2>
+ <p>E-Mail: info [at] leogtz.de</p>
+ {/* Haftung für Inhalte */}
+ <h2 className="text-2xl font-semibold mt-6">Haftung für Inhalte</h2>
+ <p>
+ Als Diensteanbieter bin ich gemäß <strong>§ 7 Abs. 1 TMG</strong> für
+ eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen
+ verantwortlich. Nach <strong>§§ 8 bis 10 TMG</strong> bin ich jedoch
+ nicht verpflichtet, übermittelte oder gespeicherte fremde
+ Informationen zu überwachen oder nach Umständen zu forschen, die auf
+ eine rechtswidrige Tätigkeit hinweisen.
+ </p>
+ <p>
+ Verpflichtungen zur Entfernung oder Sperrung der Nutzung von
+ Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt.
+ Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der
+ Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden
+ entsprechender Rechtsverletzungen werde ich diese Inhalte umgehend
+ entfernen.
+ </p>
+ {/* Haftung für Links */}
+ <h2 className="text-2xl font-semibold mt-6">Haftung für Links</h2>
+ <p>
+ Meine Webseite kann Links zu externen Webseiten Dritter enthalten, auf
+ deren Inhalte ich keinen Einfluss habe. Deshalb kann ich für diese
+ fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
+ verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der
+ Seiten verantwortlich.
+ </p>
+ {/* EU-Streitschlichtung */}
+ <h2 className="text-2xl font-semibold mt-6">EU-Streitschlichtung</h2>
+ <p>
+ Die Europäische Kommission stellt eine Plattform zur
+ Online-Streitbeilegung (OS) bereit:{" "}
+ <a
+ href="https://ec.europa.eu/consumers/odr/"
+ target="_blank"
+ rel="noopener noreferrer"
+ className="link"
+ >
+ https://ec.europa.eu/consumers/odr/
+ </a>
+ .<br />
+ Unsere E-Mail-Adresse finden Sie oben im Impressum.
+ </p>
+ <p>
+ Ich bin weder verpflichtet noch bereit, an einem
+ Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle
+ teilzunehmen.
+ </p>
+ {/* Letzte Aktualisierung */}
+ <h2 className="text-2xl font-semibold mt-6">Letzte Aktualisierung</h2>
+ <p>21. März 2026</p>
+ </div>
+ </div>
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..7c74651
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,31 @@
+import type { Metadata } from "next";
+import "./globals.css";
+import FooterSection from "@/components/FooterSection";
+
+export const metadata: Metadata = {
+ title: "Leo's VCard Generator",
+ description: "Generiere kostenlos und clientseitig deine VCard als QRCode.",
+ openGraph: {
+ type: "website",
+ url: "https://vcard.leogtz.de",
+ title: "Leo's VCard Generator",
+ description: "Generiere kostenlos und clientseitig deine VCard als QRCode.",
+ siteName: "Leo's VCard Generator",
+ images: [{ url: "https://leogtz.de/logo.png" }],
+ },
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="de">
+ <body className={`antialiased dark flex flex-col min-h-screen`}>
+ <main className="grow">{children}</main>
+ <FooterSection />
+ </body>
+ </html>
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..aed6b8d
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,155 @@
+"use client";
+import VCardExport from "@/components/VCardExport";
+import VCardForm from "@/components/VCardForm";
+import { useState } from "react";
+import { SelectionItem } from "@/lib/definitions";
+
+export default function Home() {
+ // Declare and initialize selection array
+ const [section, setSection] = useState("form");
+ const [selection, setSelection] = useState<SelectionItem[]>([
+ {
+ field: "Vorname",
+ value: "",
+ placeholder: "Max",
+ required: true,
+ },
+ {
+ field: "Nachname",
+ value: "",
+ placeholder: "Mustermann",
+ required: true,
+ },
+ {
+ field: "Spitzname",
+ value: "",
+ placeholder: "Maxi",
+ required: false,
+ },
+ {
+ field: "Email",
+ value: "",
+ placeholder: "max@example.com",
+ required: false,
+ },
+ {
+ field: "Telefonnummer (Zuhause)",
+ value: "",
+ placeholder: "+49 123 456789",
+ required: false,
+ },
+ {
+ field: "Telefonnummer (Mobile)",
+ value: "",
+ placeholder: "+49 123 456789",
+ required: false,
+ },
+ {
+ field: "Telefonnummer (Arbeit)",
+ value: "",
+ placeholder: "+49 123 456789",
+ required: false,
+ },
+ {
+ field: "Land",
+ value: "",
+ placeholder: "Deutschland",
+ required: false,
+ },
+ {
+ field: "Ort",
+ value: "",
+ placeholder: "Musterstadt",
+ required: false,
+ },
+ {
+ field: "Straße",
+ value: "",
+ placeholder: "Musterstraße 1",
+ required: false,
+ },
+ {
+ field: "Postleitzahl",
+ value: "",
+ placeholder: "12345",
+ required: false,
+ },
+ {
+ field: "Geburtstag",
+ value: "",
+ placeholder: "1990-01-01 (jahr-monat-tag)",
+ required: false,
+ },
+ {
+ field: "Firma",
+ value: "",
+ placeholder: "Beispiel GmbH",
+ required: false,
+ },
+ {
+ field: "Abteilung",
+ value: "",
+ placeholder: "Entwicklung",
+ required: false,
+ },
+ {
+ field: "Berufsbezeichnung",
+ value: "",
+ placeholder: "Softwareentwickler",
+ required: false,
+ },
+ {
+ field: "Rolle im Unternehmen",
+ value: "",
+ placeholder: "Projektleiter",
+ required: false,
+ },
+ {
+ field: "Homepage/Persönliche Webseite",
+ value: "",
+ placeholder: "https://example.com",
+ required: false,
+ },
+ {
+ field: "Notizen",
+ value: "",
+ placeholder: "Zusätzliche Informationen...",
+ required: false,
+ },
+ ]);
+ const [vcardData, setVcardData] = useState<string>("");
+
+ // Setting value for field when value changes
+ const handleValueChange = (field: string, value: string) => {
+ setSelection((prev) =>
+ prev.map((item) => (item.field === field ? { ...item, value } : item)),
+ );
+ };
+
+ const handleSectionChange = (section: string) => {
+ setSection(section);
+ };
+
+ return (
+ <div className="flex flex-col items-center mt-5 min-h-screen">
+ <h1 className="text-2xl font-bold mb-8">Leo&apos;s VCard Generator</h1>
+ {/* Show Component based on section */}
+ <div className="flex justify-center gap-8 w-full">
+ {section == "form" && (
+ <VCardForm
+ selection={selection}
+ onValueChange={handleValueChange}
+ sectionChange={handleSectionChange}
+ onGenerate={(data: any) => setVcardData(data)}
+ />
+ )}
+ {section == "export" && (
+ <VCardExport
+ sectionChange={handleSectionChange}
+ vcardText={vcardData}
+ />
+ )}
+ </div>
+ </div>
+ );
+}
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 }
diff --git a/src/lib/definitions.ts b/src/lib/definitions.ts
new file mode 100644
index 0000000..318c425
--- /dev/null
+++ b/src/lib/definitions.ts
@@ -0,0 +1,7 @@
+// declare selectionitem types
+export interface SelectionItem {
+ field: string;
+ value: string;
+ placeholder: string;
+ required: boolean;
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}