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 sidebarButtonText: 'vCard',
124 allowSetColor: true,
125 name: 'vCard Card'
126} as CardDefinition & { type: 'vcard' };