├── .eslintignore ├── __mocks__ ├── styleMock.js ├── fileMock.js ├── pages.js ├── metadata.js ├── author.js ├── links.js ├── post.js └── posts.js ├── .gitignore ├── static ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── mstile-150x150.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.json └── safari-pinned-tab.svg ├── config.js ├── routes.js ├── now.json ├── assets └── svg │ ├── baseline-title-24px.svg │ ├── facebook-f.svg │ ├── twitter.svg │ ├── reddit-alien.svg │ └── hooli.svg ├── jest.setup.js ├── utils ├── handleBackBtnClick │ └── index.js ├── constants.js ├── createCustomTheme │ └── index.js └── createTypography │ └── index.js ├── .eslintrc.js ├── components ├── AuthorInfo │ ├── AuthorInfo.test.jsx │ ├── index.jsx │ └── __snapshots__ │ │ └── AuthorInfo.test.jsx.snap ├── AppBar │ ├── AppBar.test.jsx │ ├── index.jsx │ └── __snapshots__ │ │ └── AppBar.test.jsx.snap ├── AppContainer │ └── index.jsx ├── PostPreview │ ├── PostPreview.test.jsx │ ├── index.jsx │ └── __snapshots__ │ │ └── PostPreview.test.jsx.snap ├── AppearanceDialog │ ├── AppearanceDialog.test.jsx │ ├── index.jsx │ └── __snapshots__ │ │ └── AppearanceDialog.test.jsx.snap └── SocialLinks │ ├── SocialLinks.test.jsx │ └── index.jsx ├── pages ├── post │ ├── post.test.jsx │ └── index.jsx ├── index │ ├── index.test.jsx │ ├── index.jsx │ └── __snapshots__ │ │ └── index.test.jsx.snap ├── _app.jsx └── _document.jsx ├── jest.config.js ├── state ├── appearance │ ├── actions.js │ └── reducer.js ├── index.js ├── links │ ├── reducer.js │ └── actions.js ├── metadata │ ├── reducer.js │ └── actions.js ├── posts │ ├── actions.js │ └── reducer.js └── pages │ ├── reducer.js │ └── actions.js ├── LICENSE ├── .babelrc ├── src ├── getPageContext.js └── withRoot.jsx ├── lib └── withReduxStore │ └── index.jsx ├── README.md ├── package.json └── server.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | 4 | *.env* 5 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmic-react-blog/master/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | API_URL: 'https://cosmicblog.chriso.io/api', 3 | BASE_URL: 'https://cosmicblog.chriso.io', 4 | }; 5 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const nextRoutes = require('next-routes'); 2 | 3 | const routes = nextRoutes(); 4 | routes.add('post', '/post/:slug'); 5 | 6 | module.exports = routes; 7 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": [ 3 | "cosmicblog.chriso.io", 4 | "www.cosmicblog.chriso.io" 5 | ], 6 | "dotenv": ".env.production", 7 | "public": false 8 | } 9 | -------------------------------------------------------------------------------- /assets/svg/baseline-title-24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/facebook-f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__mocks__/pages.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 1: { 3 | finishedLoadAt: 1526859679918, 4 | isLoading: false, 5 | startedLoadAt: 1526859679433, 6 | slugs: ['a-wonderful-blog-post-about-earth', 'another-wonderful-blog-post-about-earth'], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /__mocks__/metadata.js: -------------------------------------------------------------------------------- 1 | export default { 2 | finishedLoadAt: 1526859679840, 3 | isLoading: false, 4 | logo: { 5 | url: 'url.png', 6 | imgix_url: 'ingix_url.png', 7 | }, 8 | startedLoadAt: 1526859679433, 9 | tag: '"Content is king" -- Bill Gates', 10 | title: 'My Amazing Blog', 11 | }; 12 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { configure, mount, render, shallow } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import enzymeToJson from 'enzyme-to-json'; 4 | 5 | global.mount = mount; 6 | global.render = render; 7 | global.shallow = shallow; 8 | global.toJson = enzymeToJson; 9 | 10 | configure({ adapter: new Adapter() }); 11 | -------------------------------------------------------------------------------- /utils/handleBackBtnClick/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from '../../routes'; 2 | 3 | const handleBackBtnClick = () => { 4 | // window.previouslyLoaded is set to true the first time App is loaded. 5 | // /pages/_app.jsx 6 | if (window.previouslyLoaded) { 7 | return Router.back(); 8 | } 9 | 10 | return Router.pushRoute('/'); 11 | }; 12 | 13 | export default handleBackBtnClick; 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | }, 6 | parser: 'babel-eslint', 7 | plugins: ['react', 'env'], 8 | extends: 'airbnb', 9 | rules: { 10 | 'import/no-extraneous-dependencies': ['off'], 11 | 'jsx-a11y/anchor-is-valid': ['off'], 12 | 'no-undef': ['off'], 13 | 'react/react-in-jsx-scope': ['off'] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /components/AuthorInfo/AuthorInfo.test.jsx: -------------------------------------------------------------------------------- 1 | import author from '../../__mocks__/author'; 2 | import AuthorInfo from '.'; 3 | 4 | describe('AuthorInfo', () => { 5 | it('should render', () => { 6 | const component = mount(( 7 | 11 | )); 12 | return expect(toJson(component)).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /components/AppBar/AppBar.test.jsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | 3 | import AppBar from '.'; 4 | import initializeStore from '../../state'; 5 | 6 | describe('AppBar', () => { 7 | it('should render', () => { 8 | const component = mount(( 9 | 10 | 11 | 12 | )); 13 | return expect(toJson(component)).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /components/AppContainer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | const AppContainer = ({ children }) => ( 6 |
7 | {children} 8 |
9 | ); 10 | 11 | AppContainer.propTypes = { 12 | children: PropTypes.node.isRequired, 13 | }; 14 | 15 | 16 | export { AppContainer as DisconnectedAppContainer }; 17 | 18 | export default connect()(AppContainer); 19 | -------------------------------------------------------------------------------- /components/PostPreview/PostPreview.test.jsx: -------------------------------------------------------------------------------- 1 | import post from '../../__mocks__/post'; 2 | import { PurePostPreview } from '.'; 3 | 4 | describe('PostPreview', () => { 5 | it('should render', () => { 6 | const component = mount(( 7 | 14 | )); 15 | return expect(toJson(component)).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const BASE_URL = process.env.NODE_ENV === 'production' 4 | ? config.BASE_URL 5 | : `http://localhost:${process.env.PORT}`; 6 | 7 | const API_URL = process.env.NODE_ENV === 'production' 8 | ? config.API_URL 9 | : `http://localhost:${process.env.PORT || 8080}/api`; 10 | 11 | const THEMES = { 12 | DARK: 'dark', 13 | LIGHT: 'light', 14 | }; 15 | 16 | export { 17 | API_URL, 18 | BASE_URL, 19 | THEMES, 20 | }; 21 | -------------------------------------------------------------------------------- /pages/post/post.test.jsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | 3 | import { BasicPost } from '.'; 4 | import post from '../../__mocks__/post'; 5 | import initializeStore from '../../state'; 6 | 7 | describe('Post page', () => { 8 | it('should render', () => { 9 | const component = mount(( 10 | 11 | 12 | 13 | )); 14 | return expect(toJson(component)).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /components/AppearanceDialog/AppearanceDialog.test.jsx: -------------------------------------------------------------------------------- 1 | import { DisconnectedAppearanceDialog } from '.'; 2 | import { THEMES } from '../../utils/constants'; 3 | 4 | describe('AppearanceDialog', () => { 5 | it('should render', () => { 6 | const component = mount(( 7 | 13 | )); 14 | return expect(toJson(component)).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /components/SocialLinks/SocialLinks.test.jsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import initializeStore from '../../state'; 3 | import links from '../../__mocks__/links'; 4 | import { DisconnectedSocialLinks } from '.'; 5 | 6 | describe('SocialLinks', () => { 7 | it('should render', () => { 8 | const component = mount(( 9 | 10 | 11 | 12 | )); 13 | return expect(toJson(component)).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['/jest.setup.js'], 3 | testPathIgnorePatterns: ['/.next/', '/node_modules/'], 4 | unmockedModulePathPatterns: ['/node_modules/react'], 5 | verbose: true, 6 | moduleNameMapper: { 7 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 8 | '/__mocks__/fileMock.js', 9 | '\\.(css|scss|less)$': '/__mocks__/styleMock.js', 10 | }, 11 | snapshotSerializers: ['enzyme-to-json/serializer'], 12 | }; 13 | -------------------------------------------------------------------------------- /pages/index/index.test.jsx: -------------------------------------------------------------------------------- 1 | import metadata from '../../__mocks__/metadata'; 2 | import pages from '../../__mocks__/pages'; 3 | import posts from '../../__mocks__/posts'; 4 | import { DisconnectedIndex } from '.'; 5 | 6 | describe('Disconnected Index Page', () => { 7 | it('should render', () => { 8 | const wrapper = shallow(( 9 | 15 | )); 16 | return expect(toJson(wrapper)).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cosmic Blog", 3 | "name": "Cosmic Blog", 4 | "description": "Clean, minimalist, content-first Blog powered by Cosmic JS", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "start_url": "./index.html", 18 | "display": "standalone", 19 | "theme_color": "#009688", 20 | "background_color": "#fafafa" 21 | } 22 | -------------------------------------------------------------------------------- /state/appearance/actions.js: -------------------------------------------------------------------------------- 1 | const actionTypes = { 2 | DECREASE_FONT_SIZE: 'DECREASE_FONT_SIZE', 3 | INCREASE_FONT_SIZE: 'INCREASE_FONT_SIZE', 4 | SET_HTML_FONT_SIZE: 'SET_HTML_FONT_SIZE', 5 | SET_THEME: 'SET_THEME', 6 | }; 7 | 8 | const decreaseFontSize = () => ({ 9 | type: actionTypes.DECREASE_FONT_SIZE, 10 | }); 11 | 12 | const increaseFontSize = () => ({ 13 | type: actionTypes.INCREASE_FONT_SIZE, 14 | }); 15 | 16 | const setHtmlFontSize = size => ({ 17 | type: actionTypes.SET_HTML_FONT_SIZE, 18 | size, 19 | }); 20 | 21 | const setTheme = theme => ({ 22 | type: actionTypes.SET_THEME, 23 | theme, 24 | }); 25 | 26 | export { 27 | actionTypes, 28 | decreaseFontSize, 29 | increaseFontSize, 30 | setHtmlFontSize, 31 | setTheme, 32 | }; 33 | -------------------------------------------------------------------------------- /state/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunkMiddleware from 'redux-thunk'; 4 | 5 | import appearance from './appearance/reducer'; 6 | import links from './links/reducer'; 7 | import metadata from './metadata/reducer'; 8 | import pages from './pages/reducer'; 9 | import posts from './posts/reducer'; 10 | 11 | const rootReducer = combineReducers({ 12 | appearance, 13 | links, 14 | metadata, 15 | pages, 16 | posts, 17 | }); 18 | 19 | const initializeStore = (initialState = {}) => createStore( 20 | rootReducer, 21 | initialState, 22 | composeWithDevTools(applyMiddleware(thunkMiddleware)), 23 | ); 24 | 25 | export default initializeStore; 26 | -------------------------------------------------------------------------------- /assets/svg/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/reddit-alien.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /state/appearance/reducer.js: -------------------------------------------------------------------------------- 1 | import { THEMES } from '../../utils/constants'; 2 | import { actionTypes } from './actions'; 3 | 4 | const { 5 | DECREASE_FONT_SIZE, 6 | INCREASE_FONT_SIZE, 7 | SET_HTML_FONT_SIZE, 8 | SET_THEME, 9 | } = actionTypes; 10 | 11 | const defaultRootState = { 12 | htmlFontSize: 16, 13 | theme: THEMES.LIGHT, 14 | }; 15 | 16 | const rootReducer = (state = defaultRootState, action) => { 17 | switch (action.type) { 18 | case DECREASE_FONT_SIZE: 19 | return { 20 | ...state, 21 | htmlFontSize: state.htmlFontSize + 1, 22 | }; 23 | case INCREASE_FONT_SIZE: 24 | return { 25 | ...state, 26 | htmlFontSize: state.htmlFontSize - 1, 27 | }; 28 | case SET_HTML_FONT_SIZE: 29 | return { 30 | ...state, 31 | htmlFontSize: action.size, 32 | }; 33 | case SET_THEME: 34 | return { 35 | ...state, 36 | theme: action.theme, 37 | }; 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | export default rootReducer; 44 | -------------------------------------------------------------------------------- /__mocks__/author.js: -------------------------------------------------------------------------------- 1 | export default { 2 | _id: '5afdbf6c44d74b4e924f1d09', 3 | bucket: '5afdbcd115bef94f7421eb0d', 4 | slug: 'john-doe', 5 | title: 'John Doe', 6 | content: '

Something about John...

', 7 | metafields: [ 8 | { 9 | value: '9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 10 | key: 'image', 11 | title: 'Image', 12 | type: 'file', 13 | children: null, 14 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 15 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 16 | }, 17 | ], 18 | type_slug: 'authors', 19 | created: '2017-10-12T13:27:49.665Z', 20 | created_at: '2017-10-12T13:27:49.665Z', 21 | modified_at: '2018-05-17T17:44:12.977Z', 22 | status: 'published', 23 | published_at: '2018-05-17T17:44:12.977Z', 24 | metadata: { 25 | image: { 26 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 27 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Overstreet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /state/links/reducer.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from './actions'; 2 | 3 | const { 4 | CATCH_LINKS_FETCH_ERROR, 5 | RECEIVE_LINKS, 6 | REQUEST_LINKS, 7 | } = actionTypes; 8 | 9 | const defaultRootState = { 10 | error: undefined, 11 | finishedLoadAt: undefined, 12 | isLoading: false, 13 | items: undefined, 14 | startedLoadAt: undefined, 15 | }; 16 | 17 | const rootReducer = (state = defaultRootState, action) => { 18 | switch (action.type) { 19 | case CATCH_LINKS_FETCH_ERROR: 20 | return { 21 | ...state, 22 | error: action.error, 23 | finishedLoadAt: Date.now(), 24 | isLoading: false, 25 | }; 26 | case RECEIVE_LINKS: 27 | return { 28 | ...state, 29 | error: undefined, 30 | finishedLoadAt: Date.now(), 31 | isLoading: false, 32 | items: action.items, 33 | }; 34 | case REQUEST_LINKS: 35 | return { 36 | ...state, 37 | error: undefined, 38 | isLoading: true, 39 | startedLoadAt: Date.now(), 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | export default rootReducer; 47 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | [ 6 | "next/babel", 7 | { 8 | "styled-jsx": { 9 | "plugins": [ 10 | "styled-jsx-plugin-sass" 11 | ] 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | "inline-dotenv" 18 | ] 19 | }, 20 | "production": { 21 | "presets": [ 22 | [ 23 | "next/babel", 24 | { 25 | "styled-jsx": { 26 | "plugins": [ 27 | "styled-jsx-plugin-sass" 28 | ] 29 | } 30 | } 31 | ] 32 | ], 33 | "plugins": [ 34 | "transform-inline-environment-variables" 35 | ] 36 | }, 37 | "test": { 38 | "presets": [ 39 | [ 40 | "next/babel", 41 | { 42 | "preset-env": { 43 | "modules": "commonjs" 44 | }, 45 | "styled-jsx": { 46 | "plugins": [ 47 | "styled-jsx-plugin-sass" 48 | ] 49 | } 50 | } 51 | ] 52 | ] 53 | } 54 | }, 55 | "plugins": [ 56 | "inline-react-svg" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /state/metadata/reducer.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from './actions'; 2 | 3 | const { 4 | CATCH_METADATA_FETCH_ERROR, 5 | RECEIVE_SITE_METADATA, 6 | REQUEST_SITE_METADATA, 7 | } = actionTypes; 8 | 9 | const defaultRootState = { 10 | error: undefined, 11 | finishedLoadAt: undefined, 12 | isLoading: false, 13 | logo: undefined, 14 | startedLoadAt: undefined, 15 | tag: undefined, 16 | title: undefined, 17 | }; 18 | 19 | const rootReducer = (state = defaultRootState, action) => { 20 | switch (action.type) { 21 | case CATCH_METADATA_FETCH_ERROR: 22 | return { 23 | ...state, 24 | error: action.error, 25 | finishedLoadAt: Date.now(), 26 | isLoading: false, 27 | }; 28 | case RECEIVE_SITE_METADATA: 29 | return { 30 | ...state, 31 | error: undefined, 32 | finishedLoadAt: Date.now(), 33 | isLoading: false, 34 | logo: action.logo, 35 | tag: action.tag, 36 | title: action.title, 37 | }; 38 | case REQUEST_SITE_METADATA: 39 | return { 40 | ...state, 41 | error: undefined, 42 | isLoading: true, 43 | startedLoadAt: Date.now(), 44 | }; 45 | default: 46 | return state; 47 | } 48 | }; 49 | 50 | export default rootReducer; 51 | -------------------------------------------------------------------------------- /__mocks__/links.js: -------------------------------------------------------------------------------- 1 | export default { 2 | finishedLoadAt: 1527021510298, 3 | isLoading: false, 4 | items: [ 5 | { 6 | content: 'Cosmic JS', 7 | icon: 'https://cosmic-s3.imgix.net/d6286310-5dcd-11e8-9db3-17fe5e4c3dd5-logo.png', 8 | slug: 'cosmic-js', 9 | url: 'https://cosmicjs.com/', 10 | }, 11 | { 12 | content: 'Facebook', 13 | icon: 'https://cosmic-s3.imgix.net/00170740-5df5-11e8-a1ae-3923d095aa54-facebook-f.svg', 14 | slug: 'facebook', 15 | url: 'https://www.facebook.com/neildegrassetyson/', 16 | }, 17 | { 18 | content: 'Twitter', 19 | icon: 'https://cosmic-s3.imgix.net/de154760-5df4-11e8-a1ae-3923d095aa54-twitter.svg', 20 | slug: 'twitter', 21 | url: 'https://twitter.com/SpaceX?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor', 22 | }, 23 | { 24 | content: 'NASA', 25 | icon: 'https://cosmic-s3.imgix.net/6071bc30-5df4-11e8-b874-7ddc7f75f297-nasa-vector.jpg', 26 | slug: 'nasa', 27 | url: 'https://www.nasa.gov/', 28 | }, 29 | { 30 | content: 'Hooli', 31 | icon: 'https://cosmic-s3.imgix.net/107f1f10-5df4-11e8-8b90-49cb251195d1-hooli.svg', 32 | slug: 'hooli', 33 | url: 'http://www.hooli.xyz/', 34 | }, 35 | 36 | ], 37 | startedLoadAt: 1527021509874, 38 | }; 39 | -------------------------------------------------------------------------------- /src/getPageContext.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* source: https://github.com/mui-org/material-ui/blob/master/examples/nextjs/src/getPageContext.js */ 3 | import { SheetsRegistry } from 'jss'; 4 | import { createGenerateClassName } from '@material-ui/core/styles'; 5 | 6 | import createCustomTheme from '../utils/createCustomTheme'; 7 | 8 | // A theme with custom primary and secondary color. 9 | // It's optional. 10 | const theme = createCustomTheme({ 11 | htmlFontSize: 16, 12 | theme: 'light', 13 | }); 14 | 15 | function createPageContext() { 16 | return { 17 | theme, 18 | // This is needed in order to deduplicate the injection of CSS in the page. 19 | sheetsManager: new Map(), 20 | // This is needed in order to inject the critical CSS. 21 | sheetsRegistry: new SheetsRegistry(), 22 | // The standard class name generator. 23 | generateClassName: createGenerateClassName(), 24 | }; 25 | } 26 | 27 | export default function getPageContext() { 28 | // Make sure to create a new context for every server-side request so that data 29 | // isn't shared between connections (which would be bad). 30 | if (!process.browser) { 31 | return createPageContext(); 32 | } 33 | 34 | // Reuse context on the client-side. 35 | if (!global.__INIT_MATERIAL_UI__) { 36 | global.__INIT_MATERIAL_UI__ = createPageContext(); 37 | } 38 | 39 | return global.__INIT_MATERIAL_UI__; 40 | } 41 | -------------------------------------------------------------------------------- /assets/svg/hooli.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /state/posts/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch'; 2 | import { API_URL } from '../../utils/constants'; 3 | 4 | const actionTypes = { 5 | CATCH_POST_FETCH_ERROR: 'CATCH_POST_FETCH_ERROR', 6 | RECEIVE_POST: 'RECEIVE_POST', 7 | REQUEST_POST: 'REQUEST_POST', 8 | }; 9 | 10 | const catchPostFetchError = (error, slug) => ({ 11 | type: actionTypes.CATCH_POST_FETCH_ERROR, 12 | error, 13 | slug, 14 | }); 15 | 16 | const receivePost = post => ({ 17 | type: actionTypes.RECEIVE_POST, 18 | post, 19 | slug: post && post.slug, 20 | }); 21 | 22 | const requestPost = slug => ({ 23 | type: actionTypes.REQUEST_POST, 24 | slug, 25 | }); 26 | 27 | const fetchPost = slug => (dispatch) => { 28 | dispatch(requestPost(slug)); 29 | 30 | return fetch(`${API_URL}/post/${slug}`) 31 | .then(res => res.json()) 32 | .then(json => dispatch(receivePost(json.object))) 33 | .catch(err => dispatch(catchPostFetchError(err, slug))); 34 | }; 35 | 36 | const shouldFetchPost = (slug, state) => { 37 | const { posts } = state; 38 | 39 | if (!posts[slug]) return true; 40 | if (posts[slug].isLoading) return false; 41 | return !posts[slug].post; 42 | }; 43 | 44 | const fetchPostIfNeeded = slug => (dispatch, getState) => { 45 | if (shouldFetchPost(slug, getState())) return dispatch(fetchPost(slug)); 46 | return Promise.resolve(); 47 | }; 48 | 49 | export { 50 | actionTypes, 51 | catchPostFetchError, 52 | fetchPost, 53 | fetchPostIfNeeded, 54 | receivePost, 55 | requestPost, 56 | shouldFetchPost, 57 | }; 58 | -------------------------------------------------------------------------------- /state/pages/reducer.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from './actions'; 2 | 3 | const { 4 | CATCH_PAGE_FETCH_ERROR, 5 | RECEIVE_PAGE, 6 | REQUEST_PAGE, 7 | } = actionTypes; 8 | 9 | const defaultPageState = { 10 | error: undefined, 11 | finishedLoadAt: undefined, 12 | isLoading: false, 13 | startedLoadAt: undefined, 14 | slugs: undefined, 15 | }; 16 | 17 | const page = (state = defaultPageState, action) => { 18 | switch (action.type) { 19 | case CATCH_PAGE_FETCH_ERROR: 20 | return { 21 | ...state, 22 | error: action.error, 23 | finishedLoadAt: Date.now(), 24 | isLoading: false, 25 | }; 26 | case RECEIVE_PAGE: 27 | return { 28 | ...state, 29 | error: undefined, 30 | finishedLoadAt: Date.now(), 31 | isLoading: false, 32 | slugs: action.slugs, 33 | }; 34 | case REQUEST_PAGE: 35 | return { 36 | ...state, 37 | error: undefined, 38 | isLoading: true, 39 | startedLoadAt: Date.now(), 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | const defaultRootState = {}; 47 | 48 | const rootReducer = (state = defaultRootState, action) => { 49 | switch (action.type) { 50 | case CATCH_PAGE_FETCH_ERROR: 51 | case RECEIVE_PAGE: 52 | case REQUEST_PAGE: 53 | return { 54 | ...state, 55 | [action.page]: page(state[action.page], action), 56 | }; 57 | default: 58 | return state; 59 | } 60 | }; 61 | 62 | export default rootReducer; 63 | -------------------------------------------------------------------------------- /state/posts/reducer.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from './actions'; 2 | 3 | const { 4 | CATCH_POST_FETCH_ERROR, 5 | RECEIVE_POST, 6 | REQUEST_POST, 7 | } = actionTypes; 8 | 9 | const defaultPostState = { 10 | error: undefined, 11 | finishedLoadAt: undefined, 12 | isLoading: false, 13 | startedLoadAt: undefined, 14 | post: undefined, 15 | }; 16 | 17 | const post = (state = defaultPostState, action) => { 18 | switch (action.type) { 19 | case CATCH_POST_FETCH_ERROR: 20 | return { 21 | ...state, 22 | error: action.error, 23 | finishedLoadAt: Date.now(), 24 | isLoading: false, 25 | }; 26 | case RECEIVE_POST: 27 | return { 28 | ...state, 29 | error: undefined, 30 | finishedLoadAt: Date.now(), 31 | isLoading: false, 32 | post: action.post, 33 | }; 34 | case REQUEST_POST: 35 | return { 36 | ...state, 37 | error: undefined, 38 | isLoading: true, 39 | startedLoadAt: Date.now(), 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | const defaultRootState = {}; 47 | 48 | const rootReducer = (state = defaultRootState, action) => { 49 | switch (action.type) { 50 | case CATCH_POST_FETCH_ERROR: 51 | case RECEIVE_POST: 52 | case REQUEST_POST: 53 | return { 54 | ...state, 55 | [action.slug]: post(state[action.slug], action), 56 | }; 57 | default: 58 | return state; 59 | } 60 | }; 61 | 62 | export default rootReducer; 63 | -------------------------------------------------------------------------------- /lib/withReduxStore/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import initializeStore from '../../state'; 6 | 7 | const isServer = typeof window === 'undefined'; 8 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'; 9 | 10 | const getOrCreateStore = (initialState) => { 11 | if (isServer) { 12 | return initializeStore(initialState); 13 | } 14 | 15 | if (!window[__NEXT_REDUX_STORE__]) { 16 | window[__NEXT_REDUX_STORE__] = initializeStore(initialState); 17 | } 18 | 19 | return window[__NEXT_REDUX_STORE__]; 20 | }; 21 | 22 | const withStore = App => class Redux extends Component { 23 | static async getInitialProps(appContext) { 24 | const reduxStore = getOrCreateStore(); 25 | 26 | appContext.ctx.reduxStore = reduxStore; // eslint-disable-line no-param-reassign 27 | 28 | let appProps = {}; 29 | if (App.getInitialProps) { 30 | appProps = await App.getInitialProps(appContext); 31 | } 32 | 33 | return { 34 | ...appProps, 35 | initialReduxState: reduxStore.getState(), 36 | }; 37 | } 38 | 39 | static defaultProps = { 40 | initialReduxState: {}, 41 | }; 42 | 43 | static propTypes = { 44 | initialReduxState: PropTypes.shape({}), 45 | }; 46 | 47 | constructor(props) { 48 | super(props); 49 | this.reduxStore = getOrCreateStore(props.initialReduxState); 50 | } 51 | 52 | render() { 53 | return ; 54 | } 55 | }; 56 | 57 | export default withStore; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmic Blog 2 | 3 | ![Preview](https://cosmic-s3.imgix.net/36dd5d40-5dde-11e8-8b90-49cb251195d1-smartmockups_jhhwhno1.jpeg) 4 | 5 | ### [View Demo](https://cosmicblog.chriso.io) 6 | 7 | ### [Cosmic JS](https://cosmicjs.com/) 8 | This project was created to expirement with and demonstrate the Cosmic JS tooling. All of the backend data is stored, edited, and retreived using Cosmic JS. 9 | 10 | ### [React](https://reactjs.org/) + [Next.js](https://nextjs.org/) 11 | This is a universal web application, meaning it is rendered on the server, as well as the client. This provides better initial load times and search engine optimization. 12 | 13 | ### Getting Started 14 | ``` 15 | git clone https://github.com/chrisoverstreet/cosmic-blog 16 | cd cosmic-blog 17 | npm i 18 | ``` 19 | 20 | ### Develop 21 | 22 | #### Add required development config files 23 | 24 | - /.env _- secret variables (used on backend)_ 25 | ``` 26 | PORT=<__PORT__> 27 | BUCKET_SLUG=<__BUCKET_SLUG__> 28 | ``` 29 | 30 | #### Run in development 31 | ``` 32 | npm run dev 33 | ``` 34 | 35 | ### Deploy 36 | 37 | #### Add required production config files 38 | - /.env.production _- secret variables (used on backend)_ 39 | ``` 40 | PORT=<__PORT__> 41 | BUCKET_SLUG=<__BUCKET_SLUG__> 42 | ``` 43 | - /.config.js _- public variables (used on frontend)_ 44 | ``` 45 | API_URL: 'https://<__YOUR_DOMAIN__>/api', 46 | BASE_URL: 'https://<__YOUR_DOMAIN__>', 47 | ``` 48 | 49 | - /now.json _- Now deployment configuration_ 50 | ``` 51 | { 52 | "alias": [ 53 | <__YOUR_DOMAIN__> 54 | ], 55 | "dotenv": ".env.production", 56 | "public": false 57 | } 58 | ``` 59 | 60 | #### Deploy via [Now](https://zeit.co/now) 61 | ``` 62 | npm run deploy 63 | ``` 64 | -------------------------------------------------------------------------------- /state/links/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch'; 2 | import { API_URL } from '../../utils/constants'; 3 | 4 | const actionTypes = { 5 | CATCH_LINKS_FETCH_ERROR: 'CATCH_LINKS_FETCH_ERROR', 6 | RECEIVE_LINKS: 'RECEIVE_LINKS', 7 | REQUEST_LINKS: 'REQUEST_LINKS', 8 | }; 9 | 10 | const catchLinksFetchError = err => ({ 11 | type: actionTypes.CATCH_LINKS_FETCH_ERROR, 12 | error: err.message, 13 | }); 14 | 15 | const receiveLinks = json => ({ 16 | type: actionTypes.RECEIVE_LINKS, 17 | items: json.objects && json.objects.map(object => ({ 18 | content: object.content, 19 | icon: object.metadata && object.metadata.icon && object.metadata.icon.imgix_url, 20 | slug: object.slug, 21 | url: object.metadata && object.metadata.url, 22 | })), 23 | }); 24 | 25 | const requestLinks = () => ({ 26 | type: actionTypes.REQUEST_LINKS, 27 | }); 28 | 29 | const fetchLinks = () => (dispatch) => { 30 | dispatch(requestLinks()); 31 | 32 | return fetch(`${API_URL}/social-links`) 33 | .then(res => res.json()) 34 | .then(json => dispatch(receiveLinks(json))) 35 | .catch(error => dispatch(catchLinksFetchError(error))); 36 | }; 37 | 38 | const shouldFetchLinks = (state) => { 39 | const { links } = state; 40 | 41 | if (!links) return true; 42 | if (links.isLoading) return false; 43 | return !links.items || !links.items.length; 44 | }; 45 | 46 | const fetchLinksIfNeeded = () => (dispatch, getState) => { 47 | if (shouldFetchLinks(getState())) return dispatch(fetchLinks()); 48 | return Promise.resolve(); 49 | }; 50 | 51 | export { 52 | actionTypes, 53 | fetchLinks, 54 | fetchLinksIfNeeded, 55 | catchLinksFetchError, 56 | receiveLinks, 57 | requestLinks, 58 | shouldFetchLinks, 59 | }; 60 | -------------------------------------------------------------------------------- /state/pages/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch'; 2 | import { receivePost } from '../posts/actions'; 3 | import { API_URL } from '../../utils/constants'; 4 | 5 | const actionTypes = { 6 | CATCH_PAGE_FETCH_ERROR: 'CATCH_PAGE_FETCH_ERROR', 7 | RECEIVE_PAGE: 'RECEIVE_PAGE', 8 | REQUEST_PAGE: 'REQUEST_PAGE', 9 | }; 10 | 11 | const catchPageFetchError = (page, error) => ({ 12 | type: actionTypes.CATCH_PAGE_FETCH_ERROR, 13 | error, 14 | page, 15 | }); 16 | 17 | const receivePage = (page, json) => ({ 18 | type: actionTypes.RECEIVE_PAGE, 19 | page, 20 | slugs: json.objects && json.objects.map(object => object.slug), 21 | }); 22 | 23 | const requestPage = page => ({ 24 | type: actionTypes.REQUEST_PAGE, 25 | page, 26 | }); 27 | 28 | const fetchPage = page => (dispatch) => { 29 | dispatch(requestPage(page)); 30 | 31 | return fetch(`${API_URL}/posts/page/${page}`) 32 | .then(res => res.json()) 33 | .then((json) => { 34 | dispatch(receivePage(page, json)); 35 | return json.objects.forEach(post => dispatch(receivePost(post))); 36 | }) 37 | .catch(error => catchPageFetchError(page, error)); 38 | }; 39 | 40 | const shouldFetchPage = (page, state) => { 41 | const { pages } = state; 42 | 43 | if (!pages[page]) return true; 44 | if (pages[page].isLoading) return false; 45 | return pages[page].slugs && !pages[page].slugs.length; 46 | }; 47 | 48 | const fetchPageIfNeeded = page => (dispatch, getState) => { 49 | if (shouldFetchPage(page, getState())) return dispatch(fetchPage(page)); 50 | return Promise.resolve(); 51 | }; 52 | 53 | export { 54 | actionTypes, 55 | catchPageFetchError, 56 | fetchPage, 57 | fetchPageIfNeeded, 58 | receivePage, 59 | requestPage, 60 | shouldFetchPage, 61 | }; 62 | -------------------------------------------------------------------------------- /utils/createCustomTheme/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adapted from source: 3 | https://github.com/mui-org/material-ui/blob/303199d39b42a321d28347d8440d69166f872f27/packages/material-ui/src/styles/createMuiTheme.js 4 | */ 5 | import createTypography from '../createTypography'; 6 | import createBreakpoints from '../../node_modules/@material-ui/core/styles/createBreakpoints'; 7 | import createPalette from '../../node_modules/@material-ui/core/styles/createPalette'; 8 | import createMixins from '../../node_modules/@material-ui/core/styles/createMixins'; 9 | import shadows from '../../node_modules/@material-ui/core/styles/shadows'; 10 | import transitions from '../../node_modules/@material-ui/core/styles/transitions'; 11 | import zIndex from '../../node_modules/@material-ui/core/styles/zIndex'; 12 | import spacing from '../../node_modules/@material-ui/core/styles/spacing'; 13 | 14 | const createCustomTheme = ({ htmlFontSize, theme }) => { 15 | const palette = createPalette({ 16 | primary: { 17 | light: '#52c7b8', 18 | main: '#009688', 19 | dark: '#00675b', 20 | contrastText: '#000', 21 | }, 22 | secondary: { 23 | light: '#6ed9ff', 24 | main: '#28a8e0', 25 | dark: '#0079ae', 26 | contrastText: '#000', 27 | }, 28 | type: theme, 29 | }); 30 | const breakpoints = createBreakpoints({}); 31 | const mixins = createMixins(breakpoints, spacing, {}); 32 | const typography = createTypography(htmlFontSize, palette); 33 | 34 | const muiTheme = { 35 | breakpoints, 36 | direction: 'ltr', 37 | mixins, 38 | overrides: {}, 39 | palette, 40 | props: {}, 41 | shadows, 42 | typography, 43 | transitions, 44 | spacing, 45 | zIndex, 46 | }; 47 | 48 | return muiTheme; 49 | }; 50 | 51 | export default createCustomTheme; 52 | -------------------------------------------------------------------------------- /state/metadata/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch'; 2 | import { API_URL } from '../../utils/constants'; 3 | 4 | const actionTypes = { 5 | CATCH_METADATA_FETCH_ERROR: 'CATCH_METADATA_FETCH_ERROR', 6 | RECEIVE_SITE_METADATA: 'RECEIVE_SITE_METADATA', 7 | REQUEST_SITE_METADATA: 'REQUEST_SITE_METADATA', 8 | }; 9 | 10 | const catchMetadataFetchError = error => ({ 11 | type: actionTypes.CATCH_METADATA_FETCH_ERROR, 12 | error, 13 | }); 14 | 15 | const receiveSiteMetadata = json => ({ 16 | type: actionTypes.RECEIVE_SITE_METADATA, 17 | logo: json.object && json.object.metadata && json.object.metadata.site_logo, 18 | tag: json.object && json.object.metadata && json.object.metadata.site_tag, 19 | title: json.object && json.object.metadata && json.object.metadata.site_title, 20 | }); 21 | 22 | const requestSiteMetadata = () => ({ 23 | type: actionTypes.REQUEST_SITE_METADATA, 24 | }); 25 | 26 | const fetchSiteMetadata = () => (dispatch) => { 27 | dispatch(requestSiteMetadata()); 28 | 29 | return fetch(`${API_URL}/meta`) 30 | .then(res => res.json()) 31 | .then(json => dispatch(receiveSiteMetadata(json))) 32 | .catch(error => dispatch(catchMetadataFetchError(error))); 33 | }; 34 | 35 | const shouldFetchSiteMetadata = (state) => { 36 | const { metadata } = state; 37 | 38 | if (!metadata) return true; 39 | if (metadata.isLoading) return false; 40 | return !metadata.logo || !metadata.tag || !metadata.title; 41 | }; 42 | 43 | const fetchSiteMetadataIfNeeded = () => (dispatch, getState) => { 44 | if (shouldFetchSiteMetadata(getState())) return dispatch(fetchSiteMetadata()); 45 | return Promise.resolve(); 46 | }; 47 | 48 | export { 49 | actionTypes, 50 | fetchSiteMetadata, 51 | fetchSiteMetadataIfNeeded, 52 | catchMetadataFetchError, 53 | receiveSiteMetadata, 54 | requestSiteMetadata, 55 | shouldFetchSiteMetadata, 56 | }; 57 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App, { Container } from 'next/app'; 3 | import { Provider } from 'react-redux'; 4 | import { PageTransition } from 'next-page-transitions'; 5 | 6 | import withReduxStore from '../lib/withReduxStore'; 7 | import AppContainer from '../components/AppContainer'; 8 | 9 | class CustomApp extends App { 10 | componentDidUpdate() { 11 | if (!window.previouslyLoaded) { 12 | window.previouslyLoaded = true; 13 | } 14 | } 15 | 16 | render() { 17 | const { Component, pageProps, reduxStore } = this.props; 18 | 19 | return ( 20 | 21 | 22 | 23 | 27 | 28 | 29 | 47 | 48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | CustomApp.getInitialProps = async ({ Component, ctx }) => { 55 | let pageProps = {}; 56 | 57 | if (Component.getInitialProps) { 58 | pageProps = await Component.getInitialProps(ctx); 59 | } 60 | 61 | return { pageProps }; 62 | }; 63 | 64 | export { CustomApp as DisconnectedCustomApp }; 65 | 66 | export default withReduxStore(CustomApp); 67 | -------------------------------------------------------------------------------- /components/SocialLinks/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { compose } from 'recompose'; 5 | import classnames from 'classnames'; 6 | 7 | import { withStyles } from '@material-ui/core/styles'; 8 | import IconButton from '@material-ui/core/IconButton'; 9 | 10 | import { Link } from '../../routes'; 11 | 12 | const styles = theme => ({ 13 | iconButton: { 14 | margin: theme.spacing.unit * 1, 15 | }, 16 | image: { 17 | display: 'block', 18 | fill: theme.palette.primary.main, 19 | margin: 0, 20 | padding: 0, 21 | }, 22 | root: { 23 | textAlign: 'center', 24 | }, 25 | }); 26 | 27 | const SocialLinks = ({ 28 | classes, 29 | className, 30 | links, 31 | }) => ( 32 |
33 | { 34 | links && links.items && links.items.map(item => ( 35 | 36 | 37 | 38 | {item.content} 45 | 46 | 47 | 48 | )) 49 | } 50 |
51 | ); 52 | 53 | SocialLinks.defaultProps = { 54 | className: undefined, 55 | links: [], 56 | }; 57 | 58 | SocialLinks.propTypes = { 59 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 60 | className: PropTypes.string, 61 | links: PropTypes.shape({ 62 | items: PropTypes.arrayOf(PropTypes.shape({ 63 | icon: PropTypes.string, 64 | url: PropTypes.string, 65 | })), 66 | }), 67 | }; 68 | 69 | const mapStateToProps = state => ({ 70 | links: state.links, 71 | }); 72 | 73 | export const DisconnectedSocialLinks = withStyles(styles)(SocialLinks); 74 | 75 | export default compose( 76 | withStyles(styles), 77 | connect(mapStateToProps), 78 | )(SocialLinks); 79 | -------------------------------------------------------------------------------- /components/AuthorInfo/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Typography from '@material-ui/core/Typography'; 7 | 8 | const styles = theme => ({ 9 | content: { 10 | [`@media screen and (max-width: ${theme.breakpoints.values.sm}px)`]: { 11 | flexDirection: 'column', 12 | marginLeft: 0, 13 | marginTop: theme.spacing.unit * 2, 14 | }, 15 | marginLeft: theme.spacing.unit * 4, 16 | marginTop: 0, 17 | }, 18 | image: { 19 | boxShadow: '4px 4px 0px 0px rgba(0,0,0,0.7)', 20 | display: 'block', 21 | flex: 'none', 22 | height: 120, 23 | margin: 0, 24 | padding: 0, 25 | width: 120, 26 | }, 27 | root: { 28 | [`@media screen and (max-width: ${theme.breakpoints.values.sm}px)`]: { 29 | flexDirection: 'column', 30 | }, 31 | alignItems: 'center', 32 | display: 'flex', 33 | margin: '0 auto', 34 | }, 35 | }); 36 | 37 | const AuthorInfo = ({ 38 | classes, 39 | className, 40 | title, 41 | content, 42 | metadata, 43 | }) => { 44 | const imageUrl = metadata && metadata.image && metadata.image.imgix_url; 45 | 46 | return ( 47 |
48 | { 49 | imageUrl && 50 | {title} 57 | } 58 | { 59 | content && 60 | 65 | {/* eslint-disable-next-line react/no-danger */} 66 | 67 | 68 | } 69 |
70 | ); 71 | }; 72 | 73 | AuthorInfo.defaultProps = { 74 | className: undefined, 75 | content: undefined, 76 | metadata: undefined, 77 | }; 78 | 79 | AuthorInfo.propTypes = { 80 | className: PropTypes.string, 81 | title: PropTypes.string.isRequired, 82 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 83 | content: PropTypes.string, 84 | metadata: PropTypes.shape({ 85 | image: PropTypes.shape({ 86 | imgix_url: PropTypes.string, 87 | }), 88 | }), 89 | }; 90 | 91 | export default withStyles(styles)(AuthorInfo); 92 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | /* Adapted from: https://github.com/mui-org/material-ui/blob/master/examples/nextjs/pages/_document.js */ 2 | import React, { Fragment } from 'react'; 3 | import Document, { Head, Main, NextScript } from 'next/document'; 4 | import JssProvider from 'react-jss/lib/JssProvider'; 5 | import flush from 'styled-jsx/server'; 6 | 7 | import getPageContext from '../src/getPageContext'; 8 | 9 | class CustomDocument extends Document { 10 | render() { 11 | const { pageContext } = this.props; 12 | 13 | return ( 14 | 15 | 16 | Cosmic Blog 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | CustomDocument.getInitialProps = (ctx) => { 46 | const pageContext = getPageContext(); 47 | const page = ctx.renderPage(Component => props => ( 48 | 52 | 53 | 54 | )); 55 | 56 | return { 57 | ...page, 58 | pageContext, 59 | styles: ( 60 | 61 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | WithRoot.defaultProps = { 54 | pageContext: undefined, 55 | }; 56 | 57 | WithRoot.propTypes = { 58 | appearance: PropTypes.shape({ 59 | htmlFontSize: PropTypes.number.isRequired, 60 | theme: PropTypes.oneOf(Object.getOwnPropertyNames(THEMES) 61 | .map(key => THEMES[key])).isRequired, 62 | }).isRequired, 63 | pageContext: PropTypes.object, // eslint-disable-line react/forbid-prop-types 64 | }; 65 | 66 | WithRoot.getInitialProps = (ctx) => { 67 | if (Component.getInitialProps) { 68 | return Component.getInitialProps(ctx); 69 | } 70 | 71 | return {}; 72 | }; 73 | 74 | const mapStateToProps = state => ({ 75 | appearance: state.appearance, 76 | }); 77 | 78 | return connect(mapStateToProps)(WithRoot); 79 | } 80 | 81 | export default withRoot; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-blog", 3 | "version": "1.0.0", 4 | "description": "Clean, minimalist, content-first Blog powered by Cosmic JS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test jest --config jest.config.js", 8 | "test:watch": "npm run test -- --watch", 9 | "eslint": "eslint . --ext .js,.jsx", 10 | "eslint:watch": "esw . --ext .js,.jsx --watch", 11 | "dev": "node server.js", 12 | "build": "next build", 13 | "start": "npm run build; NODE_ENV=production node server.js", 14 | "deploy": "npm run eslint && npm run test && now && now alias" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/chrisoverstreet/cosmic-blog.git" 19 | }, 20 | "author": "Chris Overstreet", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/chrisoverstreet/cosmic-blog/issues" 24 | }, 25 | "homepage": "https://github.com/chrisoverstreet/cosmic-blog#readme", 26 | "dependencies": { 27 | "@material-ui/core": "^1.0.0", 28 | "@material-ui/icons": "^1.0.0", 29 | "classnames": "^2.2.5", 30 | "cosmicjs": "^3.2.7", 31 | "dotenv": "^5.0.1", 32 | "express": "^4.16.3", 33 | "isomorphic-unfetch": "^2.0.0", 34 | "jss": "^9.8.1", 35 | "moment": "^2.22.1", 36 | "next": "^6.0.3", 37 | "next-page-transitions": "^1.0.0-alpha.3", 38 | "next-routes": "^1.4.1", 39 | "prop-types": "^15.6.1", 40 | "react": "^16.3.2", 41 | "react-dom": "^16.3.2", 42 | "react-headroom": "^2.2.2", 43 | "react-imgix": "^7.1.1", 44 | "babel-plugin-inline-react-svg": "^0.5.2", 45 | "babel-plugin-transform-inline-environment-variables": "^0.4.3", 46 | "react-jss": "^8.4.0", 47 | "react-redux": "^5.0.7", 48 | "recompose": "^0.27.0", 49 | "redux": "^4.0.0", 50 | "redux-devtools-extension": "^2.13.2", 51 | "redux-thunk": "^2.2.0", 52 | "styled-jsx": "^2.2.6", 53 | "styled-jsx-plugin-sass": "^0.2.4", 54 | "extract-loader": "^2.0.1", 55 | "file-loader": "^1.1.11", 56 | "identity-obj-proxy": "^3.0.0", 57 | "jest": "^22.4.4", 58 | "node-sass": "^4.9.0", 59 | "babel-plugin-inline-dotenv": "^1.1.2", 60 | "css-loader": "^0.28.11", 61 | "enzyme": "^3.3.0", 62 | "enzyme-adapter-react-16": "^1.1.1", 63 | "enzyme-to-json": "^3.3.4" 64 | }, 65 | "devDependencies": { 66 | "babel-eslint": "^8.2.3", 67 | "eslint": "^4.19.1", 68 | "eslint-config-airbnb": "^16.1.0", 69 | "eslint-plugin-env": "0.0.3", 70 | "eslint-plugin-import": "^2.12.0", 71 | "eslint-plugin-jsx-a11y": "^6.0.3", 72 | "eslint-plugin-react": "^7.8.2", 73 | "eslint-watch": "^3.1.4", 74 | "now": "^11.1.12" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | require('dotenv').config({ path: './.env.production' }); 3 | const express = require('express'); 4 | const next = require('next'); 5 | const routes = require('./routes'); 6 | const Cosmic = require('cosmicjs'); 7 | 8 | const port = parseInt(process.env.PORT, 10) || 3000; 9 | const dev = process.env.NODE_ENV !== 'production'; 10 | const app = next({ dev }); 11 | const handler = routes.getRequestHandler(app); 12 | const api = Cosmic(); 13 | const bucket = api.bucket({ slug: process.env.BUCKET_SLUG }); 14 | 15 | app.prepare() 16 | .then(() => { 17 | const server = express(); 18 | 19 | // API endpoint for site metadata (i.e. title, tag, logo) 20 | server.get('/api/meta', (req, res) => bucket.getObject({ slug: 'header' }) 21 | .then(object => res.send(object)) 22 | .catch(err => res.status(404).json({ 23 | message: 'Error fetching header data', 24 | error: err, 25 | }))); 26 | 27 | // API endpoint for social links 28 | server.get('/api/social-links', (req, res) => { 29 | const params = { 30 | type: 'social-links', 31 | }; 32 | 33 | return bucket.getObjects(params) 34 | .then(objects => res.send(objects)) 35 | .catch(err => res.status(404).json({ 36 | message: 'Error fetching social links', 37 | error: err, 38 | })); 39 | }); 40 | 41 | // API endpoint for a list of posts (by page) 42 | server.get('/api/posts/page/:page', (req, res) => { 43 | const validatedPage = !Number.isNaN(req.params.page) && parseInt(req.params.page, 10) >= 0 44 | ? parseInt(req.params.page, 10) 45 | : 1; 46 | const postsPerPage = 10; 47 | const params = { 48 | limit: postsPerPage, 49 | skip: (validatedPage - 1) * postsPerPage, 50 | sort: '+created_at', 51 | status: 'published', 52 | type: 'posts', 53 | }; 54 | 55 | return bucket.getObjects(params) 56 | .then(objects => res.send(objects)) 57 | .catch(err => res.status(404).json({ 58 | message: `Error fetching posts for page ${validatedPage}`, 59 | error: err, 60 | })); 61 | }); 62 | 63 | // API endpoint for an individual post 64 | server.get('/api/post/:slug', (req, res) => bucket.getObject({ slug: req.params.slug }) 65 | .then(object => res.send(object)) 66 | .catch(err => res.status(404).json({ 67 | message: `Error fetching post with slug, ${req.params.slug}`, 68 | error: err, 69 | }))); 70 | 71 | // Our regular NextJS pages 72 | server.get('*', (req, res) => handler(req, res)); 73 | 74 | server 75 | .listen(port, (err) => { 76 | if (err) throw err; 77 | console.log(`> Ready on http://localhost:${port}`); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /utils/createTypography/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adapted from source: 3 | https://github.com/mui-org/material-ui/blob/303199d39b42a321d28347d8440d69166f872f27/packages/material-ui/src/styles/createTypography.js 4 | */ 5 | const round = value => Math.round(value * 1e5) / 1e5; 6 | 7 | const createTypography = (htmlFontSize = 16, palette) => { 8 | const sansFontFamily = "'Montserrat', sans-serif"; 9 | const serifFontFamily = "'Lora', serif"; 10 | const fontSize = 16; 11 | const fontWeightLight = 300; 12 | const fontWeightRegular = 400; 13 | const fontWeightMedium = 500; 14 | 15 | const coef = fontSize / 14; 16 | const pxToRem = value => `${(value / htmlFontSize) * coef}rem`; 17 | 18 | return { 19 | pxToRem, 20 | round, 21 | fontSize, 22 | fontWeightLight, 23 | fontWeightRegular, 24 | fontWeightMedium, 25 | sansFontFamily, 26 | serifFontFamily, 27 | display4: { 28 | fontSize: pxToRem(45), 29 | fontWeight: fontWeightRegular, 30 | fontFamily: sansFontFamily, 31 | lineHeight: `${round(48 / 45)}em`, 32 | marginLeft: '-.02em', 33 | color: palette.text.primary, 34 | }, 35 | display3: { 36 | fontSize: pxToRem(45), 37 | fontWeight: fontWeightRegular, 38 | fontFamily: sansFontFamily, 39 | lineHeight: `${round(48 / 45)}em`, 40 | marginLeft: '-.02em', 41 | color: palette.text.primary, 42 | }, 43 | display2: { 44 | fontSize: pxToRem(18), 45 | fontWeight: fontWeightMedium, 46 | fontFamily: sansFontFamily, 47 | lineHeight: `${round(48 / 45)}em`, 48 | marginLeft: '-.02em', 49 | color: palette.text.primary, 50 | }, 51 | display1: { 52 | fontSize: pxToRem(16), 53 | fontWeight: fontWeightMedium, 54 | fontFamily: sansFontFamily, 55 | lineHeight: `${round(41 / 34)}em`, 56 | color: palette.text.primary, 57 | }, 58 | headline: { 59 | fontSize: pxToRem(20), 60 | fontWeight: fontWeightMedium, 61 | fontFamily: sansFontFamily, 62 | lineHeight: `${round(32.5 / 24)}em`, 63 | color: palette.text.primary, 64 | }, 65 | title: { 66 | fontSize: pxToRem(14), 67 | fontWeight: fontWeightMedium, 68 | fontFamily: sansFontFamily, 69 | lineHeight: `${round(24.5 / 21)}em`, 70 | color: palette.text.primary, 71 | }, 72 | subheading: { 73 | fontSize: pxToRem(14), 74 | fontWeight: fontWeightRegular, 75 | fontFamily: sansFontFamily, 76 | lineHeight: `${round(24 / 16)}em`, 77 | color: palette.text.secondary, 78 | }, 79 | body2: { 80 | fontSize: pxToRem(14), 81 | fontWeight: fontWeightRegular, 82 | fontFamily: serifFontFamily, 83 | lineHeight: `${round(24 / 14)}em`, 84 | color: palette.text.primary, 85 | }, 86 | body1: { 87 | fontSize: pxToRem(16), 88 | fontWeight: fontWeightRegular, 89 | fontFamily: serifFontFamily, 90 | lineHeight: `${round(24 / 14)}em`, 91 | color: palette.text.primary, 92 | }, 93 | caption: { 94 | fontSize: pxToRem(12), 95 | fontWeight: fontWeightRegular, 96 | fontFamily: sansFontFamily, 97 | lineHeight: `${round(16.5 / 12)}em`, 98 | color: palette.text.secondary, 99 | }, 100 | button: { 101 | fontSize: pxToRem(14), 102 | textTransform: 'uppercase', 103 | fontWeight: fontWeightMedium, 104 | fontFamily: sansFontFamily, 105 | color: palette.text.primary, 106 | }, 107 | }; 108 | }; 109 | 110 | export default createTypography; 111 | -------------------------------------------------------------------------------- /components/AppearanceDialog/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { compose } from 'recompose'; 4 | import { connect } from 'react-redux'; 5 | 6 | import Button from '@material-ui/core/Button'; 7 | import Dialog from '@material-ui/core/Dialog'; 8 | import FormGroup from '@material-ui/core/FormGroup'; 9 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 10 | import Switch from '@material-ui/core/Switch'; 11 | import { withStyles } from '@material-ui/core/styles'; 12 | 13 | import FontSizeIcon from '../../assets/svg/baseline-title-24px.svg'; 14 | import { 15 | decreaseFontSize, 16 | increaseFontSize, 17 | setTheme, 18 | } from '../../state/appearance/actions'; 19 | import { THEMES } from '../../utils/constants'; 20 | 21 | const styles = theme => ({ 22 | button: { 23 | flex: 1, 24 | }, 25 | dialog: { 26 | // width: 250, 27 | padding: theme.spacing.unit, 28 | }, 29 | decreaseFontSizeBtn: { 30 | fill: theme.palette.text.primary, 31 | height: '1.5rem', 32 | width: '1.5rem', 33 | }, 34 | fontSizeBtnsContainer: { 35 | borderBottom: '1px solid rgba(0, 0, 0, 0.1)', 36 | display: 'flex', 37 | paddingBottom: theme.spacing.unit, 38 | width: '100%', 39 | }, 40 | increaseFontSizeBtn: { 41 | fill: theme.palette.text.primary, 42 | height: '2.2rem', 43 | width: '2.2rem', 44 | }, 45 | label: { 46 | fontFamily: theme.typography.sansFontFamily, 47 | fontSize: '1.3rem', 48 | }, 49 | switchContainer: { 50 | alignItems: 'center', 51 | display: 'flex', 52 | justifyContent: 'center', 53 | padding: theme.spacing.unit, 54 | }, 55 | themeBtnsContainer: { 56 | display: 'flex', 57 | }, 58 | }); 59 | 60 | const AppearanceDialog = ({ 61 | classes, 62 | dispatch, 63 | onClose, 64 | open, 65 | theme, 66 | }) => ( 67 | 73 |
74 | 80 | 86 |
87 |
88 | 93 | 98 | dispatch(setTheme(theme === THEMES.DARK ? THEMES.LIGHT : THEMES.DARK)) 99 | } 100 | value="theme" 101 | /> 102 | } 103 | classes={{ 104 | label: classes.label, 105 | }} 106 | label="Night mode" 107 | /> 108 | 109 |
110 |
111 | ); 112 | 113 | AppearanceDialog.defaultProps = { 114 | open: false, 115 | }; 116 | 117 | AppearanceDialog.propTypes = { 118 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 119 | dispatch: PropTypes.func.isRequired, 120 | onClose: PropTypes.func.isRequired, 121 | open: PropTypes.bool, 122 | theme: PropTypes.oneOf(Object.getOwnPropertyNames(THEMES) 123 | .map(prop => THEMES[prop])).isRequired, 124 | }; 125 | 126 | const mapStateToProps = state => ({ 127 | theme: state.appearance.theme, 128 | }); 129 | 130 | export const DisconnectedAppearanceDialog = withStyles(styles)(AppearanceDialog); 131 | 132 | export default compose( 133 | connect(mapStateToProps), 134 | withStyles(styles), 135 | )(AppearanceDialog); 136 | -------------------------------------------------------------------------------- /components/AppBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Headroom from 'react-headroom'; 4 | import classnames from 'classnames'; 5 | 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | import BackIcon from '@material-ui/icons/ArrowBack'; 11 | import SettingsIcon from '@material-ui/icons/Settings'; 12 | 13 | import AppearanceDialog from '../AppearanceDialog'; 14 | import handleBackBtnClick from '../../utils/handleBackBtnClick'; 15 | 16 | const styles = theme => ({ 17 | appearanceBtn: { 18 | justifySelf: 'end', 19 | }, 20 | backBtn: { 21 | justifySelf: 'start', 22 | }, 23 | header: { 24 | alignItems: 'center', 25 | backgroundColor: theme.palette.background.default, 26 | borderBottom: '1px solid rgba(0, 0, 0, 0.1)', 27 | display: 'grid', 28 | gridTemplateColumns: '1fr auto 1fr', 29 | padding: theme.spacing.unit, 30 | transition: 'border-bottom ease 0.3s', 31 | }, 32 | header_topOfPage: { 33 | borderBottom: '1px solid rgba(0, 0, 0, 0)', 34 | }, 35 | title: { 36 | opacity: 1, 37 | overflow: 'hidden', 38 | paddingLeft: theme.spacing.unit, 39 | paddingRight: theme.spacing.unit, 40 | textOverflow: 'ellipsis', 41 | transition: 'opacity ease 0.3s', 42 | whiteSpace: 'nowrap', 43 | }, 44 | title_topOfPage: { 45 | opacity: 0, 46 | }, 47 | }); 48 | 49 | class AppBar extends Component { 50 | constructor(props) { 51 | super(props); 52 | 53 | this.state = { 54 | atTopOfPage: true, 55 | appearanceDialogOpen: false, 56 | }; 57 | 58 | this.handleScroll = this.handleScroll.bind(this); 59 | } 60 | 61 | componentDidMount() { 62 | window.addEventListener('scroll', this.handleScroll); 63 | } 64 | 65 | componentWillUnmount() { 66 | window.removeEventListener('scroll', this.handleScroll); 67 | } 68 | 69 | handleScroll(event) { 70 | try { 71 | const pageY = event.pageY 72 | ? event.pageY // firefox 73 | : event.path.find(path => path.scrollY).scrollY; // chrome 74 | if (pageY > 40 && this.state.atTopOfPage) { 75 | this.setState({ atTopOfPage: false }); 76 | } else if (pageY <= 40 && !this.state.atTopOfPage) { 77 | this.setState({ atTopOfPage: true }); 78 | } 79 | } catch (e) { 80 | // Do nothing 81 | } 82 | } 83 | 84 | render() { 85 | const { classes, title } = this.props; 86 | const { atTopOfPage, appearanceDialogOpen } = this.state; 87 | 88 | return ( 89 | 90 | 91 |
97 | 102 | 103 | 104 | 111 | {title} 112 | 113 | this.setState({ appearanceDialogOpen: !appearanceDialogOpen })} 117 | > 118 | 119 | 120 |
121 |
122 | this.setState({ appearanceDialogOpen: false })} 125 | /> 126 |
127 | ); 128 | } 129 | } 130 | 131 | AppBar.defaultProps = { 132 | title: '', 133 | }; 134 | 135 | AppBar.propTypes = { 136 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 137 | title: PropTypes.string, 138 | }; 139 | 140 | export default withStyles(styles)(AppBar); 141 | -------------------------------------------------------------------------------- /components/AppearanceDialog/__snapshots__/AppearanceDialog.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AppearanceDialog should render 1`] = ` 4 | 10 | 28 | 38 | 66 | 83 | 121 | 122 | 123 | 124 | 125 | 126 | `; 127 | -------------------------------------------------------------------------------- /components/PostPreview/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | import { compose } from 'recompose'; 5 | 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import withWidth from '@material-ui/core/withWidth'; 8 | import Hidden from '@material-ui/core/Hidden'; 9 | import Typography from '@material-ui/core/Typography'; 10 | 11 | import { Router } from '../../routes'; 12 | 13 | const styles = theme => ({ 14 | authorAndDate: { 15 | alignItems: 'center', 16 | display: 'flex', 17 | }, 18 | authorImage: { 19 | display: 'block', 20 | margin: '0 8px 0 0', 21 | padding: 0, 22 | }, 23 | date: { 24 | display: 'inline-block', 25 | paddingLeft: theme.spacing.unit, 26 | }, 27 | button: { 28 | margin: 0, 29 | marginBottom: theme.spacing.unit * 3, 30 | paddingTop: theme.spacing.unit * 2, 31 | paddingBottom: theme.spacing.unit * 2, 32 | backgroundColor: 'transparent', 33 | borderStyle: 'none', 34 | color: 'inherit', 35 | cursor: 'pointer', 36 | display: 'flex', 37 | fontSize: 'inherit', 38 | textAlign: 'inherit', 39 | textDecoration: 'inherit', 40 | width: '100%', 41 | }, 42 | content: { 43 | flex: 1, 44 | }, 45 | heroFigure: { 46 | padding: 0, 47 | paddingLeft: theme.mixins.gutters().paddingLeft * 2, 48 | display: 'block', 49 | flex: 'none', 50 | margin: 0, 51 | }, 52 | heroImage: { 53 | boxShadow: '4px 4px 0px 0px rgba(0,0,0,0.7)', 54 | display: 'block', 55 | height: 140, 56 | margin: 0, 57 | padding: 0, 58 | width: 140, 59 | }, 60 | root: { 61 | display: 'block', 62 | margin: '0 auto', 63 | maxWidth: 720, 64 | }, 65 | }); 66 | 67 | const PostPreview = ({ 68 | classes, 69 | metadata, 70 | published_at, // eslint-disable-line camelcase 71 | slug, 72 | title, 73 | }) => { 74 | const authorName = metadata && metadata.author && metadata.author.title; 75 | const authorImage = metadata && metadata.author && metadata.author.metadata 76 | && metadata.author.metadata.image && metadata.author.metadata.image.imgix_url; 77 | const heroImage = metadata && metadata.hero && metadata.hero.imgix_url; 78 | const teaser = metadata && metadata.teaser; 79 | 80 | return ( 81 | 82 |
  • 83 | 134 |
  • 135 |
    136 | ); 137 | }; 138 | 139 | PostPreview.defaultProps = { 140 | metadata: undefined, 141 | published_at: undefined, 142 | }; 143 | 144 | PostPreview.propTypes = { 145 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 146 | metadata: PropTypes.shape({ 147 | author: PropTypes.shape({ 148 | content: PropTypes.string, 149 | metadata: PropTypes.shape({ 150 | image: PropTypes.shape({ 151 | imgix_url: PropTypes.string, 152 | }), 153 | }), 154 | title: PropTypes.string, 155 | }), 156 | hero: PropTypes.shape({ 157 | imgix_url: PropTypes.string, 158 | }), 159 | teaser: PropTypes.string, 160 | }), 161 | published_at: PropTypes.string, 162 | slug: PropTypes.string.isRequired, 163 | title: PropTypes.string.isRequired, 164 | }; 165 | 166 | export const PurePostPreview = PostPreview; 167 | 168 | export default compose( 169 | withStyles(styles), 170 | withWidth(), 171 | )(PostPreview); 172 | -------------------------------------------------------------------------------- /components/AuthorInfo/__snapshots__/AuthorInfo.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AuthorInfo should render 1`] = ` 4 | 39 | 81 |
    84 | John Doe 91 | 100 | 150 |
    153 | Something about John...

    ", 157 | } 158 | } 159 | /> 160 |
    161 |
    162 |
    163 |
    164 |
    165 |
    166 | `; 167 | -------------------------------------------------------------------------------- /__mocks__/post.js: -------------------------------------------------------------------------------- 1 | export default { 2 | _id: '5afdbf6c44d74b4e924f1d0b', 3 | bucket: '5afdbcd115bef94f7421eb0d', 4 | slug: 'a-wonderful-blog-post-about-earth', 5 | title: 'A Wonderful Blog Post About Earth', 6 | content: '

    When I orbited the Earth in a spaceship, I saw for the first time how beautiful our planet is. Mankind, let us preserve and increase this beauty, and not destroy it!

    Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.

    If you could see the earth illuminated when you were in a place as dark as night, it would look to you more splendid than the moon.

    To be the first to enter the cosmos, to engage, single-handed, in an unprecedented duel with nature—could one dream of anything more?

    We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard, because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win.

    NASA is not about the ‘Adventure of Human Space Exploration’…We won’t be doing it just to get out there in space – we’ll be doing it because the things we learn out there will be making life better for a lot of people who won’t be able to go.

    Problems look mighty small from 150 miles up.

    That's one small step for [a] man, one giant leap for mankind.

    Where ignorance lurks, so too do the frontiers of discovery and imagination.

    Dinosaurs are extinct today because they lacked opposable thumbs and the brainpower to build a space program.

    ', 7 | metafields: [ 8 | { 9 | value: '56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 10 | key: 'hero', 11 | title: 'Hero', 12 | type: 'file', 13 | children: null, 14 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 15 | imgix_url: 'https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 16 | }, { 17 | value: '

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ', 18 | key: 'teaser', 19 | title: 'Teaser', 20 | type: 'html-textarea', 21 | children: null, 22 | }, { 23 | object_type: 'authors', 24 | value: '5afdbf6c44d74b4e924f1d09', 25 | key: 'author', 26 | title: 'Author', 27 | type: 'object', 28 | children: null, 29 | object: { 30 | _id: '5afdbf6c44d74b4e924f1d09', 31 | bucket: '5afdbcd115bef94f7421eb0d', 32 | slug: 'john-doe', 33 | title: 'John Doe', 34 | content: '

    Something about John...

    ', 35 | metafields: [ 36 | { 37 | value: '9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 38 | key: 'image', 39 | title: 'Image', 40 | type: 'file', 41 | children: null, 42 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 43 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 44 | }, 45 | ], 46 | type_slug: 'authors', 47 | created: '2017-10-12T13:27:49.665Z', 48 | created_at: '2017-10-12T13:27:49.665Z', 49 | modified_at: '2018-05-17T17:44:12.977Z', 50 | status: 'published', 51 | published_at: '2018-05-17T17:44:12.977Z', 52 | metadata: { 53 | image: { 54 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 55 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 56 | }, 57 | }, 58 | }, 59 | }, 60 | ], 61 | type_slug: 'posts', 62 | created: '2017-10-12T13:27:49.665Z', 63 | created_at: '2017-10-12T13:27:49.665Z', 64 | modified_at: '2017-10-12T17:39:54.476Z', 65 | status: 'published', 66 | published_at: '2018-05-17T17:44:12.979Z', 67 | metadata: { 68 | hero: { 69 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 70 | imgix_url: 'https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 71 | }, 72 | teaser: '

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ', 73 | author: { 74 | _id: '5afdbf6c44d74b4e924f1d09', 75 | bucket: '5afdbcd115bef94f7421eb0d', 76 | slug: 'john-doe', 77 | title: 'John Doe', 78 | content: '

    Something about John...

    ', 79 | metafields: [ 80 | { 81 | value: '9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 82 | key: 'image', 83 | title: 'Image', 84 | type: 'file', 85 | children: null, 86 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 87 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 88 | }, 89 | ], 90 | type_slug: 'authors', 91 | created: '2017-10-12T13:27:49.665Z', 92 | created_at: '2017-10-12T13:27:49.665Z', 93 | modified_at: '2018-05-17T17:44:12.977Z', 94 | status: 'published', 95 | published_at: '2018-05-17T17:44:12.977Z', 96 | metadata: { 97 | image: { 98 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 99 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 100 | }, 101 | }, 102 | }, 103 | }, 104 | }; 105 | -------------------------------------------------------------------------------- /pages/index/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import { compose } from 'recompose'; 6 | import Head from 'next/head'; 7 | 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | import withRoot from '../../src/withRoot'; 11 | import { fetchLinksIfNeeded } from '../../state/links/actions'; 12 | import { fetchSiteMetadataIfNeeded } from '../../state/metadata/actions'; 13 | import { fetchPageIfNeeded } from '../../state/pages/actions'; 14 | import PostPreview from '../../components/PostPreview'; 15 | import SocialLinks from '../../components/SocialLinks'; 16 | 17 | const styles = theme => ({ 18 | divider: { 19 | border: 0, 20 | borderTop: `1px solid ${theme.palette.text.secondary}`, 21 | height: 0, 22 | marginBottom: theme.spacing.unit * 6, 23 | maxWidth: 700, 24 | opacity: 0.2, 25 | width: '100%', 26 | }, 27 | root: { 28 | ...theme.mixins.gutters(), 29 | paddingTop: theme.spacing.unit * 6, 30 | paddingBottom: theme.spacing.unit * 6, 31 | }, 32 | logo: { 33 | display: 'block', 34 | margin: '0 auto', 35 | maxinWidth: 700, 36 | overflow: 'hidden', 37 | paddingTop: theme.spacing.unit * 6, 38 | paddingBottom: theme.spacing.unit * 6, 39 | }, 40 | postPreviews: { 41 | listStyle: 'none', 42 | marginBottom: theme.spacing.unit * 4, 43 | marginTop: theme.spacing.unit * 4, 44 | padding: 0, 45 | }, 46 | postPreviewsHeaderContainer: { 47 | marginTop: theme.spacing.unit * 4, 48 | padding: 0, 49 | }, 50 | postPreviewsHeader: { 51 | margin: '0 auto', 52 | maxWidth: 700, 53 | position: 'relative', 54 | }, 55 | postPreviewsHeaderTitle: { 56 | bottom: -1, 57 | borderBottom: `1px solid ${theme.palette.text.secondary}`, 58 | display: 'inline-block', 59 | padding: theme.spacing.unit, 60 | position: 'absolute', 61 | left: 0, 62 | }, 63 | postPreviewsHeaderContainerDivider: { 64 | border: 0, 65 | borderTop: `1px solid ${theme.palette.text.secondary}`, 66 | disaplay: 'block', 67 | height: 0, 68 | margin: '0 auto', 69 | maxWidth: 700, 70 | opacity: 0.2, 71 | padding: 0, 72 | width: '100%', 73 | }, 74 | tag: { 75 | display: 'block', 76 | textAlign: 'center', 77 | }, 78 | title: { 79 | display: 'block', 80 | textAlign: 'center', 81 | }, 82 | }); 83 | 84 | const Index = ({ 85 | classes, 86 | metadata, 87 | page, 88 | pages, 89 | posts, 90 | }) => { 91 | let postPreviews; 92 | try { 93 | postPreviews = pages[page].slugs.map(slug => posts[slug]); 94 | } catch (e) { 95 | postPreviews = undefined; 96 | } 97 | 98 | return ( 99 |
    100 | 101 | {(metadata && metadata.title) || 'Cosmic Blog'} 102 | { 103 | metadata && metadata.logo && metadata.logo.imgix_url && 104 | 105 | } 106 | { 107 | metadata && metadata.logo && metadata.logo.imgix_url && 108 | 109 | } 110 | { 111 | metadata && metadata.logo && metadata.logo.imgix_url && 112 | 113 | } 114 | { 115 | metadata && metadata.logo && metadata.logo.imgix_url && 116 | 117 | } 118 | 119 | 120 | 121 | 122 | { 123 | metadata && metadata.title && 124 | 125 | {metadata.title} 126 | 127 | } 128 | { 129 | metadata && metadata.tag && 130 | 131 | {metadata.tag} 132 | 133 | } 134 | { 135 | metadata && metadata.logo && metadata.logo.imgix_url && 136 | 141 | } 142 |
    143 |
    144 | 145 | Most Recent 146 | 147 |
    148 |
    149 |
    150 | { 151 | postPreviews && 152 |
      153 | { 154 | postPreviews.map(({ post }) => ) 155 | } 156 |
    157 | } 158 |
    159 | 160 |
    161 | ); 162 | }; 163 | 164 | Index.getInitialProps = async ({ query, reduxStore }) => { 165 | const { p } = query; 166 | const page = p && parseInt(p, 10) > 1 ? parseInt(p, 10) : 1; 167 | 168 | try { 169 | const { dispatch } = reduxStore; 170 | await Promise.all([ 171 | dispatch(fetchLinksIfNeeded()), 172 | dispatch(fetchPageIfNeeded(page)), 173 | dispatch(fetchSiteMetadataIfNeeded()), 174 | ]); 175 | } catch (e) { 176 | console.error(e); // eslint-disable-line no-console 177 | // Do nothing 178 | } 179 | 180 | return { page }; 181 | }; 182 | 183 | Index.propTypes = { 184 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 185 | metadata: PropTypes.shape({ 186 | logo: PropTypes.shape({ 187 | imgix_url: PropTypes.string, 188 | }), 189 | tag: PropTypes.string, 190 | title: PropTypes.string, 191 | }).isRequired, 192 | page: PropTypes.number.isRequired, 193 | pages: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 194 | posts: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 195 | }; 196 | 197 | const mapStateToProps = state => ({ 198 | metadata: state.metadata, 199 | pages: state.pages, 200 | posts: state.posts, 201 | }); 202 | 203 | export const DisconnectedIndex = withStyles(styles)(Index); 204 | 205 | export default compose( 206 | connect(mapStateToProps), 207 | withRoot, 208 | withStyles(styles), 209 | )(Index); 210 | -------------------------------------------------------------------------------- /pages/post/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { compose } from 'recompose'; 4 | import classnames from 'classnames'; 5 | import moment from 'moment'; 6 | import Head from 'next/head'; 7 | 8 | import { withStyles } from '@material-ui/core/styles'; 9 | import Typography from '@material-ui/core/Typography'; 10 | 11 | import withRoot from '../../src/withRoot'; 12 | import { fetchLinksIfNeeded } from '../../state/links/actions'; 13 | import { fetchPostIfNeeded } from '../../state/posts/actions'; 14 | import AppBar from '../../components/AppBar'; 15 | import AuthorInfo from '../../components/AuthorInfo'; 16 | import SocialLinks from '../../components/SocialLinks'; 17 | import { BASE_URL } from '../../utils/constants'; 18 | 19 | const styles = theme => ({ 20 | authorContainer: { 21 | alignItems: 'center', 22 | display: 'flex', 23 | marginBottom: theme.spacing.unit * 2, 24 | }, 25 | authorInfo: { 26 | margin: theme.spacing.unit * 6, 27 | marginTop: theme.spacing.unit * 4, 28 | }, 29 | authorFigure: { 30 | height: 16, 31 | margin: 0, 32 | marginRight: theme.spacing.unit, 33 | width: 16, 34 | }, 35 | centerColumn: { 36 | [theme.breakpoints.down('xs')]: { 37 | gridColumn: '1 / span 1', 38 | }, 39 | gridColumn: '2 / span 1', 40 | }, 41 | date: { 42 | marginBottom: theme.spacing.unit * 2, 43 | }, 44 | divider: { 45 | border: 0, 46 | borderTop: `1px solid ${theme.palette.text.secondary}`, 47 | height: 0, 48 | marginBottom: theme.spacing.unit * 2, 49 | opacity: 0.2, 50 | width: '100%', 51 | }, 52 | fullWidth: { 53 | [theme.breakpoints.down('xs')]: { 54 | gridColumn: '1 / span 1', 55 | }, 56 | gridColumn: '1 / span 3', 57 | }, 58 | heroFigure: { 59 | display: 'block', 60 | margin: 0, 61 | overflow: 'hidden', 62 | padding: `${theme.spacing.unit * 2}px 0`, 63 | }, 64 | heroImg: { 65 | display: 'block', 66 | margin: 0, 67 | padding: 0, 68 | width: '100%', 69 | }, 70 | main: { 71 | ...theme.mixins.gutters(), 72 | [theme.breakpoints.down('xs')]: { 73 | gridTemplateColumns: 'auto', 74 | }, 75 | gridTemplateColumns: `${theme.spacing.unit * 4}px auto ${theme.spacing.unit * 4}px`, 76 | display: 'grid', 77 | marginBottom: theme.spacing.unit * 4, 78 | marginLeft: 'auto', 79 | marginRight: 'auto', 80 | marginTop: 0, 81 | maxWidth: 900, 82 | }, 83 | root: { 84 | paddingTop: theme.spacing.unit * 6, 85 | paddingBottom: theme.spacing.unit * 6, 86 | }, 87 | title: { 88 | marginBottom: theme.spacing.unit * 2, 89 | }, 90 | }); 91 | 92 | const Post = ({ 93 | asPath, 94 | classes, 95 | content, 96 | metadata, 97 | published_at, // eslint-disable-line camelcase 98 | title, 99 | }) => { 100 | const author = metadata && metadata.author; 101 | const authorName = author && author.title; 102 | const authorImageUrl = author && author.metadata && author.metadata.image 103 | && author.metadata.image.imgix_url; 104 | const heroUrl = metadata && metadata.hero && metadata.hero.imgix_url; 105 | 106 | return ( 107 | 108 | 109 | {title} 110 | {heroUrl && } 111 | {heroUrl && } 112 | {heroUrl && } 113 | {heroUrl && } 114 | {content && } 115 | {title && } 116 | {asPath && } 117 | 118 | 119 |
    120 |
    121 | { 122 | (authorName || authorImageUrl) && 123 |
    129 | { 130 | authorImageUrl && 131 |
    132 | 138 |
    139 | } 140 | { 141 | authorName && 142 | 143 | {authorName} 144 | 145 | } 146 |
    147 | } 148 | { 149 | title && 150 | 159 | {title} 160 | 161 | } 162 | { 163 | published_at && // eslint-disable-line camelcase 164 | 173 | {moment(published_at).format('MMMM Do, YYYY')} 174 | 175 | } 176 |
    182 | { 183 | heroUrl && 184 |
    190 | 195 |
    196 | } 197 | { 198 | content && 199 | 200 | 201 | {/* eslint-disable-next-line react/no-danger */} 202 | 203 | 204 | 205 | } 206 |
    212 | { 213 | author && 214 | 221 | } 222 |
    228 |
    229 | 230 |
    231 |
    232 | ); 233 | }; 234 | 235 | Post.getInitialProps = async ({ query, reduxStore }) => { 236 | const { slug } = query; 237 | const { dispatch } = reduxStore; 238 | 239 | try { 240 | await Promise.all([ 241 | dispatch(fetchPostIfNeeded(slug)), 242 | dispatch(fetchLinksIfNeeded()), 243 | ]); 244 | const { posts } = reduxStore.getState(); 245 | const { post } = posts[slug]; 246 | return { ...post }; 247 | } catch (e) { 248 | console.error(e); // eslint-disable-line no-console 249 | return { slug }; 250 | } 251 | }; 252 | 253 | Post.defaultProps = { 254 | asPath: undefined, 255 | content: undefined, 256 | metadata: undefined, 257 | published_at: undefined, 258 | title: undefined, 259 | }; 260 | 261 | Post.propTypes = { 262 | asPath: PropTypes.string, 263 | classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 264 | content: PropTypes.string, 265 | metadata: PropTypes.shape({ 266 | author: PropTypes.shape({ 267 | content: PropTypes.string, 268 | metadata: PropTypes.shape({ 269 | image: PropTypes.shape({ 270 | imgix_url: PropTypes.string, 271 | }), 272 | }), 273 | title: PropTypes.string, 274 | }), 275 | hero: PropTypes.shape({ 276 | imgix_url: PropTypes.string, 277 | }), 278 | }), 279 | published_at: PropTypes.string, 280 | title: PropTypes.string, 281 | }; 282 | 283 | export const BasicPost = withStyles(styles)(Post); 284 | 285 | export default compose( 286 | withRoot, 287 | withStyles(styles), 288 | )(Post); 289 | -------------------------------------------------------------------------------- /static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 38 | 68 | 77 | 88 | 115 | 124 | 156 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /__mocks__/posts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'a-wonderful-blog-post-about-earth': { 3 | post: { 4 | _id: '5afdbf6c44d74b4e924f1d0b', 5 | bucket: '5afdbcd115bef94f7421eb0d', 6 | slug: 'a-wonderful-blog-post-about-earth', 7 | title: 'A Wonderful Blog Post About Earth', 8 | content: '

    When I orbited the Earth in a spaceship, I saw for the first time how beautiful our planet is. Mankind, let us preserve and increase this beauty, and not destroy it!

    Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.

    If you could see the earth illuminated when you were in a place as dark as night, it would look to you more splendid than the moon.

    To be the first to enter the cosmos, to engage, single-handed, in an unprecedented duel with nature—could one dream of anything more?

    We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard, because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win.

    NASA is not about the ‘Adventure of Human Space Exploration’…We won’t be doing it just to get out there in space – we’ll be doing it because the things we learn out there will be making life better for a lot of people who won’t be able to go.

    Problems look mighty small from 150 miles up.

    That's one small step for [a] man, one giant leap for mankind.

    Where ignorance lurks, so too do the frontiers of discovery and imagination.

    Dinosaurs are extinct today because they lacked opposable thumbs and the brainpower to build a space program.

    ', 9 | metafields: [ 10 | { 11 | value: '56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 12 | key: 'hero', 13 | title: 'Hero', 14 | type: 'file', 15 | children: null, 16 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 17 | imgix_url: 'https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 18 | }, { 19 | value: '

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ', 20 | key: 'teaser', 21 | title: 'Teaser', 22 | type: 'html-textarea', 23 | children: null, 24 | }, { 25 | object_type: 'authors', 26 | value: '5afdbf6c44d74b4e924f1d09', 27 | key: 'author', 28 | title: 'Author', 29 | type: 'object', 30 | children: null, 31 | object: { 32 | _id: '5afdbf6c44d74b4e924f1d09', 33 | bucket: '5afdbcd115bef94f7421eb0d', 34 | slug: 'john-doe', 35 | title: 'John Doe', 36 | content: '

    Something about John...

    ', 37 | metafields: [ 38 | { 39 | value: '9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 40 | key: 'image', 41 | title: 'Image', 42 | type: 'file', 43 | children: null, 44 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 45 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 46 | }, 47 | ], 48 | type_slug: 'authors', 49 | created: '2017-10-12T13:27:49.665Z', 50 | created_at: '2017-10-12T13:27:49.665Z', 51 | modified_at: '2018-05-17T17:44:12.977Z', 52 | status: 'published', 53 | published_at: '2018-05-17T17:44:12.977Z', 54 | metadata: { 55 | image: { 56 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 57 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 58 | }, 59 | }, 60 | }, 61 | }, 62 | ], 63 | type_slug: 'posts', 64 | created: '2017-10-12T13:27:49.665Z', 65 | created_at: '2017-10-12T13:27:49.665Z', 66 | modified_at: '2017-10-12T17:39:54.476Z', 67 | status: 'published', 68 | published_at: '2018-05-17T17:44:12.979Z', 69 | metadata: { 70 | hero: { 71 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 72 | imgix_url: 'https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg', 73 | }, 74 | teaser: '

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ', 75 | author: { 76 | _id: '5afdbf6c44d74b4e924f1d09', 77 | bucket: '5afdbcd115bef94f7421eb0d', 78 | slug: 'john-doe', 79 | title: 'John Doe', 80 | content: '

    Something about John...

    ', 81 | metafields: [ 82 | { 83 | value: '9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 84 | key: 'image', 85 | title: 'Image', 86 | type: 'file', 87 | children: null, 88 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 89 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 90 | }, 91 | ], 92 | type_slug: 'authors', 93 | created: '2017-10-12T13:27:49.665Z', 94 | created_at: '2017-10-12T13:27:49.665Z', 95 | modified_at: '2018-05-17T17:44:12.977Z', 96 | status: 'published', 97 | published_at: '2018-05-17T17:44:12.977Z', 98 | metadata: { 99 | image: { 100 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 101 | imgix_url: 'https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg', 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | 'another-wonderful-blog-post-about-earth': { 109 | post: { 110 | _id: '5afdbf6c44d74b4e924f1d0d', 111 | bucket: '5afdbcd115bef94f7421eb0d', 112 | slug: 'another-wonderful-blog-post-about-earth', 113 | title: 'Another Wonderful Blog Post About Earth', 114 | content: '

    That's one small step for [a] man, one giant leap for mankind.

    To be the first to enter the cosmos, to engage, single-handed, in an unprecedented duel with nature—could one dream of anything more?

    The dreams of yesterday are the hopes of today and the reality of tomorrow.

    The sky is the limit only for those who aren't afraid to fly!

    Problems look mighty small from 150 miles up.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    Houston, Tranquillity Base here. The Eagle has landed.

    You know, being a test pilot isn't always the healthiest business in the world.

    ', 115 | metafields: [ 116 | { 117 | value: '99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg', 118 | key: 'hero', 119 | title: 'Hero', 120 | type: 'file', 121 | children: null, 122 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg', 123 | imgix_url: 'https://cosmic-s3.imgix.net/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg', 124 | }, { 125 | value: '

    Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.

    Houston, Tranquillity Base here. The Eagle has landed...

    ', 126 | key: 'teaser', 127 | title: 'Teaser', 128 | type: 'html-textarea', 129 | children: null, 130 | }, { 131 | object_type: 'authors', 132 | value: '5afdbf6c44d74b4e924f1d06', 133 | key: 'author', 134 | title: 'Author', 135 | type: 'object', 136 | children: null, 137 | object: { 138 | _id: '5afdbf6c44d74b4e924f1d06', 139 | bucket: '5afdbcd115bef94f7421eb0d', 140 | slug: 'jane-doe', 141 | title: 'Jane Doe', 142 | content: '

    Something about Jane...

    ', 143 | metafields: [ 144 | { 145 | value: '99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 146 | key: 'image', 147 | title: 'Image', 148 | type: 'file', 149 | children: null, 150 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 151 | imgix_url: 'https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 152 | }, 153 | ], 154 | type_slug: 'authors', 155 | created: '2017-10-12T13:27:49.663Z', 156 | created_at: '2017-10-12T13:27:49.663Z', 157 | modified_at: '2018-05-17T17:44:12.973Z', 158 | status: 'published', 159 | published_at: '2018-05-17T17:44:12.973Z', 160 | metadata: { 161 | image: { 162 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 163 | imgix_url: 'https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 164 | }, 165 | }, 166 | }, 167 | }, { 168 | object_type: 'categories', 169 | value: '58f565290687f7353b0009bb,58f565210687f7353b0009ba', 170 | key: 'categories', 171 | title: 'Categories', 172 | type: 'objects', 173 | children: null, 174 | objects: [null, null], 175 | }, 176 | ], 177 | type_slug: 'posts', 178 | created: '2017-10-12T13:27:49.666Z', 179 | created_at: '2017-10-12T13:27:49.666Z', 180 | modified_at: '2018-05-17T17:44:12.981Z', 181 | status: 'published', 182 | published_at: '2018-05-17T17:44:12.981Z', 183 | metadata: { 184 | hero: { 185 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg', 186 | imgix_url: 'https://cosmic-s3.imgix.net/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg', 187 | }, 188 | teaser: '

    Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.

    Houston, Tranquillity Base here. The Eagle has landed...

    ', 189 | author: { 190 | _id: '5afdbf6c44d74b4e924f1d06', 191 | bucket: '5afdbcd115bef94f7421eb0d', 192 | slug: 'jane-doe', 193 | title: 'Jane Doe', 194 | content: '

    Something about Jane...

    ', 195 | metafields: [ 196 | { 197 | value: '99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 198 | key: 'image', 199 | title: 'Image', 200 | type: 'file', 201 | children: null, 202 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 203 | imgix_url: 'https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 204 | }, 205 | ], 206 | type_slug: 'authors', 207 | created: '2017-10-12T13:27:49.663Z', 208 | created_at: '2017-10-12T13:27:49.663Z', 209 | modified_at: '2018-05-17T17:44:12.973Z', 210 | status: 'published', 211 | published_at: '2018-05-17T17:44:12.973Z', 212 | metadata: { 213 | image: { 214 | url: 'https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 215 | imgix_url: 'https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg', 216 | }, 217 | }, 218 | }, 219 | categories: [null, null], 220 | }, 221 | }, 222 | }, 223 | }; 224 | -------------------------------------------------------------------------------- /pages/index/__snapshots__/index.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Disconnected Index Page should render 1`] = ` 4 | When I orbited the Earth in a spaceship, I saw for the first time how beautiful our planet is. Mankind, let us preserve and increase this beauty, and not destroy it!

    Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.

    If you could see the earth illuminated when you were in a place as dark as night, it would look to you more splendid than the moon.

    To be the first to enter the cosmos, to engage, single-handed, in an unprecedented duel with nature—could one dream of anything more?

    We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard, because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win.

    NASA is not about the ‘Adventure of Human Space Exploration’…We won’t be doing it just to get out there in space – we’ll be doing it because the things we learn out there will be making life better for a lot of people who won’t be able to go.

    Problems look mighty small from 150 miles up.

    That's one small step for [a] man, one giant leap for mankind.

    Where ignorance lurks, so too do the frontiers of discovery and imagination.

    Dinosaurs are extinct today because they lacked opposable thumbs and the brainpower to build a space program.

    ", 53 | "created": "2017-10-12T13:27:49.665Z", 54 | "created_at": "2017-10-12T13:27:49.665Z", 55 | "metadata": Object { 56 | "author": Object { 57 | "_id": "5afdbf6c44d74b4e924f1d09", 58 | "bucket": "5afdbcd115bef94f7421eb0d", 59 | "content": "

    Something about John...

    ", 60 | "created": "2017-10-12T13:27:49.665Z", 61 | "created_at": "2017-10-12T13:27:49.665Z", 62 | "metadata": Object { 63 | "image": Object { 64 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 65 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 66 | }, 67 | }, 68 | "metafields": Array [ 69 | Object { 70 | "children": null, 71 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 72 | "key": "image", 73 | "title": "Image", 74 | "type": "file", 75 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 76 | "value": "9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 77 | }, 78 | ], 79 | "modified_at": "2018-05-17T17:44:12.977Z", 80 | "published_at": "2018-05-17T17:44:12.977Z", 81 | "slug": "john-doe", 82 | "status": "published", 83 | "title": "John Doe", 84 | "type_slug": "authors", 85 | }, 86 | "hero": Object { 87 | "imgix_url": "https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 88 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 89 | }, 90 | "teaser": "

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ", 91 | }, 92 | "metafields": Array [ 93 | Object { 94 | "children": null, 95 | "imgix_url": "https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 96 | "key": "hero", 97 | "title": "Hero", 98 | "type": "file", 99 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 100 | "value": "56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 101 | }, 102 | Object { 103 | "children": null, 104 | "key": "teaser", 105 | "title": "Teaser", 106 | "type": "html-textarea", 107 | "value": "

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ", 108 | }, 109 | Object { 110 | "children": null, 111 | "key": "author", 112 | "object": Object { 113 | "_id": "5afdbf6c44d74b4e924f1d09", 114 | "bucket": "5afdbcd115bef94f7421eb0d", 115 | "content": "

    Something about John...

    ", 116 | "created": "2017-10-12T13:27:49.665Z", 117 | "created_at": "2017-10-12T13:27:49.665Z", 118 | "metadata": Object { 119 | "image": Object { 120 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 121 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 122 | }, 123 | }, 124 | "metafields": Array [ 125 | Object { 126 | "children": null, 127 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 128 | "key": "image", 129 | "title": "Image", 130 | "type": "file", 131 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 132 | "value": "9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 133 | }, 134 | ], 135 | "modified_at": "2018-05-17T17:44:12.977Z", 136 | "published_at": "2018-05-17T17:44:12.977Z", 137 | "slug": "john-doe", 138 | "status": "published", 139 | "title": "John Doe", 140 | "type_slug": "authors", 141 | }, 142 | "object_type": "authors", 143 | "title": "Author", 144 | "type": "object", 145 | "value": "5afdbf6c44d74b4e924f1d09", 146 | }, 147 | ], 148 | "modified_at": "2017-10-12T17:39:54.476Z", 149 | "published_at": "2018-05-17T17:44:12.979Z", 150 | "slug": "a-wonderful-blog-post-about-earth", 151 | "status": "published", 152 | "title": "A Wonderful Blog Post About Earth", 153 | "type_slug": "posts", 154 | }, 155 | }, 156 | "another-wonderful-blog-post-about-earth": Object { 157 | "post": Object { 158 | "_id": "5afdbf6c44d74b4e924f1d0d", 159 | "bucket": "5afdbcd115bef94f7421eb0d", 160 | "content": "

    That's one small step for [a] man, one giant leap for mankind.

    To be the first to enter the cosmos, to engage, single-handed, in an unprecedented duel with nature—could one dream of anything more?

    The dreams of yesterday are the hopes of today and the reality of tomorrow.

    The sky is the limit only for those who aren't afraid to fly!

    Problems look mighty small from 150 miles up.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.

    Houston, Tranquillity Base here. The Eagle has landed.

    You know, being a test pilot isn't always the healthiest business in the world.

    ", 161 | "created": "2017-10-12T13:27:49.666Z", 162 | "created_at": "2017-10-12T13:27:49.666Z", 163 | "metadata": Object { 164 | "author": Object { 165 | "_id": "5afdbf6c44d74b4e924f1d06", 166 | "bucket": "5afdbcd115bef94f7421eb0d", 167 | "content": "

    Something about Jane...

    ", 168 | "created": "2017-10-12T13:27:49.663Z", 169 | "created_at": "2017-10-12T13:27:49.663Z", 170 | "metadata": Object { 171 | "image": Object { 172 | "imgix_url": "https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 173 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 174 | }, 175 | }, 176 | "metafields": Array [ 177 | Object { 178 | "children": null, 179 | "imgix_url": "https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 180 | "key": "image", 181 | "title": "Image", 182 | "type": "file", 183 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 184 | "value": "99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 185 | }, 186 | ], 187 | "modified_at": "2018-05-17T17:44:12.973Z", 188 | "published_at": "2018-05-17T17:44:12.973Z", 189 | "slug": "jane-doe", 190 | "status": "published", 191 | "title": "Jane Doe", 192 | "type_slug": "authors", 193 | }, 194 | "categories": Array [ 195 | null, 196 | null, 197 | ], 198 | "hero": Object { 199 | "imgix_url": "https://cosmic-s3.imgix.net/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg", 200 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg", 201 | }, 202 | "teaser": "

    Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.

    Houston, Tranquillity Base here. The Eagle has landed...

    ", 203 | }, 204 | "metafields": Array [ 205 | Object { 206 | "children": null, 207 | "imgix_url": "https://cosmic-s3.imgix.net/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg", 208 | "key": "hero", 209 | "title": "Hero", 210 | "type": "file", 211 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg", 212 | "value": "99fd6650-23f5-11e7-875c-3f5dc9c15c2b-beach.jpg", 213 | }, 214 | Object { 215 | "children": null, 216 | "key": "teaser", 217 | "title": "Teaser", 218 | "type": "html-textarea", 219 | "value": "

    Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.

    Houston, Tranquillity Base here. The Eagle has landed...

    ", 220 | }, 221 | Object { 222 | "children": null, 223 | "key": "author", 224 | "object": Object { 225 | "_id": "5afdbf6c44d74b4e924f1d06", 226 | "bucket": "5afdbcd115bef94f7421eb0d", 227 | "content": "

    Something about Jane...

    ", 228 | "created": "2017-10-12T13:27:49.663Z", 229 | "created_at": "2017-10-12T13:27:49.663Z", 230 | "metadata": Object { 231 | "image": Object { 232 | "imgix_url": "https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 233 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 234 | }, 235 | }, 236 | "metafields": Array [ 237 | Object { 238 | "children": null, 239 | "imgix_url": "https://cosmic-s3.imgix.net/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 240 | "key": "image", 241 | "title": "Image", 242 | "type": "file", 243 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 244 | "value": "99f48cb0-23f5-11e7-875c-3f5dc9c15c2b-female.jpg", 245 | }, 246 | ], 247 | "modified_at": "2018-05-17T17:44:12.973Z", 248 | "published_at": "2018-05-17T17:44:12.973Z", 249 | "slug": "jane-doe", 250 | "status": "published", 251 | "title": "Jane Doe", 252 | "type_slug": "authors", 253 | }, 254 | "object_type": "authors", 255 | "title": "Author", 256 | "type": "object", 257 | "value": "5afdbf6c44d74b4e924f1d06", 258 | }, 259 | Object { 260 | "children": null, 261 | "key": "categories", 262 | "object_type": "categories", 263 | "objects": Array [ 264 | null, 265 | null, 266 | ], 267 | "title": "Categories", 268 | "type": "objects", 269 | "value": "58f565290687f7353b0009bb,58f565210687f7353b0009ba", 270 | }, 271 | ], 272 | "modified_at": "2018-05-17T17:44:12.981Z", 273 | "published_at": "2018-05-17T17:44:12.981Z", 274 | "slug": "another-wonderful-blog-post-about-earth", 275 | "status": "published", 276 | "title": "Another Wonderful Blog Post About Earth", 277 | "type_slug": "posts", 278 | }, 279 | }, 280 | } 281 | } 282 | /> 283 | `; 284 | -------------------------------------------------------------------------------- /components/AppBar/__snapshots__/AppBar.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AppBar should render 1`] = ` 4 | 15 | 18 | 31 | 44 |
    52 |
    67 |
    70 | 75 | 92 | 100 | 118 | 205 | 206 | 207 | 208 | 209 | 213 | 263 |

    266 | Title 267 |

    268 |
    269 |
    270 | 275 | 292 | 300 | 318 | 405 | 406 | 407 | 408 | 409 |
    410 |
    411 |
    412 |
    413 | 417 | 423 | 441 | 451 | 479 | 496 | 534 | 535 | 536 | 537 | 538 | 539 | 540 |
    541 |
    542 |
    543 | `; 544 | -------------------------------------------------------------------------------- /components/PostPreview/__snapshots__/PostPreview.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PostPreview should render 1`] = ` 4 | Something about John...

    ", 22 | "created": "2017-10-12T13:27:49.665Z", 23 | "created_at": "2017-10-12T13:27:49.665Z", 24 | "metadata": Object { 25 | "image": Object { 26 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 27 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 28 | }, 29 | }, 30 | "metafields": Array [ 31 | Object { 32 | "children": null, 33 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 34 | "key": "image", 35 | "title": "Image", 36 | "type": "file", 37 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 38 | "value": "9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 39 | }, 40 | ], 41 | "modified_at": "2018-05-17T17:44:12.977Z", 42 | "published_at": "2018-05-17T17:44:12.977Z", 43 | "slug": "john-doe", 44 | "status": "published", 45 | "title": "John Doe", 46 | "type_slug": "authors", 47 | }, 48 | "hero": Object { 49 | "imgix_url": "https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 50 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 51 | }, 52 | "teaser": "

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ", 53 | } 54 | } 55 | metafields={ 56 | Array [ 57 | Object { 58 | "children": null, 59 | "imgix_url": "https://cosmic-s3.imgix.net/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 60 | "key": "hero", 61 | "title": "Hero", 62 | "type": "file", 63 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 64 | "value": "56363630-af74-11e7-b864-313f959a776e-react-blog.jpg", 65 | }, 66 | Object { 67 | "children": null, 68 | "key": "teaser", 69 | "title": "Teaser", 70 | "type": "html-textarea", 71 | "value": "

    I don't know what you could say about a day in which you have seen four beautiful sunsets.

    It suddenly struck me that that tiny pea, pretty and blue, was the Earth. I put up my thumb and shut one eye, and my thumb blotted out the planet Earth. I didn't feel like a giant. I felt very, very small....

    ", 72 | }, 73 | Object { 74 | "children": null, 75 | "key": "author", 76 | "object": Object { 77 | "_id": "5afdbf6c44d74b4e924f1d09", 78 | "bucket": "5afdbcd115bef94f7421eb0d", 79 | "content": "

    Something about John...

    ", 80 | "created": "2017-10-12T13:27:49.665Z", 81 | "created_at": "2017-10-12T13:27:49.665Z", 82 | "metadata": Object { 83 | "image": Object { 84 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 85 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 86 | }, 87 | }, 88 | "metafields": Array [ 89 | Object { 90 | "children": null, 91 | "imgix_url": "https://cosmic-s3.imgix.net/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 92 | "key": "image", 93 | "title": "Image", 94 | "type": "file", 95 | "url": "https://s3-us-west-2.amazonaws.com/cosmicjs/9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 96 | "value": "9a0a85b0-23f5-11e7-875c-3f5dc9c15c2b-male.jpg", 97 | }, 98 | ], 99 | "modified_at": "2018-05-17T17:44:12.977Z", 100 | "published_at": "2018-05-17T17:44:12.977Z", 101 | "slug": "john-doe", 102 | "status": "published", 103 | "title": "John Doe", 104 | "type_slug": "authors", 105 | }, 106 | "object_type": "authors", 107 | "title": "Author", 108 | "type": "object", 109 | "value": "5afdbf6c44d74b4e924f1d09", 110 | }, 111 | ] 112 | } 113 | modified_at="2017-10-12T17:39:54.476Z" 114 | published_at="2018-05-17T17:44:12.979Z" 115 | slug="a-wonderful-blog-post-about-earth" 116 | status="published" 117 | title="A Wonderful Blog Post About Earth" 118 | type_slug="posts" 119 | > 120 |
  • 121 | 643 |
  • 644 |
    645 | `; 646 | --------------------------------------------------------------------------------