Files
windwiderstand/src/lib/components/QrModal.svelte
T
Peter Meier 982a7a3b73
Deploy / verify (push) Successful in 1m10s
Deploy / deploy (push) Successful in 1m45s
feat: QR-Code, vCard-Export, Org-Gruppenansicht im Adressbuch
- 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>
2026-06-04 11:15:57 +02:00

102 lines
2.7 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import QRCode from "qrcode";
let {
value,
label = "",
sublabel = "",
onclose,
}: {
value: string | null;
label?: string;
sublabel?: string;
onclose: () => void;
} = $props();
let qrSvg = $state("");
let qrPngUrl = $state("");
$effect(() => {
if (!value) { qrSvg = ""; qrPngUrl = ""; return; }
QRCode.toString(value, {
type: "svg",
errorCorrectionLevel: "M",
margin: 2,
color: { dark: "#1c1917", light: "#ffffff" },
}).then((svg) => { qrSvg = svg; });
QRCode.toDataURL(value, {
errorCorrectionLevel: "M",
margin: 2,
width: 512,
color: { dark: "#1c1917", light: "#ffffff" },
}).then((url) => { qrPngUrl = url; });
});
function downloadPng() {
if (!qrPngUrl) return;
const a = document.createElement("a");
a.href = qrPngUrl;
a.download = `${(label || "qrcode").toLowerCase().replace(/\s+/g, "-")}.png`;
a.click();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") onclose();
}
onMount(() => {
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
});
</script>
{#if value}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="not-prose fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={onclose}
>
<div
class="relative w-full max-w-xs rounded-sm border border-stein-200 bg-white p-6 shadow-xl"
onclick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
>
<button
type="button"
onclick={onclose}
class="absolute right-3 top-3 text-stein-400 hover:text-stein-700"
aria-label="Schließen"
>
<Icon icon="mdi:close" class="size-5" />
</button>
{#if label}
<p class="mb-1 text-sm font-medium text-stein-900">{label}</p>
{/if}
{#if sublabel}
<p class="mb-4 text-xs text-stein-400">{sublabel}</p>
{:else}
<div class="mb-4"></div>
{/if}
{#if qrSvg}
{@html qrSvg}
{:else}
<div class="flex h-48 items-center justify-center text-xs text-stein-300">Generiere…</div>
{/if}
<p class="mt-3 text-center text-[0.65rem] text-stein-400">QR-Code scannen</p>
<button
type="button"
onclick={downloadPng}
disabled={!qrPngUrl}
class="btn-outline btn-sm mt-3 w-full justify-center"
>
<Icon icon="mdi:download" class="size-3.5" />
Als PNG herunterladen
</button>
</div>
</div>
{/if}