vcard refactor

This commit is contained in:
Matthias Kretschmann 2024-02-05 15:16:18 +00:00
parent 934ed0fff5
commit c0da3a8311
Signed by: m
GPG Key ID: 606EEEF3C479A91F
15 changed files with 70 additions and 134 deletions

View File

@ -5,8 +5,7 @@
"author": {
"name": "Matthias Kretschmann",
"label": "Designer & Developer",
"email": "m@kretschmann.io",
"picture": "../src/images/avatar.jpg"
"email": "m@kretschmann.io"
},
"availability": {
"status": false,

33
package-lock.json generated
View File

@ -21,14 +21,14 @@
"react-dom": "^18.2.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-html": "^16.0.1",
"vcf": "github:jhermsmeier/node-vcf"
"remark-html": "^16.0.1"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.4.1",
"@testing-library/react": "^14.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"chalk": "^5.3.0",
@ -4914,6 +4914,12 @@
"@types/ms": "*"
}
},
"node_modules/@types/file-saver": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
"dev": true
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@ -7646,11 +7652,6 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
"node_modules/foldline": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/foldline/-/foldline-1.1.0.tgz",
"integrity": "sha512-9SheyADS50hjvFYjFJ3OB/GlDz2mD1T2CHd7auIk4Uto5YYWPBcw8iYo3F+gENJ+/SOeH9tT0loHZSqlUlumTA=="
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -14425,24 +14426,6 @@
"node": ">=10.12.0"
}
},
"node_modules/vcf": {
"version": "2.1.2",
"resolved": "git+ssh://git@github.com/jhermsmeier/node-vcf.git#55a70ace8faa8210fca883e4084e1cd3b10b1fe4",
"integrity": "sha512-o5XI+c332wFAL2Mn1NgOnmxmwiqc7afTRgGBc4rvVLq/Ez+mixzhN82/+DwYTKUIPeq47mvDhHzzZGPIlLxnWg==",
"license": "MIT",
"dependencies": {
"camelcase": "^5.0.0",
"foldline": "^1.1.0"
}
},
"node_modules/vcf/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"engines": {
"node": ">=6"
}
},
"node_modules/vfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",

View File

@ -36,14 +36,14 @@
"react-dom": "^18.2.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-html": "^16.0.1",
"vcf": "github:jhermsmeier/node-vcf"
"remark-html": "^16.0.1"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.4.1",
"@testing-library/react": "^14.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"chalk": "^5.3.0",

View File

@ -2,7 +2,7 @@ import fs from 'fs'
import yaml from 'js-yaml'
import ora from 'ora'
import { join } from 'path'
import type ProjectType from '../../src/types/project.js'
import type ProjectType from '@/types/project.js'
import { transformProject } from './transformProject.js'
const contentDirectory = join(process.cwd(), '_content')

View File

@ -1,7 +1,7 @@
import type ImageType from '@/src/types/image'
import fs from 'fs'
import { join } from 'path'
import sharp from 'sharp'
import type ImageType from '@/types/image'
import { rgbDataURL } from './rgbDataURL.js'
const imagesDirectory = join(process.cwd(), 'public', 'images')

View File

