From b2bd3587bfd42ec8efdab12f090996f600720c0d Mon Sep 17 00:00:00 2001 From: Leo Goetz Date: Fri, 8 May 2026 15:43:50 +0200 Subject: inital commit --- src/app/datenschutz/page.tsx | 387 ++++++++++++++++++++++++++++++++++++ src/app/globals.css | 168 ++++++++++++++++ src/app/impressum/page.tsx | 71 +++++++ src/app/layout.tsx | 31 +++ src/app/page.tsx | 155 +++++++++++++++ src/components/FooterSection.tsx | 17 ++ src/components/VCardExport.tsx | 88 ++++++++ src/components/VCardForm.tsx | 158 +++++++++++++++ src/components/ui/button.tsx | 59 ++++++ src/components/ui/card.tsx | 92 +++++++++ src/components/ui/checkbox.tsx | 32 +++ src/components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++++++++ src/components/ui/input.tsx | 21 ++ src/lib/definitions.ts | 7 + src/lib/utils.ts | 6 + 15 files changed, 1549 insertions(+) create mode 100644 src/app/datenschutz/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/impressum/page.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/FooterSection.tsx create mode 100644 src/components/VCardExport.tsx create mode 100644 src/components/VCardForm.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/lib/definitions.ts create mode 100644 src/lib/utils.ts (limited to 'src') 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 ( +
+
+

+ Datenschutzerklärung +

+ +
+

+ 1. Verantwortlicher für die Datenverarbeitung +

+

+ Leo Götz +

c/o IP-Management #9337

+

Ludwig-Erhard-Str. 18

+

20459 Hamburg

+

+

E-Mail: info [at] leogtz.de

+

+ 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. +

+
+ +
+

+ 2. Allgemeine Informationen zur Datenverarbeitung +

+

+ Umfang der Verarbeitung personenbezogener Daten +

+

+ 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. +

+

+ Rechtsgrundlagen der Datenverarbeitung +

+

+ Die Verarbeitung personenbezogener Daten erfolgt auf Basis der + folgenden Rechtsgrundlagen: +

+
    +
  • + Art. 6 Abs. 1 lit. f DSGVO – Verarbeitung erfolgt + aufgrund meines berechtigten Interesses an der Sicherheit und + Stabilität der Website sowie zur Fehleranalyse und Optimierung. +
  • +
+

+ Hosting und Server-Logfiles +

+

Diese Website wird gehostet bei:

+

+ IP-Projects GmbH & Co. KG +
+

+ Am Vogelherd 14 +
D - 97295 Waldbrunn +

+ + ip-projects.de + +

+

+ 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: +

+
    +
  • IP-Adresse des anfragenden Geräts
  • +
  • Datum und Uhrzeit des Zugriffs
  • +
  • Aufgerufene URL (Seite oder Datei)
  • +
  • HTTP-Methode (z. B. GET, POST)
  • +
  • HTTP-Statuscode der Antwort
  • +
  • Übertragene Datenmenge in Bytes
  • +
  • Referrer-URL (zuvor besuchte Seite, sofern übermittelt)
  • +
  • + Browser-Kennung (User-Agent), einschließlich verwendetem Browser, + Betriebssystem und Spracheinstellungen +
  • +
+

+ 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. +

+

+ Speicherdauer: Die Logfiles werden nach spätestens 7 Tagen + automatisch gelöscht, sofern keine sicherheitsrelevanten Vorfälle + eine längere Aufbewahrung erfordern +

+
+ +
+

+ 3. Erhebung und Speicherung personenbezogener Daten sowie Art und + Zweck der Verwendung +

+

+ 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. +

+

+ Die einzigen Daten, die im Rahmen der technischen Bereitstellung + meiner Website erfasst werden, sind in den Server-Logfiles + gespeichert (siehe Abschnitt 2: + + Allgemeine Informationen zur Datenverarbeitung + + ). Diese Daten dienen ausschließlich der Sicherstellung des + störungsfreien Betriebs der Website sowie zur Fehleranalyse und + werden nicht mit anderen Datenquellen zusammengeführt. +

+
+ +
+

+ 4. Weitergabe von Daten an Dritte +

+

Ich übermittle keine personenbezogenen Daten an Dritte.

+

+ 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. +

+
+ +
+

+ 5. Rechte der betroffenen Personen +

+

+ Als betroffene Person haben Sie nach der Datenschutz-Grundverordnung + (DSGVO) folgende Rechte in Bezug auf die Verarbeitung Ihrer + personenbezogenen Daten: +

+ +

+ Recht auf Auskunft (Art. 15 DSGVO) +

+

+ 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. +

+ +

+ Recht auf Berichtigung (Art. 16 DSGVO) +

+

+ Falls Ihre personenbezogenen Daten unrichtig oder unvollständig + sind, haben Sie das Recht, die unverzügliche Berichtigung oder + Vervollständigung dieser Daten zu verlangen. +

+ +

+ Recht auf Löschung („Recht auf Vergessenwerden“) (Art. 17 DSGVO) +

+

+ Sie können verlangen, dass Ihre personenbezogenen Daten gelöscht + werden, sofern einer der folgenden Gründe zutrifft: +

+
    +
  • + Die Daten sind für die Zwecke, für die sie erhoben wurden, nicht + mehr erforderlich. +
  • +
  • + Sie widerrufen Ihre Einwilligung und es gibt keine andere + Rechtsgrundlage für die Verarbeitung. +
  • +
  • + Sie legen Widerspruch gegen die Verarbeitung ein (siehe Art. 21 + DSGVO). +
  • +
  • Die Daten wurden unrechtmäßig verarbeitet.
  • +
  • + Die Löschung ist zur Erfüllung einer rechtlichen Verpflichtung + erforderlich. +
  • +
+ +

+ Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO) +

