diff --git a/src/App.js b/src/App.js index b238810..8287583 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import Helmet from 'react-helmet' +import _merge from 'lodash/merge' import ScrollToTop from './components/ScrollToTop' import Meta from './components/Meta' @@ -13,11 +14,38 @@ import Footer from './components/Footer' import GithubCorner from './components/GithubCorner' import ServiceWorkerNotifications from './components/ServiceWorkerNotifications' import AOS from './components/AOS' +import Spinner from './components/Spinner' +import { fetchContent } from './util/fetch-content' import data from './data.json' class App extends Component { state = { - data + data, + loading: false + } + + componentDidMount = () => { + this.fetchPreviewContent() + } + + fetchPreviewContent = () => { + if ( + !window.netlifyIdentity || + !window.netlifyIdentity.currentUser() || + process.env.NODE_ENV === 'development' + ) { + return false + } + + this.setState({ loading: true }) + fetchContent() + .then(newData => { + this.setState(prevState => { + const data = _merge(prevState.data, newData) + return { data, loading: false } + }) + }) + .catch(() => this.setState({ loading: false })) } getDocument = (collection, name) => @@ -39,6 +67,7 @@ class App extends Component { return (
+ {this.state.loading && } diff --git a/src/components/Spinner.css b/src/components/Spinner.css new file mode 100644 index 0000000..0a2465b --- /dev/null +++ b/src/components/Spinner.css @@ -0,0 +1,84 @@ +.Spinner { + position: fixed; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.7); + z-index: 2; + display: flex; + align-items: center; + justify-content: center; +} + +.semipolar-spinner, +.semipolar-spinner * { + box-sizing: border-box; +} + +.semipolar-spinner { + --size: 150px; + --duration: 750ms; + --color: var(--primary); + height: var(--size); + width: var(--size); + position: relative; +} + +.semipolar-spinner .ring { + border-radius: 50%; + position: absolute; + border: calc(var(--size) * 0.05) solid transparent; + border-top-color: var(--color); + border-left-color: var(--color); + animation: semipolar-spinner-animation 2s infinite; +} + +.semipolar-spinner .ring:nth-child(1) { + height: calc(var(--size) - var(--size) * 0.2 * 0); + width: calc(var(--size) - var(--size) * 0.2 * 0); + top: calc(var(--size) * 0.1 * 0); + left: calc(var(--size) * 0.1 * 0); + animation-delay: calc(var(--duration) * 0.1 * 4); + z-index: 5; +} + +.semipolar-spinner .ring:nth-child(2) { + height: calc(var(--size) - var(--size) * 0.2 * 1); + width: calc(var(--size) - var(--size) * 0.2 * 1); + top: calc(var(--size) * 0.1 * 1); + left: calc(var(--size) * 0.1 * 1); + animation-delay: calc(var(--duration) * 0.1 * 3); + z-index: 4; +} + +.semipolar-spinner .ring:nth-child(3) { + height: calc(var(--size) - var(--size) * 0.2 * 2); + width: calc(var(--size) - var(--size) * 0.2 * 2); + top: calc(var(--size) * 0.1 * 2); + left: calc(var(--size) * 0.1 * 2); + animation-delay: calc(var(--duration) * 0.1 * 2); + z-index: 3; +} + +.semipolar-spinner .ring:nth-child(4) { + height: calc(var(--size) - var(--size) * 0.2 * 3); + width: calc(var(--size) - var(--size) * 0.2 * 3); + top: calc(var(--size) * 0.1 * 3); + left: calc(var(--size) * 0.1 * 3); + animation-delay: calc(var(--duration) * 0.1 * 1); + z-index: 2; +} + +.semipolar-spinner .ring:nth-child(5) { + height: calc(var(--size) - var(--size) * 0.2 * 4); + width: calc(var(--size) - var(--size) * 0.2 * 4); + top: calc(var(--size) * 0.1 * 4); + left: calc(var(--size) * 0.1 * 4); + animation-delay: calc(var(--duration) * 0.1 * 0); + z-index: 1; +} + +@keyframes semipolar-spinner-animation { + 50% { + transform: rotate(360deg) scale(0.7); + } +} diff --git a/src/components/Spinner.js b/src/components/Spinner.js new file mode 100644 index 0000000..d8be1ed --- /dev/null +++ b/src/components/Spinner.js @@ -0,0 +1,14 @@ +import React from 'react' +import './Spinner.css' + +export default () => ( +
+
+
+
+
+
+
+
+
+) diff --git a/src/util/fetch-content.js b/src/util/fetch-content.js new file mode 100644 index 0000000..2c2ad56 --- /dev/null +++ b/src/util/fetch-content.js @@ -0,0 +1,103 @@ +const matter = require('gray-matter') +const yaml = require('js-yaml') + +const b64DecodeUnicode = str => + // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings#30106551 + decodeURIComponent( + Array.prototype.map + .call(atob(str), function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join('') + ) + +const getFileExtension = str => { + const matches = str.match(/.*\.(.{2,4})$/i) + return matches && matches[1] +} + +const parseMarkdown = data => { + console.log('Parsing md') + data = matter(data) + data = { ...data, ...data.data } + delete data.data + return data +} + +const parseYaml = data => { + console.log('Parsing yaml') + return yaml.safeLoad(data) || {} +} + +export const fetchContent = async (rateLimit = 100) => { + if (!window.localStorage || !window.netlifyIdentity) { + return Promise.resolve(null) + } + + const currentUser = window.netlifyIdentity.currentUser() + + if (!currentUser) return Promise.resolve(null) + + const token = await currentUser.jwt() + + const wait = time => + new Promise((resolve, reject) => { + setTimeout(() => resolve(), time) + }) + + const fetchGithub = (path = 'content') => { + const siteUrl = window.localStorage.netlifySiteURL || '' + const endpoint = `${siteUrl}/.netlify/git/github/contents/${path}` + console.log(`Fetching ${path}`) + return wait(rateLimit) + .then(() => + fetch(endpoint, { + headers: { + Authorization: `Bearer ${token}` + } + }) + ) + .then(res => res.json()) + .then(res => { + if (res.code === 401) throw new Error(res.msg) + return res + }) + .catch(console.error) + } + + let data = {} + return fetchGithub() + .then(items => { + if (!items) throw new Error('No items found') + const dirs = items.filter(item => item.type === 'dir') + return dirs + }) + .then(dirs => { + const dirsToFetch = dirs.map(dir => { + const dirKey = dir.path.split('/').pop() + data[dirKey] = [] + return fetchGithub(dir.path) + .then(files => { + const filesToFetch = files + .filter(file => file.type === 'file') + .map(file => wait(rateLimit).then(() => fetchGithub(file.path))) + return Promise.all(filesToFetch) + }) + .then(files => { + files.forEach(file => { + const fileType = getFileExtension(file.name) + const fileContents = b64DecodeUnicode(file.content) + const json = + fileType === 'md' + ? parseMarkdown(fileContents) + : parseYaml(fileContents) + if (json) data[dirKey].push(json) + }) + return data + }) + }) + return Promise.all(dirsToFetch) + }) + .then(() => data) + .catch(console.error) +}