@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'
import Header from './Header'
import Header from '.'
describe('Header', () => {
it('renders correctly', async () => {

View File

@ -1,5 +1,5 @@
import Availability from '../Availability'
import Location from '../Location/Location'
import { Location } from '../Location'
import LogoUnit from '../LogoUnit'
import Networks from '../Networks'
import styles from './Hero.module.css'

View File

@ -9,7 +9,7 @@ import { Flag } from './Flag'
import styles from './Location.module.css'
import { UseLocation } from './types'
export default function Location() {
export function Location() {
const shouldReduceMotion = useReducedMotion()
const [isPending, startTransition] = useTransition()
const [location, setLocation] = useState<UseLocation | null>(null)

View File

@ -14,7 +14,6 @@ type Props = {
const containerVariants = {
enter: {
transition: {
delay: 0.2,
staggerChildren: 0.1
}
}

View File

@ -1,13 +1,9 @@
import meta from '../../../_content/meta.json'
import { constructVcard, init, toDataURL } from './_utils'
import { constructVcard, init } from './_utils'
const metaMock = {
...meta,
name: meta.author.name,
label: meta.author.label,
email: meta.author.email,
profiles: [...meta.profiles]
}
jest.mock('./imageToDataUrl', () => ({
__esModule: true,
imageToDataUrl: jest.fn().mockResolvedValue('data:image/png;base64,')
}))
describe('Vcard/_utils', () => {
beforeEach(() => {
@ -15,17 +11,12 @@ describe('Vcard/_utils', () => {
})
it('combined vCard download process finishes', async () => {
await init(metaMock)
await init()
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
})
it('vCard can be constructed', async () => {
const vcard = await constructVcard(metaMock, 'data:image/jpeg;base64,00')
it('vCard can be constructed', () => {
const vcard = constructVcard('data:image/jpeg;base64,00')
expect(vcard).toBeDefined()
})
it('Base64 from image can be constructed', async () => {
const dataUrl = await toDataURL('hello', 'image/jpeg')
expect(dataUrl).toBeDefined()
})
})

View File

@ -1,67 +1,43 @@
import saveAs from 'file-saver'
import vCard from 'vcf'
import avatar from '@/images/avatar.jpg'
import meta from '@content/meta.json'
import { imageToDataUrl } from './imageToDataUrl'
export async function toDataURL(photoSrc: string, outputFormat) {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.src = photoSrc
img.onload = () => {}
// yeah, we're gonna create a fake canvas to render the image
// and then create a base64 string from the rendered result
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let dataURL
canvas.height = img.naturalHeight
canvas.width = img.naturalWidth
ctx.drawImage(img, 0, 0)
dataURL = canvas.toDataURL(outputFormat)
// img.src = photoSrc
// if (img.complete || img.complete === undefined) {
// img.src =
// 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
// img.src = photoSrc
// }
return dataURL
}
export async function constructVcard(meta, dataUrl: string) {
const contact = new vCard()
export function constructVcard(dataUrl: string) {
const blog = meta.profiles.filter(({ network }) => network === 'Blog')[0].url
const github = meta.profiles.filter(({ network }) => network === 'GitHub')[0]
.url
// stripping this data out of base64 string is required
// for vcard to actually display the image for whatever reason
// const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
// contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
contact.set('fn', meta.name)
contact.set('title', meta.label)
contact.set('email', meta.email)
contact.set('nickname', 'kremalicious')
contact.set('url', meta.url, { type: 'Portfolio' })
contact.add('url', blog, { type: 'Blog' })
contact.add('x-socialprofile', github, { type: 'GitHub' })
const dataUrlCleaned = dataUrl.replace(
/^data:image\/(png|jpg|jpeg);base64,/,
''
)
const vCard = `BEGIN:VCARD
VERSION:3.0
PHOTO;ENCODING=B;TYPE=JPEG:${dataUrlCleaned},
FN:${meta.author.name}
TITLE:${meta.author.label}
EMAIL:${meta.author.email}
NICKNAME:kremalicious
URL;TYPE=portfolio:${meta.url}
URL;TYPE=blog:${blog}
X-SOCIALPROFILE;TYPE=github:${github}
END:VCARD`
const vcard = contact.toString('3.0')
return vcard
return vCard
}
export async function init(meta) {
// first, convert the avatar to base64, then construct all vCard elements
const dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg')
const vcard = await constructVcard(meta, dataUrl)
export async function init() {
const dataUrl = await imageToDataUrl(avatar.src)
const vcard = constructVcard(dataUrl)
// Construct the download from a blob of the just constructed vCard,
const { addressbook } = meta
const name = addressbook.split('/').join('')
const blob = new Blob([vcard], {
type: 'text/x-vcard'
})
const blob = new Blob([vcard], { type: 'text/x-vcard' })
// save it to user's file system
saveAs(blob, name)
}

View File

@ -0,0 +1,15 @@
export async function imageToDataUrl(path: string): Promise<string> {
const response = await fetch(path)
const blob = await response.blob()
return new Promise((onSuccess, onError) => {
try {
const reader = new FileReader()
reader.onload = function () {
onSuccess(this.result as string)
}
reader.readAsDataURL(blob)
} catch (e) {
onError(e)
}
})
}

View File

@ -1,6 +1,11 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import Vcard from '.'
jest.mock('./imageToDataUrl', () => ({
__esModule: true,
imageToDataUrl: jest.fn().mockResolvedValue('data:image/png;base64,')
}))
describe('Vcard', () => {
beforeEach(() => {
global.URL.createObjectURL = jest.fn()

View File

@ -4,23 +4,10 @@ import { MouseEvent } from 'react'
import meta from '@content/meta.json'
export default function Vcard() {
const { name, label, email } = meta.author
const vCardMeta = {
...meta,
/// photoSrc,
name,
label,
email,
profiles: meta.profiles
}
const handleAddressbookClick = (e: MouseEvent) => {
e.preventDefault()
import('./_utils').then(({ init }) => {
init(vCardMeta)
})
import('./_utils').then(({ init }) => init())
}
return (

View File

@ -1,19 +0,0 @@
export declare type Meta = {
name?: string
label?: string
email?: string
profiles?: (
| { network: string; url: string; username?: undefined }
| { network: string; username: string; url: string }
)[]
description?: string
img?: string
url?: string
author?: { name: string; label: string; email: string; picture: string }
availability?: { status: boolean; available: string; unavailable: string }
gpg?: string
addressbook?: any
bugs?: string
allowedHosts?: string[]
photoSrc?: any
}