+

+ 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. +

+ +

+ Recht auf Datenübertragbarkeit (Art. 20 DSGVO) +

+

+ 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. +

+ +

+ Widerspruchsrecht gegen die Verarbeitung (Art. 21 DSGVO) +

+

+ 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. +

+ +

+ Recht auf Beschwerde bei einer Aufsichtsbehörde (Art. 77 DSGVO) +

+

+ 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. +

+ +

+ Zuständige Aufsichtsbehörde für Datenschutz in Deutschland: +

+

+ + Bayerisches Landesamt für Datenschutzaufsicht (BayLDA) + +
+ Promenade 18

+ 91522 Ansbach

+ Deutschland +

+

+ Telefon: +49 (0) 981 180093-0

+ E-Mail: + + poststelle@lda.bayern.de + +

+ Website: + + https://www.lda.bayern.de + +

+

+ Eine Liste aller Datenschutzaufsichtsbehörden in Deutschland finden + Sie hier:

+ + https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html + +

+
+ +
+

+ 6. Verwendung von Cookies und Tracking-Technologien +

+

+ Meine Website verwendet keine Cookies und setzt keine + Tracking-Technologien ein, die personenbezogene Daten speichern oder + an Dritte weitergeben. +

+
+ +
+

+ 7. Einsatz von Drittanbietern und Tools +

+

+ Ich verwende keine externen Drittanbieter oder Cloud-Dienste zur + Verarbeitung personenbezogener Daten. +

+
+ +
+

8. Datensicherheit

+

+ 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. +

+

+ Zu den von mir eingesetzten Sicherheitsmaßnahmen gehören unter + anderem: +

+
    +
  • + Regelmäßige Aktualisierung des Servers: Mein VPS + wird kontinuierlich mit Sicherheitsupdates versorgt, um bekannte + Sicherheitslücken zu schließen. +
  • +
  • + SSL/TLS-Verschlüsselung: 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. +
  • +
  • + Firewall-Schutz: Zum Schutz gegen unbefugte + Zugriffe und Angriffe setze ich Firewalls ein, die den + Datenverkehr überwachen und verdächtige Aktivitäten blockieren. +
  • +
  • + DDoS-Schutz: Maßnahmen zum Schutz gegen + Distributed-Denial-of-Service (DDoS)-Angriffe sorgen dafür, dass + meine Website auch bei unerwartet hohem Datenverkehr erreichbar + bleibt. +
  • +
