1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-12-22 17:23:22 +01:00

refactorings

This commit is contained in:
Matthias Kretschmann 2019-04-16 21:21:01 +02:00
parent 0e78f34d15
commit f5f6c0b5fd
Signed by: m
GPG Key ID: 606EEEF3C479A91F
21 changed files with 289 additions and 195 deletions

View File

@ -1,6 +1,6 @@
import './src/styles/global.scss'
import React from 'react'
import AppProvider from './src/store/provider'
import AppProvider from './src/store/Provider'
import wrapPageElementWithTransition from './src/helpers/wrapPageElement'
// IntersectionObserver polyfill for gatsby-image (Safari, IE)

View File

@ -1,6 +1,6 @@
import React from 'react'
import { renderToString } from 'react-dom/server'
import AppProvider from './src/store/provider'
import AppProvider from './src/store/Provider'
import wrapPageElementWithTransition from './src/helpers/wrapPageElement'
export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {

View File

@ -2,3 +2,5 @@ import 'jest-dom/extend-expect'
// this is basically: afterEach(cleanup)
import 'react-testing-library/cleanup-after-each'
import 'jest-canvas-mock'

View File

@ -71,6 +71,7 @@
"eslint-plugin-react": "^7.12.4",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.7.1",
"jest-canvas-mock": "^2.0.0-beta.1",
"jest-dom": "^3.1.3",
"ora": "^3.4.0",
"prepend": "^1.0.2",

View File

@ -41,7 +41,7 @@ export default class Vcard extends PureComponent {
const handleAddressbookClick = e => {
e.preventDefault()
constructVcard(meta)
init(meta)
}
return (
@ -60,6 +60,16 @@ export default class Vcard extends PureComponent {
}
}
export const init = async meta => {
const photoSrc = meta.avatar.childImageSharp.resize.src
// first, convert the avatar to base64, then construct all vCard elements
const dataUrl = await toDataURL(photoSrc, 'image/jpeg')
const vcard = await constructVcard(dataUrl, meta)
downloadVcard(vcard, meta)
}
// Construct the download from a blob of the just constructed vCard,
// and save it to user's file system
export const downloadVcard = (vcard, meta) => {
@ -69,60 +79,54 @@ export const downloadVcard = (vcard, meta) => {
saveAs(blob, name)
}
export const constructVcard = meta => {
export const constructVcard = async (dataUrl, meta) => {
const contact = new vCard()
const photoSrc = meta.avatar.childImageSharp.resize.src
// first, convert the avatar to base64, then construct all vCard elements
toDataURL(
photoSrc,
dataUrl => {
// 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.title)
contact.set('title', meta.tagline)
contact.set('email', meta.email)
contact.set('url', meta.url, { type: 'Portfolio' })
contact.add('url', meta.social.Blog, { type: 'Blog' })
contact.set('nickname', 'kremalicious')
contact.add('x-socialprofile', meta.social.Twitter, { type: 'twitter' })
contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' })
// 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.title)
contact.set('title', meta.tagline)
contact.set('email', meta.email)
contact.set('url', meta.url, { type: 'Portfolio' })
contact.add('url', meta.social.Blog, { type: 'Blog' })
contact.set('nickname', 'kremalicious')
contact.add('x-socialprofile', meta.social.Twitter, { type: 'twitter' })
contact.add('x-socialprofile', meta.social.GitHub, { type: 'GitHub' })
const vcard = contact.toString('3.0')
const vcard = contact.toString('3.0')
downloadVcard(vcard, meta)
},
'image/jpeg'
)
return vcard
}
// Helper function to create base64 string from avatar image
// without the need to read image file from file system
export const toDataURL = (src, callback, outputFormat) => {
export const toDataURL = async (photoSrc, outputFormat) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.src = photoSrc
img.onload = function() {
// 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
img.onload = () => {}
canvas.height = this.naturalHeight
canvas.width = this.naturalWidth
ctx.drawImage(this, 0, 0)
dataURL = canvas.toDataURL(outputFormat)
callback(dataURL)
}
// 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
img.src = src
canvas.height = img.naturalHeight
canvas.width = img.naturalWidth
ctx.drawImage(img, 0, 0)
dataURL = canvas.toDataURL(outputFormat)
if (img.complete || img.complete === undefined) {
img.src =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
img.src = src
}
// img.src = photoSrc
// if (img.complete || img.complete === undefined) {
// img.src =
// 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
// img.src = photoSrc
// }
return dataURL
}

View File

@ -1,37 +1,36 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery } from 'gatsby'
import vCard from 'vcf'
import Vcard, { constructVcard, downloadVcard, toDataURL } from './Vcard'
import Vcard, { constructVcard, toDataURL, init } from './Vcard'
import data from '../../../jest/__fixtures__/meta.json'
describe('Vcard', () => {
beforeEach(() => {
StaticQuery.mockImplementationOnce(({ render }) => render({ ...data }))
global.URL.createObjectURL = jest.fn()
})
it('renders correctly', () => {
const { container } = render(<Vcard />)
expect(container.firstChild).toBeInTheDocument()
})
it('vCard can be constructed', async () => {
await constructVcard(data.contentYaml)
})
it('vCard can be downloaded', async () => {
const contact = new vCard()
const vcard = contact.toString('3.0')
global.URL.createObjectURL = jest.fn(() => 'details')
await downloadVcard(vcard, data.contentYaml)
it('combined vCard download process finishes', async () => {
await init(data.contentYaml)
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1)
})
it('Base64 from image can be constructed', () => {
const photoSrc = data.contentYaml.avatar.childImageSharp.resize.src
it('vCard can be constructed', async () => {
const vcard = await constructVcard(
'data:image/jpeg;base64,00',
data.contentYaml
)
expect(vcard).toBeDefined()
})
toDataURL(photoSrc, () => null, 'image/jpeg')
it('Base64 from image can be constructed', async () => {
const dataUrl = await toDataURL('hello', 'image/jpeg')
expect(dataUrl).toBeDefined()
})
})

View File

@ -6,9 +6,7 @@ import data from '../../../jest/__fixtures__/meta.json'
describe('Availability', () => {
it('renders correctly from data file values', () => {
useStaticQuery.mockImplementation(() => {
return { ...data }
})
useStaticQuery.mockImplementation(() => ({ ...data }))
const { container } = render(<Availability />)
expect(container.firstChild).toBeInTheDocument()
})

View File

@ -5,9 +5,7 @@ import Networks from './Networks'
import data from '../../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return { ...data }
})
useStaticQuery.mockImplementationOnce(() => ({ ...data }))
})
describe('Networks', () => {

View File

@ -1,6 +1,6 @@
import React from 'react'
import { render } from 'react-testing-library'
import AppProvider from '../../store/provider'
import AppProvider from '../../store/Provider'
import ThemeSwitch from './ThemeSwitch'
describe('ThemeSwitch', () => {

View File

@ -11,9 +11,6 @@ const query = graphql`
query {
# the package.json file
portfolioJson {
name
homepage
repository
bugs
}
@ -25,7 +22,7 @@ const query = graphql`
}
`
export const FooterMarkup = ({ pkg, meta, year }) => {
const FooterMarkup = ({ pkg, meta, year }) => {
const classes = classNames('h-card', [styles.footer])
return (

View File

@ -0,0 +1,22 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery, useStaticQuery } from 'gatsby'
import Footer from './Footer'
import data from '../../../jest/__fixtures__/meta.json'
describe('Header', () => {
beforeEach(() => {
StaticQuery.mockImplementation(({ render }) =>
render({
...data,
portfolioJson: { bugs: '' }
})
)
useStaticQuery.mockImplementation(() => ({ ...data }))
})
it('renders correctly', () => {
const { container } = render(<Footer />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,22 @@
import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery, useStaticQuery } from 'gatsby'
import Header from './Header'
import { Provider } from '../../store/createContext'
import data from '../../../jest/__fixtures__/meta.json'
describe('Header', () => {
beforeEach(() => {
StaticQuery.mockImplementation(({ render }) => render({ ...data }))
useStaticQuery.mockImplementation(() => ({ ...data }))
})
it('renders correctly', () => {
const { container } = render(
<Provider value={{ dark: false, toggleDark: () => null }}>
<Header />
</Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -5,9 +5,7 @@ import { useMeta } from './use-meta'
import data from '../../jest/__fixtures__/meta.json'
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => {
return { ...data }
})
useStaticQuery.mockImplementationOnce(() => ({ ...data }))
})
describe('useMeta', () => {

73
src/store/Provider.jsx Normal file
View File

@ -0,0 +1,73 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Provider } from './createContext'
import { getLocationTimes } from '../utils/getLocationTimes'
import { getCountry } from '../utils/getCountry'
export default class AppProvider extends PureComponent {
state = {
dark: false,
toggleDark: () => this.toggleDark,
location: null
}
static propTypes = {
children: PropTypes.any.isRequired
}
store = typeof localStorage === 'undefined' ? null : localStorage
mounted = false
async componentDidMount() {
this.mounted = true
const location = await getCountry()
this.setState({ location })
this.checkDark()
}
componentWillUnmount() {
this.mounted = false
}
setDark() {
this.mounted && this.setState({ dark: true })
}
setLight() {
this.mounted && this.setState({ dark: false })
}
darkLocalStorageMode(darkLocalStorage) {
darkLocalStorage === 'true' ? this.setDark() : this.setLight()
}
//
// All the checks to see if we should go dark or light
//
async checkTimes() {
const { location, dark } = this.state
const { sunset, sunrise } = await getLocationTimes(location)
const now = new Date().getHours()
const weWantItDarkTimes = now >= sunset || now <= sunrise
!dark && weWantItDarkTimes ? this.setDark() : this.setLight()
}
async checkDark() {
const darkLocalStorage = await this.store.getItem('dark')
darkLocalStorage
? this.darkLocalStorageMode(darkLocalStorage)
: this.checkTimes()
}
toggleDark = () => {
this.setState({ dark: !this.state.dark })
this.store && this.store.setItem('dark', !this.state.dark)
}
render() {
return <Provider value={this.state}>{this.props.children}</Provider>
}
}

View File

@ -0,0 +1,11 @@
import React from 'react'
import { render } from 'react-testing-library'
import AppProvider from './Provider.jsx'
describe('AppProvider', () => {
it('renders correctly', () => {
const { container } = render(<AppProvider>Hello</AppProvider>)
expect(container.firstChild.textContent).toBe('Hello')
})
})

View File

@ -1,120 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import SunCalc from 'suncalc'
import { Provider } from './createContext'
import countrycodes from './countrycode-latlong.json'
export default class AppProvider extends PureComponent {
state = {
dark: false,
toggleDark: () => this.toggleDark,
location: null
}
static propTypes = {
children: PropTypes.any.isRequired
}
store = typeof localStorage === 'undefined' ? null : localStorage
//
// Get user location from Cloudflare's geo location HTTP header
//
getCountry = async () => {
let trace = []
try {
const data = await fetch('/cdn-cgi/trace?no-cache=1')
const text = await data.text()
const lines = text.split('\n')
let keyValue
lines.forEach(line => {
keyValue = line.split('=')
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '')
if (keyValue[0] === 'loc' && trace['loc'] !== 'XX') {
this.setState({ location: trace['loc'] })
} else {
return
}
})
} catch (error) {
return null // fail silently
}
}
setDark() {
this.setState({ dark: true })
}
setLight() {
this.setState({ dark: false })
}
darkLocalStorageMode(darkLocalStorage) {
if (darkLocalStorage === 'true') {
this.setDark()
} else {
this.setLight()
}
}
//
// All the checks to see if we should go dark or light
//
darkMode() {
const now = new Date().getHours()
const { location } = this.state
// fallback times, in hours
let sunrise = 7
let sunset = 19
// times based on detected country code
if (location && location !== 'XX' && location !== 'T1') {
const country = this.state.location.toLowerCase()
const times = SunCalc.getTimes(
new Date(),
countrycodes[country][0],
countrycodes[country][1]
)
sunrise = times.sunrise.getHours()
sunset = times.sunset.getHours()
}
if (!this.state.dark && (now >= sunset || now <= sunrise)) {
this.setDark()
} else {
this.setLight()
}
}
checkDark() {
const darkLocalStorage = this.store.getItem('dark')
if (darkLocalStorage) {
this.darkLocalStorageMode(darkLocalStorage)
} else {
this.darkMode()
}
}
toggleDark = () => {
this.setState({ dark: !this.state.dark })
if (this.store) {
this.store.setItem('dark', !this.state.dark)
}
}
componentDidMount() {
this.getCountry().then(() => {
this.checkDark()
})
}
render() {
return <Provider value={this.state}>{this.props.children}</Provider>
}
}

28
src/utils/getCountry.js Normal file
View File

@ -0,0 +1,28 @@
//
// Get user location from Cloudflare's geo location HTTP header
//
// @returns country: string
//
export const getCountry = async () => {
try {
const data = await fetch('/cdn-cgi/trace?no-cache=1')
const text = await data.text().replace(/ /g, '')
const lines = text.split('\n')
let keyValue
let trace = []
await lines.forEach(line => {
keyValue = line.split('=')
trace[keyValue[0]] = decodeURIComponent(keyValue[1] || '')
})
const country = trace['loc']
if (country && country !== 'XX') {
return country
}
} catch (error) {
return null // fail silently
}
}

View File

@ -0,0 +1,27 @@
import { getCountry } from './getCountry'
const responseMock = 'loc=DE'
const mockFetch = data =>
jest.fn().mockImplementationOnce(() =>
Promise.resolve({
ok: true,
Id: '123',
text: () => data
// json: () => data
})
)
describe('getCountry', () => {
beforeEach(() => {
global.fetch = mockFetch(responseMock)
})
it('fetches and returns correct value', async () => {
const country = await getCountry()
expect(global.fetch).toHaveBeenCalledTimes(1)
expect(global.fetch).toHaveBeenCalledWith('/cdn-cgi/trace?no-cache=1')
expect(country).toBe('DE')
})
})

View File

@ -0,0 +1,25 @@
import SunCalc from 'suncalc'
import countrycodes from './countrycode-latlong.json'
//
// All the checks to see if we should go dark or light
//
export const getLocationTimes = location => {
// fallback times, in hours
let sunrise = 7
let sunset = 19
// times based on detected country code
if (location && location !== 'XX' && location !== 'T1') {
const country = location.toLowerCase()
const times = SunCalc.getTimes(
new Date(),
countrycodes[country][0],
countrycodes[country][1]
)
sunrise = times.sunrise.getHours()
sunset = times.sunset.getHours()
}
return { sunrise, sunset }
}

View File

@ -0,0 +1,9 @@
import { getLocationTimes } from './getLocationTimes'
describe('getLocationTimes', () => {
it('returns values', async () => {
const { sunset, sunrise } = await getLocationTimes('DE')
expect(sunset).toBeDefined()
expect(sunrise).toBeDefined()
})
})