your personal website on atproto - mirror
blento.app
1import { user } from '$lib/atproto/auth.svelte';
2import type { CardDefinition } from '../types';
3import VCardCard from './VCardCard.svelte';
4import VCardCardSettings from './VCardCardSettings.svelte';
5
6// vCard spec: https://wikipedia.org/wiki/VCard
7
8export type VCardFields = {
9 firstName: string;
10 lastName: string;
11 org: string;
12 title: string;
13 email: string;
14 bday: string; // YYYY-MM-DD for input, stored as YYYYMMDD
15 website: string;
16 address: string;
17 note: string;
18};
19
20export const emptyVCardFields: VCardFields = {
21 firstName: '',
22 lastName: '',
23 org: '',
24 title: '',
25 email: '',
26 bday: '',
27 website: '',
28 address: '',
29 note: ''
30};
31
32// Convert YYYY-MM-DD to YYYYMMDD for vCard
33export function formatBdayToVCard(date: string): string {
34 return date.replace(/-/g, '');
35}
36
37// Convert YYYYMMDD to YYYY-MM-DD for input
38export function formatBdayFromVCard(bday: string): string {
39 if (bday.length === 8) {
40 return `${bday.slice(0, 4)}-${bday.slice(4, 6)}-${bday.slice(6, 8)}`;
41 }
42 return bday;
43}
44
45// Generate vCard v4 string from fields
46export function generateVCard(f: VCardFields): string {
47 const lines = ['BEGIN:VCARD', 'VERSION:4.0'];
48 const fn = `${f.firstName} ${f.lastName}`.trim();
49 if (fn) lines.push(`FN:${fn}`);
50 if (f.lastName || f.firstName) lines.push(`N:${f.lastName};${f.firstName};;;`);
51 if (f.org) lines.push(`ORG:${f.org}`);
52 if (f.title) lines.push(`TITLE:${f.title}`);
53 if (f.email) lines.push(`EMAIL:${f.email}`);
54 if (f.bday) lines.push(`BDAY:${formatBdayToVCard(f.bday)}`);
55 if (f.website) lines.push(`URL:${f.website}`);
56 if (f.address) lines.push(`ADR:;;${f.address};;;;`);
57 if (f.note) lines.push(`NOTE:${f.note}`);
58 lines.push('END:VCARD');
59 return lines.join('\n');
60}
61
62// Parse vCard string to fields (supports v3 & v4)
63export function parseVCard(vcard: string): VCardFields {
64 const get = (key: string) => {
65 const m = vcard.match(new RegExp(`^${key}[;:](.*)$`, 'im'));
66 return m?.[1]?.trim() || '';
67 };
68
69 const n = get('N').split(';');
70 let lastName = n[0] || '';
71 let firstName = n[1] || '';
72
73 if (!lastName && !firstName) {
74 const fn = get('FN').split(' ');
75 firstName = fn[0] || '';
76 lastName = fn.slice(1).join(' ') || '';
77 }
78
79 const adr = get('ADR').split(';');
80
81 return {
82 firstName,
83 lastName,
84 org: get('ORG').split(';')[0],
85 title: get('TITLE'),
86 email: get('EMAIL'),
87 bday: formatBdayFromVCard(get('BDAY')),
88 website: get('URL'),
89 address: adr[2] || '',
90 note: get('NOTE')
91 };
92}
93
94// Parse FN (formatted name) or N from vCard
95export function parseVCardName(vcard: string): string {
96 const f = parseVCard(vcard);
97 return `${f.firstName} ${f.lastName}`.trim();
98}
99
100// Parse ORG from vCard
101export function parseVCardOrg(vcard: string): string {
102 return parseVCard(vcard).org;
103}
104
105export const VCardCardDefinition = {
106 type: 'vcard',
107 contentComponent: VCardCard,
108 settingsComponent: VCardCardSettings,
109
110 createNew: (card) => {
111 card.w = 2;
112 card.h = 2;
113 card.mobileW = 4;
114 card.mobileH = 4;
115 const displayName = user.profile?.displayName || user.profile?.handle || '';
116 card.cardData.vcard = generateVCard({
117 ...emptyVCardFields,
118 lastName: displayName
119 });
120 card.cardData.displayName = displayName;
121 },
122
123 allowSetColor: true,
124 name: 'vCard Card',
125 keywords: ['contact', 'phone', 'email', 'address', 'business card'],
126 groups: ['Social'],
127 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z" /></svg>`
128} as CardDefinition & { type: 'vcard' };