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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user