diff --git a/gatsby-browser.js b/gatsby-browser.js
index 487a949..ad5009f 100644
--- a/gatsby-browser.js
+++ b/gatsby-browser.js
@@ -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)
diff --git a/gatsby-ssr.js b/gatsby-ssr.js
index 1f8c9ea..3c4acbe 100644
--- a/gatsby-ssr.js
+++ b/gatsby-ssr.js
@@ -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 }) => {
diff --git a/jest/setup-test-env.js b/jest/setup-test-env.js
index 99c14cd..395c3d5 100644
--- a/jest/setup-test-env.js
+++ b/jest/setup-test-env.js
@@ -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'
diff --git a/package.json b/package.json
index 44e8ef0..ce78ab2 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/atoms/Vcard.jsx b/src/components/atoms/Vcard.jsx
index 6ea6626..e6b25a8 100644
--- a/src/components/atoms/Vcard.jsx
+++ b/src/components/atoms/Vcard.jsx
@@ -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
}
diff --git a/src/components/atoms/Vcard.test.jsx b/src/components/atoms/Vcard.test.jsx
index 45b42a7..d346120 100644
--- a/src/components/atoms/Vcard.test.jsx
+++ b/src/components/atoms/Vcard.test.jsx
@@ -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()
-
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()
})
})
diff --git a/src/components/molecules/Availability.test.jsx b/src/components/molecules/Availability.test.jsx
index b552111..d656d2a 100644
--- a/src/components/molecules/Availability.test.jsx
+++ b/src/components/molecules/Availability.test.jsx
@@ -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()
expect(container.firstChild).toBeInTheDocument()
})
diff --git a/src/components/molecules/Networks.test.jsx b/src/components/molecules/Networks.test.jsx
index 9ef5674..921e86f 100644
--- a/src/components/molecules/Networks.test.jsx
+++ b/src/components/molecules/Networks.test.jsx
@@ -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', () => {
diff --git a/src/components/molecules/ThemeSwitch.test.jsx b/src/components/molecules/ThemeSwitch.test.jsx
index 4ddb029..08828e1 100644
--- a/src/components/molecules/ThemeSwitch.test.jsx
+++ b/src/components/molecules/ThemeSwitch.test.jsx
@@ -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', () => {
diff --git a/src/components/organisms/Footer.jsx b/src/components/organisms/Footer.jsx
index 8ec65f8..cd100b0 100644
--- a/src/components/organisms/Footer.jsx
+++ b/src/components/organisms/Footer.jsx
@@ -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 (
diff --git a/src/components/organisms/Footer.test.jsx b/src/components/organisms/Footer.test.jsx
new file mode 100644
index 0000000..2127680
--- /dev/null
+++ b/src/components/organisms/Footer.test.jsx
@@ -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()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+})
diff --git a/src/components/organisms/Header.test.jsx b/src/components/organisms/Header.test.jsx
new file mode 100644
index 0000000..5c26c66
--- /dev/null
+++ b/src/components/organisms/Header.test.jsx
@@ -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(
+ null }}>
+
+
+ )
+ expect(container.firstChild).toBeInTheDocument()
+ })
+})
diff --git a/src/hooks/use-meta.test.js b/src/hooks/use-meta.test.js
index 7ca0541..8f03f6f 100644
--- a/src/hooks/use-meta.test.js
+++ b/src/hooks/use-meta.test.js
@@ -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', () => {
diff --git a/src/store/Provider.jsx b/src/store/Provider.jsx
new file mode 100644
index 0000000..76cebaa
--- /dev/null
+++ b/src/store/Provider.jsx
@@ -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 {this.props.children}
+ }
+}
diff --git a/src/store/Provider.test.jsx b/src/store/Provider.test.jsx
new file mode 100644
index 0000000..f0b433f
--- /dev/null
+++ b/src/store/Provider.test.jsx
@@ -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(Hello)
+
+ expect(container.firstChild.textContent).toBe('Hello')
+ })
+})
diff --git a/src/store/provider.jsx b/src/store/provider.jsx
deleted file mode 100644
index 4f5fdfa..0000000
--- a/src/store/provider.jsx
+++ /dev/null
@@ -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 {this.props.children}
- }
-}
diff --git a/src/store/countrycode-latlong.json b/src/utils/countrycode-latlong.json
similarity index 100%
rename from src/store/countrycode-latlong.json
rename to src/utils/countrycode-latlong.json
diff --git a/src/utils/getCountry.js b/src/utils/getCountry.js
new file mode 100644
index 0000000..f2603c5
--- /dev/null
+++ b/src/utils/getCountry.js
@@ -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
+ }
+}
diff --git a/src/utils/getCountry.test.js b/src/utils/getCountry.test.js
new file mode 100644
index 0000000..75ef049
--- /dev/null
+++ b/src/utils/getCountry.test.js
@@ -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')
+ })
+})
diff --git a/src/utils/getLocationTimes.js b/src/utils/getLocationTimes.js
new file mode 100644
index 0000000..c627fd7
--- /dev/null
+++ b/src/utils/getLocationTimes.js
@@ -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 }
+}
diff --git a/src/utils/getLocationTimes.test.js b/src/utils/getLocationTimes.test.js
new file mode 100644
index 0000000..96bafde
--- /dev/null
+++ b/src/utils/getLocationTimes.test.js
@@ -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()
+ })
+})