├── components ├── SinglePostContainer.js ├── NoMorePosts.js ├── SearchWidget.js ├── CommentsWidgetContainer.js ├── Footer.js ├── Sidebar.js ├── Head.js ├── CategoriesWidget.js ├── SinglePost.js ├── Home.js ├── Post.js ├── PostPage.js ├── ScrollDown.js ├── SinglePostHeader.js ├── LoadMorePosts.js ├── CategoriesWidgetContainer.js ├── PostsWidget.js ├── SearchPage.js ├── Results.js ├── SingleComment.js ├── Main.js ├── Search.js ├── Hero.js ├── Spinner.js ├── CommentsWidget.js ├── Posts.js ├── SinglePostComments.js ├── SinglePostFooter.js └── CommentForm.js ├── .gitignore ├── static ├── header.jpg ├── twentyNext.css └── twentyseventeen.css ├── config.js ├── .tmuxinator.yml ├── redux ├── InitialState.js ├── helpers.js ├── index.js ├── readme.md ├── actions.js └── reducers.js ├── .tern-project ├── wp.js ├── readme.md ├── package.json └── pages ├── index.js ├── search.js └── post.js /components/SinglePostContainer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /static/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pixel2HTML/wp-nextjs/HEAD/static/header.jpg -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default { 4 | endpoint: 'https://examples.pixel2html.com/nextjs/wp-json' 5 | } 6 | -------------------------------------------------------------------------------- /.tmuxinator.yml: -------------------------------------------------------------------------------- 1 | # ~/.tmuxinator/nextjs.yml 2 | 3 | name: nextjs 4 | root: ~/git/nextjs 5 | windows: 6 | - editor: vim 7 | - server: yarn run dev 8 | -------------------------------------------------------------------------------- /redux/InitialState.js: -------------------------------------------------------------------------------- 1 | let initialState = { 2 | site: {}, 3 | posts: [], 4 | activePost: {}, 5 | searchResults: [], 6 | categories: [], 7 | comments: [] 8 | } 9 | 10 | export default initialState 11 | 12 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "es_modules": {}, 4 | "node": {} 5 | }, 6 | "libs": [ 7 | "ecma5", 8 | "ecma6", 9 | "browser", 10 | "react" 11 | ], 12 | "ecmaVersion": 6 13 | } 14 | -------------------------------------------------------------------------------- /wp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import WPAPI from 'wpapi' 4 | import config from './config' 5 | 6 | const wp = new WPAPI({ 7 | endpoint: config.endpoint 8 | }) 9 | 10 | const Site = WPAPI.site(config.endpoint) 11 | 12 | export { Site } 13 | 14 | export default wp 15 | -------------------------------------------------------------------------------- /components/NoMorePosts.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | export default class NoMorePosts extends Component { 4 | render () { 5 | return ( 6 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/SearchWidget.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import Search from './Search' 3 | 4 | export default class SearchWidget extends Component { 5 | render () { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /components/CommentsWidgetContainer.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import CommentsWidget from './CommentsWidget' 3 | 4 | class CommentsWidgetContainer extends Component { 5 | constructor (props) { 6 | super(props) 7 | this.props = props 8 | } 9 | 10 | render () { 11 | return ( 12 | 13 | ) 14 | } 15 | } 16 | 17 | export default CommentsWidgetContainer 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Twenty Seventeen - NextJS 2 | 3 | A re-building effort to make the standard wordpress theme into a NextJS component based isomorphic app. 4 | 5 | ## How to Install Everything 6 | 7 | Make sure you are using Node with at least 6.9.2 then run `npm install`. 8 | Go into the `config.js` file and add your wordpress api url 9 | 10 | Run `npm run dev` 11 | 12 | Navigate into `localhost:3000` and enjoy! 13 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | export default class Footer extends Component { 4 | render () { 5 | return ( 6 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /redux/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helps us return an array of posts without duplicated ids 3 | * @param {array} mixedPosts both new and old 4 | * @returns {array} posts 5 | */ 6 | 7 | export function preventDuplicatePosts (mixedPosts) { 8 | let posts = mixedPosts.concat() 9 | for (var i = 0; i < posts.length; ++i) { 10 | for (var j = i + 1; j < posts.length; ++j) { 11 | if (posts[i].id === posts[j].id) { 12 | posts.splice(j--, 1) 13 | } 14 | } 15 | } 16 | return posts 17 | } 18 | -------------------------------------------------------------------------------- /components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SearchWidget from './SearchWidget' 3 | import PostsWidget from './PostsWidget' 4 | import CommentsWidgetContainer from './CommentsWidgetContainer' 5 | import CategoriesWidgetContainer from './CategoriesWidgetContainer' 6 | 7 | class Sidebar extends React.Component { 8 | render () { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | } 19 | 20 | export default Sidebar 21 | -------------------------------------------------------------------------------- /components/Head.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | export default ({title}) => ( 5 | 6 | {title} 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-nextjs", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "next", 7 | "start": "next start", 8 | "build": "next build" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "babel-eslint": "^7.1.1", 13 | "moment": "^2.17.0", 14 | "next": "^1.2.3", 15 | "react-redux": "^4.4.6", 16 | "react-scroll": "^1.4.4", 17 | "redux": "^3.6.0", 18 | "redux-thunk": "^2.1.0", 19 | "self": "^1.0.0", 20 | "standard": "^8.6.0", 21 | "wpapi": "^0.12.1" 22 | }, 23 | "standard": { 24 | "parser": "babel-eslint" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/CategoriesWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class CategoriesWidget extends React.Component { 4 | render () { 5 | let { categories } = this.props 6 | return ( 7 |
8 |

