119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
import { fullContactDisplayName, type FullContact } from "./types"
|
|
|
|
function escapeVCardValue(value: string): string {
|
|
return value
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/;/g, "\\;")
|
|
.replace(/,/g, "\\,")
|
|
.replace(/\n/g, "\\n")
|
|
}
|
|
|
|
function escapeCsvField(value: string): string {
|
|
if (/[",\n\r]/.test(value)) {
|
|
return `"${value.replace(/"/g, '""')}"`
|
|
}
|
|
return value
|
|
}
|
|
|
|
function sanitizeFilename(name: string): string {
|
|
return name.replace(/[/\\?%*:|"<>]/g, "-").trim() || "contact"
|
|
}
|
|
|
|
export function contactToVCard(contact: FullContact): string {
|
|
const lines: string[] = ["BEGIN:VCARD", "VERSION:3.0"]
|
|
|
|
const last = contact.lastName ?? ""
|
|
const first = contact.firstName ?? ""
|
|
const middle = contact.middleName ?? ""
|
|
const prefix = contact.namePrefix ?? ""
|
|
const suffix = contact.nameSuffix ?? ""
|
|
lines.push(
|
|
`N:${escapeVCardValue(last)};${escapeVCardValue(first)};${escapeVCardValue(middle)};${escapeVCardValue(prefix)};${escapeVCardValue(suffix)}`
|
|
)
|
|
|
|
const fn = fullContactDisplayName(contact) || contact.emails[0]?.value || contact.phones[0]?.value
|
|
if (fn) lines.push(`FN:${escapeVCardValue(fn)}`)
|
|
|
|
for (const e of contact.emails) {
|
|
if (e.value.trim()) {
|
|
lines.push(`EMAIL;TYPE=${escapeVCardValue(e.label || "INTERNET")}:${escapeVCardValue(e.value.trim())}`)
|
|
}
|
|
}
|
|
|
|
for (const p of contact.phones) {
|
|
if (p.value.trim()) {
|
|
lines.push(`TEL;TYPE=${escapeVCardValue(p.label || "VOICE")}:${escapeVCardValue(p.value.trim())}`)
|
|
}
|
|
}
|
|
|
|
if (contact.company?.trim()) {
|
|
const org = contact.jobTitle?.trim()
|
|
? `${escapeVCardValue(contact.company.trim())};${escapeVCardValue(contact.jobTitle.trim())}`
|
|
: escapeVCardValue(contact.company.trim())
|
|
lines.push(`ORG:${org}`)
|
|
} else if (contact.jobTitle?.trim()) {
|
|
lines.push(`TITLE:${escapeVCardValue(contact.jobTitle.trim())}`)
|
|
}
|
|
|
|
if (contact.department?.trim()) {
|
|
lines.push(`X-ABLabel:${escapeVCardValue(contact.department.trim())}`)
|
|
}
|
|
|
|
if (contact.birthday?.month && contact.birthday?.day) {
|
|
const y = contact.birthday.year ?? 1900
|
|
const m = String(contact.birthday.month).padStart(2, "0")
|
|
const d = String(contact.birthday.day).padStart(2, "0")
|
|
lines.push(`BDAY:${y}${m}${d}`)
|
|
}
|
|
|
|
if (contact.notes?.trim()) {
|
|
lines.push(`NOTE:${escapeVCardValue(contact.notes.trim())}`)
|
|
}
|
|
|
|
lines.push("END:VCARD")
|
|
return lines.join("\r\n")
|
|
}
|
|
|
|
export function contactsToVCard(contacts: FullContact[]): string {
|
|
return contacts.map(contactToVCard).join("\r\n")
|
|
}
|
|
|
|
export function contactsToCsv(contacts: FullContact[]): string {
|
|
const header = ["Name", "Email", "Phone", "Company", "Job Title", "Notes"]
|
|
const rows = contacts.map((c) => {
|
|
const name = fullContactDisplayName(c)
|
|
const email = c.emails.map((e) => e.value).join("; ")
|
|
const phone = c.phones.map((p) => p.value).join("; ")
|
|
const company = c.company ?? ""
|
|
const jobTitle = c.jobTitle ?? ""
|
|
const notes = c.notes ?? ""
|
|
return [name, email, phone, company, jobTitle, notes].map(escapeCsvField).join(",")
|
|
})
|
|
return [header.join(","), ...rows].join("\r\n")
|
|
}
|
|
|
|
export function downloadTextFile(content: string, filename: string, mimeType: string): void {
|
|
const blob = new Blob([content], { type: mimeType })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement("a")
|
|
link.href = url
|
|
link.download = filename
|
|
link.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
export function downloadContactVCard(contact: FullContact): void {
|
|
const base = sanitizeFilename(
|
|
fullContactDisplayName(contact) || contact.emails[0]?.value || "contact"
|
|
)
|
|
downloadTextFile(contactToVCard(contact), `${base}.vcf`, "text/vcard;charset=utf-8")
|
|
}
|
|
|
|
export function downloadContactsVCard(contacts: FullContact[], filename = "contacts.vcf"): void {
|
|
downloadTextFile(contactsToVCard(contacts), filename, "text/vcard;charset=utf-8")
|
|
}
|
|
|
|
export function downloadContactsCsv(contacts: FullContact[], filename = "contacts.csv"): void {
|
|
downloadTextFile(contactsToCsv(contacts), filename, "text/csv;charset=utf-8")
|
|
}
|