+

+ 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. +

+

+ Falls Sie Fragen zur Sicherheit meiner Website haben, können Sie + mich jederzeit unter + + info@leogtz.de + {" "} + kontaktieren. +

+
+ +
+

+ 9. Änderungen der Datenschutzerklärung +

+

+ 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. +

+

+ Falls durch eine Änderung der Datenschutzerklärung eine Mitwirkung + Ihrerseits erforderlich wird (z. B. eine erneute Einwilligung), + werde ich Sie rechtzeitig darüber informieren. +

+

+ Letzte Aktualisierung: 21. März 2026 +

+
+
+
+ ); +} 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 ( +
+
+

Impressum

+ {/* Angaben gemäß § 5 TMG */} +

Angaben gemäß § 5 TMG

+

+ Leo Götz +

+

c/o IP-Management #9337

+

Ludwig-Erhard-Str. 18

+

20459 Hamburg

+ {/* Kontakt */} +

Kontakt

+

E-Mail: info [at] leogtz.de

+ {/* Haftung für Inhalte */} +

Haftung für Inhalte

+

+ Als Diensteanbieter bin ich gemäß § 7 Abs. 1 TMG für + eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen + verantwortlich. Nach §§ 8 bis 10 TMG 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. +

+

+ 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. +

+ {/* Haftung für Links */} +

Haftung für Links

+

+ 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. +

+ {/* EU-Streitschlichtung */} +

EU-Streitschlichtung

+

+ Die Europäische Kommission stellt eine Plattform zur + Online-Streitbeilegung (OS) bereit:{" "} + + https://ec.europa.eu/consumers/odr/ + + .
+ Unsere E-Mail-Adresse finden Sie oben im Impressum. +

+

+ Ich bin weder verpflichtet noch bereit, an einem + Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle + teilzunehmen. +

+ {/* Letzte Aktualisierung */} +

Letzte Aktualisierung

+

21. März 2026

+
+
+ ); +} 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 ( + + +
{children}
+ + + + ); +} 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([ + { + 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(""); + + // 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 ( +
+

Leo's VCard Generator

+ {/* Show Component based on section */} +
+ {section == "form" && ( + setVcardData(data)} + /> + )} + {section == "export" && ( + + )} +
+
+ ); +} 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 ( +
+
+ + Impressum + + + Datenschutz + +
+

© 2025 Leo Götz

+
+ ); +} diff --git a/src/components/VCardExport.tsx b/src/components/VCardExport.tsx new file mode 100644 index 0000000..82afa59 --- /dev/null +++ b/src/components/VCardExport.tsx @@ -0,0 +1,88 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardTitle, + CardFooter, +} from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useEffect, useRef, useState } from "react"; +import QRCode from "qrcode"; + +interface VCardExportProps { + sectionChange: (section: string) => void; + vcardText: any; +} + +export default function VCardExport({ + sectionChange, + vcardText, +}: VCardExportProps) { + const canvasRef = useRef(null); + const [resolution, setResolution] = useState("512"); + + useEffect(() => { + if (canvasRef.current) { + QRCode.toCanvas(canvasRef.current, vcardText, { + width: 256, + margin: 2, + }).catch(console.error); + } + }, [vcardText]); + + const downloadQRCode = () => { + const offscreenCanvas = document.createElement("canvas"); + QRCode.toCanvas(offscreenCanvas, vcardText, { + width: Number(resolution), + margin: 2, + }) + .then(() => { + const pngUrl = offscreenCanvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = pngUrl; + link.download = "vcard_qrcode.png"; + link.click(); + }) + .catch(console.error); + }; + + return ( + + + VCard Qrcode + + Sie können nun die Größe (Standard = 512x512px) auswählen und den + QRCode downloaden. + + + + + + + + + + + + 1024x1024 + + 512x512 + 256x256 + + + {" "} + + + + ); +} diff --git a/src/components/VCardForm.tsx b/src/components/VCardForm.tsx new file mode 100644 index 0000000..3033cac --- /dev/null +++ b/src/components/VCardForm.tsx @@ -0,0 +1,158 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + CardFooter, +} from "@/components/ui/card"; +import { SelectionItem } from "@/lib/definitions"; + +interface VCardFormProps { + selection: SelectionItem[]; + onValueChange: (field: string, value: string) => void; + sectionChange: (section: string) => void; + onGenerate: any; +} + +export default function VCardForm({ + selection, + onValueChange, + sectionChange, + onGenerate, +}: VCardFormProps) { + function generateVCardText(fields: SelectionItem[]) { + const vcard = []; + const getValue = (name: string) => + fields.find((f) => f.field === name)?.value?.trim() || ""; + + // Hilfsfelder sammeln + const firstName = getValue("Vorname"); + const lastName = getValue("Nachname"); + const nickname = getValue("Spitzname"); + const emailHome = getValue("Email (Privat)"); + const emailWork = getValue("Email (Arbeit)"); + const telHome = getValue("Telefonnummer (Zuhause)"); + const telMobile = getValue("Telefonnummer (Mobile)"); + const telWork = getValue("Telefonnummer (Arbeit)"); + const country = getValue("Land"); + const city = getValue("Ort"); + const street = getValue("Straße"); + const zip = getValue("Postleitzahl"); + const bday = getValue("Geburtstag"); + const org = getValue("Firma"); + const department = getValue("Abteilung"); + const title = getValue("Berufsbezeichnung"); + const role = getValue("Rolle im Unternehmen"); + const url = getValue("Homepage/Persönliche Webseite"); + const note = getValue("Notizen"); + + // Start vCard + vcard.push("BEGIN:VCARD"); + vcard.push("VERSION:3.0"); + + // N (strukturierter Name) + if (firstName || lastName) { + vcard.push(`N:${lastName};${firstName};;;`); + vcard.push(`FN:${firstName} ${lastName}`.trim()); + } + + // Spitzname + if (nickname) vcard.push(`NICKNAME:${nickname}`); + + // Email + if (emailHome) vcard.push(`EMAIL;TYPE=HOME:${emailHome}`); + if (emailWork) vcard.push(`EMAIL;TYPE=WORK:${emailWork}`); + + // Telefonnummer + if (telHome) vcard.push(`TEL;TYPE=HOME:${telHome}`); + if (telMobile) vcard.push(`TEL;TYPE=CELL:${telMobile}`); + if (telWork) vcard.push(`TEL;TYPE=WORK:${telWork}`); + + // Geburtstag + if (bday) vcard.push(`BDAY:${bday}`); + + // Organisation + if (org || department) + vcard.push(`ORG:${org}${department ? ";" + department : ""}`); + + // Berufsbezeichnung + if (title) vcard.push(`TITLE:${title}`); + + // Rolle + if (role) vcard.push(`ROLE:${role}`); + + // URL + if (url) vcard.push(`URL:${url}`); + + // Adresse + if (street || city || zip || country) { + vcard.push(`ADR:;;${street};${city};;${zip};${country}`); + } + + // Notizen + if (note) vcard.push(`NOTE:${note}`); + + // Ende der vCard + vcard.push("END:VCARD"); + + // Rückgabe + return vcard.join("\r\n"); + } + const handleGenerate = () => { + const text = generateVCardText(selection); + onGenerate(text); + sectionChange("export"); + }; + return ( + + + VCard Daten eingeben + + Alle Eingaben werden ausschließlich lokal im Browser verarbeitet. +
Es werden keine Daten an den Server gesendet. +
+ (*) Felder sind pflicht! +
+
+ +
+ {/* Loop trough selection and if field is required add a (*) */} + {selection.map((item) => ( +
+ + onValueChange(item.field, e.target.value)} + required={item.required} + /> +
+ ))} +
+
+ + + +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..ec51e9c --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } 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)); +} -- cgit v1.3.1