feat: QR-Code, vCard-Export, Org-Gruppenansicht im Adressbuch
Deploy / verify (push) Successful in 1m10s
Deploy / deploy (push) Successful in 1m45s

- vcard.ts: shared vCard-Generator (downloadVCard, downloadVCardAll, buildVCardString)
- QrModal.svelte + QrButton.svelte: wiederverwendbare QR-Komponenten mit PNG-Download
- AdressbuchBlock: Org-Gruppenansicht (Toggle), QR + vCard pro Kontakt,
  Buchstaben-Trennzeilen, Such-Highlight, "Alle exportieren", Print-Styles
- ContactCard: vCard-Download-Button
- PostActions: QR-Button für Seiten-URL
- qrcode npm-Paket hinzugefügt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-04 11:15:57 +02:00
parent 4642b32271
commit 982a7a3b73
10 changed files with 802 additions and 104 deletions
+47
View File
@@ -0,0 +1,47 @@
import type { ContactEntry } from "./block-types";
function vcardEscape(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/,/g, "\\,").replace(/;/g, "\\;").replace(/\n/g, "\\n");
}
function buildVCard(c: ContactEntry): string {
const name = vcardEscape(c.name ?? "");
const orgs = (c.organisations ?? [])
.filter((o): o is { name?: string } => typeof o === "object" && !!o.name)
.map((o) => vcardEscape(o.name ?? ""))
.join(", ");
return [
"BEGIN:VCARD",
"VERSION:3.0",
`FN:${name}`,
"N:;;;;",
orgs ? `ORG:${orgs}` : null,
c.role ? `TITLE:${vcardEscape(c.role)}` : null,
c.phone ? `TEL;TYPE=CELL:${c.phone.replace(/\s/g, "")}` : null,
c.email ? `EMAIL:${vcardEscape(c.email)}` : null,
c.note ? `ADR:;;${vcardEscape(c.note)};;;;` : null,
"END:VCARD",
].filter(Boolean).join("\r\n");
}
export function buildVCardString(c: ContactEntry): string {
return buildVCard(c);
}
function triggerDownload(content: string, filename: string): void {
const blob = new Blob([content], { type: "text/vcard;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
export function downloadVCard(c: ContactEntry): void {
triggerDownload(buildVCard(c), `${(c.name ?? "kontakt").toLowerCase().replace(/\s+/g, "-")}.vcf`);
}
export function downloadVCardAll(contacts: ContactEntry[], filename = "kontakte.vcf"): void {
triggerDownload(contacts.map(buildVCard).join("\r\n"), filename);
}