Phase A: acht weitere QR-Typen
E-Mail, SMS, WhatsApp, WLAN, Standort, vCard, Zahlung und App-Link sind umgesetzt. Damit sind 9 der 15 Typen nutzbar. Erweiterungen am Typ-Vertrag, die dafuer noetig waren: - supportsStatic: nicht jeder Typ kann statisch. Beim App-Link steht das Ziel erst beim Scannen fest, also gibt es ihn nur dynamisch. - RedirectContext: die Weiterleitung reicht den User-Agent durch, damit der App-Link iOS, Android und alles Uebrige unterscheiden kann. - assertDynamicPossible(): supportsDynamic ist eine Eigenschaft des Typs, aber nicht jede Eingabe taugt dafuer. Ein GiroCode ist kein Web-Ziel, obwohl der Typ 'Zahlung' als Zahlungslink sehr wohl dynamisch sein kann. Ohne diese Probe liess sich ein Code speichern, der erst beim Scannen aufgefallen waere - also nach dem Drucken. Greift beim Anlegen und beim Bearbeiten. Sorgfalt bei den Formaten, gesammelt in src/lib/qr/encoding.ts: - WLAN maskiert ; , : \\ und setzt rein hexadezimale Werte in Anfuehrungszeichen, sonst deuten Geraete sie als Hexzahl. - vCard 3.0 mit CRLF, Maskierung und Faltung nach 75 Oktetten, oktettweise gezaehlt, damit kein mehrbytiges Zeichen zerrissen wird. - GiroCode nach EPC069-12 mit fester Feldreihenfolge, IBAN-Pruefsumme nach Modulo 97 und der 331-Byte-Grenze. - Rufnummern werden nach E.164 normalisiert, '(0)' faellt weg. - Standort nimmt Koordinaten auch aus einem Google-Maps- oder Apple-Link an. Formular-Bausteine (Select, Textarea, Checkbox, FieldRow, FieldSet) fuer die mehrfeldrigen Typen ergaenzt. Geprueft: 225 Pruefungen, davon 80 neu - Dekodierung je Typ aus dem fertigen PNG, Validierung, Betriebsarten, geraeteabhaengige Weiterleitung und die Formulare im Browser.
This commit is contained in:
@@ -1,18 +1,36 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { WebsiteForm } from "@/components/qr-forms/website-form";
|
||||
import { PaymentForm } from "@/components/qr-forms/payment-form";
|
||||
import {
|
||||
AppForm,
|
||||
EmailForm,
|
||||
LocationForm,
|
||||
SmsForm,
|
||||
WhatsappForm,
|
||||
WifiForm,
|
||||
} from "@/components/qr-forms/simple-forms";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
import { VCardForm } from "@/components/qr-forms/vcard-form";
|
||||
import { WebsiteForm } from "@/components/qr-forms/website-form";
|
||||
import type { QrTypeId } from "@/lib/qr/type-module";
|
||||
|
||||
/**
|
||||
* Zuordnung QR-Typ → Formular-Komponente.
|
||||
*
|
||||
* Gegenstück zur Typ-Registry in `src/lib/qr/registry.ts`: dort liegt die
|
||||
* Logik (Validierung, Kodierung), hier die Eingabemaske. Ein V2-Typ braucht
|
||||
* genau einen zusätzlichen Eintrag an beiden Stellen.
|
||||
* Logik (Validierung, Kodierung), hier die Eingabemaske. Ein neuer Typ
|
||||
* braucht genau einen zusätzlichen Eintrag an beiden Stellen.
|
||||
*/
|
||||
export const qrForms: Partial<Record<QrTypeId, ComponentType<QrFormProps>>> = {
|
||||
website: WebsiteForm,
|
||||
vcard: VCardForm,
|
||||
app: AppForm,
|
||||
whatsapp: WhatsappForm,
|
||||
email: EmailForm,
|
||||
sms: SmsForm,
|
||||
location: LocationForm,
|
||||
payment: PaymentForm,
|
||||
wifi: WifiForm,
|
||||
};
|
||||
|
||||
export type { QrFormProps };
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { Field, FieldRow, Notice, Select, TextInput } from "@/components/ui";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
import { formatIban } from "@/lib/qr/encoding";
|
||||
import {
|
||||
PAYMENT_VARIANTS,
|
||||
PAYMENT_VARIANT_LABELS,
|
||||
type PaymentVariant,
|
||||
} from "@/lib/qr/types/payment";
|
||||
|
||||
function text(value: Record<string, unknown>, key: string): string {
|
||||
const raw = value[key];
|
||||
return typeof raw === "string" ? raw : "";
|
||||
}
|
||||
|
||||
export function PaymentForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const variante = (
|
||||
PAYMENT_VARIANTS.includes(value.variante as PaymentVariant)
|
||||
? value.variante
|
||||
: "girocode"
|
||||
) as PaymentVariant;
|
||||
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field label="Zahlungsart" htmlFor="pay-variante">
|
||||
<Select
|
||||
id="pay-variante"
|
||||
value={variante}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("variante", event.target.value)}
|
||||
>
|
||||
{PAYMENT_VARIANTS.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{PAYMENT_VARIANT_LABELS[option]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
{variante === "girocode" ? (
|
||||
<>
|
||||
<Field label="Empfänger" htmlFor="pay-empfaenger" error={error}>
|
||||
<TextInput
|
||||
id="pay-empfaenger"
|
||||
placeholder="Name des Kontoinhabers"
|
||||
value={text(value, "empfaenger")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("empfaenger", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="IBAN"
|
||||
htmlFor="pay-iban"
|
||||
hint="Wird beim Verlassen des Feldes in Vierergruppen dargestellt."
|
||||
>
|
||||
<TextInput
|
||||
id="pay-iban"
|
||||
spellCheck={false}
|
||||
placeholder="DE02 1203 0000 0000 2020 51"
|
||||
className="font-mono"
|
||||
value={text(value, "iban")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("iban", event.target.value.toUpperCase())}
|
||||
onBlur={(event) => set("iban", formatIban(event.target.value))}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<FieldRow>
|
||||
<Field label="BIC (optional)" htmlFor="pay-bic">
|
||||
<TextInput
|
||||
id="pay-bic"
|
||||
spellCheck={false}
|
||||
className="font-mono uppercase"
|
||||
placeholder="BYLADEM1001"
|
||||
value={text(value, "bic")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("bic", event.target.value.toUpperCase())}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Betrag in Euro (optional)"
|
||||
htmlFor="pay-betrag"
|
||||
hint="Leer lassen, damit der Zahlende ihn selbst einträgt."
|
||||
>
|
||||
<TextInput
|
||||
id="pay-betrag"
|
||||
inputMode="decimal"
|
||||
placeholder="49,90"
|
||||
value={text(value, "betrag")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("betrag", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</FieldRow>
|
||||
|
||||
<Field label="Verwendungszweck (optional)" htmlFor="pay-zweck">
|
||||
<TextInput
|
||||
id="pay-zweck"
|
||||
maxLength={140}
|
||||
placeholder="Rechnung 2026-0815"
|
||||
value={text(value, "verwendungszweck")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("verwendungszweck", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Notice tone="info">
|
||||
Der GiroCode wird von Banking-Apps gelesen und füllt die
|
||||
Überweisung vor. Er funktioniert nur als statischer Code — für
|
||||
einen dynamischen Code die Variante „Zahlungslink“ wählen.
|
||||
</Notice>
|
||||
</>
|
||||
) : (
|
||||
<Field
|
||||
label="Zahlungsadresse"
|
||||
htmlFor="pay-link"
|
||||
error={error}
|
||||
hint="Zum Beispiel PayPal.Me, ein Stripe-Zahlungslink oder die Zahlungsseite deines Shops."
|
||||
>
|
||||
<TextInput
|
||||
id="pay-link"
|
||||
placeholder="https://paypal.me/name/49.90"
|
||||
value={text(value, "zahlungslink")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("zahlungslink", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
Field,
|
||||
FieldRow,
|
||||
Notice,
|
||||
Select,
|
||||
TextArea,
|
||||
TextInput,
|
||||
} from "@/components/ui";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
import { parseCoordinates } from "@/lib/qr/encoding";
|
||||
import {
|
||||
WIFI_SECURITY,
|
||||
WIFI_SECURITY_LABELS,
|
||||
type WifiSecurity,
|
||||
} from "@/lib/qr/types/wifi";
|
||||
|
||||
/** Liest ein Textfeld aus den generischen Formulardaten. */
|
||||
function text(value: Record<string, unknown>, key: string): string {
|
||||
const raw = value[key];
|
||||
return typeof raw === "string" ? raw : "";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ E-Mail */
|
||||
|
||||
export function EmailForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field label="Empfänger" htmlFor="email-empfaenger" error={error}>
|
||||
<TextInput
|
||||
id="email-empfaenger"
|
||||
type="email"
|
||||
inputMode="email"
|
||||
autoComplete="email"
|
||||
placeholder="name@example.com"
|
||||
value={text(value, "empfaenger")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("empfaenger", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Betreff (optional)" htmlFor="email-betreff">
|
||||
<TextInput
|
||||
id="email-betreff"
|
||||
value={text(value, "betreff")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("betreff", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Nachricht (optional)"
|
||||
htmlFor="email-text"
|
||||
hint="Wird als vorbereiteter Text in die E-Mail übernommen."
|
||||
>
|
||||
<TextArea
|
||||
id="email-text"
|
||||
value={text(value, "text")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("text", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- SMS */
|
||||
|
||||
export function SmsForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field
|
||||
label="Rufnummer"
|
||||
htmlFor="sms-nummer"
|
||||
error={error}
|
||||
hint="International mit Ländervorwahl, z. B. +49 151 23456789"
|
||||
>
|
||||
<TextInput
|
||||
id="sms-nummer"
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
placeholder="+49 151 23456789"
|
||||
value={text(value, "nummer")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("nummer", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Nachricht (optional)" htmlFor="sms-text">
|
||||
<TextArea
|
||||
id="sms-text"
|
||||
value={text(value, "text")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("text", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- WhatsApp */
|
||||
|
||||
export function WhatsappForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field
|
||||
label="WhatsApp-Nummer"
|
||||
htmlFor="wa-nummer"
|
||||
error={error}
|
||||
hint="Mit Ländervorwahl und ohne führende Null, z. B. +49 151 23456789"
|
||||
>
|
||||
<TextInput
|
||||
id="wa-nummer"
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
placeholder="+49 151 23456789"
|
||||
value={text(value, "nummer")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("nummer", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Vorbereitete Nachricht (optional)"
|
||||
htmlFor="wa-text"
|
||||
hint="Steht beim Öffnen des Chats bereits im Eingabefeld."
|
||||
>
|
||||
<TextArea
|
||||
id="wa-text"
|
||||
value={text(value, "text")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("text", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- WLAN */
|
||||
|
||||
export function WifiForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const security = (
|
||||
WIFI_SECURITY.includes(value.verschluesselung as WifiSecurity)
|
||||
? value.verschluesselung
|
||||
: "WPA"
|
||||
) as WifiSecurity;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field label="Netzwerkname (SSID)" htmlFor="wifi-ssid" error={error}>
|
||||
<TextInput
|
||||
id="wifi-ssid"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder="Gaeste-WLAN"
|
||||
value={text(value, "ssid")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange({ ...value, ssid: event.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Verschlüsselung" htmlFor="wifi-security">
|
||||
<Select
|
||||
id="wifi-security"
|
||||
value={security}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, verschluesselung: event.target.value })
|
||||
}
|
||||
>
|
||||
{WIFI_SECURITY.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{WIFI_SECURITY_LABELS[option]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
{security !== "nopass" ? (
|
||||
<Field label="Passwort" htmlFor="wifi-password">
|
||||
<TextInput
|
||||
id="wifi-password"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={text(value, "passwort")}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, passwort: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
|
||||
<Checkbox
|
||||
id="wifi-hidden"
|
||||
label="Netzwerk ist versteckt"
|
||||
hint="Nur ankreuzen, wenn die SSID nicht gesendet wird."
|
||||
checked={value.versteckt === true}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, versteckt: event.target.checked })
|
||||
}
|
||||
/>
|
||||
|
||||
<Notice tone="info">
|
||||
Der Code enthält das Passwort im Klartext — wer ihn scannt oder
|
||||
abfotografiert, kennt es. Für Gästenetze ist das gewollt, für das
|
||||
eigene Netz eher nicht.
|
||||
</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Standort */
|
||||
|
||||
export function LocationForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
/** Erlaubt es, einen Kartenlink einzufügen, statt Zahlen abzutippen. */
|
||||
const handlePaste = (raw: string) => {
|
||||
const parsed = parseCoordinates(raw);
|
||||
if (parsed) {
|
||||
onChange({
|
||||
...value,
|
||||
breitengrad: String(parsed.latitude),
|
||||
laengengrad: String(parsed.longitude),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field
|
||||
label="Kartenlink einfügen (optional)"
|
||||
htmlFor="geo-paste"
|
||||
hint="Adresse aus Google Maps, Apple Karten oder OpenStreetMap — die Koordinaten werden übernommen."
|
||||
>
|
||||
<TextInput
|
||||
id="geo-paste"
|
||||
placeholder="https://www.google.com/maps/@52.516,13.377,15z"
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
if (handlePaste(event.target.value)) {
|
||||
event.target.value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<FieldRow>
|
||||
<Field label="Breitengrad" htmlFor="geo-lat" error={error}>
|
||||
<TextInput
|
||||
id="geo-lat"
|
||||
inputMode="decimal"
|
||||
placeholder="52.516"
|
||||
value={text(value, "breitengrad")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("breitengrad", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Längengrad" htmlFor="geo-lon">
|
||||
<TextInput
|
||||
id="geo-lon"
|
||||
inputMode="decimal"
|
||||
placeholder="13.377"
|
||||
value={text(value, "laengengrad")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("laengengrad", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</FieldRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- App-Link */
|
||||
|
||||
export function AppForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const set = (key: string, next: string) =>
|
||||
onChange({ ...value, [key]: next });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Notice tone="info">
|
||||
Der App-Link gibt es nur als dynamischen Code: erst beim Scannen steht
|
||||
fest, welches Gerät ihn öffnet — und damit, wohin es geht.
|
||||
</Notice>
|
||||
|
||||
<Field
|
||||
label="iOS — App Store"
|
||||
htmlFor="app-ios"
|
||||
error={error}
|
||||
hint="https://apps.apple.com/…"
|
||||
>
|
||||
<TextInput
|
||||
id="app-ios"
|
||||
placeholder="https://apps.apple.com/de/app/…"
|
||||
value={text(value, "ios")}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("ios", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Android — Google Play"
|
||||
htmlFor="app-android"
|
||||
hint="https://play.google.com/store/apps/…"
|
||||
>
|
||||
<TextInput
|
||||
id="app-android"
|
||||
placeholder="https://play.google.com/store/apps/details?id=…"
|
||||
value={text(value, "android")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("android", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Alle übrigen Geräte"
|
||||
htmlFor="app-fallback"
|
||||
hint="Wird auf Rechnern und unbekannten Geräten geöffnet — meist die Website der App."
|
||||
>
|
||||
<TextInput
|
||||
id="app-fallback"
|
||||
placeholder="https://example.com"
|
||||
value={text(value, "fallback")}
|
||||
disabled={disabled}
|
||||
onChange={(event) => set("fallback", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldRow,
|
||||
FieldSet,
|
||||
TextArea,
|
||||
TextInput,
|
||||
} from "@/components/ui";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
|
||||
function text(value: Record<string, unknown>, key: string): string {
|
||||
const raw = value[key];
|
||||
return typeof raw === "string" ? raw : "";
|
||||
}
|
||||
|
||||
export function VCardForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const field = (key: string) => ({
|
||||
value: text(value, key),
|
||||
disabled,
|
||||
onChange: (event: { target: { value: string } }) =>
|
||||
onChange({ ...value, [key]: event.target.value }),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<FieldRow>
|
||||
<Field label="Vorname" htmlFor="vc-vorname">
|
||||
<TextInput id="vc-vorname" autoComplete="given-name" {...field("vorname")} />
|
||||
</Field>
|
||||
<Field label="Nachname" htmlFor="vc-nachname" error={error}>
|
||||
<TextInput
|
||||
id="vc-nachname"
|
||||
autoComplete="family-name"
|
||||
invalid={Boolean(error)}
|
||||
{...field("nachname")}
|
||||
/>
|
||||
</Field>
|
||||
</FieldRow>
|
||||
|
||||
<FieldSet legend="Firma">
|
||||
<FieldRow>
|
||||
<Field label="Organisation" htmlFor="vc-org">
|
||||
<TextInput id="vc-org" autoComplete="organization" {...field("organisation")} />
|
||||
</Field>
|
||||
<Field label="Position" htmlFor="vc-position">
|
||||
<TextInput
|
||||
id="vc-position"
|
||||
autoComplete="organization-title"
|
||||
{...field("position")}
|
||||
/>
|
||||
</Field>
|
||||
</FieldRow>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend="Kontakt">
|
||||
<FieldRow>
|
||||
<Field label="Telefon" htmlFor="vc-tel">
|
||||
<TextInput id="vc-tel" type="tel" inputMode="tel" {...field("telefon")} />
|
||||
</Field>
|
||||
<Field label="Mobil" htmlFor="vc-mobil">
|
||||
<TextInput id="vc-mobil" type="tel" inputMode="tel" {...field("mobil")} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<Field label="E-Mail" htmlFor="vc-email">
|
||||
<TextInput
|
||||
id="vc-email"
|
||||
type="email"
|
||||
inputMode="email"
|
||||
autoComplete="email"
|
||||
{...field("email")}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Webseite" htmlFor="vc-web">
|
||||
<TextInput id="vc-web" placeholder="https://example.com" {...field("webseite")} />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend="Anschrift">
|
||||
<Field label="Straße und Hausnummer" htmlFor="vc-strasse">
|
||||
<TextInput id="vc-strasse" autoComplete="street-address" {...field("strasse")} />
|
||||
</Field>
|
||||
<FieldRow>
|
||||
<Field label="Postleitzahl" htmlFor="vc-plz">
|
||||
<TextInput id="vc-plz" autoComplete="postal-code" {...field("plz")} />
|
||||
</Field>
|
||||
<Field label="Ort" htmlFor="vc-ort">
|
||||
<TextInput id="vc-ort" autoComplete="address-level2" {...field("ort")} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<Field label="Land" htmlFor="vc-land">
|
||||
<TextInput id="vc-land" autoComplete="country-name" {...field("land")} />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend="Sonstiges">
|
||||
<Field
|
||||
label="Notiz"
|
||||
htmlFor="vc-notiz"
|
||||
hint="Landet im Notizfeld des Kontakts."
|
||||
>
|
||||
<TextArea id="vc-notiz" {...field("notiz")} />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user