982a7a3b73
- 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>
70 lines
2.5 KiB
Svelte
70 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
import Icon from "@iconify/svelte";
|
|
import "$lib/iconify-offline";
|
|
import type { ContactEntry } from "$lib/block-types";
|
|
import { downloadVCard } from "$lib/vcard";
|
|
|
|
let { contact }: { contact: ContactEntry } = $props();
|
|
|
|
const orgNames = $derived(
|
|
(contact.organisations ?? [])
|
|
.filter((o): o is { _slug?: string; name?: string } => typeof o === "object" && o !== null)
|
|
.map((o) => o.name)
|
|
.filter(Boolean) as string[]
|
|
);
|
|
</script>
|
|
|
|
<div
|
|
class="not-prose card-surface relative flex h-full min-w-0 flex-col gap-2 overflow-hidden rounded-sm p-4 transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md"
|
|
>
|
|
<div class="flex flex-col gap-1.5">
|
|
<p class="truncate pr-6 text-sm font-medium leading-tight text-stein-900">{contact.name ?? ""}</p>
|
|
|
|
{#if contact.role || orgNames.length > 0}
|
|
<div class="flex flex-wrap gap-1">
|
|
{#if contact.role}
|
|
<span class="shrink-0 rounded-full border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">
|
|
{contact.role}
|
|
</span>
|
|
{/if}
|
|
{#each orgNames as org}
|
|
<span class="shrink-0 rounded-full border border-wald-200 bg-wald-50 px-1.5 py-0 text-[0.65rem] font-medium text-wald-700">
|
|
{org}
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- vCard download -->
|
|
<button
|
|
type="button"
|
|
title="Kontakt speichern (.vcf)"
|
|
onclick={() => downloadVCard(contact)}
|
|
class="absolute top-3 right-3 text-stein-300 transition-colors hover:text-wald-600"
|
|
>
|
|
<Icon icon="mdi:card-account-details-outline" class="size-4" />
|
|
</button>
|
|
|
|
{#if contact.phone || contact.email}
|
|
<div class="flex flex-col gap-1">
|
|
{#if contact.phone}
|
|
<a href="tel:{contact.phone.replace(/\s/g, '')}" class="inline-flex items-center gap-1.5 text-xs text-stein-600 no-underline hover:text-wald-700">
|
|
<Icon icon="mdi:phone-outline" class="size-3.5 shrink-0 text-stein-400" />
|
|
<span class="truncate">{contact.phone}</span>
|
|
</a>
|
|
{/if}
|
|
{#if contact.email}
|
|
<a href="mailto:{contact.email}" class="inline-flex items-center gap-1.5 text-xs text-stein-600 no-underline hover:text-wald-700">
|
|
<Icon icon="mdi:email-outline" class="size-3.5 shrink-0 text-stein-400" />
|
|
<span class="truncate">{contact.email}</span>
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contact.note}
|
|
<p class="text-[11px] text-stein-400">{contact.note}</p>
|
|
{/if}
|
|
</div>
|