mirror of
https://github.com/kremalicious/portfolio.git
synced 2024-12-22 09:13:19 +01:00
vcard refactor
This commit is contained in:
parent
934ed0fff5
commit
c0da3a8311
@ -5,8 +5,7 @@
|
|||||||
"author": {
|
"author": {
|
||||||
"name": "Matthias Kretschmann",
|
"name": "Matthias Kretschmann",
|
||||||
"label": "Designer & Developer",
|
"label": "Designer & Developer",
|
||||||
"email": "m@kretschmann.io",
|
"email": "m@kretschmann.io"
|
||||||
"picture": "../src/images/avatar.jpg"
|
|
||||||
},
|
},
|
||||||
"availability": {
|
"availability": {
|
||||||
"status": false,
|
"status": false,
|
||||||
|
33
package-lock.json
generated
33
package-lock.json
generated
@ -21,14 +21,14 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-html": "^16.0.1",
|
"remark-html": "^16.0.1"
|
||||||
"vcf": "github:jhermsmeier/node-vcf"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@testing-library/jest-dom": "^6.4.1",
|
"@testing-library/jest-dom": "^6.4.1",
|
||||||
"@testing-library/react": "^14.2.0",
|
"@testing-library/react": "^14.2.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
@ -4914,6 +4914,12 @@
|
|||||||
"@types/ms": "*"
|
"@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": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
|
||||||
@ -7646,11 +7652,6 @@
|
|||||||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@ -14425,24 +14426,6 @@
|
|||||||
"node": ">=10.12.0"
|
"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": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
|
||||||
|
@ -36,14 +36,14 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-html": "^16.0.1",
|
"remark-html": "^16.0.1"
|
||||||
"vcf": "github:jhermsmeier/node-vcf"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@testing-library/jest-dom": "^6.4.1",
|
"@testing-library/jest-dom": "^6.4.1",
|
||||||
"@testing-library/react": "^14.2.0",
|
"@testing-library/react": "^14.2.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
@ -2,7 +2,7 @@ import fs from 'fs'
|
|||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import ora from 'ora'
|
import ora from 'ora'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import type ProjectType from '../../src/types/project.js'
|
import type ProjectType from '@/types/project.js'
|
||||||
import { transformProject } from './transformProject.js'
|
import { transformProject } from './transformProject.js'
|
||||||
|
|
||||||
const contentDirectory = join(process.cwd(), '_content')
|
const contentDirectory = join(process.cwd(), '_content')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type ImageType from '@/src/types/image'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
import type ImageType from '@/types/image'
|
||||||
import { rgbDataURL } from './rgbDataURL.js'
|
import { rgbDataURL } from './rgbDataURL.js'
|
||||||
|
|
||||||
const imagesDirectory = join(process.cwd(), 'public', 'images')
|
const imagesDirectory = join(process.cwd(), 'public', 'images')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import Header from './Header'
|
import Header from '.'
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('renders correctly', async () => {
|
it('renders correctly', async () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Availability from '../Availability'
|
import Availability from '../Availability'
|
||||||
import Location from '../Location/Location'
|
import { Location } from '../Location'
|
||||||
import LogoUnit from '../LogoUnit'
|
import LogoUnit from '../LogoUnit'
|
||||||
import Networks from '../Networks'
|
import Networks from '../Networks'
|
||||||
import styles from './Hero.module.css'
|
import styles from './Hero.module.css'
|
||||||
|
@ -9,7 +9,7 @@ import { Flag } from './Flag'
|
|||||||
import styles from './Location.module.css'
|
import styles from './Location.module.css'
|
||||||
import { UseLocation } from './types'
|
import { UseLocation } from './types'
|
||||||
|
|
||||||
export default function Location() {
|
export function Location() {
|
||||||
const shouldReduceMotion = useReducedMotion()
|
const shouldReduceMotion = useReducedMotion()
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
const [location, setLocation] = useState<UseLocation | null>(null)
|
const [location, setLocation] = useState<UseLocation | null>(null)
|
||||||
|
@ -14,7 +14,6 @@ type Props = {
|
|||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
enter: {
|
enter: {
|
||||||
transition: {
|
transition: {
|
||||||
delay: 0.2,
|
|
||||||
staggerChildren: 0.1
|
staggerChildren: 0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import meta from '../../../_content/meta.json'
|
import { constructVcard, init } from './_utils'
|
||||||
import { constructVcard, init, toDataURL } from './_utils'
|
|
||||||
|
|
||||||
const metaMock = {
|
jest.mock('./imageToDataUrl', () => ({
|
||||||
...meta,
|
__esModule: true,
|
||||||
name: meta.author.name,
|
imageToDataUrl: jest.fn().mockResolvedValue('data:image/png;base64,')
|
||||||
label: meta.author.label,
|
}))
|
||||||
email: meta.author.email,
|
|
||||||
profiles: [...meta.profiles]
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Vcard/_utils', () => {
|
describe('Vcard/_utils', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -15,17 +11,12 @@ describe('Vcard/_utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('combined vCard download process finishes', async () => {
|
it('combined vCard download process finishes', async () => {
|
||||||
await init(metaMock)
|
await init()
|
||||||
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
|
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('vCard can be constructed', async () => {
|
it('vCard can be constructed', () => {
|
||||||
const vcard = await constructVcard(metaMock, 'data:image/jpeg;base64,00')
|
const vcard = constructVcard('data:image/jpeg;base64,00')
|
||||||
expect(vcard).toBeDefined()
|
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 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) {
|
export function constructVcard(dataUrl: string) {
|
||||||
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()
|
|
||||||
const blog = meta.profiles.filter(({ network }) => network === 'Blog')[0].url
|
const blog = meta.profiles.filter(({ network }) => network === 'Blog')[0].url
|
||||||
const github = meta.profiles.filter(({ network }) => network === 'GitHub')[0]
|
const github = meta.profiles.filter(({ network }) => network === 'GitHub')[0]
|
||||||
.url
|
.url
|
||||||
|
|
||||||
// stripping this data out of base64 string is required
|
// stripping this data out of base64 string is required
|
||||||
// for vcard to actually display the image for whatever reason
|
// for vcard to actually display the image for whatever reason
|
||||||
// const dataUrlCleaned = dataUrl.split('data:image/jpeg;base64,').join('')
|
const dataUrlCleaned = dataUrl.replace(
|
||||||
// contact.set('photo', dataUrlCleaned, { encoding: 'b', type: 'JPEG' })
|
/^data:image\/(png|jpg|jpeg);base64,/,
|
||||||
contact.set('fn', meta.name)
|
''
|
||||||
contact.set('title', meta.label)
|
)
|
||||||
contact.set('email', meta.email)
|
const vCard = `BEGIN:VCARD
|
||||||
contact.set('nickname', 'kremalicious')
|
VERSION:3.0
|
||||||
contact.set('url', meta.url, { type: 'Portfolio' })
|
PHOTO;ENCODING=B;TYPE=JPEG:${dataUrlCleaned},
|
||||||
contact.add('url', blog, { type: 'Blog' })
|
FN:${meta.author.name}
|
||||||
contact.add('x-socialprofile', github, { type: 'GitHub' })
|
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) {
|
export async function init() {
|
||||||
// first, convert the avatar to base64, then construct all vCard elements
|
const dataUrl = await imageToDataUrl(avatar.src)
|
||||||
const dataUrl = await toDataURL(meta.photoSrc, 'image/jpeg')
|
const vcard = constructVcard(dataUrl)
|
||||||
const vcard = await constructVcard(meta, dataUrl)
|
|
||||||
|
|
||||||
// Construct the download from a blob of the just constructed vCard,
|
// Construct the download from a blob of the just constructed vCard,
|
||||||
const { addressbook } = meta
|
const { addressbook } = meta
|
||||||
const name = addressbook.split('/').join('')
|
const name = addressbook.split('/').join('')
|
||||||
const blob = new Blob([vcard], {
|
const blob = new Blob([vcard], { type: 'text/x-vcard' })
|
||||||
type: 'text/x-vcard'
|
|
||||||
})
|
|
||||||
// save it to user's file system
|
// save it to user's file system
|
||||||
saveAs(blob, name)
|
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 { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import Vcard from '.'
|
import Vcard from '.'
|
||||||
|
|
||||||
|
jest.mock('./imageToDataUrl', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
imageToDataUrl: jest.fn().mockResolvedValue('data:image/png;base64,')
|
||||||
|
}))
|
||||||
|
|
||||||
describe('Vcard', () => {
|
describe('Vcard', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.URL.createObjectURL = jest.fn()
|
global.URL.createObjectURL = jest.fn()
|
||||||
|
@ -4,23 +4,10 @@ import { MouseEvent } from 'react'
|
|||||||
import meta from '@content/meta.json'
|
import meta from '@content/meta.json'
|
||||||
|
|
||||||
export default function Vcard() {
|
export default function Vcard() {
|
||||||
const { name, label, email } = meta.author
|
|
||||||
|
|
||||||
const vCardMeta = {
|
|
||||||
...meta,
|
|
||||||
/// photoSrc,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
email,
|
|
||||||
profiles: meta.profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddressbookClick = (e: MouseEvent) => {
|
const handleAddressbookClick = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
import('./_utils').then(({ init }) => {
|
import('./_utils').then(({ init }) => init())
|
||||||
init(vCardMeta)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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