Categories

9 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | export default CategoriesWidget 22 | -------------------------------------------------------------------------------- /redux/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | 4 | import reducers from './reducers' 5 | 6 | export const reducer = reducers 7 | 8 | /* 9 | * Creating an isomorphic store. When in server make a new one in client persist it 10 | */ 11 | export const initStore = (reducer, initialState, isServer) => { 12 | if (isServer && typeof window === 'undefined') { 13 | return createStore(reducer, initialState, applyMiddleware(thunkMiddleware)) 14 | } else { 15 | if (!window.store) { 16 | window.store = createStore(reducer, initialState, applyMiddleware(thunkMiddleware)) 17 | } 18 | return window.store 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/SinglePost.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import React from 'react' 3 | import PostHeader from './SinglePostHeader' 4 | import PostFooter from './SinglePostFooter' 5 | import PostComments from './SinglePostComments' 6 | 7 | class SinglePost extends React.Component { 8 | render () { 9 | let { post, author } = this.props 10 | return ( 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | export default SinglePost 24 | -------------------------------------------------------------------------------- /components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from './Head' 3 | import Hero from './Hero' 4 | import Main from './Main' 5 | import Posts from './Posts' 6 | 7 | import { connect } from 'react-redux' 8 | 9 | const mapStoreToProps = (store) => { 10 | return { 11 | title: store.site.root.name, 12 | description: store.site.root.description 13 | } 14 | } 15 | 16 | class Home extends React.Component { 17 | render () { 18 | let { title, description } = this.props 19 | return ( 20 |
21 | 22 | 23 |
24 | 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | export default connect(mapStoreToProps)(Home) 32 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { initStore, reducer } from '../redux' 3 | import { Provider } from 'react-redux' 4 | import { getSite } from '../redux/actions' 5 | import { Site } from '../wp' 6 | import Home from '../components/Home' 7 | 8 | export default class extends React.Component { 9 | static async getInitialProps ({ req }) { 10 | const isServer = !!req 11 | const store = initStore(reducer, {}, isServer) 12 | const site = await Site.root() 13 | store.dispatch(getSite(site)) 14 | return { 15 | initialState: store.getState(), 16 | isServer 17 | } 18 | } 19 | 20 | constructor (props) { 21 | super(props) 22 | this.store = initStore(reducer, props.initialState, props.isServer) 23 | } 24 | 25 | render () { 26 | return ( 27 | 28 | 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/Post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | const moment = require('moment') 4 | 5 | export default class Post extends React.Component { 6 | 7 | render () { 8 | let now = moment(this.props.time).format('LL') 9 | return ( 10 |
11 |
12 |
13 | Posted on 14 | 15 |
16 |

17 | {this.props.title} 18 |

19 |
20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /redux/readme.md: -------------------------------------------------------------------------------- 1 | # Redux implementation of wordpress. 2 | 3 | The whole idea here is to mantain everything in small components yet having them interact with one another, there's where redux comes into play to help us. 4 | 5 | However we still need to define the global state object which is tightly the same as the wpapi endpoints so far the basic top level containers would be: 6 | 7 | - Site: Contains the root details like blog name and description 8 | - Posts: The list at start of the top 10 posts and the rest can be added here by requesting them. 9 | - Active post: The current post that is being rendered. 10 | - Search Results: The list of posts that match what was searched with the search widget 11 | - Categories: List of categories to be rendered on the sidebar. 12 | - Recent Comments: List of recent comments to be rendered on the sidebar. 13 | - UI State: Some strings to tell us if there is data being loaded, or has loaded to display spinners or render it. 14 | 15 | -------------------------------------------------------------------------------- /components/PostPage.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | import Head from './Head' 3 | import Hero from './Hero' 4 | import Main from './Main' 5 | import SinglePost from './SinglePost' 6 | import { connect } from 'react-redux' 7 | 8 | const mapStoreToProps = (store) => { 9 | return { 10 | title: store.site.root.name, 11 | description: store.site.root.description, 12 | post: store.post.data, 13 | postTitle: store.post.data.title.rendered, 14 | author: store.post.author 15 | } 16 | } 17 | 18 | class PostPage extends Component { 19 | render () { 20 | let {title, description, post, author, postTitle} = this.props 21 | return ( 22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | export default connect(mapStoreToProps)(PostPage) 34 | -------------------------------------------------------------------------------- /components/ScrollDown.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class ScrollDown extends React.Component { 4 | render () { 5 | return ( 6 | 7 | 14 | Scroll Down 15 | 16 | ) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /components/SinglePostHeader.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | const moment = require('moment') 5 | 6 | export default class PostHeader extends React.Component { 7 | render () { 8 | let post = this.props.post 9 | let author = this.props.author 10 | return ( 11 |
12 |
13 | 14 | Posted On 15 | 16 | 19 | 20 | 21 | 22 | by 23 | 24 | {author.name} 25 | 26 | 27 |
28 |

{post.title.rendered}

29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/LoadMorePosts.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | import {connect} from 'react-redux' 3 | import {requestPosts, receivePosts} from '../redux/actions' 4 | import wp from '../wp' 5 | 6 | const actionCreators = { 7 | requestPosts, 8 | receivePosts 9 | } 10 | 11 | const mapStoreToProps = (store) => { 12 | return { 13 | currentPage: store.posts.currentPage 14 | } 15 | } 16 | 17 | class LoadMorePosts extends Component { 18 | constructor (props) { 19 | super(props) 20 | this.reqPosts = this.reqPosts.bind(this) 21 | } 22 | 23 | async reqPosts () { 24 | let { requestPosts, receivePosts, currentPage } = this.props 25 | requestPosts() 26 | const posts = await wp.posts().page(currentPage) 27 | receivePosts(posts) 28 | } 29 | 30 | render () { 31 | return ( 32 | 36 | ) 37 | } 38 | } 39 | 40 | export default connect(mapStoreToProps, actionCreators)(LoadMorePosts) 41 | -------------------------------------------------------------------------------- /components/CategoriesWidgetContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CategoriesWidget from './CategoriesWidget' 3 | import Spinner from './Spinner' 4 | import wp from '../wp' 5 | 6 | import { connect } from 'react-redux' 7 | import { receivedCategories } from '../redux/actions' 8 | 9 | function mapStateToProps (store) { 10 | return { 11 | isFetching: store.categories.isFetching, 12 | categories: store.categories.items 13 | } 14 | } 15 | 16 | const dispatchToProps = { receivedCategories } 17 | 18 | class CategoriesWidgetContainer extends React.Component { 19 | 20 | async componentDidMount () { 21 | const {receivedCategories} = this.props 22 | let categories = await wp.categories() 23 | receivedCategories(categories) 24 | } 25 | 26 | render () { 27 | let {categories, isFetching} = this.props 28 | if (!isFetching) { 29 | return ( 30 | 31 | ) 32 | } else { 33 | return ( 34 | 35 | ) 36 | } 37 | } 38 | } 39 | 40 | export default connect(mapStateToProps, dispatchToProps)(CategoriesWidgetContainer) 41 | -------------------------------------------------------------------------------- /components/PostsWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import Link from 'next/link' 5 | import Spinner from './Spinner' 6 | import wp from '../wp' 7 | 8 | class PostsWidget extends React.Component { 9 | constructor (props) { 10 | super(props) 11 | this.state = { 12 | posts: [] 13 | } 14 | this.renderPosts = this.renderPosts.bind(this) 15 | } 16 | 17 | async componentDidMount () { 18 | const posts = await wp.posts() 19 | this.setState({posts: posts.slice(0, 5)}) 20 | } 21 | 22 | renderPosts (posts) { 23 | return posts.map(post => ( 24 |
  • 25 | {post.title.rendered} 26 |
  • 27 | )) 28 | } 29 | 30 | render () { 31 | let posts = this.state.posts 32 | return ( 33 |
    34 |

    Recent Posts

    35 |
      36 | {posts.length ? this.renderPosts(posts) : } 37 |
    38 |
    39 | ) 40 | } 41 | } 42 | 43 | export default PostsWidget 44 | -------------------------------------------------------------------------------- /components/SearchPage.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import Head from './Head' 3 | import Hero from './Hero' 4 | import Main from './Main' 5 | import Results from './Results' 6 | import { connect } from 'react-redux' 7 | 8 | const mapStoreToProps = store => { 9 | return { 10 | title: store.site.root.name, 11 | description: store.site.root.description, 12 | query: store.search.query, 13 | results: store.search.results 14 | } 15 | } 16 | 17 | class SearchPage extends Component { 18 | 19 | checkTitle (query, results) { 20 | if (results.length) { 21 | return `Search Results for ${query}` 22 | } else { 23 | return 'Nothing Found' 24 | } 25 | } 26 | 27 | render () { 28 | let {title, description, query, results} = this.props 29 | return ( 30 |
    31 | 32 | 33 |
    34 | 35 |
    36 |
    37 | ) 38 | } 39 | } 40 | 41 | export default connect(mapStoreToProps)(SearchPage) 42 | -------------------------------------------------------------------------------- /pages/search.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import wp, { Site } from '../wp' 3 | import SearchPage from '../components/SearchPage' 4 | 5 | import { Provider } from 'react-redux' 6 | import { initStore, reducer } from '../redux' 7 | import { getSite, receiveResults, receiveSearchQuery } from '../redux/actions' 8 | 9 | export default class extends Component { 10 | static async getInitialProps ({ 11 | query: { s }, 12 | req 13 | }) { 14 | const isServer = !!req 15 | const store = initStore(reducer, {}, isServer) 16 | 17 | const site = await Site.root() 18 | const results = await wp.posts().search(s) 19 | 20 | store.dispatch(getSite(site)) 21 | store.dispatch(receiveResults(results)) 22 | store.dispatch(receiveSearchQuery(s)) 23 | 24 | return { 25 | initialState: store.getState(), 26 | isServer 27 | } 28 | } 29 | 30 | constructor (props) { 31 | super(props) 32 | this.store = initStore(reducer, props.initialState, props.isServer) 33 | } 34 | 35 | render () { 36 | return ( 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /components/Results.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import Post from './Post' 3 | import Search from './Search' 4 | import { connect } from 'react-redux' 5 | 6 | function mapStoreToProps (store) { 7 | return { 8 | results: store.search.results, 9 | search: store.search 10 | } 11 | } 12 | 13 | class Results extends Component { 14 | 15 | renderPosts (posts) { 16 | return posts.map(post => ( 17 | 25 | )) 26 | } 27 | 28 | renderEmpty () { 29 | return ( 30 |
    31 |

    Sorry, but nothing matched your search terms. Please try again with some different keywords.

    32 | 33 |
    34 | ) 35 | } 36 | 37 | render () { 38 | let { results } = this.props 39 | // Logic to display or not the get more posts button 40 | return ( 41 |
    42 | {results.length ? this.renderPosts(results) : this.renderEmpty()} 43 |
    44 | ) 45 | } 46 | } 47 | 48 | export default connect(mapStoreToProps)(Results) 49 | -------------------------------------------------------------------------------- /components/SingleComment.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | const moment = require('moment') 3 | 4 | export default class SingleComment extends Component { 5 | render () { 6 | const {avatarUrl, authorUrl, authorName, date, content} = this.props 7 | let time = moment(date).format('MMMM DD YYYY, h:mm a') 8 | return ( 9 |
  • 10 | 31 |
  • 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Sidebar from './Sidebar' 3 | import Footer from './Footer' 4 | 5 | export default class Main extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | this.renderHeader = this.renderHeader.bind(this) 9 | } 10 | 11 | renderHeader () { 12 | let shouldHaveHeader = this.props.hasHeader 13 | if (shouldHaveHeader) { 14 | return ( 15 |
    16 |

    {this.props.headerTitle}

    17 |
    18 | ) 19 | } 20 | } 21 | 22 | render () { 23 | let siteContent = 'site-content-contain' 24 | if (this.props.hasSidebar) siteContent += ' has-sidebar' 25 | if (this.props.isBlog) siteContent += ' blog' 26 | 27 | return ( 28 |
    29 |
    30 |
    31 | {this.renderHeader()} 32 |
    33 | {this.props.children} 34 |
    35 | 38 |
    39 |
    40 |
    41 |
    42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import wp, { Site } from '../wp' 3 | import PostPage from '../components/PostPage' 4 | 5 | import { Provider } from 'react-redux' 6 | import { initStore, reducer } from '../redux' 7 | import { getSite, receivePost, receiveAuthor } from '../redux/actions' 8 | 9 | export default class extends React.Component { 10 | static async getInitialProps ({ 11 | query: { id }, 12 | req 13 | } 14 | ) { 15 | // The usual store initialization 16 | const isServer = !!req 17 | const store = initStore(reducer, {}, isServer) 18 | 19 | // Get all of of important data to begin with 20 | const post = await wp.posts().id(id) 21 | const site = await Site.root() 22 | const author = await wp.users().id(post.author) 23 | 24 | // Placing all of that data into the store 25 | store.dispatch(getSite(site)) 26 | store.dispatch(receivePost(post)) 27 | store.dispatch(receiveAuthor(author)) 28 | // Placing the store as initial props 29 | return { 30 | initialState: store.getState(), 31 | isServer 32 | } 33 | } 34 | 35 | constructor (props) { 36 | super(props) 37 | this.store = initStore(reducer, props.initialState, props.isServer) 38 | } 39 | 40 | render () { 41 | return ( 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /components/Search.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | class Search extends Component { 4 | render () { 5 | return ( 6 |
    12 | 15 | 21 | 27 |
    28 | ) 29 | } 30 | } 31 | 32 | export default Search 33 | -------------------------------------------------------------------------------- /static/twentyNext.css: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------- 2 | 13.1 Header 3 | --------------------------------------------------------------*/ 4 | .o-header.has-header-image .site-title, 5 | .o-header.has-header-image .site-title a { 6 | color: #fff; 7 | } 8 | 9 | .o-header.has-header-image .site-description { 10 | color: #fff; 11 | opacity: 0.8; 12 | } 13 | 14 | .o-header.home.title-tagline-hidden.has-header-image .custom-logo-link img { 15 | max-height: 200px; 16 | max-width: 100%; 17 | } 18 | 19 | .o-header:not(.title-tagline-hidden) .site-branding-text { 20 | display: inline-block; 21 | vertical-align: middle; 22 | } 23 | 24 | /* Hides div in Customizer preview when header images or videos change. */ 25 | 26 | body:not(.has-header-image) .custom-header-image { 27 | display: block; 28 | } 29 | 30 | .twentyseventeen-front-page.has-header-image .site-branding, 31 | .home.blog.has-header-image .site-branding { 32 | margin-bottom: 0; 33 | } 34 | 35 | 36 | /*-------------------------------------------------------------- 37 | Main Content Area 38 | --------------------------------------------------------------*/ 39 | body:not(.has-sidebar):not(.page-one-column) .page-header, 40 | body.has-sidebar.error404 #primary .page-header, 41 | body.page-two-column:not(.archive) #primary .entry-header, 42 | body.page-two-column.archive:not(.has-sidebar) #primary .page-header { 43 | float: initial; 44 | width: initial; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /components/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ScrollDown from './ScrollDown' 3 | 4 | export default class Hero extends React.Component { 5 | constructor (props) { 6 | super(props) 7 | this.shouldScroll = this.shouldScroll.bind(this) 8 | } 9 | 10 | shouldScroll () { 11 | let scroll = this.props.frontPage 12 | if (scroll) { 13 | return () 14 | } 15 | } 16 | 17 | render () { 18 | let header = 'o-header' 19 | if (this.props.hasimage) header += ' has-header-image' 20 | if (this.props.frontPage) header += ' home twentyseventeen-front-page' 21 | return ( 22 |
    23 | 43 |
    44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/Spinner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | 5 | class Spinner extends React.Component { 6 | render () { 7 | return ( 8 | 9 | 10 | 11 | 18 | 25 | 26 | 27 | 34 | 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | export default Spinner 49 | -------------------------------------------------------------------------------- /components/CommentsWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Spinner from './Spinner' 3 | import wp from '../wp' 4 | import Link from 'next/link' 5 | 6 | class CommentsWidget extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | this.state = { 10 | comments: [] 11 | } 12 | this.fetchPostForComment = this.fetchPostForComment.bind(this) 13 | this.renderComments = this.renderComments.bind(this) 14 | } 15 | 16 | async fetchFromAPI (path) { 17 | return wp[path]() 18 | } 19 | 20 | async fetchPostForComment (comment) { 21 | let post = await wp.posts().id(comment.post) 22 | comment.post_name = post.title.rendered 23 | return comment 24 | } 25 | 26 | async componentDidMount () { 27 | const comments = await this.fetchFromAPI('comments') 28 | Promise.all(comments.slice(0, 5).map(this.fetchPostForComment)) 29 | .then(comments => { this.setState({comments}) }) 30 | } 31 | 32 | renderComments (comments) { 33 | return comments.map(comment => ( 34 |
  • 35 | 36 | {comment.author_name} 37 | on {comment.post_name} 38 |
  • 39 | )) 40 | } 41 | 42 | render () { 43 | let comments = this.state.comments 44 | return ( 45 |
    46 |

    Recent Comments

    47 |
      48 | {comments.length ? this.renderComments(comments) : } 49 |
    50 |
    51 | ) 52 | } 53 | } 54 | 55 | export default CommentsWidget 56 | -------------------------------------------------------------------------------- /components/Posts.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import Post from './Post' 3 | import Spinner from './Spinner' 4 | import LoadMorePosts from './LoadMorePosts' 5 | import wp from '../wp' 6 | 7 | import { connect } from 'react-redux' 8 | import { requestPosts, receivePosts } from '../redux/actions' 9 | 10 | function mapStoreToProps (store) { 11 | return { 12 | posts: store.posts.items, 13 | isFetching: store.posts.isFetching, 14 | currentPage: store.posts.currentPage, 15 | totalPages: store.posts.totalPages 16 | } 17 | } 18 | 19 | const dispatchPropsToStore = { 20 | requestPosts, 21 | receivePosts 22 | } 23 | 24 | class Posts extends Component { 25 | async componentDidMount () { 26 | let { currentPage, requestPosts, receivePosts } = this.props 27 | requestPosts() 28 | const posts = await wp.posts().page(currentPage) 29 | receivePosts(posts) 30 | } 31 | 32 | renderPosts (posts) { 33 | return posts.map(post => ( 34 | 42 | )) 43 | } 44 | 45 | renderLoadButton (condition) { 46 | if (condition) { 47 | return () 48 | } 49 | } 50 | 51 | render () { 52 | let { posts, currentPage, isFetching, totalPages } = this.props 53 | // Logic to display or not the get more posts button 54 | return ( 55 |
    56 | {posts.length ? this.renderPosts(posts) : null} 57 | {isFetching ? : null} 58 | { this.renderLoadButton(currentPage < totalPages && !isFetching) } 59 |
    60 | ) 61 | } 62 | } 63 | 64 | export default connect(mapStoreToProps, dispatchPropsToStore)(Posts) 65 | -------------------------------------------------------------------------------- /components/SinglePostComments.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | import SingleComment from './SingleComment' 3 | import CommentForm from './CommentForm' 4 | 5 | import wp from '../wp' 6 | import {connect} from 'react-redux' 7 | import { requestPostComments, receivePostComments } from '../redux/actions' 8 | 9 | const mapStoreToProps = (store) => { 10 | return { 11 | postTitle: store.post.data.title.rendered, 12 | postID: store.post.data.id, 13 | comments: store.post.comments.data, 14 | commentStatus: store.post.data.comment_status, 15 | totalComments: store.post.comments.total, 16 | isFetching: store.post.comments.isFetching, 17 | debug: store.post 18 | } 19 | } 20 | 21 | const dispatchPropsToStore = { 22 | requestPostComments, 23 | receivePostComments 24 | } 25 | 26 | class PostComments extends Component { 27 | async componentDidMount () { 28 | let { postID, requestPostComments, receivePostComments } = this.props 29 | requestPostComments() 30 | let comments = await wp.comments().forPost(postID) 31 | receivePostComments(comments) 32 | } 33 | 34 | renderComments (comments) { 35 | if (comments && comments.length) { 36 | return ( 37 | comments.map(comment => ( 38 | 46 | )) 47 | ) 48 | } 49 | } 50 | 51 | commentCount (title, count) { 52 | if (count && count === 1) { 53 | return (

    1 Reply to "{title}"

    ) 54 | } else if (count && count > 1) { 55 | return (

    {count} Replies to "{title}"

    ) 56 | } 57 | } 58 | 59 | render () { 60 | let { comments, postTitle, totalComments } = this.props 61 | return ( 62 |
    63 | {this.commentCount(postTitle, totalComments)} 64 |
      65 | {this.renderComments(comments)} 66 |
    67 | 68 |
    69 | ) 70 | } 71 | } 72 | 73 | export default connect(mapStoreToProps, dispatchPropsToStore)(PostComments) 74 | -------------------------------------------------------------------------------- /redux/actions.js: -------------------------------------------------------------------------------- 1 | // All of our action types 2 | export const GET_SITE = 'GET_SITE' 3 | export const GET_CATEGORIES = 'GET_CATEGORIES' 4 | export const GOT_CATEGORIES = 'GOT_CATEGORIES' 5 | export const GOT_COMMENTS = 'GOT_COMMENTS' 6 | export const REQUEST_POSTS = 'REQUEST_POSTS' 7 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 8 | export const RECEIVE_POST = 'RECEIVE_POST' 9 | export const RECEIVE_AUTHOR = 'RECEIVE_AUTHOR' 10 | export const REQUEST_POST_COMMENTS = 'REQUEST_POST_COMMENTS' 11 | export const RECEIVE_POST_COMMENTS = 'RECEIVE_POST_COMMENTS' 12 | export const RECEIVE_RESULTS = 'RECEIVE_RESULTS' 13 | export const RECEIVE_SEARCH_QUERY = 'RECEIVE_SEARCH_QUERY' 14 | 15 | /** 16 | * Fetch the root site info 17 | * @param {object} site - the new site to update 18 | */ 19 | export function getSite (site) { 20 | return { 21 | type: GET_SITE, 22 | site 23 | } 24 | } 25 | 26 | /** 27 | * Get the categories from a source of truth 28 | * @param {array} categories - the categories to update 29 | */ 30 | export function requestCategories (categories) { 31 | return { 32 | type: GET_CATEGORIES, 33 | categories 34 | } 35 | } 36 | 37 | /** 38 | * Trigger a state update when getting categories 39 | * 40 | * @param {array} categories 41 | */ 42 | 43 | export function receivedCategories (categories) { 44 | return { 45 | type: GOT_CATEGORIES, 46 | categories 47 | } 48 | } 49 | 50 | /** 51 | * Trigger a state update when you get some recent comments 52 | * @param {array} comments 53 | * @returns {object} action to pass to the reducer 54 | */ 55 | export function receivedComments (comments) { 56 | return { 57 | type: GOT_COMMENTS, 58 | comments 59 | } 60 | } 61 | 62 | /** 63 | * Let the state know we're about to fetch some comments 64 | */ 65 | export function requestPosts () { 66 | return { 67 | type: REQUEST_POSTS 68 | } 69 | } 70 | 71 | /** 72 | * Trigger a state update once we get all the comments 73 | */ 74 | export function receivePosts (posts) { 75 | return { 76 | type: RECEIVE_POSTS, 77 | posts, 78 | totalPages: parseInt(posts._paging.totalPages) 79 | } 80 | } 81 | 82 | /** 83 | * Change the active post once we get it from a source of truth 84 | */ 85 | export function receivePost (post) { 86 | return { 87 | type: RECEIVE_POST, 88 | post 89 | } 90 | } 91 | 92 | /** 93 | * Receive an author once we get it from a source of truth 94 | */ 95 | export function receiveAuthor (author) { 96 | return { 97 | type: RECEIVE_AUTHOR, 98 | author 99 | } 100 | } 101 | 102 | /** 103 | * Let the state know we are about to get some comments 104 | */ 105 | export function requestPostComments () { 106 | return { 107 | type: REQUEST_POST_COMMENTS 108 | } 109 | } 110 | 111 | /** 112 | * Receive comments for a post from a source of truth 113 | */ 114 | export function receivePostComments (comments) { 115 | if (comments._paging) { 116 | return { 117 | type: RECEIVE_POST_COMMENTS, 118 | comments, 119 | total: parseInt(comments._paging.total) 120 | } 121 | } else { 122 | return { 123 | type: RECEIVE_POST_COMMENTS, 124 | comments, 125 | total: 0 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Receive results from a source of truth 132 | */ 133 | export function receiveResults (results) { 134 | return { 135 | type: RECEIVE_RESULTS, 136 | results 137 | } 138 | } 139 | 140 | /** 141 | * Receive the search query to show on the frontend 142 | */ 143 | 144 | export function receiveSearchQuery (query) { 145 | return { 146 | type: RECEIVE_SEARCH_QUERY, 147 | query 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /components/SinglePostFooter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import Spinner from './Spinner' 5 | import wp from '../wp' 6 | 7 | class PostFooter extends React.Component { 8 | constructor (props) { 9 | super(props) 10 | this.state = { 11 | categories: [], 12 | tags: [] 13 | } 14 | this.fetchCategory = this.fetchCategory.bind(this) 15 | this.renderCategories = this.renderCategories.bind(this) 16 | this.fetchTag = this.fetchTag.bind(this) 17 | } 18 | 19 | async fetchCategory (categoryId) { 20 | return await wp.categories().id(categoryId) 21 | } 22 | 23 | async fetchTag (tagId) { 24 | return await wp.tags().id(tagId) 25 | } 26 | 27 | async componentDidMount () { 28 | let categories = this.props.post.categories 29 | Promise.all(categories.map(this.fetchCategory)) 30 | .then(categories => { 31 | if (categories.length > 0) { 32 | this.setState({categories}) 33 | } else { 34 | this.setState({categories: [{id: 8888, link: '#', name: 'Uncategorized'}]}) 35 | } 36 | }) 37 | 38 | let tags = this.props.post.tags 39 | Promise.all(tags.map(this.fetchTag)) 40 | .then(tags => { 41 | if (tags.length > 0) { 42 | this.setState({tags}) 43 | } else { 44 | this.setState({tags: [{id: 99999, link: '#', name: 'Untagged'}]}) 45 | } 46 | }) 47 | } 48 | 49 | renderCategories (categories) { 50 | return categories.map(category => ( 51 | {category.name} 52 | )) 53 | } 54 | 55 | render () { 56 | let categories = this.state.categories 57 | let tags = this.state.tags 58 | return ( 59 |
    60 | 61 | 62 | 65 | Categories 66 | {categories.length ? this.renderCategories(categories) : } 67 | 68 | 69 | 72 | Tags 73 | {tags.length ? this.renderCategories(tags) : } 74 | 75 | 76 |
    77 | ) 78 | } 79 | } 80 | 81 | export default PostFooter 82 | -------------------------------------------------------------------------------- /redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | // Custom actions 3 | import { 4 | GET_SITE, 5 | GET_CATEGORIES, 6 | GOT_CATEGORIES, 7 | GOT_COMMENTS, 8 | REQUEST_POSTS, 9 | RECEIVE_POSTS, 10 | RECEIVE_POST, 11 | RECEIVE_AUTHOR, 12 | REQUEST_POST_COMMENTS, 13 | RECEIVE_POST_COMMENTS, 14 | RECEIVE_RESULTS, 15 | RECEIVE_SEARCH_QUERY 16 | } from './actions' 17 | 18 | import { preventDuplicatePosts } from './helpers' 19 | /** 20 | * Get or update the site from a source of truth 21 | * @param {object} state 22 | * @param {object} action Must have a type and modifier 23 | * @returns {object} A new state gets returned 24 | */ 25 | function site (state = {}, action) { 26 | switch (action.type) { 27 | case GET_SITE: 28 | return Object.assign({}, state, { 29 | root: action.site 30 | }) 31 | default: return state 32 | } 33 | } 34 | 35 | /** 36 | * Get or update the results page from a source of truth 37 | */ 38 | function search (state = { results: [] }, action) { 39 | switch (action.type) { 40 | case RECEIVE_RESULTS: 41 | return Object.assign({}, state, { 42 | results: action.results 43 | }) 44 | case RECEIVE_SEARCH_QUERY: 45 | return Object.assign({}, state, { 46 | query: action.query 47 | }) 48 | default: return state 49 | } 50 | } 51 | 52 | /** 53 | * Get or update the categories from a source of truth 54 | * since this is async we need some params for state update 55 | * @param {object} state 56 | * @param {object} action 57 | * @returns {object} a new state 58 | */ 59 | function categories (state = { 60 | isFetching: false, 61 | gotError: false, 62 | items: [] 63 | }, action) { 64 | switch (action.type) { 65 | case GET_CATEGORIES: 66 | return Object.assign({}, state, { 67 | isFetching: true 68 | }) 69 | case GOT_CATEGORIES: 70 | return Object.assign({}, state, { 71 | isFetching: false, 72 | items: action.categories 73 | }) 74 | default: return state 75 | } 76 | } 77 | 78 | /** 79 | * Get or update the recent comments from a source of truth 80 | */ 81 | function comments (state = { 82 | isFetching: false, 83 | gotError: false, 84 | items: [] 85 | }, action) { 86 | switch (action.type) { 87 | case GOT_COMMENTS: 88 | return Object.assign({}, state, { 89 | isFetching: false, 90 | items: action.comments 91 | }) 92 | default: return state 93 | } 94 | } 95 | 96 | /** 97 | * Get or update the recent posts from a source of truth 98 | */ 99 | function posts (state = { 100 | isFetching: false, 101 | gotError: false, 102 | items: [], 103 | currentPage: 1, 104 | totalPages: 1 105 | }, action) { 106 | switch (action.type) { 107 | case REQUEST_POSTS: 108 | return Object.assign({}, state, { 109 | isFetching: true 110 | }) 111 | case RECEIVE_POSTS: 112 | return Object.assign({}, state, { 113 | isFetching: false, 114 | items: preventDuplicatePosts(state.items.concat(action.posts)), 115 | currentPage: state.currentPage + 1, 116 | totalPages: action.totalPages 117 | }) 118 | default: return state 119 | } 120 | } 121 | 122 | /** 123 | * Get or update the active post from a source of truth 124 | */ 125 | function post (state = { 126 | data: {}, 127 | author: {}, 128 | comments: { 129 | isFetching: false, 130 | data: [], 131 | total: 0 132 | } 133 | }, action) { 134 | switch (action.type) { 135 | case RECEIVE_POST: 136 | return Object.assign({}, state, { 137 | data: action.post, 138 | comments: {} 139 | }) 140 | case RECEIVE_AUTHOR: 141 | return Object.assign({}, state, { 142 | author: action.author 143 | }) 144 | case REQUEST_POST_COMMENTS: 145 | return Object.assign({}, state, { 146 | comments: { 147 | isFetching: true, 148 | data: [], 149 | total: 0 150 | } 151 | }) 152 | case RECEIVE_POST_COMMENTS: 153 | return Object.assign({}, state, { 154 | comments: { 155 | isFetching: false, 156 | data: action.comments, 157 | total: action.total 158 | } 159 | }) 160 | default: return state 161 | } 162 | } 163 | 164 | /* 165 | * Our whole reducer combined from every other reducer 166 | */ 167 | export default combineReducers({ 168 | site, 169 | search, 170 | categories, 171 | comments, 172 | posts, 173 | post 174 | }) 175 | -------------------------------------------------------------------------------- /components/CommentForm.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | import wp from '../wp' 3 | import { connect } from 'react-redux' 4 | import { requestPostComments, receivePostComments } from '../redux/actions' 5 | 6 | const mapStoreToProps = (store) => { 7 | return { 8 | postID: store.post.data.id 9 | } 10 | } 11 | 12 | const actionCreators = { 13 | requestPostComments, 14 | receivePostComments 15 | } 16 | 17 | class CommentForm extends Component { 18 | // All of our fields displayed in the state 19 | state = { 20 | comment: '', 21 | name: '', 22 | email: '', 23 | website: '' 24 | } 25 | 26 | // Handlers to update the UI as we type 27 | commentHandler = ev => { this.setState({comment: ev.target.value}) } 28 | nameHandler = ev => { this.setState({name: ev.target.value}) } 29 | emailHandler = ev => { this.setState({email: ev.target.value}) } 30 | websiteHandler = ev => { this.setState({website: ev.target.value}) } 31 | 32 | // Handler to actually perform our submit logic 33 | submitHandler = (ev) => { 34 | ev.preventDefault() 35 | let { comment, name, email, website } = this.state 36 | let { postID, requestPostComments, receivePostComments } = this.props 37 | 38 | requestPostComments() 39 | 40 | wp.comments().create({ 41 | author_name: name, 42 | author_email: email, 43 | author_url: website, 44 | content: comment, 45 | post: postID, 46 | date: new Date() 47 | }) 48 | .then(res => { 49 | wp.comments().forPost(postID) 50 | .then(comments => { 51 | receivePostComments(comments) 52 | this.setState({ 53 | comment: '', 54 | name: '', 55 | email: '', 56 | website: '' 57 | }) 58 | }) 59 | }) 60 | } 61 | 62 | render () { 63 | return ( 64 |
    65 |

    66 | Leave a Reply 67 |

    68 |
    69 |

    70 | 71 | Your email address will not be published. 72 | Required fields are marked * 73 |

    74 |

    75 | 76 |