mirror of
https://github.com/kremalicious/portfolio.git
synced 2024-12-22 01:03:20 +01:00
vcard refactor
This commit is contained in:
parent
934ed0fff5
commit
c0da3a8311
@ -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
33
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Header from './Header'
|
||||
import Header from '.'
|
||||
|
||||
describe('Header', () => {
|
||||
it('renders correctly', async () => {
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -14,7 +14,6 @@ type Props = {
|
||||
const containerVariants = {
|
||||
enter: {
|
||||
transition: {
|
||||
delay: 0.2,
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
15
src/components/Vcard/imageToDataUrl.ts
Normal file
15
src/components/Vcard/imageToDataUrl.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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()
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user