diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/datenschutz/page.tsx | 387 | ||||
| -rw-r--r-- | src/app/globals.css | 168 | ||||
| -rw-r--r-- | src/app/impressum/page.tsx | 71 | ||||
| -rw-r--r-- | src/app/layout.tsx | 31 | ||||
| -rw-r--r-- | src/app/page.tsx | 155 | ||||
| -rw-r--r-- | src/components/FooterSection.tsx | 17 | ||||
| -rw-r--r-- | src/components/VCardExport.tsx | 88 | ||||
| -rw-r--r-- | src/components/VCardForm.tsx | 158 | ||||
| -rw-r--r-- | src/components/ui/button.tsx | 59 | ||||
| -rw-r--r-- | src/components/ui/card.tsx | 92 | ||||
| -rw-r--r-- | src/components/ui/checkbox.tsx | 32 | ||||
| -rw-r--r-- | src/components/ui/dropdown-menu.tsx | 257 | ||||
| -rw-r--r-- | src/components/ui/input.tsx | 21 | ||||
| -rw-r--r-- | src/lib/definitions.ts | 7 | ||||
| -rw-r--r-- | src/lib/utils.ts | 6 |
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'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)); +} |
